@squiz/dx-json-schema-lib 1.82.2 → 1.82.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,456 @@
1
+ import { JSONSchemaService } from './JsonSchemaService';
2
+ import { ComponentInputMetaSchema } from './JsonSchemaService';
3
+ import { TypeResolverBuilder } from './jsonTypeResolution/TypeResolverBuilder';
4
+
5
+ describe('JSONSchemaService - hasAllOfCombinator', () => {
6
+ let jsonSchemaService: JSONSchemaService<any, any>;
7
+
8
+ beforeAll(() => {
9
+ const typeResolverBuilder = new TypeResolverBuilder();
10
+ const typeResolver = typeResolverBuilder.build();
11
+ jsonSchemaService = new JSONSchemaService(typeResolver, ComponentInputMetaSchema);
12
+ });
13
+
14
+ // Helper to access private method
15
+ const hasAllOfCombinator = (schema: any) => (jsonSchemaService as any).hasAllOfCombinator(schema);
16
+
17
+ describe('Direct allOf detection', () => {
18
+ it('should detect direct allOf usage', () => {
19
+ const schema = {
20
+ type: 'object',
21
+ allOf: [{ properties: { name: { type: 'string' } } }, { properties: { age: { type: 'number' } } }],
22
+ };
23
+
24
+ expect(hasAllOfCombinator(schema)).toBe(true);
25
+ });
26
+
27
+ it('should detect empty allOf array', () => {
28
+ const schema = {
29
+ type: 'object',
30
+ allOf: [],
31
+ };
32
+
33
+ expect(hasAllOfCombinator(schema)).toBe(true);
34
+ });
35
+ });
36
+
37
+ describe('Nested allOf detection', () => {
38
+ it('should detect allOf in properties', () => {
39
+ const schema = {
40
+ type: 'object',
41
+ properties: {
42
+ user: {
43
+ allOf: [{ properties: { name: { type: 'string' } } }, { properties: { email: { type: 'string' } } }],
44
+ },
45
+ },
46
+ };
47
+
48
+ expect(hasAllOfCombinator(schema)).toBe(true);
49
+ });
50
+
51
+ it('should detect allOf in deeply nested properties', () => {
52
+ const schema = {
53
+ type: 'object',
54
+ properties: {
55
+ componentContent: {
56
+ type: 'object',
57
+ properties: {
58
+ heroSection: {
59
+ allOf: [
60
+ { properties: { title: { type: 'string' } } },
61
+ { properties: { subtitle: { type: 'string' } } },
62
+ ],
63
+ },
64
+ },
65
+ },
66
+ },
67
+ };
68
+
69
+ expect(hasAllOfCombinator(schema)).toBe(true);
70
+ });
71
+
72
+ it('should detect allOf in array items', () => {
73
+ const schema = {
74
+ type: 'object',
75
+ properties: {
76
+ items: {
77
+ type: 'array',
78
+ items: {
79
+ allOf: [{ properties: { id: { type: 'number' } } }, { properties: { name: { type: 'string' } } }],
80
+ },
81
+ },
82
+ },
83
+ };
84
+
85
+ expect(hasAllOfCombinator(schema)).toBe(true);
86
+ });
87
+ });
88
+
89
+ describe('Combinator traversal', () => {
90
+ it('should detect allOf within oneOf', () => {
91
+ const schema = {
92
+ type: 'object',
93
+ oneOf: [
94
+ {
95
+ allOf: [{ properties: { type: { const: 'A' } } }, { properties: { valueA: { type: 'string' } } }],
96
+ },
97
+ { properties: { type: { const: 'B' } } },
98
+ ],
99
+ };
100
+
101
+ expect(hasAllOfCombinator(schema)).toBe(true);
102
+ });
103
+
104
+ it('should detect allOf within anyOf', () => {
105
+ const schema = {
106
+ type: 'object',
107
+ anyOf: [
108
+ { properties: { simple: { type: 'string' } } },
109
+ {
110
+ allOf: [{ properties: { complex: { type: 'object' } } }, { properties: { nested: { type: 'array' } } }],
111
+ },
112
+ ],
113
+ };
114
+
115
+ expect(hasAllOfCombinator(schema)).toBe(true);
116
+ });
117
+
118
+ it('should detect allOf within not schema', () => {
119
+ const schema = {
120
+ type: 'object',
121
+ not: {
122
+ allOf: [{ properties: { forbidden: { type: 'string' } } }, { properties: { invalid: { type: 'number' } } }],
123
+ },
124
+ };
125
+
126
+ expect(hasAllOfCombinator(schema)).toBe(true);
127
+ });
128
+ });
129
+
130
+ describe('Conditional schema detection', () => {
131
+ it('should detect allOf in if condition', () => {
132
+ const schema = {
133
+ type: 'object',
134
+ if: {
135
+ allOf: [{ properties: { type: { const: 'special' } } }, { properties: { mode: { const: 'advanced' } } }],
136
+ },
137
+ then: { properties: { specialValue: { type: 'string' } } },
138
+ };
139
+
140
+ expect(hasAllOfCombinator(schema)).toBe(true);
141
+ });
142
+
143
+ it('should detect allOf in then branch', () => {
144
+ const schema = {
145
+ type: 'object',
146
+ if: { properties: { type: { const: 'special' } } },
147
+ then: {
148
+ allOf: [{ properties: { requiredProp: { type: 'string' } } }, { required: ['requiredProp'] }],
149
+ },
150
+ };
151
+
152
+ expect(hasAllOfCombinator(schema)).toBe(true);
153
+ });
154
+
155
+ it('should detect allOf in else branch', () => {
156
+ const schema = {
157
+ type: 'object',
158
+ if: { properties: { type: { const: 'simple' } } },
159
+ then: { properties: { simpleValue: { type: 'string' } } },
160
+ else: {
161
+ allOf: [
162
+ { properties: { complexValue: { type: 'object' } } },
163
+ { properties: { metadata: { type: 'object' } } },
164
+ ],
165
+ },
166
+ };
167
+
168
+ expect(hasAllOfCombinator(schema)).toBe(true);
169
+ });
170
+ });
171
+
172
+ describe('Complex nested scenarios', () => {
173
+ it('should detect allOf in Hero Banner-like schema', () => {
174
+ const heroBannerSchema = {
175
+ type: 'object',
176
+ properties: {
177
+ componentContent: {
178
+ allOf: [
179
+ {
180
+ if: { properties: { heroType: { const: 'Supporting content' } } },
181
+ then: {
182
+ type: 'object',
183
+ properties: {
184
+ heroType: { type: 'string' },
185
+ heading: { type: 'string' },
186
+ content: { type: 'string' },
187
+ links: {
188
+ type: 'array',
189
+ items: {
190
+ type: 'object',
191
+ properties: {
192
+ text: { type: 'string' },
193
+ url: { type: 'string' },
194
+ target: { type: 'string' },
195
+ },
196
+ },
197
+ },
198
+ },
199
+ },
200
+ },
201
+ {
202
+ if: { properties: { heroType: { const: 'Supporting image' } } },
203
+ then: {
204
+ properties: {
205
+ image: { type: 'object' },
206
+ },
207
+ },
208
+ },
209
+ ],
210
+ },
211
+ },
212
+ };
213
+
214
+ expect(hasAllOfCombinator(heroBannerSchema)).toBe(true);
215
+ });
216
+
217
+ it('should detect allOf in very deeply nested structure', () => {
218
+ const deepSchema = {
219
+ type: 'object',
220
+ properties: {
221
+ level1: {
222
+ properties: {
223
+ level2: {
224
+ oneOf: [
225
+ {
226
+ properties: {
227
+ level3: {
228
+ anyOf: [
229
+ {
230
+ allOf: [{ properties: { deep: { type: 'string' } } }],
231
+ },
232
+ ],
233
+ },
234
+ },
235
+ },
236
+ ],
237
+ },
238
+ },
239
+ },
240
+ },
241
+ };
242
+
243
+ expect(hasAllOfCombinator(deepSchema)).toBe(true);
244
+ });
245
+ });
246
+
247
+ describe('Negative cases - should return false', () => {
248
+ it('should return false for simple object schema', () => {
249
+ const schema = {
250
+ type: 'object',
251
+ properties: {
252
+ name: { type: 'string' },
253
+ age: { type: 'number' },
254
+ },
255
+ required: ['name'],
256
+ };
257
+
258
+ expect(hasAllOfCombinator(schema)).toBe(false);
259
+ });
260
+
261
+ it('should return false for schema with only oneOf', () => {
262
+ const schema = {
263
+ type: 'object',
264
+ oneOf: [{ properties: { typeA: { type: 'string' } } }, { properties: { typeB: { type: 'number' } } }],
265
+ };
266
+
267
+ expect(hasAllOfCombinator(schema)).toBe(false);
268
+ });
269
+
270
+ it('should return false for schema with only anyOf', () => {
271
+ const schema = {
272
+ type: 'object',
273
+ anyOf: [{ properties: { option1: { type: 'string' } } }, { properties: { option2: { type: 'boolean' } } }],
274
+ };
275
+
276
+ expect(hasAllOfCombinator(schema)).toBe(false);
277
+ });
278
+
279
+ it('should return false for array schema without allOf', () => {
280
+ const schema = {
281
+ type: 'array',
282
+ items: {
283
+ type: 'object',
284
+ properties: {
285
+ id: { type: 'number' },
286
+ name: { type: 'string' },
287
+ },
288
+ },
289
+ };
290
+
291
+ expect(hasAllOfCombinator(schema)).toBe(false);
292
+ });
293
+
294
+ it('should return false for conditional schema without allOf', () => {
295
+ const schema = {
296
+ type: 'object',
297
+ if: { properties: { type: { const: 'special' } } },
298
+ then: { properties: { specialValue: { type: 'string' } } },
299
+ else: { properties: { normalValue: { type: 'string' } } },
300
+ };
301
+
302
+ expect(hasAllOfCombinator(schema)).toBe(false);
303
+ });
304
+ });
305
+
306
+ describe('Edge cases', () => {
307
+ it('should return false for null', () => {
308
+ expect(hasAllOfCombinator(null)).toBe(false);
309
+ });
310
+
311
+ it('should return false for undefined', () => {
312
+ expect(hasAllOfCombinator(undefined)).toBe(false);
313
+ });
314
+
315
+ it('should return false for string', () => {
316
+ expect(hasAllOfCombinator('not an object')).toBe(false);
317
+ });
318
+
319
+ it('should return false for number', () => {
320
+ expect(hasAllOfCombinator(42)).toBe(false);
321
+ });
322
+
323
+ it('should return false for boolean', () => {
324
+ expect(hasAllOfCombinator(true)).toBe(false);
325
+ });
326
+
327
+ it('should return false for array', () => {
328
+ expect(hasAllOfCombinator(['not', 'a', 'schema'])).toBe(false);
329
+ });
330
+
331
+ it('should return false for empty object', () => {
332
+ expect(hasAllOfCombinator({})).toBe(false);
333
+ });
334
+
335
+ it('should handle circular references gracefully', () => {
336
+ const schema: any = {
337
+ type: 'object',
338
+ properties: {
339
+ self: null,
340
+ },
341
+ };
342
+ schema.properties.self = schema; // Create circular reference
343
+
344
+ // Should not crash and should return false (no allOf)
345
+ expect(hasAllOfCombinator(schema)).toBe(false);
346
+ });
347
+
348
+ it('should handle circular references with allOf', () => {
349
+ const schema: any = {
350
+ type: 'object',
351
+ allOf: [{ properties: { name: { type: 'string' } } }],
352
+ properties: {
353
+ self: null,
354
+ },
355
+ };
356
+ schema.properties.self = schema; // Create circular reference
357
+
358
+ // Should detect allOf despite circular reference
359
+ expect(hasAllOfCombinator(schema)).toBe(true);
360
+ });
361
+
362
+ it('should handle deeply circular references', () => {
363
+ const schemaA: any = {
364
+ type: 'object',
365
+ properties: {
366
+ b: null,
367
+ },
368
+ };
369
+
370
+ const schemaB: any = {
371
+ type: 'object',
372
+ properties: {
373
+ a: schemaA,
374
+ c: null,
375
+ },
376
+ };
377
+
378
+ const schemaC: any = {
379
+ type: 'object',
380
+ properties: {
381
+ b: schemaB,
382
+ },
383
+ };
384
+
385
+ // Create circular chain: A -> B -> C -> B
386
+ schemaA.properties.b = schemaB;
387
+ schemaB.properties.c = schemaC;
388
+
389
+ // Should not crash and should return false (no allOf in any of them)
390
+ expect(hasAllOfCombinator(schemaA)).toBe(false);
391
+ expect(hasAllOfCombinator(schemaB)).toBe(false);
392
+ expect(hasAllOfCombinator(schemaC)).toBe(false);
393
+ });
394
+
395
+ it('should handle malformed properties object', () => {
396
+ const schema = {
397
+ type: 'object',
398
+ properties: null, // Malformed properties
399
+ };
400
+
401
+ expect(hasAllOfCombinator(schema)).toBe(false);
402
+ });
403
+ });
404
+
405
+ describe('Performance characteristics', () => {
406
+ it('should quickly process large schema without allOf', () => {
407
+ const largeSchema = {
408
+ type: 'object',
409
+ properties: {},
410
+ };
411
+
412
+ // Generate 100 properties
413
+ for (let i = 0; i < 100; i++) {
414
+ (largeSchema.properties as any)[`prop${i}`] = {
415
+ type: 'object',
416
+ properties: {
417
+ [`nested${i}`]: { type: 'string' },
418
+ [`array${i}`]: {
419
+ type: 'array',
420
+ items: { type: 'number' },
421
+ },
422
+ },
423
+ };
424
+ }
425
+
426
+ const startTime = performance.now();
427
+ const result = hasAllOfCombinator(largeSchema);
428
+ const endTime = performance.now();
429
+
430
+ expect(result).toBe(false);
431
+ expect(endTime - startTime).toBeLessThan(10); // Should be very fast (< 10ms)
432
+ });
433
+
434
+ it('should quickly detect allOf in large schema', () => {
435
+ const largeSchemaWithAllOf = {
436
+ type: 'object',
437
+ allOf: [{ properties: { detected: { type: 'string' } } }],
438
+ properties: {},
439
+ };
440
+
441
+ // Generate 100 properties
442
+ for (let i = 0; i < 100; i++) {
443
+ (largeSchemaWithAllOf.properties as any)[`prop${i}`] = {
444
+ type: 'string',
445
+ };
446
+ }
447
+
448
+ const startTime = performance.now();
449
+ const result = hasAllOfCombinator(largeSchemaWithAllOf);
450
+ const endTime = performance.now();
451
+
452
+ expect(result).toBe(true);
453
+ expect(endTime - startTime).toBeLessThan(5); // Should be very fast due to early detection
454
+ });
455
+ });
456
+ });