@learncard/credential-library 1.0.10 → 1.0.12
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 +104 -98
- package/package.json +49 -39
- package/src/__tests__/issuance.test.ts +51 -0
- package/src/__tests__/registry.test.ts +457 -0
- package/src/fixtures/boost/basic.ts +50 -0
- package/src/fixtures/boost/boost-id.ts +59 -0
- package/src/fixtures/boost/community-award.ts +96 -0
- package/src/fixtures/boost/delegate.ts +60 -0
- package/src/fixtures/boost/with-skills.ts +80 -0
- package/src/fixtures/clr/competency-aligned.ts +737 -0
- package/src/fixtures/clr/great-plains-full.ts +7841 -0
- package/src/fixtures/clr/minimal.ts +53 -0
- package/src/fixtures/clr/multi-achievement.ts +125 -0
- package/src/fixtures/clr/nd-student-transcript.ts +411 -0
- package/src/fixtures/clr/university-transcript.ts +328 -0
- package/src/fixtures/clr/westbridge-full.ts +2524 -0
- package/src/fixtures/index.ts +159 -0
- package/src/fixtures/invalid/empty-type.ts +27 -0
- package/src/fixtures/invalid/missing-context.ts +27 -0
- package/src/fixtures/invalid/missing-issuer.ts +27 -0
- package/src/fixtures/obv3/1edtech-full.ts +237 -0
- package/src/fixtures/obv3/course-completion.ts +82 -0
- package/src/fixtures/obv3/endorsement.ts +58 -0
- package/src/fixtures/obv3/full-badge.ts +121 -0
- package/src/fixtures/obv3/k12-diploma.ts +92 -0
- package/src/fixtures/obv3/micro-credential.ts +170 -0
- package/src/fixtures/obv3/minimal-badge.ts +40 -0
- package/src/fixtures/obv3/plugfest-jff2.ts +57 -0
- package/src/fixtures/obv3/professional-cert.ts +102 -0
- package/src/fixtures/obv3/with-alignment.ts +62 -0
- package/src/fixtures/obv3/with-endorsement.ts +62 -0
- package/src/fixtures/render-method/render-method-example.ts +45 -0
- package/src/fixtures/vc-v1/alumni-credential.ts +37 -0
- package/src/fixtures/vc-v1/basic.ts +27 -0
- package/src/fixtures/vc-v1/with-status.ts +44 -0
- package/src/fixtures/vc-v2/basic.ts +27 -0
- package/src/fixtures/vc-v2/digital-id.ts +78 -0
- package/src/fixtures/vc-v2/education-degree.ts +77 -0
- package/src/fixtures/vc-v2/employment-credential.ts +64 -0
- package/src/fixtures/vc-v2/license-credential.ts +79 -0
- package/src/fixtures/vc-v2/membership-credential.ts +62 -0
- package/src/fixtures/vc-v2/multiple-subjects.ts +39 -0
- package/src/fixtures/vc-v2/with-evidence.ts +50 -0
- package/src/index.ts +45 -0
- package/src/prepare.ts +212 -0
- package/src/registry.ts +209 -0
- package/src/types.ts +164 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
|
+
import { UnsignedVCValidator } from '@learncard/types';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getAllFixtures,
|
|
6
|
+
getFixture,
|
|
7
|
+
findFixture,
|
|
8
|
+
getFixtures,
|
|
9
|
+
getUnsignedFixtures,
|
|
10
|
+
getValidFixtures,
|
|
11
|
+
getInvalidFixtures,
|
|
12
|
+
getStats,
|
|
13
|
+
resetRegistry,
|
|
14
|
+
registerFixtures,
|
|
15
|
+
prepareFixture,
|
|
16
|
+
prepareFixtureById,
|
|
17
|
+
} from '../index';
|
|
18
|
+
|
|
19
|
+
import { ALL_FIXTURES } from '../fixtures';
|
|
20
|
+
|
|
21
|
+
import type { CredentialFixture } from '../types';
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Ensure fixtures are loaded
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
let fixtures: readonly CredentialFixture[];
|
|
28
|
+
|
|
29
|
+
beforeAll(() => {
|
|
30
|
+
fixtures = getAllFixtures();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Registry integrity
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
describe('Registry integrity', () => {
|
|
38
|
+
it('has at least 15 fixtures registered', () => {
|
|
39
|
+
expect(fixtures.length).toBeGreaterThanOrEqual(15);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('has no duplicate IDs', () => {
|
|
43
|
+
const ids = fixtures.map(f => f.id);
|
|
44
|
+
const uniqueIds = new Set(ids);
|
|
45
|
+
|
|
46
|
+
expect(uniqueIds.size).toBe(ids.length);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('every fixture has required metadata fields', () => {
|
|
50
|
+
for (const f of fixtures) {
|
|
51
|
+
expect(f.id).toBeTruthy();
|
|
52
|
+
expect(f.name).toBeTruthy();
|
|
53
|
+
expect(f.description).toBeTruthy();
|
|
54
|
+
expect(f.spec).toBeTruthy();
|
|
55
|
+
expect(f.profile).toBeTruthy();
|
|
56
|
+
expect(f.source).toBeTruthy();
|
|
57
|
+
expect(typeof f.signed).toBe('boolean');
|
|
58
|
+
expect(f.validity).toBeTruthy();
|
|
59
|
+
expect(f.credential).toBeTruthy();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Per-fixture validation
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
describe('Fixture validation', () => {
|
|
69
|
+
describe('Valid fixtures pass their declared validator', () => {
|
|
70
|
+
const validFixtures = () => getAllFixtures().filter(f => f.validity === 'valid');
|
|
71
|
+
|
|
72
|
+
it.each(validFixtures().map(f => [f.id, f] as const))('%s', (_id, fixture) => {
|
|
73
|
+
if (!fixture.validator) return;
|
|
74
|
+
|
|
75
|
+
const result = fixture.validator.safeParse(fixture.credential);
|
|
76
|
+
|
|
77
|
+
expect(result.success).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('Valid fixtures also pass base UnsignedVC validator', () => {
|
|
82
|
+
const validFixtures = () => getAllFixtures().filter(f => f.validity === 'valid');
|
|
83
|
+
|
|
84
|
+
it.each(validFixtures().map(f => [f.id, f] as const))('%s', (_id, fixture) => {
|
|
85
|
+
const result = UnsignedVCValidator.safeParse(fixture.credential);
|
|
86
|
+
|
|
87
|
+
expect(result.success).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('Invalid fixtures fail their declared validator', () => {
|
|
92
|
+
const invalidFixtures = () =>
|
|
93
|
+
getAllFixtures().filter(f => f.validity === 'invalid' || f.validity === 'tampered');
|
|
94
|
+
|
|
95
|
+
it.each(invalidFixtures().map(f => [f.id, f] as const))('%s', (_id, fixture) => {
|
|
96
|
+
if (!fixture.validator) return;
|
|
97
|
+
|
|
98
|
+
const result = fixture.validator.safeParse(fixture.credential);
|
|
99
|
+
|
|
100
|
+
expect(result.success).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Query API
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
describe('Query API', () => {
|
|
110
|
+
it('getFixture returns the correct fixture by ID', () => {
|
|
111
|
+
const fixture = getFixture('vc-v2/basic');
|
|
112
|
+
|
|
113
|
+
expect(fixture.id).toBe('vc-v2/basic');
|
|
114
|
+
expect(fixture.spec).toBe('vc-v2');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('getFixture throws for unknown ID', () => {
|
|
118
|
+
expect(() => getFixture('nonexistent/fixture')).toThrow('not found');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('findFixture returns undefined for unknown ID', () => {
|
|
122
|
+
expect(findFixture('nonexistent/fixture')).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('filters by spec', () => {
|
|
126
|
+
const obv3 = getFixtures({ spec: 'obv3' });
|
|
127
|
+
|
|
128
|
+
expect(obv3.length).toBeGreaterThan(0);
|
|
129
|
+
expect(obv3.every(f => f.spec === 'obv3')).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('filters by multiple specs', () => {
|
|
133
|
+
const results = getFixtures({ spec: ['vc-v1', 'vc-v2'] });
|
|
134
|
+
|
|
135
|
+
expect(results.length).toBeGreaterThan(0);
|
|
136
|
+
expect(results.every(f => f.spec === 'vc-v1' || f.spec === 'vc-v2')).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('filters by profile', () => {
|
|
140
|
+
const badges = getFixtures({ profile: 'badge' });
|
|
141
|
+
|
|
142
|
+
expect(badges.length).toBeGreaterThan(0);
|
|
143
|
+
expect(badges.every(f => f.profile === 'badge')).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('filters by features (AND logic)', () => {
|
|
147
|
+
const results = getFixtures({ features: ['evidence', 'alignment'] });
|
|
148
|
+
|
|
149
|
+
expect(results.length).toBeGreaterThan(0);
|
|
150
|
+
|
|
151
|
+
for (const f of results) {
|
|
152
|
+
expect(f.features).toContain('evidence');
|
|
153
|
+
expect(f.features).toContain('alignment');
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('filters by featuresAny (OR logic)', () => {
|
|
158
|
+
const results = getFixtures({ featuresAny: ['endorsement', 'results'] });
|
|
159
|
+
|
|
160
|
+
expect(results.length).toBeGreaterThan(0);
|
|
161
|
+
|
|
162
|
+
for (const f of results) {
|
|
163
|
+
const hasAny = f.features.includes('endorsement') || f.features.includes('results');
|
|
164
|
+
|
|
165
|
+
expect(hasAny).toBe(true);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('filters by validity', () => {
|
|
170
|
+
const valid = getValidFixtures();
|
|
171
|
+
const invalid = getInvalidFixtures();
|
|
172
|
+
|
|
173
|
+
expect(valid.length).toBeGreaterThan(0);
|
|
174
|
+
expect(invalid.length).toBeGreaterThan(0);
|
|
175
|
+
expect(valid.every(f => f.validity === 'valid')).toBe(true);
|
|
176
|
+
expect(invalid.every(f => f.validity === 'invalid' || f.validity === 'tampered')).toBe(
|
|
177
|
+
true
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('filters by tags', () => {
|
|
182
|
+
const plugfest = getFixtures({ tags: ['plugfest'] });
|
|
183
|
+
|
|
184
|
+
expect(plugfest.length).toBeGreaterThan(0);
|
|
185
|
+
|
|
186
|
+
for (const f of plugfest) {
|
|
187
|
+
expect(f.tags).toContain('plugfest');
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('getUnsignedFixtures returns only unsigned', () => {
|
|
192
|
+
const unsigned = getUnsignedFixtures();
|
|
193
|
+
|
|
194
|
+
expect(unsigned.length).toBeGreaterThan(0);
|
|
195
|
+
expect(unsigned.every(f => f.signed === false)).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('combined filters work together', () => {
|
|
199
|
+
const results = getFixtures({
|
|
200
|
+
spec: 'obv3',
|
|
201
|
+
profile: 'badge',
|
|
202
|
+
validity: 'valid',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(results.length).toBeGreaterThan(0);
|
|
206
|
+
|
|
207
|
+
for (const f of results) {
|
|
208
|
+
expect(f.spec).toBe('obv3');
|
|
209
|
+
expect(f.profile).toBe('badge');
|
|
210
|
+
expect(f.validity).toBe('valid');
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Stats
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
describe('Stats', () => {
|
|
220
|
+
it('reports correct totals', () => {
|
|
221
|
+
const stats = getStats();
|
|
222
|
+
|
|
223
|
+
expect(stats.total).toBe(fixtures.length);
|
|
224
|
+
expect(stats.signed + stats.unsigned).toBe(stats.total);
|
|
225
|
+
|
|
226
|
+
const specTotal = Object.values(stats.bySpec).reduce((sum, n) => sum + n, 0);
|
|
227
|
+
|
|
228
|
+
expect(specTotal).toBe(stats.total);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Coverage — ensure we have at least one fixture per spec category
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
describe('Spec coverage', () => {
|
|
237
|
+
const requiredSpecs = ['vc-v1', 'vc-v2', 'obv3', 'clr-v2'] as const;
|
|
238
|
+
|
|
239
|
+
it.each(requiredSpecs)('has at least one fixture for %s', spec => {
|
|
240
|
+
const results = getFixtures({ spec });
|
|
241
|
+
|
|
242
|
+
expect(results.length).toBeGreaterThan(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('has at least one invalid fixture', () => {
|
|
246
|
+
const invalid = getInvalidFixtures();
|
|
247
|
+
|
|
248
|
+
expect(invalid.length).toBeGreaterThan(0);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('has at least one boost fixture', () => {
|
|
252
|
+
const boosts = getFixtures({ profile: 'boost' });
|
|
253
|
+
|
|
254
|
+
expect(boosts.length).toBeGreaterThan(0);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// prepareFixture — bridge to wallet issuance
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
describe('prepareFixture', () => {
|
|
263
|
+
const issuerDid = 'did:key:z6MkTestIssuer123';
|
|
264
|
+
const subjectDid = 'did:key:z6MkTestSubject456';
|
|
265
|
+
|
|
266
|
+
type UnknownRecord = Record<string, unknown>;
|
|
267
|
+
type UnknownArrayRecord = Record<string, unknown> & {
|
|
268
|
+
[key: string]: unknown;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
it('replaces string issuer with provided DID', () => {
|
|
272
|
+
const fixture = getFixture('vc-v2/basic');
|
|
273
|
+
const prepared = prepareFixture(fixture, { issuerDid });
|
|
274
|
+
|
|
275
|
+
expect(prepared.issuer).toBe(issuerDid);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('replaces object issuer.id with provided DID', () => {
|
|
279
|
+
const fixture = getFixture('obv3/full-badge');
|
|
280
|
+
const prepared = prepareFixture(fixture, { issuerDid });
|
|
281
|
+
|
|
282
|
+
expect((prepared.issuer as Record<string, unknown>).id).toBe(issuerDid);
|
|
283
|
+
expect((prepared.issuer as Record<string, unknown>).name).toBeTruthy();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('replaces credentialSubject.id with subjectDid', () => {
|
|
287
|
+
const fixture = getFixture('obv3/minimal-badge');
|
|
288
|
+
const prepared = prepareFixture(fixture, { issuerDid, subjectDid });
|
|
289
|
+
const subject = prepared.credentialSubject as Record<string, unknown>;
|
|
290
|
+
|
|
291
|
+
expect(subject.id).toBe(subjectDid);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('generates fresh UUIDs for id fields by default', () => {
|
|
295
|
+
const fixture = getFixture('obv3/minimal-badge');
|
|
296
|
+
const prepared = prepareFixture(fixture, { issuerDid });
|
|
297
|
+
|
|
298
|
+
expect(prepared.id).toMatch(/^urn:uuid:/);
|
|
299
|
+
expect(prepared.id).not.toBe(fixture.credential.id);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('keeps internal CLR references aligned when fresh IDs are generated', () => {
|
|
303
|
+
const fixture = getFixture('clr/westbridge-full');
|
|
304
|
+
const prepared = prepareFixture(fixture, { issuerDid });
|
|
305
|
+
|
|
306
|
+
const ids = new Set<string>();
|
|
307
|
+
const collectIds = (value: unknown): void => {
|
|
308
|
+
if (Array.isArray(value)) {
|
|
309
|
+
value.forEach(collectIds);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!value || typeof value !== 'object') return;
|
|
314
|
+
|
|
315
|
+
const record = value as Record<string, unknown>;
|
|
316
|
+
if (typeof record.id === 'string') {
|
|
317
|
+
ids.add(record.id);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
Object.values(record).forEach(collectIds);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
collectIds(prepared);
|
|
324
|
+
|
|
325
|
+
const subject = prepared.credentialSubject as UnknownRecord;
|
|
326
|
+
const nested = subject.verifiableCredential as UnknownRecord[];
|
|
327
|
+
|
|
328
|
+
const programVc = nested.find(vc => {
|
|
329
|
+
const vcSubject = vc.credentialSubject as UnknownRecord | undefined;
|
|
330
|
+
const achievement = vcSubject?.achievement as UnknownRecord | undefined;
|
|
331
|
+
return achievement?.achievementType === 'BachelorDegree';
|
|
332
|
+
}) as UnknownRecord | undefined;
|
|
333
|
+
|
|
334
|
+
const programSubject = programVc?.credentialSubject as UnknownRecord | undefined;
|
|
335
|
+
const programAchievement = programSubject?.achievement as UnknownRecord | undefined;
|
|
336
|
+
const programResults = Array.isArray(programSubject?.result)
|
|
337
|
+
? (programSubject?.result as UnknownArrayRecord[])
|
|
338
|
+
: [];
|
|
339
|
+
const programResultDescriptions = Array.isArray(programAchievement?.resultDescription)
|
|
340
|
+
? (programAchievement?.resultDescription as UnknownArrayRecord[])
|
|
341
|
+
: [];
|
|
342
|
+
const programResultDescriptionId = programResults[0]?.resultDescription as
|
|
343
|
+
| string
|
|
344
|
+
| undefined;
|
|
345
|
+
const programResultDescription = programResultDescriptions[0]?.id as string | undefined;
|
|
346
|
+
|
|
347
|
+
expect(programResultDescriptionId).toBe(programResultDescription);
|
|
348
|
+
expect(programResultDescriptionId && ids.has(programResultDescriptionId)).toBe(true);
|
|
349
|
+
|
|
350
|
+
const nestedAssociations = subject.association as UnknownRecord[];
|
|
351
|
+
for (const assoc of nestedAssociations) {
|
|
352
|
+
const sourceId = assoc.sourceId as string | undefined;
|
|
353
|
+
const targetId = assoc.targetId as string | undefined;
|
|
354
|
+
|
|
355
|
+
expect(sourceId && ids.has(sourceId)).toBe(true);
|
|
356
|
+
expect(targetId && ids.has(targetId)).toBe(true);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const gpaRecord = nested.find(vc => {
|
|
360
|
+
const vcSubject = vc.credentialSubject as UnknownRecord | undefined;
|
|
361
|
+
const achievement = vcSubject?.achievement as UnknownRecord | undefined;
|
|
362
|
+
return achievement?.achievementType === 'BachelorDegree';
|
|
363
|
+
}) as UnknownRecord | undefined;
|
|
364
|
+
const gpaSubject = gpaRecord?.credentialSubject as UnknownRecord | undefined;
|
|
365
|
+
const gpaResults = Array.isArray(gpaSubject?.result)
|
|
366
|
+
? (gpaSubject?.result as UnknownArrayRecord[])
|
|
367
|
+
: [];
|
|
368
|
+
const gpaResultDescriptionId = gpaResults[0]?.resultDescription as string | undefined;
|
|
369
|
+
|
|
370
|
+
expect(gpaResultDescriptionId && ids.has(gpaResultDescriptionId)).toBe(true);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('preserves original UUIDs when freshIds is false', () => {
|
|
374
|
+
const fixture = getFixture('obv3/minimal-badge');
|
|
375
|
+
const prepared = prepareFixture(fixture, { issuerDid, freshIds: false });
|
|
376
|
+
|
|
377
|
+
expect(prepared.id).toBe(fixture.credential.id);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('sets validFrom to now when not specified', () => {
|
|
381
|
+
const before = new Date().toISOString();
|
|
382
|
+
const fixture = getFixture('vc-v2/basic');
|
|
383
|
+
const prepared = prepareFixture(fixture, { issuerDid });
|
|
384
|
+
const after = new Date().toISOString();
|
|
385
|
+
|
|
386
|
+
expect(prepared.validFrom).toBeDefined();
|
|
387
|
+
expect(prepared.validFrom! >= before).toBe(true);
|
|
388
|
+
expect(prepared.validFrom! <= after).toBe(true);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('uses provided validFrom date', () => {
|
|
392
|
+
const fixture = getFixture('vc-v2/basic');
|
|
393
|
+
const customDate = '2030-01-01T00:00:00Z';
|
|
394
|
+
const prepared = prepareFixture(fixture, { issuerDid, validFrom: customDate });
|
|
395
|
+
|
|
396
|
+
expect(prepared.validFrom).toBe(customDate);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('does not mutate the original fixture', () => {
|
|
400
|
+
const fixture = getFixture('vc-v2/basic');
|
|
401
|
+
const originalId = fixture.credential.id;
|
|
402
|
+
|
|
403
|
+
prepareFixture(fixture, { issuerDid });
|
|
404
|
+
|
|
405
|
+
expect(fixture.credential.id).toBe(originalId);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('prepared credential still passes base validator', () => {
|
|
409
|
+
const fixture = getFixture('obv3/full-badge');
|
|
410
|
+
const prepared = prepareFixture(fixture, { issuerDid, subjectDid });
|
|
411
|
+
const result = UnsignedVCValidator.safeParse(prepared);
|
|
412
|
+
|
|
413
|
+
expect(result.success).toBe(true);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('handles multiple credentialSubjects', () => {
|
|
417
|
+
const fixture = getFixture('vc-v2/multiple-subjects');
|
|
418
|
+
const prepared = prepareFixture(fixture, { issuerDid, subjectDid });
|
|
419
|
+
const subjects = prepared.credentialSubject as Record<string, unknown>[];
|
|
420
|
+
|
|
421
|
+
expect(Array.isArray(subjects)).toBe(true);
|
|
422
|
+
expect(subjects.every(s => s.id === subjectDid)).toBe(true);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
describe('prepareFixtureById', () => {
|
|
427
|
+
it('works as a shorthand for getFixture + prepareFixture', () => {
|
|
428
|
+
const issuerDid = 'did:key:z6MkShorthand';
|
|
429
|
+
const prepared = prepareFixtureById('vc-v2/basic', { issuerDid });
|
|
430
|
+
|
|
431
|
+
expect(prepared.issuer).toBe(issuerDid);
|
|
432
|
+
expect(prepared.type).toContain('VerifiableCredential');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('throws for unknown fixture ID', () => {
|
|
436
|
+
expect(() =>
|
|
437
|
+
prepareFixtureById('nonexistent/fixture', { issuerDid: 'did:key:z6MkTest' })
|
|
438
|
+
).toThrow('not found');
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
// resetRegistry
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
describe('resetRegistry', () => {
|
|
447
|
+
it('clears and lazily re-populates the registry', () => {
|
|
448
|
+
const countBefore = getAllFixtures().length;
|
|
449
|
+
|
|
450
|
+
expect(countBefore).toBeGreaterThan(0);
|
|
451
|
+
|
|
452
|
+
resetRegistry();
|
|
453
|
+
|
|
454
|
+
// After reset, getAllFixtures() lazily re-initializes from ALL_FIXTURES
|
|
455
|
+
expect(getAllFixtures().length).toBe(countBefore);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { UnsignedAchievementCredentialValidator } from '@learncard/types';
|
|
2
|
+
|
|
3
|
+
import type { CredentialFixture } from '../../types';
|
|
4
|
+
|
|
5
|
+
export const boostBasic: CredentialFixture = {
|
|
6
|
+
id: 'boost/basic',
|
|
7
|
+
name: 'Basic LearnCard Boost',
|
|
8
|
+
description:
|
|
9
|
+
'LearnCard BoostCredential — an OBv3-compatible badge with the BoostCredential extension context',
|
|
10
|
+
spec: 'obv3',
|
|
11
|
+
profile: 'boost',
|
|
12
|
+
features: ['display'],
|
|
13
|
+
source: 'synthetic',
|
|
14
|
+
signed: false,
|
|
15
|
+
validity: 'valid',
|
|
16
|
+
validator: UnsignedAchievementCredentialValidator,
|
|
17
|
+
tags: ['learncard', 'boost'],
|
|
18
|
+
|
|
19
|
+
credential: {
|
|
20
|
+
'@context': [
|
|
21
|
+
'https://www.w3.org/ns/credentials/v2',
|
|
22
|
+
'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json',
|
|
23
|
+
'https://ctx.learncard.com/boosts/1.0.3.json',
|
|
24
|
+
],
|
|
25
|
+
id: 'urn:uuid:b8c9d0e1-0008-4000-8000-000000000001',
|
|
26
|
+
type: ['VerifiableCredential', 'OpenBadgeCredential', 'BoostCredential'],
|
|
27
|
+
name: 'Community Contributor',
|
|
28
|
+
issuer: { id: 'did:example:orgABC' },
|
|
29
|
+
validFrom: '2024-07-01T00:00:00Z',
|
|
30
|
+
credentialSubject: {
|
|
31
|
+
id: 'did:example:member42',
|
|
32
|
+
type: ['AchievementSubject'],
|
|
33
|
+
achievement: {
|
|
34
|
+
id: 'urn:uuid:b8c9d0e1-0008-4000-8000-000000000002',
|
|
35
|
+
type: ['Achievement'],
|
|
36
|
+
achievementType: 'Badge',
|
|
37
|
+
name: 'Community Contributor',
|
|
38
|
+
description: 'Recognized for making meaningful contributions to the community.',
|
|
39
|
+
image: 'https://example.com/badges/contributor.png',
|
|
40
|
+
criteria: {
|
|
41
|
+
narrative: 'Contribute to at least 3 community projects.',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
display: {
|
|
46
|
+
backgroundColor: '#2563EB',
|
|
47
|
+
backgroundImage: '',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { UnsignedAchievementCredentialValidator } from '@learncard/types';
|
|
2
|
+
|
|
3
|
+
import type { CredentialFixture } from '../../types';
|
|
4
|
+
|
|
5
|
+
export const boostId: CredentialFixture = {
|
|
6
|
+
id: 'boost/boost-id',
|
|
7
|
+
name: 'LearnCard Boost ID',
|
|
8
|
+
description:
|
|
9
|
+
'LearnCard BoostID credential — an identity-style credential with custom display, used for membership cards and IDs',
|
|
10
|
+
spec: 'obv3',
|
|
11
|
+
profile: 'boost-id',
|
|
12
|
+
features: ['display', 'image'],
|
|
13
|
+
source: 'synthetic',
|
|
14
|
+
signed: false,
|
|
15
|
+
validity: 'valid',
|
|
16
|
+
validator: UnsignedAchievementCredentialValidator,
|
|
17
|
+
tags: ['learncard', 'boost', 'id'],
|
|
18
|
+
|
|
19
|
+
credential: {
|
|
20
|
+
'@context': [
|
|
21
|
+
'https://www.w3.org/ns/credentials/v2',
|
|
22
|
+
'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json',
|
|
23
|
+
'https://ctx.learncard.com/boosts/1.0.3.json',
|
|
24
|
+
'https://ctx.learncard.com/boostIDs/1.0.0.json',
|
|
25
|
+
],
|
|
26
|
+
id: 'urn:uuid:c9d0e1f2-0009-4000-8000-000000000001',
|
|
27
|
+
type: ['VerifiableCredential', 'OpenBadgeCredential', 'BoostCredential', 'BoostID'],
|
|
28
|
+
name: 'Scouts Troop 42 Member ID',
|
|
29
|
+
issuer: { id: 'did:example:troopLeader' },
|
|
30
|
+
validFrom: '2024-09-01T00:00:00Z',
|
|
31
|
+
validUntil: '2025-09-01T00:00:00Z',
|
|
32
|
+
image: 'https://example.com/troop42/badge.png',
|
|
33
|
+
credentialSubject: {
|
|
34
|
+
id: 'did:example:scout123',
|
|
35
|
+
type: ['AchievementSubject'],
|
|
36
|
+
achievement: {
|
|
37
|
+
id: 'urn:uuid:c9d0e1f2-0009-4000-8000-000000000002',
|
|
38
|
+
type: ['Achievement'],
|
|
39
|
+
achievementType: 'Membership',
|
|
40
|
+
name: 'Troop 42 Membership',
|
|
41
|
+
description: 'Active member of Scouts Troop 42.',
|
|
42
|
+
image: '',
|
|
43
|
+
criteria: {
|
|
44
|
+
narrative: 'Registered and active in the current program year.',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
display: {
|
|
49
|
+
backgroundColor: '#16A34A',
|
|
50
|
+
backgroundImage: '',
|
|
51
|
+
},
|
|
52
|
+
boostID: {
|
|
53
|
+
fontColor: '#FFFFFF',
|
|
54
|
+
accentColor: '#15803D',
|
|
55
|
+
idBackgroundColor: '#16A34A',
|
|
56
|
+
repeatIdBackgroundImage: false,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { CredentialFixture } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const boostCommunityAward: CredentialFixture = {
|
|
4
|
+
id: 'boost/community-award',
|
|
5
|
+
name: 'Community Leadership Award Boost',
|
|
6
|
+
description:
|
|
7
|
+
'A LearnCard Boost credential recognizing community leadership contributions, with skills and evidence. Represents real-world community recognition use cases on the LearnCard network.',
|
|
8
|
+
spec: 'obv3',
|
|
9
|
+
profile: 'boost',
|
|
10
|
+
features: ['evidence', 'skills', 'image'],
|
|
11
|
+
source: 'synthetic',
|
|
12
|
+
signed: false,
|
|
13
|
+
validity: 'valid',
|
|
14
|
+
tags: ['boost', 'community', 'leadership', 'recognition', 'learncard'],
|
|
15
|
+
|
|
16
|
+
credential: {
|
|
17
|
+
'@context': [
|
|
18
|
+
'https://www.w3.org/ns/credentials/v2',
|
|
19
|
+
'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json',
|
|
20
|
+
],
|
|
21
|
+
id: 'urn:uuid:d8c4f2a1-3b67-4e89-a1d0-5f9e7c2b8a43',
|
|
22
|
+
type: ['VerifiableCredential', 'OpenBadgeCredential', 'BoostCredential'],
|
|
23
|
+
name: 'Community Leadership Award',
|
|
24
|
+
description:
|
|
25
|
+
'Awarded for outstanding community leadership, demonstrating commitment to mentoring, volunteer coordination, and community engagement over the past year.',
|
|
26
|
+
image: {
|
|
27
|
+
id: 'https://network.learncard.com/boosts/community-leadership/badge.png',
|
|
28
|
+
type: 'Image',
|
|
29
|
+
caption: 'Community Leadership Award badge',
|
|
30
|
+
},
|
|
31
|
+
credentialSubject: {
|
|
32
|
+
id: 'did:example:community-leader-042',
|
|
33
|
+
type: ['AchievementSubject'],
|
|
34
|
+
achievement: {
|
|
35
|
+
id: 'https://network.learncard.com/boosts/community-leadership',
|
|
36
|
+
type: ['Achievement'],
|
|
37
|
+
achievementType: 'Badge',
|
|
38
|
+
name: 'Community Leadership Award',
|
|
39
|
+
description:
|
|
40
|
+
'Recognizes individuals who have made exceptional contributions to their community through leadership, mentoring, and volunteer coordination. Recipients demonstrate initiative, empathy, and the ability to mobilize others toward shared goals.',
|
|
41
|
+
criteria: {
|
|
42
|
+
id: 'https://network.learncard.com/boosts/community-leadership/criteria',
|
|
43
|
+
narrative:
|
|
44
|
+
'Nominated by at least 3 community members and approved by the Community Advisory Board. Must demonstrate: (1) Active mentoring of at least 5 community members, (2) Organization of at least 2 community events, (3) Consistent participation over at least 6 months.',
|
|
45
|
+
},
|
|
46
|
+
image: {
|
|
47
|
+
id: 'https://network.learncard.com/boosts/community-leadership/badge.png',
|
|
48
|
+
type: 'Image',
|
|
49
|
+
caption: 'Community Leadership Award badge',
|
|
50
|
+
},
|
|
51
|
+
creator: {
|
|
52
|
+
id: 'did:web:network.learncard.com:organizations:community-hub',
|
|
53
|
+
type: ['Profile'],
|
|
54
|
+
name: 'LearnCard Community Hub',
|
|
55
|
+
url: 'https://network.learncard.com/organizations/community-hub',
|
|
56
|
+
description: 'The LearnCard Community Hub connects learners, educators, and community leaders.',
|
|
57
|
+
},
|
|
58
|
+
tag: [
|
|
59
|
+
'leadership',
|
|
60
|
+
'mentoring',
|
|
61
|
+
'community-engagement',
|
|
62
|
+
'volunteer',
|
|
63
|
+
'collaboration',
|
|
64
|
+
'communication',
|
|
65
|
+
'project-management',
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
issuer: {
|
|
70
|
+
id: 'did:web:network.learncard.com:organizations:community-hub',
|
|
71
|
+
type: ['Profile'],
|
|
72
|
+
name: 'LearnCard Community Hub',
|
|
73
|
+
url: 'https://network.learncard.com/organizations/community-hub',
|
|
74
|
+
image: {
|
|
75
|
+
id: 'https://network.learncard.com/organizations/community-hub/logo.png',
|
|
76
|
+
type: 'Image',
|
|
77
|
+
caption: 'LearnCard Community Hub logo',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
validFrom: '2025-03-01T00:00:00Z',
|
|
81
|
+
evidence: [
|
|
82
|
+
{
|
|
83
|
+
id: 'https://network.learncard.com/evidence/community-leader-042/mentoring-log',
|
|
84
|
+
type: ['Evidence'],
|
|
85
|
+
narrative:
|
|
86
|
+
'Mentored 8 new community members over 9 months, providing weekly 1-on-1 sessions covering career development, technical skills, and networking strategies.',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'https://network.learncard.com/evidence/community-leader-042/events',
|
|
90
|
+
type: ['Evidence'],
|
|
91
|
+
narrative:
|
|
92
|
+
'Organized and led 4 community events: two technical workshops (Introduction to Web3, Building with Verifiable Credentials), one networking mixer, and one community hackathon with 45 participants.',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
};
|