@pdtf/schemas 3.5.1-3 → 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.
- package/README.md +58 -0
- package/index.js +23 -1
- package/package.json +1 -1
- package/src/examples/v3/exampleAddParticipantVouch.json +3 -0
- package/src/examples/v3/exampleDocumentedVouch.json +3 -0
- package/src/examples/v3/exampleElectronicRecord.json +3 -0
- package/src/examples/v3/exampleVouch.json +3 -0
- package/src/tests/v3/patternProperties.test.js +569 -0
package/README.md
CHANGED
|
@@ -277,6 +277,9 @@ const verifiedClaims = [
|
|
|
277
277
|
},
|
|
278
278
|
],
|
|
279
279
|
},
|
|
280
|
+
terms_of_use: {
|
|
281
|
+
confidentiality_level: "public",
|
|
282
|
+
},
|
|
280
283
|
claims: {
|
|
281
284
|
"/propertyPack/priceInformation/price": 350000,
|
|
282
285
|
"/propertyPack/energyEfficiency/epcRating": "C",
|
|
@@ -297,6 +300,61 @@ if (errors.length === 0) {
|
|
|
297
300
|
- **Provenance Tracking** - Each claim includes verification evidence and source attribution
|
|
298
301
|
- **Trust Framework Integration** - Claims operate within the UK PDTF trust framework
|
|
299
302
|
- **Attachment Support** - Supporting documents can be cryptographically linked to claims
|
|
303
|
+
- **Confidentiality Controls** - Terms of use define access levels and permitted recipients
|
|
304
|
+
|
|
305
|
+
#### Confidentiality Levels
|
|
306
|
+
|
|
307
|
+
Verified claims support three confidentiality levels through a pdtf-specific extension: a `terms_of_use` field, enabling fine-grained access control:
|
|
308
|
+
|
|
309
|
+
**Public Data** - Freely accessible data from official sources or public listing information:
|
|
310
|
+
|
|
311
|
+
```javascript
|
|
312
|
+
{
|
|
313
|
+
terms_of_use: {
|
|
314
|
+
confidentiality_level: "public";
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Examples: Land Registry data, Energy Performance Certificates, planning records, council tax information
|
|
320
|
+
|
|
321
|
+
**Restricted Data** - Limited to transaction participants:
|
|
322
|
+
|
|
323
|
+
```javascript
|
|
324
|
+
{
|
|
325
|
+
terms_of_use: {
|
|
326
|
+
confidentiality_level: "restricted";
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Examples: Commercial property reports, contents of legal forms
|
|
332
|
+
|
|
333
|
+
**Confidential Data** - Role-based access with explicit authorization:
|
|
334
|
+
|
|
335
|
+
```javascript
|
|
336
|
+
{
|
|
337
|
+
terms_of_use: {
|
|
338
|
+
confidentiality_level: "confidential",
|
|
339
|
+
allowed_roles: [
|
|
340
|
+
"Estate Agent",
|
|
341
|
+
"Seller's Conveyancer",
|
|
342
|
+
"Buyer's Conveyancer",
|
|
343
|
+
"Mortgage Broker",
|
|
344
|
+
"Lender"
|
|
345
|
+
]
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Examples: Identity verification reports, anti-money laundering checks, sensitive personal information
|
|
351
|
+
|
|
352
|
+
**Guidelines:**
|
|
353
|
+
|
|
354
|
+
- Use **public** for government/official data sources
|
|
355
|
+
- Use **restricted** for commercial data providers and processed information
|
|
356
|
+
- Use **confidential** for sensitive personal data requiring explicit role authorization
|
|
357
|
+
- Always consider data source and sensitivity when assigning levels
|
|
300
358
|
|
|
301
359
|
**Roadmap - W3C Verifiable Credentials:**
|
|
302
360
|
The next version of the PDTF framework will migrate to [W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) to provide:
|
package/index.js
CHANGED
|
@@ -208,9 +208,30 @@ const getCachedSchemaData = (path, schemaId, overlays) => {
|
|
|
208
208
|
if (pathArray.length >= 1) {
|
|
209
209
|
subSchema = pathArray.reduce((schema, pathElement) => {
|
|
210
210
|
if (!schema) return undefined;
|
|
211
|
-
const { type, items, properties, oneOf } = schema;
|
|
211
|
+
const { type, items, properties, patternProperties, oneOf } = schema;
|
|
212
|
+
|
|
213
|
+
// Check array type
|
|
212
214
|
if (type === "array") return items;
|
|
215
|
+
|
|
216
|
+
// Check explicit properties first
|
|
213
217
|
if (properties?.[pathElement]) return properties[pathElement];
|
|
218
|
+
|
|
219
|
+
// Check patternProperties
|
|
220
|
+
if (patternProperties) {
|
|
221
|
+
for (const pattern in patternProperties) {
|
|
222
|
+
try {
|
|
223
|
+
const regex = new RegExp(pattern);
|
|
224
|
+
if (regex.test(pathElement)) {
|
|
225
|
+
return patternProperties[pattern];
|
|
226
|
+
}
|
|
227
|
+
} catch (e) {
|
|
228
|
+
// Invalid regex pattern in schema - skip it
|
|
229
|
+
console.warn(`Invalid regex pattern in patternProperties: ${pattern}`, e);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check oneOf discriminators
|
|
214
235
|
if (oneOf) {
|
|
215
236
|
let matchingProperty;
|
|
216
237
|
oneOf.forEach((aOneOf) => {
|
|
@@ -222,6 +243,7 @@ const getCachedSchemaData = (path, schemaId, overlays) => {
|
|
|
222
243
|
});
|
|
223
244
|
if (matchingProperty) return matchingProperty;
|
|
224
245
|
}
|
|
246
|
+
|
|
225
247
|
return undefined;
|
|
226
248
|
}, sourceSchema);
|
|
227
249
|
}
|
package/package.json
CHANGED
|
@@ -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
|
+
});
|