@pdtf/schemas 3.5.1-2 → 3.5.1-4

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,569 @@
1
+ const {
2
+ isPathValid,
3
+ getSubschema,
4
+ getSubschemaValidator,
5
+ validateVerifiedClaims,
6
+ } = require("../../../index.js");
7
+
8
+ const schemaId =
9
+ "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json";
10
+
11
+ describe("patternProperties support in path validation", () => {
12
+ describe("isPathValid with patternProperties", () => {
13
+ test("validates path with patternProperties - simple offer ID", () => {
14
+ const path = "/offers/offer-123";
15
+ const isValid = isPathValid(path, schemaId);
16
+ expect(isValid).toBe(true);
17
+ });
18
+
19
+ test("validates path with patternProperties - UUID", () => {
20
+ const path = "/offers/550e8400-e29b-41d4-a716-446655440000";
21
+ const isValid = isPathValid(path, schemaId);
22
+ expect(isValid).toBe(true);
23
+ });
24
+
25
+ test("validates path with patternProperties - DID format", () => {
26
+ const path = "/offers/did:example:buyer123";
27
+ const isValid = isPathValid(path, schemaId);
28
+ expect(isValid).toBe(true);
29
+ });
30
+
31
+ test("validates path with patternProperties - alphanumeric with special chars", () => {
32
+ const path = "/offers/1sorzJCFHbWVlHA2tYXA";
33
+ const isValid = isPathValid(path, schemaId);
34
+ expect(isValid).toBe(true);
35
+ });
36
+
37
+ test("validates nested path within patternProperties", () => {
38
+ const path = "/offers/offer-456/amount";
39
+ const isValid = isPathValid(path, schemaId);
40
+ expect(isValid).toBe(true);
41
+ });
42
+
43
+ test("validates deeply nested path within patternProperties", () => {
44
+ const path = "/offers/offer-456/inclusions";
45
+ const isValid = isPathValid(path, schemaId);
46
+ expect(isValid).toBe(true);
47
+ });
48
+
49
+ test("rejects invalid nested path within patternProperties", () => {
50
+ const path = "/offers/offer-456/nonExistentField";
51
+ const isValid = isPathValid(path, schemaId);
52
+ expect(isValid).toBe(false);
53
+ });
54
+
55
+ test("validates path with patternProperties exists at root /offers", () => {
56
+ const path = "/offers";
57
+ const isValid = isPathValid(path, schemaId);
58
+ expect(isValid).toBe(true);
59
+ });
60
+ });
61
+
62
+ describe("getSubschema with patternProperties", () => {
63
+ test("returns correct subschema for patternProperties path", () => {
64
+ const path = "/offers/offer-123";
65
+ const subschema = getSubschema(path, schemaId);
66
+
67
+ expect(subschema).toBeDefined();
68
+ expect(subschema.type).toBe("object");
69
+ expect(subschema.properties).toBeDefined();
70
+ expect(subschema.properties.amount).toBeDefined();
71
+ expect(subschema.properties.currency).toBeDefined();
72
+ expect(subschema.properties.status).toBeDefined();
73
+ });
74
+
75
+ test("returns correct subschema for nested property in patternProperties", () => {
76
+ const path = "/offers/offer-123/amount";
77
+ const subschema = getSubschema(path, schemaId);
78
+
79
+ expect(subschema).toBeDefined();
80
+ expect(subschema.type).toBe("number");
81
+ expect(subschema.minimum).toBe(0);
82
+ });
83
+
84
+ test("returns correct subschema for status enum", () => {
85
+ const path = "/offers/offer-123/status";
86
+ const subschema = getSubschema(path, schemaId);
87
+
88
+ expect(subschema).toBeDefined();
89
+ expect(subschema.type).toBe("string");
90
+ expect(subschema.enum).toContain("pending");
91
+ expect(subschema.enum).toContain("accepted");
92
+ expect(subschema.enum).toContain("rejected");
93
+ expect(subschema.enum).toContain("withdrawn");
94
+ expect(subschema.enum).toContain("noteOfInterest");
95
+ });
96
+
97
+ test("returns correct subschema for currency pattern", () => {
98
+ const path = "/offers/offer-123/currency";
99
+ const subschema = getSubschema(path, schemaId);
100
+
101
+ expect(subschema).toBeDefined();
102
+ expect(subschema.type).toBe("string");
103
+ expect(subschema.pattern).toBe("^[A-Z]{3}$");
104
+ });
105
+
106
+ test("returns offers object schema", () => {
107
+ const path = "/offers";
108
+ const subschema = getSubschema(path, schemaId);
109
+
110
+ expect(subschema).toBeDefined();
111
+ expect(subschema.type).toBe("object");
112
+ expect(subschema.patternProperties).toBeDefined();
113
+ expect(subschema.propertyNames).toBeDefined();
114
+ });
115
+
116
+ test("returns undefined for invalid path", () => {
117
+ const path = "/offers/offer-123/invalidField";
118
+ const subschema = getSubschema(path, schemaId);
119
+
120
+ expect(subschema).toBeUndefined();
121
+ });
122
+ });
123
+
124
+ describe("getSubschemaValidator with patternProperties", () => {
125
+ test("returns working validator for patternProperties path", () => {
126
+ const path = "/offers/offer-123";
127
+ const validator = getSubschemaValidator(path, schemaId);
128
+
129
+ expect(validator).toBeDefined();
130
+ expect(typeof validator).toBe("function");
131
+
132
+ const validOffer = {
133
+ amount: 500000,
134
+ currency: "GBP",
135
+ status: "pending",
136
+ };
137
+
138
+ const isValid = validator(validOffer);
139
+ expect(isValid).toBe(true);
140
+ });
141
+
142
+ test("validator correctly validates valid offer data", () => {
143
+ const path = "/offers/offer-456";
144
+ const validator = getSubschemaValidator(path, schemaId);
145
+
146
+ const validOffer = {
147
+ amount: 300000,
148
+ currency: "EUR",
149
+ status: "accepted",
150
+ inclusions: ["carpets", "curtains"],
151
+ exclusions: ["garden furniture"],
152
+ conditions: ["Subject to survey"],
153
+ };
154
+
155
+ const isValid = validator(validOffer);
156
+ expect(isValid).toBe(true);
157
+ });
158
+
159
+ test("validator correctly rejects invalid offer data - missing required field", () => {
160
+ const path = "/offers/offer-789";
161
+ const validator = getSubschemaValidator(path, schemaId);
162
+
163
+ const invalidOffer = {
164
+ amount: 400000,
165
+ currency: "GBP",
166
+ // missing required status field
167
+ };
168
+
169
+ const isValid = validator(invalidOffer);
170
+ expect(isValid).toBe(false);
171
+ expect(validator.errors).toBeDefined();
172
+ expect(validator.errors.length).toBeGreaterThan(0);
173
+ });
174
+
175
+ test("validator correctly rejects invalid offer data - negative amount", () => {
176
+ const path = "/offers/offer-999";
177
+ const validator = getSubschemaValidator(path, schemaId);
178
+
179
+ const invalidOffer = {
180
+ amount: -100000,
181
+ currency: "GBP",
182
+ status: "pending",
183
+ };
184
+
185
+ const isValid = validator(invalidOffer);
186
+ expect(isValid).toBe(false);
187
+ expect(validator.errors.some((e) => e.keyword === "minimum")).toBe(true);
188
+ });
189
+
190
+ test("validator correctly rejects invalid offer data - invalid status", () => {
191
+ const path = "/offers/offer-abc";
192
+ const validator = getSubschemaValidator(path, schemaId);
193
+
194
+ const invalidOffer = {
195
+ amount: 250000,
196
+ currency: "GBP",
197
+ status: "invalid-status",
198
+ };
199
+
200
+ const isValid = validator(invalidOffer);
201
+ expect(isValid).toBe(false);
202
+ expect(validator.errors.some((e) => e.keyword === "enum")).toBe(true);
203
+ });
204
+
205
+ test("validator for nested path works correctly", () => {
206
+ const path = "/offers/offer-nested/amount";
207
+ const validator = getSubschemaValidator(path, schemaId);
208
+
209
+ expect(validator(500000)).toBe(true);
210
+ expect(validator(0)).toBe(true);
211
+ expect(validator(-1)).toBe(false);
212
+ expect(validator("not a number")).toBe(false);
213
+ });
214
+ });
215
+
216
+ describe("validateVerifiedClaims with patternProperties paths", () => {
217
+ test("accepts valid claim with patternProperties path", () => {
218
+ const claim = {
219
+ timestamp: 1643115903108,
220
+ verification: {
221
+ trust_framework: "uk_pdtf",
222
+ time: "2025-01-15T12:00:00Z",
223
+ },
224
+ claims: {
225
+ "/offers/offer-001": {
226
+ amount: 500000,
227
+ currency: "GBP",
228
+ status: "pending",
229
+ },
230
+ },
231
+ };
232
+
233
+ const errors = validateVerifiedClaims([claim], schemaId);
234
+ expect(errors).toHaveLength(0);
235
+ });
236
+
237
+ test("accepts valid claim with UUID offer ID", () => {
238
+ const claim = {
239
+ timestamp: 1643115903108,
240
+ verification: {
241
+ trust_framework: "uk_pdtf",
242
+ time: "2025-01-15T12:00:00Z",
243
+ },
244
+ claims: {
245
+ "/offers/1sorzJCFHbWVlHA2tYXA": {
246
+ amount: 750000,
247
+ currency: "GBP",
248
+ status: "accepted",
249
+ },
250
+ },
251
+ };
252
+
253
+ const errors = validateVerifiedClaims([claim], schemaId);
254
+ expect(errors).toHaveLength(0);
255
+ });
256
+
257
+ test("accepts valid claim with DID format offer ID", () => {
258
+ const claim = {
259
+ timestamp: 1643115903108,
260
+ verification: {
261
+ trust_framework: "uk_pdtf",
262
+ time: "2025-01-15T12:00:00Z",
263
+ },
264
+ claims: {
265
+ "/offers/did:example:buyer123": {
266
+ amount: 600000,
267
+ currency: "EUR",
268
+ status: "noteOfInterest",
269
+ },
270
+ },
271
+ };
272
+
273
+ const errors = validateVerifiedClaims([claim], schemaId);
274
+ expect(errors).toHaveLength(0);
275
+ });
276
+
277
+ test("accepts valid claim with nested patternProperties path", () => {
278
+ const claim = {
279
+ timestamp: 1643115903108,
280
+ verification: {
281
+ trust_framework: "uk_pdtf",
282
+ time: "2025-01-15T12:00:00Z",
283
+ },
284
+ claims: {
285
+ "/offers/offer-002/amount": 450000,
286
+ },
287
+ };
288
+
289
+ const errors = validateVerifiedClaims([claim], schemaId);
290
+ expect(errors).toHaveLength(0);
291
+ });
292
+
293
+ test("accepts claim with all optional offer fields", () => {
294
+ const claim = {
295
+ timestamp: 1643115903108,
296
+ verification: {
297
+ trust_framework: "uk_pdtf",
298
+ time: "2025-01-15T12:00:00Z",
299
+ },
300
+ claims: {
301
+ "/offers/offer-complete": {
302
+ amount: 550000,
303
+ currency: "GBP",
304
+ status: "accepted",
305
+ inclusions: ["carpets", "curtains", "light fixtures"],
306
+ exclusions: ["garden shed"],
307
+ conditions: ["Subject to survey", "Subject to mortgage approval"],
308
+ },
309
+ },
310
+ };
311
+
312
+ const errors = validateVerifiedClaims([claim], schemaId);
313
+ expect(errors).toHaveLength(0);
314
+ });
315
+
316
+ test("rejects claim with invalid offer data - missing required field", () => {
317
+ const claim = {
318
+ timestamp: 1643115903108,
319
+ verification: {
320
+ trust_framework: "uk_pdtf",
321
+ time: "2025-01-15T12:00:00Z",
322
+ },
323
+ claims: {
324
+ "/offers/offer-invalid": {
325
+ amount: 500000,
326
+ currency: "GBP",
327
+ // missing required status field
328
+ },
329
+ },
330
+ };
331
+
332
+ const errors = validateVerifiedClaims([claim], schemaId);
333
+ expect(errors.length).toBeGreaterThan(0);
334
+ });
335
+
336
+ test("rejects claim with invalid offer data - negative amount", () => {
337
+ const claim = {
338
+ timestamp: 1643115903108,
339
+ verification: {
340
+ trust_framework: "uk_pdtf",
341
+ time: "2025-01-15T12:00:00Z",
342
+ },
343
+ claims: {
344
+ "/offers/offer-negative": {
345
+ amount: -100000,
346
+ currency: "GBP",
347
+ status: "pending",
348
+ },
349
+ },
350
+ };
351
+
352
+ const errors = validateVerifiedClaims([claim], schemaId);
353
+ expect(errors.length).toBeGreaterThan(0);
354
+ });
355
+
356
+ test("rejects claim with invalid offer data - invalid status enum", () => {
357
+ const claim = {
358
+ timestamp: 1643115903108,
359
+ verification: {
360
+ trust_framework: "uk_pdtf",
361
+ time: "2025-01-15T12:00:00Z",
362
+ },
363
+ claims: {
364
+ "/offers/offer-bad-status": {
365
+ amount: 500000,
366
+ currency: "GBP",
367
+ status: "completed", // not a valid enum value
368
+ },
369
+ },
370
+ };
371
+
372
+ const errors = validateVerifiedClaims([claim], schemaId);
373
+ expect(errors.length).toBeGreaterThan(0);
374
+ });
375
+
376
+ test("rejects claim with invalid offer data - invalid currency format", () => {
377
+ const claim = {
378
+ timestamp: 1643115903108,
379
+ verification: {
380
+ trust_framework: "uk_pdtf",
381
+ time: "2025-01-15T12:00:00Z",
382
+ },
383
+ claims: {
384
+ "/offers/offer-bad-currency": {
385
+ amount: 500000,
386
+ currency: "gbp", // should be uppercase
387
+ status: "pending",
388
+ },
389
+ },
390
+ };
391
+
392
+ const errors = validateVerifiedClaims([claim], schemaId);
393
+ expect(errors.length).toBeGreaterThan(0);
394
+ });
395
+
396
+ test("accepts multiple claims with different patternProperties paths", () => {
397
+ const claims = [
398
+ {
399
+ timestamp: 1643115903108,
400
+ verification: {
401
+ trust_framework: "uk_pdtf",
402
+ time: "2025-01-15T12:00:00Z",
403
+ },
404
+ claims: {
405
+ "/offers/offer-1": {
406
+ amount: 500000,
407
+ currency: "GBP",
408
+ status: "pending",
409
+ },
410
+ },
411
+ },
412
+ {
413
+ timestamp: 1643115903109,
414
+ verification: {
415
+ trust_framework: "uk_pdtf",
416
+ time: "2025-01-15T12:01:00Z",
417
+ },
418
+ claims: {
419
+ "/offers/offer-2": {
420
+ amount: 520000,
421
+ currency: "GBP",
422
+ status: "accepted",
423
+ },
424
+ },
425
+ },
426
+ ];
427
+
428
+ const errors = validateVerifiedClaims(claims, schemaId);
429
+ expect(errors).toHaveLength(0);
430
+ });
431
+
432
+ test("accepts claim with multiple patternProperties paths", () => {
433
+ const claim = {
434
+ timestamp: 1643115903108,
435
+ verification: {
436
+ trust_framework: "uk_pdtf",
437
+ time: "2025-01-15T12:00:00Z",
438
+ },
439
+ claims: {
440
+ "/offers/offer-1": {
441
+ amount: 500000,
442
+ currency: "GBP",
443
+ status: "pending",
444
+ },
445
+ "/offers/offer-2": {
446
+ amount: 510000,
447
+ currency: "GBP",
448
+ status: "withdrawn",
449
+ },
450
+ "/offers/offer-3/status": "rejected",
451
+ },
452
+ };
453
+
454
+ const errors = validateVerifiedClaims([claim], schemaId);
455
+ expect(errors).toHaveLength(0);
456
+ });
457
+
458
+ test("rejects claim for non-existent nested property in patternProperties", () => {
459
+ const claim = {
460
+ timestamp: 1643115903108,
461
+ verification: {
462
+ trust_framework: "uk_pdtf",
463
+ time: "2025-01-15T12:00:00Z",
464
+ },
465
+ claims: {
466
+ "/offers/offer-1/nonExistentField": "some value",
467
+ },
468
+ };
469
+
470
+ const errors = validateVerifiedClaims([claim], schemaId);
471
+ expect(errors.length).toBeGreaterThan(0);
472
+ expect(errors.some((e) => e.includes("not a valid PDTF schema path"))).toBe(
473
+ true
474
+ );
475
+ });
476
+ });
477
+
478
+ describe("precedence: explicit properties over patternProperties", () => {
479
+ // This test documents the expected behavior that explicit properties
480
+ // should take precedence over patternProperties matches
481
+ test("explicit property should match before patternProperties", () => {
482
+ // If offers had an explicit property called "special" and also had
483
+ // patternProperties that would match "special", the explicit property
484
+ // should win. This is standard JSON Schema behavior.
485
+
486
+ // Since our current schema doesn't have this scenario, this test
487
+ // documents the expected behavior for future schema changes
488
+ expect(true).toBe(true);
489
+ });
490
+ });
491
+
492
+ describe("caching with patternProperties paths", () => {
493
+ test("caches subschema for patternProperties paths", () => {
494
+ const path = "/offers/cached-offer-123";
495
+
496
+ // First call - cache miss
497
+ const subschema1 = getSubschema(path, schemaId);
498
+ expect(subschema1).toBeDefined();
499
+
500
+ // Second call - should hit cache
501
+ const subschema2 = getSubschema(path, schemaId);
502
+ expect(subschema2).toBeDefined();
503
+ expect(subschema2).toEqual(subschema1);
504
+ });
505
+
506
+ test("caches validator for patternProperties paths", () => {
507
+ const path = "/offers/cached-offer-456";
508
+
509
+ // First call - cache miss
510
+ const validator1 = getSubschemaValidator(path, schemaId);
511
+ expect(validator1).toBeDefined();
512
+
513
+ // Second call - should hit cache
514
+ const validator2 = getSubschemaValidator(path, schemaId);
515
+ expect(validator2).toBeDefined();
516
+
517
+ // Should be the same validator function
518
+ expect(validator2).toBe(validator1);
519
+ });
520
+
521
+ test("different patternProperties paths get separate cache entries", () => {
522
+ const path1 = "/offers/offer-aaa";
523
+ const path2 = "/offers/offer-bbb";
524
+
525
+ const validator1 = getSubschemaValidator(path1, schemaId);
526
+ const validator2 = getSubschemaValidator(path2, schemaId);
527
+
528
+ // Both should work but point to same schema (since they match same pattern)
529
+ expect(validator1).toBeDefined();
530
+ expect(validator2).toBeDefined();
531
+
532
+ // They should be the same since they resolve to the same schema
533
+ expect(validator1).toBe(validator2);
534
+ });
535
+ });
536
+
537
+ describe("edge cases", () => {
538
+ test("handles offer ID with dots", () => {
539
+ const path = "/offers/offer.123.abc";
540
+ const isValid = isPathValid(path, schemaId);
541
+ expect(isValid).toBe(true);
542
+ });
543
+
544
+ test("handles offer ID with underscores", () => {
545
+ const path = "/offers/offer_123_abc";
546
+ const isValid = isPathValid(path, schemaId);
547
+ expect(isValid).toBe(true);
548
+ });
549
+
550
+ test("handles offer ID with colons (DID format)", () => {
551
+ const path = "/offers/did:web:example.com:users:alice";
552
+ const isValid = isPathValid(path, schemaId);
553
+ expect(isValid).toBe(true);
554
+ });
555
+
556
+ test("handles offer ID with plus signs", () => {
557
+ const path = "/offers/offer+123";
558
+ const isValid = isPathValid(path, schemaId);
559
+ expect(isValid).toBe(true);
560
+ });
561
+
562
+ test("handles very long offer ID", () => {
563
+ const longId = "a".repeat(100);
564
+ const path = `/offers/${longId}`;
565
+ const isValid = isPathValid(path, schemaId);
566
+ expect(isValid).toBe(true);
567
+ });
568
+ });
569
+ });