Skip to content

Commit b453c2a

Browse files
authored
Merge pull request #65 from objectstack-ai/copilot/test-coverage-blitz
2 parents f89fc6f + e409495 commit b453c2a

15 files changed

Lines changed: 5621 additions & 0 deletions

packages/spec/src/api/contract.test.ts

Lines changed: 444 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { DatasetSchema, DatasetMode, type Dataset } from './dataset.zod';
3+
4+
describe('DatasetMode', () => {
5+
it('should accept valid dataset modes', () => {
6+
const validModes = ['insert', 'update', 'upsert', 'replace', 'ignore'];
7+
8+
validModes.forEach(mode => {
9+
expect(() => DatasetMode.parse(mode)).not.toThrow();
10+
});
11+
});
12+
13+
it('should reject invalid modes', () => {
14+
expect(() => DatasetMode.parse('merge')).toThrow();
15+
expect(() => DatasetMode.parse('delete')).toThrow();
16+
expect(() => DatasetMode.parse('')).toThrow();
17+
});
18+
});
19+
20+
describe('DatasetSchema', () => {
21+
it('should accept valid minimal dataset', () => {
22+
const validDataset: Dataset = {
23+
object: 'user',
24+
records: [
25+
{ name: 'John', email: 'john@example.com' },
26+
{ name: 'Jane', email: 'jane@example.com' }
27+
]
28+
};
29+
30+
expect(() => DatasetSchema.parse(validDataset)).not.toThrow();
31+
});
32+
33+
it('should accept dataset with all fields', () => {
34+
const fullDataset: Dataset = {
35+
object: 'account',
36+
externalId: 'code',
37+
mode: 'upsert',
38+
env: ['prod', 'dev', 'test'],
39+
records: [
40+
{ code: 'ACC001', name: 'Acme Corp' },
41+
{ code: 'ACC002', name: 'Beta Inc' }
42+
]
43+
};
44+
45+
expect(() => DatasetSchema.parse(fullDataset)).not.toThrow();
46+
});
47+
48+
it('should apply default values', () => {
49+
const dataset = DatasetSchema.parse({
50+
object: 'product',
51+
records: [{ name: 'Widget' }]
52+
});
53+
54+
expect(dataset.externalId).toBe('name');
55+
expect(dataset.mode).toBe('upsert');
56+
expect(dataset.env).toEqual(['prod', 'dev', 'test']);
57+
});
58+
59+
it('should validate object name format (snake_case)', () => {
60+
expect(() => DatasetSchema.parse({
61+
object: 'valid_object_name',
62+
records: []
63+
})).not.toThrow();
64+
65+
expect(() => DatasetSchema.parse({
66+
object: 'InvalidObject',
67+
records: []
68+
})).toThrow();
69+
70+
expect(() => DatasetSchema.parse({
71+
object: 'invalid-object',
72+
records: []
73+
})).toThrow();
74+
});
75+
76+
it('should accept different modes', () => {
77+
const modes: Array<Dataset['mode']> = ['insert', 'update', 'upsert', 'replace', 'ignore'];
78+
79+
modes.forEach(mode => {
80+
const dataset = DatasetSchema.parse({
81+
object: 'test_object',
82+
mode,
83+
records: []
84+
});
85+
expect(dataset.mode).toBe(mode);
86+
});
87+
});
88+
89+
it('should accept environment scopes', () => {
90+
const dataset1 = DatasetSchema.parse({
91+
object: 'test_object',
92+
env: ['dev'],
93+
records: []
94+
});
95+
expect(dataset1.env).toEqual(['dev']);
96+
97+
const dataset2 = DatasetSchema.parse({
98+
object: 'test_object',
99+
env: ['prod', 'test'],
100+
records: []
101+
});
102+
expect(dataset2.env).toEqual(['prod', 'test']);
103+
});
104+
105+
it('should reject invalid environment values', () => {
106+
expect(() => DatasetSchema.parse({
107+
object: 'test_object',
108+
env: ['production'],
109+
records: []
110+
})).toThrow();
111+
112+
expect(() => DatasetSchema.parse({
113+
object: 'test_object',
114+
env: ['staging'],
115+
records: []
116+
})).toThrow();
117+
});
118+
119+
it('should accept empty records array', () => {
120+
const dataset = DatasetSchema.parse({
121+
object: 'empty_table',
122+
records: []
123+
});
124+
125+
expect(dataset.records).toEqual([]);
126+
});
127+
128+
it('should accept records with various data types', () => {
129+
const dataset = DatasetSchema.parse({
130+
object: 'mixed_data',
131+
records: [
132+
{
133+
string: 'text',
134+
number: 42,
135+
boolean: true,
136+
null_value: null,
137+
object: { nested: 'value' },
138+
array: [1, 2, 3]
139+
}
140+
]
141+
});
142+
143+
expect(dataset.records[0]).toHaveProperty('string', 'text');
144+
expect(dataset.records[0]).toHaveProperty('number', 42);
145+
expect(dataset.records[0]).toHaveProperty('boolean', true);
146+
});
147+
148+
it('should validate externalId field name', () => {
149+
const validExternalIds = ['name', 'code', 'external_id', 'username', 'slug'];
150+
151+
validExternalIds.forEach(externalId => {
152+
expect(() => DatasetSchema.parse({
153+
object: 'test_object',
154+
externalId,
155+
records: []
156+
})).not.toThrow();
157+
});
158+
});
159+
160+
it('should handle seed data use case', () => {
161+
const seedData = DatasetSchema.parse({
162+
object: 'country',
163+
externalId: 'code',
164+
mode: 'upsert',
165+
env: ['prod', 'dev', 'test'],
166+
records: [
167+
{ code: 'US', name: 'United States' },
168+
{ code: 'CA', name: 'Canada' },
169+
{ code: 'MX', name: 'Mexico' }
170+
]
171+
});
172+
173+
expect(seedData.records).toHaveLength(3);
174+
expect(seedData.mode).toBe('upsert');
175+
});
176+
177+
it('should handle demo data use case', () => {
178+
const demoData = DatasetSchema.parse({
179+
object: 'project',
180+
externalId: 'name',
181+
mode: 'replace',
182+
env: ['dev'],
183+
records: [
184+
{ name: 'Demo Project 1', status: 'active' },
185+
{ name: 'Demo Project 2', status: 'completed' }
186+
]
187+
});
188+
189+
expect(demoData.env).toEqual(['dev']);
190+
expect(demoData.mode).toBe('replace');
191+
});
192+
193+
it('should handle test data use case', () => {
194+
const testData = DatasetSchema.parse({
195+
object: 'test_user',
196+
mode: 'ignore',
197+
env: ['test'],
198+
records: [
199+
{ name: 'Test User', email: 'test@example.com' }
200+
]
201+
});
202+
203+
expect(testData.env).toEqual(['test']);
204+
expect(testData.mode).toBe('ignore');
205+
});
206+
207+
it('should reject dataset without required fields', () => {
208+
expect(() => DatasetSchema.parse({
209+
records: []
210+
})).toThrow();
211+
212+
expect(() => DatasetSchema.parse({
213+
object: 'test_object'
214+
})).toThrow();
215+
});
216+
217+
it('should reject invalid mode value', () => {
218+
expect(() => DatasetSchema.parse({
219+
object: 'test_object',
220+
mode: 'invalid_mode',
221+
records: []
222+
})).toThrow();
223+
});
224+
225+
it('should handle large datasets', () => {
226+
const largeDataset = DatasetSchema.parse({
227+
object: 'bulk_data',
228+
records: Array.from({ length: 1000 }, (_, i) => ({
229+
id: i,
230+
name: `Record ${i}`
231+
}))
232+
});
233+
234+
expect(largeDataset.records).toHaveLength(1000);
235+
});
236+
});

packages/spec/src/data/filter.test.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
RangeOperatorSchema,
1010
StringOperatorSchema,
1111
SpecialOperatorSchema,
12+
NormalizedFilterSchema,
1213
FILTER_OPERATORS,
1314
LOGICAL_OPERATORS,
1415
ALL_OPERATORS,
@@ -620,3 +621,135 @@ describe('Real-World Use Cases', () => {
620621
expect(() => QueryFilterSchema.parse(filter)).not.toThrow();
621622
});
622623
});
624+
625+
describe('NormalizedFilterSchema', () => {
626+
it('should accept normalized $and condition', () => {
627+
const filter = {
628+
$and: [
629+
{ age: { $eq: 18 } },
630+
{ role: { $eq: 'admin' } }
631+
]
632+
};
633+
634+
expect(() => NormalizedFilterSchema.parse(filter)).not.toThrow();
635+
});
636+
637+
it('should accept normalized $or condition', () => {
638+
const filter = {
639+
$or: [
640+
{ status: { $eq: 'active' } },
641+
{ status: { $eq: 'pending' } }
642+
]
643+
};
644+
645+
expect(() => NormalizedFilterSchema.parse(filter)).not.toThrow();
646+
});
647+
648+
it('should accept normalized $not condition', () => {
649+
const filter = {
650+
$not: { deleted: { $eq: true } }
651+
};
652+
653+
expect(() => NormalizedFilterSchema.parse(filter)).not.toThrow();
654+
});
655+
656+
it('should accept nested normalized filters in $and', () => {
657+
const filter = {
658+
$and: [
659+
{ age: { $gte: 18 } },
660+
{
661+
$or: [
662+
{ role: { $eq: 'admin' } },
663+
{ role: { $eq: 'moderator' } }
664+
]
665+
}
666+
]
667+
};
668+
669+
expect(() => NormalizedFilterSchema.parse(filter)).not.toThrow();
670+
});
671+
672+
it('should accept nested normalized filters in $or', () => {
673+
const filter = {
674+
$or: [
675+
{ status: { $eq: 'active' } },
676+
{
677+
$and: [
678+
{ status: { $eq: 'pending' } },
679+
{ verified: { $eq: true } }
680+
]
681+
}
682+
]
683+
};
684+
685+
expect(() => NormalizedFilterSchema.parse(filter)).not.toThrow();
686+
});
687+
688+
it('should accept nested normalized filter in $not', () => {
689+
const filter = {
690+
$not: {
691+
$and: [
692+
{ deleted: { $eq: true } },
693+
{ archived: { $eq: true } }
694+
]
695+
}
696+
};
697+
698+
expect(() => NormalizedFilterSchema.parse(filter)).not.toThrow();
699+
});
700+
701+
it('should accept complex deeply nested normalized filters', () => {
702+
const filter = {
703+
$and: [
704+
{ active: { $eq: true } },
705+
{
706+
$or: [
707+
{ type: { $eq: 'premium' } },
708+
{
709+
$and: [
710+
{ type: { $eq: 'basic' } },
711+
{ credits: { $gte: 100 } }
712+
]
713+
}
714+
]
715+
}
716+
]
717+
};
718+
719+
expect(() => NormalizedFilterSchema.parse(filter)).not.toThrow();
720+
});
721+
722+
it('should accept multiple operators in $and', () => {
723+
const filter = {
724+
$and: [
725+
{ age: { $gte: 18 } },
726+
{ age: { $lte: 65 } },
727+
{ role: { $in: ['user', 'admin'] } }
728+
]
729+
};
730+
731+
expect(() => NormalizedFilterSchema.parse(filter)).not.toThrow();
732+
});
733+
734+
it('should accept empty optional operators', () => {
735+
const filter = {};
736+
expect(() => NormalizedFilterSchema.parse(filter)).not.toThrow();
737+
});
738+
739+
it('should accept combination of all logical operators', () => {
740+
const filter = {
741+
$and: [
742+
{ active: { $eq: true } }
743+
],
744+
$or: [
745+
{ role: { $eq: 'admin' } },
746+
{ role: { $eq: 'moderator' } }
747+
],
748+
$not: {
749+
deleted: { $eq: true }
750+
}
751+
};
752+
753+
expect(() => NormalizedFilterSchema.parse(filter)).not.toThrow();
754+
});
755+
});

0 commit comments

Comments
 (0)