@kravc/schema 2.7.5 → 2.8.0-alpha.0

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.
Files changed (170) hide show
  1. package/README.md +19 -14
  2. package/dist/CredentialFactory.d.ts +345 -0
  3. package/dist/CredentialFactory.d.ts.map +1 -0
  4. package/dist/CredentialFactory.js +381 -0
  5. package/dist/CredentialFactory.js.map +1 -0
  6. package/dist/Schema.d.ts +448 -0
  7. package/dist/Schema.d.ts.map +1 -0
  8. package/dist/Schema.js +506 -0
  9. package/dist/Schema.js.map +1 -0
  10. package/dist/ValidationError.d.ts +70 -0
  11. package/dist/ValidationError.d.ts.map +1 -0
  12. package/dist/ValidationError.js +78 -0
  13. package/dist/ValidationError.js.map +1 -0
  14. package/dist/Validator.d.ts +483 -0
  15. package/dist/Validator.d.ts.map +1 -0
  16. package/dist/Validator.js +570 -0
  17. package/dist/Validator.js.map +1 -0
  18. package/dist/helpers/JsonSchema.d.ts +99 -0
  19. package/dist/helpers/JsonSchema.d.ts.map +1 -0
  20. package/dist/helpers/JsonSchema.js +3 -0
  21. package/dist/helpers/JsonSchema.js.map +1 -0
  22. package/dist/helpers/cleanupAttributes.d.ts +34 -0
  23. package/dist/helpers/cleanupAttributes.d.ts.map +1 -0
  24. package/dist/helpers/cleanupAttributes.js +113 -0
  25. package/dist/helpers/cleanupAttributes.js.map +1 -0
  26. package/dist/helpers/cleanupNulls.d.ts +27 -0
  27. package/dist/helpers/cleanupNulls.d.ts.map +1 -0
  28. package/dist/helpers/cleanupNulls.js +96 -0
  29. package/dist/helpers/cleanupNulls.js.map +1 -0
  30. package/dist/helpers/getReferenceIds.d.ts +169 -0
  31. package/dist/helpers/getReferenceIds.d.ts.map +1 -0
  32. package/dist/helpers/getReferenceIds.js +241 -0
  33. package/dist/helpers/getReferenceIds.js.map +1 -0
  34. package/dist/helpers/got.d.ts +60 -0
  35. package/dist/helpers/got.d.ts.map +1 -0
  36. package/dist/helpers/got.js +72 -0
  37. package/dist/helpers/got.js.map +1 -0
  38. package/dist/helpers/mapObjectProperties.d.ts +150 -0
  39. package/dist/helpers/mapObjectProperties.d.ts.map +1 -0
  40. package/dist/helpers/mapObjectProperties.js +229 -0
  41. package/dist/helpers/mapObjectProperties.js.map +1 -0
  42. package/dist/helpers/normalizeAttributes.d.ts +213 -0
  43. package/dist/helpers/normalizeAttributes.d.ts.map +1 -0
  44. package/dist/helpers/normalizeAttributes.js +243 -0
  45. package/dist/helpers/normalizeAttributes.js.map +1 -0
  46. package/dist/helpers/normalizeProperties.d.ts +168 -0
  47. package/dist/helpers/normalizeProperties.d.ts.map +1 -0
  48. package/dist/helpers/normalizeProperties.js +223 -0
  49. package/dist/helpers/normalizeProperties.js.map +1 -0
  50. package/dist/helpers/normalizeRequired.d.ts +159 -0
  51. package/dist/helpers/normalizeRequired.d.ts.map +1 -0
  52. package/dist/helpers/normalizeRequired.js +206 -0
  53. package/dist/helpers/normalizeRequired.js.map +1 -0
  54. package/dist/helpers/normalizeType.d.ts +81 -0
  55. package/dist/helpers/normalizeType.d.ts.map +1 -0
  56. package/dist/helpers/normalizeType.js +210 -0
  57. package/dist/helpers/normalizeType.js.map +1 -0
  58. package/dist/helpers/nullifyEmptyValues.d.ts +139 -0
  59. package/dist/helpers/nullifyEmptyValues.d.ts.map +1 -0
  60. package/dist/helpers/nullifyEmptyValues.js +191 -0
  61. package/dist/helpers/nullifyEmptyValues.js.map +1 -0
  62. package/dist/helpers/removeRequiredAndDefault.d.ts +106 -0
  63. package/dist/helpers/removeRequiredAndDefault.d.ts.map +1 -0
  64. package/dist/helpers/removeRequiredAndDefault.js +138 -0
  65. package/dist/helpers/removeRequiredAndDefault.js.map +1 -0
  66. package/dist/helpers/validateId.d.ts +39 -0
  67. package/dist/helpers/validateId.d.ts.map +1 -0
  68. package/dist/helpers/validateId.js +51 -0
  69. package/dist/helpers/validateId.js.map +1 -0
  70. package/dist/index.d.ts +7 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +17 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/ld/documentLoader.d.ts +8 -0
  75. package/dist/ld/documentLoader.d.ts.map +1 -0
  76. package/dist/ld/documentLoader.js +24 -0
  77. package/dist/ld/documentLoader.js.map +1 -0
  78. package/dist/ld/getLinkedDataAttributeType.d.ts +10 -0
  79. package/dist/ld/getLinkedDataAttributeType.d.ts.map +1 -0
  80. package/dist/ld/getLinkedDataAttributeType.js +32 -0
  81. package/dist/ld/getLinkedDataAttributeType.js.map +1 -0
  82. package/dist/ld/getLinkedDataContext.d.ts +19 -0
  83. package/dist/ld/getLinkedDataContext.d.ts.map +1 -0
  84. package/dist/ld/getLinkedDataContext.js +50 -0
  85. package/dist/ld/getLinkedDataContext.js.map +1 -0
  86. package/eslint.config.mjs +32 -52
  87. package/examples/credentials/createAccountCredential.ts +27 -0
  88. package/examples/credentials/createMineSweeperScoreCredential.ts +115 -0
  89. package/examples/index.ts +7 -0
  90. package/examples/schemas/FavoriteItemSchema.ts +27 -0
  91. package/examples/{Preferences.yaml → schemas/Preferences.yaml} +2 -0
  92. package/examples/schemas/PreferencesSchema.ts +29 -0
  93. package/examples/schemas/ProfileSchema.ts +91 -0
  94. package/examples/schemas/Status.yaml +3 -0
  95. package/examples/schemas/StatusSchema.ts +12 -0
  96. package/jest.config.mjs +5 -0
  97. package/package.json +28 -21
  98. package/src/CredentialFactory.ts +392 -0
  99. package/src/Schema.ts +583 -0
  100. package/src/ValidationError.ts +90 -0
  101. package/src/Validator.ts +603 -0
  102. package/src/__tests__/CredentialFactory.test.ts +588 -0
  103. package/src/__tests__/Schema.test.ts +371 -0
  104. package/src/__tests__/ValidationError.test.ts +235 -0
  105. package/src/__tests__/Validator.test.ts +787 -0
  106. package/src/helpers/JsonSchema.ts +119 -0
  107. package/src/helpers/__tests__/cleanupAttributes.test.ts +943 -0
  108. package/src/helpers/__tests__/cleanupNulls.test.ts +772 -0
  109. package/src/helpers/__tests__/getReferenceIds.test.ts +975 -0
  110. package/src/helpers/__tests__/got.test.ts +193 -0
  111. package/src/helpers/__tests__/mapObjectProperties.test.ts +1126 -0
  112. package/src/helpers/__tests__/normalizeAttributes.test.ts +1435 -0
  113. package/src/helpers/__tests__/normalizeProperties.test.ts +727 -0
  114. package/src/helpers/__tests__/normalizeRequired.test.ts +669 -0
  115. package/src/helpers/__tests__/normalizeType.test.ts +772 -0
  116. package/src/helpers/__tests__/nullifyEmptyValues.test.ts +735 -0
  117. package/src/helpers/__tests__/removeRequiredAndDefault.test.ts +734 -0
  118. package/src/helpers/__tests__/validateId.test.ts +118 -0
  119. package/src/helpers/cleanupAttributes.ts +151 -0
  120. package/src/helpers/cleanupNulls.ts +106 -0
  121. package/src/helpers/getReferenceIds.ts +273 -0
  122. package/src/helpers/got.ts +73 -0
  123. package/src/helpers/mapObjectProperties.ts +272 -0
  124. package/src/helpers/normalizeAttributes.ts +247 -0
  125. package/src/helpers/normalizeProperties.ts +249 -0
  126. package/src/helpers/normalizeRequired.ts +233 -0
  127. package/src/helpers/normalizeType.ts +235 -0
  128. package/src/helpers/nullifyEmptyValues.ts +207 -0
  129. package/src/helpers/removeRequiredAndDefault.ts +151 -0
  130. package/src/helpers/validateId.ts +53 -0
  131. package/src/index.ts +13 -0
  132. package/src/ld/__tests__/documentLoader.test.ts +57 -0
  133. package/src/ld/__tests__/getLinkedDataAttributeType.test.ts +212 -0
  134. package/src/ld/__tests__/getLinkedDataContext.test.ts +378 -0
  135. package/src/ld/documentLoader.ts +28 -0
  136. package/src/ld/getLinkedDataAttributeType.ts +46 -0
  137. package/src/ld/getLinkedDataContext.ts +80 -0
  138. package/tsconfig.json +27 -0
  139. package/types/credentials-context.d.ts +14 -0
  140. package/types/security-context.d.ts +6 -0
  141. package/examples/Status.yaml +0 -3
  142. package/examples/createAccountCredential.js +0 -27
  143. package/examples/createMineSweeperScoreCredential.js +0 -63
  144. package/examples/index.js +0 -9
  145. package/src/CredentialFactory.js +0 -67
  146. package/src/CredentialFactory.spec.js +0 -131
  147. package/src/Schema.js +0 -104
  148. package/src/Schema.spec.js +0 -172
  149. package/src/ValidationError.js +0 -31
  150. package/src/Validator.js +0 -128
  151. package/src/Validator.spec.js +0 -355
  152. package/src/helpers/cleanupAttributes.js +0 -71
  153. package/src/helpers/cleanupNulls.js +0 -42
  154. package/src/helpers/getReferenceIds.js +0 -71
  155. package/src/helpers/mapObject.js +0 -65
  156. package/src/helpers/normalizeAttributes.js +0 -28
  157. package/src/helpers/normalizeProperties.js +0 -61
  158. package/src/helpers/normalizeRequired.js +0 -37
  159. package/src/helpers/normalizeType.js +0 -41
  160. package/src/helpers/nullifyEmptyValues.js +0 -57
  161. package/src/helpers/removeRequiredAndDefault.js +0 -30
  162. package/src/helpers/validateId.js +0 -19
  163. package/src/index.d.ts +0 -25
  164. package/src/index.js +0 -8
  165. package/src/ld/documentLoader.js +0 -25
  166. package/src/ld/documentLoader.spec.js +0 -12
  167. package/src/ld/getLinkedDataContext.js +0 -63
  168. package/src/ld/getLinkedDataType.js +0 -38
  169. /package/examples/{FavoriteItem.yaml → schemas/FavoriteItem.yaml} +0 -0
  170. /package/examples/{Profile.yaml → schemas/Profile.yaml} +0 -0
@@ -0,0 +1,588 @@
1
+ import { canonize } from 'jsonld';
2
+ import { Schema, CredentialFactory, documentLoader } from '../../src';
3
+ import ValidationError from '../ValidationError';
4
+ import {
5
+ createAccountCredential,
6
+ createMineSweeperScoreCredential
7
+ } from '../../examples';
8
+
9
+ // Wrap documentLoader to return a Promise for jsonld compatibility
10
+ // eslint-disable-next-line jsdoc/require-jsdoc
11
+ const asyncDocumentLoader = async (url: string) => {
12
+ const result = documentLoader(url);
13
+ return {
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ document: result.document as any,
16
+ contextUrl: result.contextUrl ?? undefined,
17
+ documentUrl: result.documentUrl
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ } as any;
20
+ };
21
+
22
+ describe('CredentialFactory', () => {
23
+ describe('CredentialFactory.constructor(uri, schemas)', () => {
24
+ it('throws error if "uri" parameter is missing', () => {
25
+ expect(
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ () => new CredentialFactory(undefined as any, [])
28
+ ).toThrow();
29
+ });
30
+
31
+ it('throws error if "uri" parameter is not a URL', () => {
32
+ expect(
33
+ () => new CredentialFactory('BAD_URL', [])
34
+ ).toThrow();
35
+ });
36
+
37
+ it('creates factory with minimal schema', () => {
38
+ const minimalSchema = new Schema({}, 'MinimalSchema');
39
+ const factory = new CredentialFactory('https://example.com/schema/TestV1', [minimalSchema]);
40
+ expect(factory).toBeDefined();
41
+ expect(factory.credentialType).toBe('TestV1');
42
+ });
43
+
44
+ it('creates factory with single schema', () => {
45
+ const schema = new Schema({
46
+ name: { type: 'string', required: true }
47
+ }, 'TestSchema');
48
+
49
+ const factory = new CredentialFactory('https://example.com/schema/TestV1', [schema]);
50
+ expect(factory).toBeDefined();
51
+ expect(factory.credentialType).toBe('TestV1');
52
+ });
53
+
54
+ it('creates factory with multiple schemas', () => {
55
+ const schema1 = new Schema({ name: { type: 'string' } }, 'Schema1');
56
+ const schema2 = new Schema({ value: { type: 'number' } }, 'Schema2');
57
+
58
+ const factory = new CredentialFactory('https://example.com/schema/TestV1', [schema1, schema2]);
59
+ expect(factory).toBeDefined();
60
+ });
61
+
62
+ it('processes schemas without URL by creating new Schema with factory URI', () => {
63
+ const schemaWithoutUrl = new Schema({
64
+ name: { type: 'string', required: true }
65
+ }, 'TestSchema');
66
+
67
+ expect(schemaWithoutUrl.url).toBeUndefined();
68
+
69
+ const factory = new CredentialFactory('https://example.com/schema/TestV1', [schemaWithoutUrl]);
70
+ const context = factory.context;
71
+
72
+ // Schema should now have linkedDataType with factory URI
73
+ expect(context.TestSchema).toBeDefined();
74
+ expect(context.TestSchema['@id']).toContain('https://example.com/schema/TestV1');
75
+ });
76
+
77
+ it('preserves schemas with existing URL', () => {
78
+ const schemaWithUrl = new Schema({
79
+ name: { type: 'string', required: true }
80
+ }, 'TestSchema', 'https://schema.org/');
81
+
82
+ expect(schemaWithUrl.url).toBe('https://schema.org/');
83
+
84
+ const factory = new CredentialFactory('https://example.com/schema/TestV1', [schemaWithUrl]);
85
+ const context = factory.context;
86
+
87
+ // Schema should keep its original URL
88
+ expect(context.TestSchema).toBeDefined();
89
+ expect(context.TestSchema['@id']).toContain('https://schema.org/');
90
+ expect(context.TestSchema['@id']).not.toContain('https://example.com/schema/TestV1');
91
+ });
92
+
93
+ it('throws error if schema validation fails', () => {
94
+ // Create schemas with invalid reference that will fail validation
95
+ const schemaWithInvalidRef = new Schema({
96
+ invalid: {
97
+ $ref: 'NonExistentSchema',
98
+ required: true
99
+ }
100
+ }, 'InvalidSchema');
101
+
102
+ // This will fail during Validator construction because referenced schema doesn't exist
103
+ expect(
104
+ () => new CredentialFactory('https://example.com/schema/TestV1', [schemaWithInvalidRef])
105
+ ).toThrow('Schemas validation failed');
106
+ });
107
+ });
108
+
109
+ describe('.credentialType', () => {
110
+ let minimalSchema: Schema;
111
+
112
+ beforeAll(() => {
113
+ minimalSchema = new Schema({}, 'MinimalSchema');
114
+ });
115
+
116
+ it('returns credential type from URI', () => {
117
+ const factory = new CredentialFactory('https://example.com/schema/AccountV1', [minimalSchema]);
118
+ expect(factory.credentialType).toBe('AccountV1');
119
+ });
120
+
121
+ it('returns last segment of URI path', () => {
122
+ const factory = new CredentialFactory('https://example.com/path/to/CredentialV2', [minimalSchema]);
123
+ expect(factory.credentialType).toBe('CredentialV2');
124
+ });
125
+
126
+ it('handles URI with trailing slash', () => {
127
+ const factory = new CredentialFactory('https://example.com/schema/AccountV1/', [minimalSchema]);
128
+ expect(factory.credentialType).toBe('');
129
+ });
130
+
131
+ it('handles URI with query parameters', () => {
132
+ const factory = new CredentialFactory('https://example.com/schema/AccountV1?version=1', [minimalSchema]);
133
+ expect(factory.credentialType).toBe('AccountV1?version=1');
134
+ });
135
+
136
+ it('handles URI with hash fragment', () => {
137
+ const factory = new CredentialFactory('https://example.com/schema/AccountV1#section', [minimalSchema]);
138
+ expect(factory.credentialType).toBe('AccountV1#section');
139
+ });
140
+ });
141
+
142
+ describe('.context', () => {
143
+ let minimalSchema: Schema;
144
+
145
+ beforeAll(() => {
146
+ minimalSchema = new Schema({}, 'MinimalSchema');
147
+ });
148
+
149
+ it('returns context with credential type mapping', () => {
150
+ const factory = new CredentialFactory('https://example.com/schema/AccountV1', [minimalSchema]);
151
+ const context = factory.context;
152
+
153
+ expect(context.AccountV1).toEqual({ '@id': 'https://example.com/schema/AccountV1' });
154
+ });
155
+
156
+ it('includes schema linked data types in context', () => {
157
+ const schema = new Schema({
158
+ name: { type: 'string', required: true }
159
+ }, 'Account', 'https://example.com/schema/');
160
+
161
+ const factory = new CredentialFactory('https://example.com/schema/AccountV1', [schema]);
162
+ const context = factory.context;
163
+
164
+ expect(context.AccountV1).toBeDefined();
165
+ expect(context.Account).toBeDefined();
166
+ expect(context.Account['@id']).toBeDefined();
167
+ expect(context.Account['@context']).toBeDefined();
168
+ });
169
+
170
+ it('includes multiple schema types in context', () => {
171
+ const schema1 = new Schema({ name: { type: 'string' } }, 'Schema1', 'https://example.com/');
172
+ const schema2 = new Schema({ value: { type: 'number' } }, 'Schema2', 'https://example.com/');
173
+
174
+ const factory = new CredentialFactory('https://example.com/schema/TestV1', [schema1, schema2]);
175
+ const context = factory.context;
176
+
177
+ expect(context.TestV1).toBeDefined();
178
+ expect(context.Schema1).toBeDefined();
179
+ expect(context.Schema2).toBeDefined();
180
+ });
181
+
182
+ it('does not include schemas without linkedDataType in context', () => {
183
+ // Schema without URL won't have linkedDataType
184
+ const schemaWithoutUrl = new Schema({
185
+ name: { type: 'string' }
186
+ }, 'TestSchema');
187
+
188
+ const factory = new CredentialFactory('https://example.com/schema/TestV1', [schemaWithoutUrl]);
189
+ const context = factory.context;
190
+
191
+ // Only credential type should be present, schema without URL won't have linkedDataType
192
+ expect(context.TestV1).toBeDefined();
193
+ // Note: Schema without URL will get linkedDataType created during factory construction
194
+ // So TestSchema should actually be present
195
+ expect(context.TestSchema).toBeDefined();
196
+ });
197
+
198
+ it('merges credential type with schema contexts', () => {
199
+ const schema = new Schema({
200
+ name: { type: 'string' }
201
+ }, 'Account', 'https://example.com/schema/');
202
+
203
+ const factory = new CredentialFactory('https://example.com/schema/AccountV1', [schema]);
204
+ const context = factory.context;
205
+
206
+ // Credential type should be first, then schema contexts
207
+ expect(Object.keys(context)[0]).toBe('AccountV1');
208
+ expect(context.Account).toBeDefined();
209
+ });
210
+ });
211
+
212
+ describe('.createCredential(id, holder, subject = {})', () => {
213
+ let factory: CredentialFactory;
214
+ let simpleFactory: CredentialFactory;
215
+
216
+ beforeAll(() => {
217
+ const videoGameSchema = new Schema({
218
+ id: {},
219
+ name: { type: 'string', required: true },
220
+ version: { type: 'string', required: true }
221
+ }, 'VideoGame', 'https://schema.org/');
222
+
223
+ factory = new CredentialFactory('https://example.com/StarCraft', [videoGameSchema]);
224
+
225
+ const simpleSchema = new Schema({
226
+ id: {},
227
+ name: { type: 'string', required: true },
228
+ email: { type: 'string', format: 'email' }
229
+ }, 'User');
230
+
231
+ simpleFactory = new CredentialFactory('https://example.com/schema/UserV1', [simpleSchema]);
232
+ });
233
+
234
+ it('returns single schema based credential', async () => {
235
+ const credential = await createAccountCredential('did:PLAYER_ID', 'CAHTEP');
236
+
237
+ expect(credential).toBeDefined();
238
+ await canonize(credential, { documentLoader: asyncDocumentLoader });
239
+
240
+ const { credentialSubject: { createdAt } } = credential;
241
+ expect(credential).toEqual({
242
+ '@context': [
243
+ 'https://www.w3.org/2018/credentials/v1',
244
+ {
245
+ AccountV1: { '@id': 'https://example.com/schema/AccountV1' },
246
+ Account: {
247
+ '@id': 'https://example.com/schema/AccountV1#Account',
248
+ '@context': {
249
+ '@vocab': 'https://example.com/schema/AccountV1#',
250
+ '@version': 1.1,
251
+ '@protected': true,
252
+ schema: 'https://schema.org/',
253
+ username: { '@id': 'username' },
254
+ createdAt: { '@id': 'createdAt', '@type': 'schema:DateTime' },
255
+ dateOfBirth: { '@id': 'dateOfBirth', '@type': 'schema:Date' }
256
+ }
257
+ }
258
+ }
259
+ ],
260
+ id: 'https://example.com/account/CAHTEP',
261
+ type: [ 'VerifiableCredential', 'AccountV1' ],
262
+ holder: 'did:PLAYER_ID',
263
+ credentialSubject: {
264
+ id: 'did:PLAYER_ID',
265
+ username: 'CAHTEP',
266
+ createdAt,
267
+ type: 'Account'
268
+ }
269
+ });
270
+ });
271
+
272
+ it('returns multiple schemas based credential', async () => {
273
+ const playerScore = {
274
+ wins: 5,
275
+ losses: 5,
276
+ winRate: 50,
277
+ UNDEFINED: 'VALUE',
278
+ bestScore: 23450,
279
+ endurance: 'P5M22S',
280
+ dateCreated: '2020-10-10T00:00:00Z',
281
+ bestRoundTime: 10000
282
+ };
283
+
284
+ const credential = await createMineSweeperScoreCredential(
285
+ 'did:GAME_ID',
286
+ 'did:PLAYER_ID',
287
+ playerScore
288
+ );
289
+
290
+ expect(credential).toBeDefined();
291
+ await canonize(credential, { documentLoader: asyncDocumentLoader });
292
+
293
+ const { credentialSubject } = credential;
294
+ expect(credentialSubject.type).toBe('Player');
295
+ expect(credentialSubject.hasVideoGameScore.type).toBe('VideoGameScore');
296
+ expect(credentialSubject.hasVideoGameScore.game.type).toBe('VideoGame');
297
+ expect(credentialSubject.hasVideoGameScore.UNDEFINED).toBeUndefined();
298
+
299
+ const customContext = credential['@context'][1];
300
+ expect(customContext.VideoGame).toEqual({
301
+ '@id': 'https://schema.org/VideoGame',
302
+ '@context': {
303
+ '@protected': true,
304
+ '@version': 1.1,
305
+ '@vocab': 'https://schema.org/',
306
+ name: { '@id': 'name' },
307
+ version: { '@id': 'version' }
308
+ }
309
+ });
310
+ });
311
+
312
+ it('throws error if "id" parameter is missing', () => {
313
+ expect(
314
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
315
+ () => factory.createCredential(undefined as any, 'did:HOLDER_ID')
316
+ ).toThrow('Parameter "id" is required');
317
+ });
318
+
319
+ it('throws error if "holder" parameter is missing', () => {
320
+ expect(
321
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
322
+ () => factory.createCredential('did:CREDENTIAL_ID', undefined as any)
323
+ ).toThrow('Parameter "holder" is required');
324
+ });
325
+
326
+ it('throws error if "id" parameter is not a valid URL', () => {
327
+ expect(
328
+ () => factory.createCredential('INVALID_ID', 'did:HOLDER_ID')
329
+ ).toThrow('Parameter "id" must be a URL');
330
+ });
331
+
332
+ it('throws error if "holder" parameter is not a valid URL', () => {
333
+ expect(
334
+ () => factory.createCredential('did:CREDENTIAL_ID', 'INVALID_HOLDER')
335
+ ).toThrow('Parameter "holder" must be a URL');
336
+ });
337
+
338
+ it('creates credential with empty subject when schema allows it', () => {
339
+ const optionalSchema = new Schema({
340
+ id: {},
341
+ name: { type: 'string' } // Not required
342
+ }, 'OptionalUser');
343
+
344
+ const optionalFactory = new CredentialFactory(
345
+ 'https://example.com/schema/OptionalUserV1',
346
+ [optionalSchema]
347
+ );
348
+
349
+ const credential = optionalFactory.createCredential(
350
+ 'https://example.com/credentials/123',
351
+ 'did:holder:456',
352
+ { id: 'did:holder:456' }
353
+ );
354
+
355
+ expect(credential).toBeDefined();
356
+ expect(credential.id).toBe('https://example.com/credentials/123');
357
+ expect(credential.holder).toBe('did:holder:456');
358
+ expect(credential.type).toEqual(['VerifiableCredential', 'OptionalUserV1']);
359
+ expect(credential['@context']).toBeDefined();
360
+ expect(credential.credentialSubject).toBeDefined();
361
+ });
362
+
363
+ it('creates credential with minimal required fields', () => {
364
+ const credential = simpleFactory.createCredential(
365
+ 'https://example.com/credentials/123',
366
+ 'did:holder:456',
367
+ {
368
+ id: 'did:holder:456',
369
+ name: 'John Doe'
370
+ }
371
+ );
372
+
373
+ expect(credential.credentialSubject).toEqual({
374
+ id: 'did:holder:456',
375
+ name: 'John Doe',
376
+ type: 'User'
377
+ });
378
+ });
379
+
380
+ it('includes VerifiableCredential in type array', () => {
381
+ const credential = simpleFactory.createCredential(
382
+ 'https://example.com/credentials/123',
383
+ 'did:holder:456',
384
+ { id: 'did:holder:456', name: 'John' }
385
+ );
386
+
387
+ expect(credential.type).toContain('VerifiableCredential');
388
+ expect(credential.type).toContain('UserV1');
389
+ expect(credential.type).toHaveLength(2);
390
+ });
391
+
392
+ it('includes credentials context URL in @context', () => {
393
+ const credential = simpleFactory.createCredential(
394
+ 'https://example.com/credentials/123',
395
+ 'did:holder:456',
396
+ { id: 'did:holder:456', name: 'John' }
397
+ );
398
+
399
+ expect(credential['@context']).toBeInstanceOf(Array);
400
+ expect(credential['@context'][0]).toBe('https://www.w3.org/2018/credentials/v1');
401
+ expect(credential['@context'][1]).toBeDefined();
402
+ });
403
+
404
+ it('includes factory context in @context', () => {
405
+ const credential = simpleFactory.createCredential(
406
+ 'https://example.com/credentials/123',
407
+ 'did:holder:456',
408
+ { id: 'did:holder:456', name: 'John' }
409
+ );
410
+
411
+ const customContext = credential['@context'][1];
412
+ expect(customContext.UserV1).toBeDefined();
413
+ expect(customContext.UserV1['@id']).toBe('https://example.com/schema/UserV1');
414
+ });
415
+
416
+ it('validates subject against root schema', () => {
417
+ expect(
418
+ () => simpleFactory.createCredential(
419
+ 'https://example.com/credentials/123',
420
+ 'did:holder:456',
421
+ {
422
+ id: 'did:holder:456'
423
+ // Missing required 'name' field
424
+ }
425
+ )
426
+ ).toThrow(ValidationError);
427
+ });
428
+
429
+ it('throws ValidationError with correct schema ID', () => {
430
+ try {
431
+ simpleFactory.createCredential(
432
+ 'https://example.com/credentials/123',
433
+ 'did:holder:456',
434
+ {
435
+ id: 'did:holder:456'
436
+ // Missing required 'name' field
437
+ }
438
+ );
439
+ fail('Should have thrown ValidationError');
440
+ } catch (error) {
441
+ expect(error).toBeInstanceOf(ValidationError);
442
+ const validationError = error as ValidationError;
443
+ const errorJson = validationError.toJSON();
444
+ expect(errorJson.schemaId).toBe('User');
445
+ expect(errorJson.message).toBe('"User" validation failed');
446
+ }
447
+ });
448
+
449
+ it('applies schema defaults to subject', () => {
450
+ const schemaWithDefault = new Schema({
451
+ id: {},
452
+ name: { type: 'string', required: true },
453
+ status: { type: 'string', default: 'active' }
454
+ }, 'UserWithDefault');
455
+
456
+ const factoryWithDefault = new CredentialFactory(
457
+ 'https://example.com/schema/UserV1',
458
+ [schemaWithDefault]
459
+ );
460
+
461
+ const credential = factoryWithDefault.createCredential(
462
+ 'https://example.com/credentials/123',
463
+ 'did:holder:456',
464
+ {
465
+ id: 'did:holder:456',
466
+ name: 'John'
467
+ }
468
+ );
469
+
470
+ expect(credential.credentialSubject.status).toBe('active');
471
+ });
472
+
473
+ it('normalizes subject data according to schema', () => {
474
+ const credential = simpleFactory.createCredential(
475
+ 'https://example.com/credentials/123',
476
+ 'did:holder:456',
477
+ {
478
+ id: 'did:holder:456',
479
+ name: 'John Doe',
480
+ email: 'john@example.com'
481
+ }
482
+ );
483
+
484
+ expect(credential.credentialSubject.email).toBe('john@example.com');
485
+ });
486
+
487
+ it('sets credentialSubject type to root schema ID', () => {
488
+ const credential = simpleFactory.createCredential(
489
+ 'https://example.com/credentials/123',
490
+ 'did:holder:456',
491
+ {
492
+ id: 'did:holder:456',
493
+ name: 'John'
494
+ }
495
+ );
496
+
497
+ expect(credential.credentialSubject.type).toBe('User');
498
+ });
499
+
500
+ it('handles subject with nested objects when schemas support it', () => {
501
+ const nestedSchema = new Schema({
502
+ id: {},
503
+ profile: {
504
+ type: 'object',
505
+ properties: {
506
+ name: { type: 'string', required: true },
507
+ age: { type: 'integer' }
508
+ },
509
+ required: true
510
+ }
511
+ }, 'UserWithProfile');
512
+
513
+ const nestedFactory = new CredentialFactory(
514
+ 'https://example.com/schema/UserV1',
515
+ [nestedSchema]
516
+ );
517
+
518
+ const credential = nestedFactory.createCredential(
519
+ 'https://example.com/credentials/123',
520
+ 'did:holder:456',
521
+ {
522
+ id: 'did:holder:456',
523
+ profile: {
524
+ name: 'John',
525
+ age: 30
526
+ }
527
+ }
528
+ );
529
+
530
+ expect(credential.credentialSubject.profile).toBeDefined();
531
+ expect(credential.credentialSubject.profile.name).toBe('John');
532
+ expect(credential.credentialSubject.profile.age).toBe(30);
533
+ });
534
+
535
+ it('removes undefined properties from subject', () => {
536
+ const credential = simpleFactory.createCredential(
537
+ 'https://example.com/credentials/123',
538
+ 'did:holder:456',
539
+ {
540
+ id: 'did:holder:456',
541
+ name: 'John',
542
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
543
+ undefinedField: undefined as any
544
+ }
545
+ );
546
+
547
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
548
+ expect((credential.credentialSubject as any).undefinedField).toBeUndefined();
549
+ });
550
+
551
+ it('handles credential ID with different URL formats', () => {
552
+ const testCases = [
553
+ 'https://example.com/credentials/123',
554
+ 'did:example:123',
555
+ 'http://example.com/credentials/456'
556
+ ];
557
+
558
+ testCases.forEach(credentialId => {
559
+ const credential = simpleFactory.createCredential(
560
+ credentialId,
561
+ 'did:holder:456',
562
+ { id: 'did:holder:456', name: 'John' }
563
+ );
564
+
565
+ expect(credential.id).toBe(credentialId);
566
+ });
567
+ });
568
+
569
+ it('handles holder with different identifier formats', () => {
570
+ const testCases = [
571
+ 'did:example:123',
572
+ 'https://example.com/users/123',
573
+ 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK'
574
+ ];
575
+
576
+ testCases.forEach(holder => {
577
+ const credential = simpleFactory.createCredential(
578
+ 'https://example.com/credentials/123',
579
+ holder,
580
+ { id: holder, name: 'John' }
581
+ );
582
+
583
+ expect(credential.holder).toBe(holder);
584
+ expect(credential.credentialSubject.id).toBe(holder);
585
+ });
586
+ });
587
+ });
588
+ });