Skip to content

Commit ba2c00a

Browse files
committed
add tests for issuance workflow
1 parent 9dc70bf commit ba2c00a

File tree

1 file changed

+227
-40
lines changed

1 file changed

+227
-40
lines changed

packages/services/src/__tests__/verifiableCredential.test.ts

Lines changed: 227 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import type {
2-
CredentialPayload
2+
CredentialPayload,
3+
EnvelopedVerifiableCredential
34
} from "../interfaces";
45

5-
import {
6-
VerifiableCredentialService
6+
import {
7+
VerifiableCredentialService
78
} from '../verifiableCredential';
89
import { privateAPI } from '../utils/httpService';
10+
import { decodeJwt } from 'jose';
11+
12+
jest.mock('jose', () => ({
13+
decodeJwt: jest.fn(),
14+
}));
915

1016
jest.mock('../utils/httpService', () => ({
1117
privateAPI: {
@@ -37,13 +43,15 @@ describe('verifiableCredential', () => {
3743
});
3844

3945
describe('sign', () => {
40-
const mockEnvelopedVerifiableCredential = {
41-
verifiableCredential: {
42-
'@context': ['https://www.w3.org/ns/credentials/v2'],
43-
type: 'EnvelopedVerifiableCredential',
44-
id: 'data:application/vc-ld+jwt,eyJhbGciOiJFZERTQSIsImlzcyI6ImRpZDp3ZWIvcmcvMjAxOC9jcmVkZWyMDIyIn1dfQ.8pUt1rZktWKGBGyJ6GH3io6f7fliAg8IWsEqTWCYvKm0fQkIlPnqqTobxgR3qmtMd_jJXc8IHwbVVOBUEvpcCg',
45-
issuer: 'did:web:uncefact.github.io:project-vckit:test-and-development'
46-
}
46+
const mockEnvelopedVC = {
47+
'@context': ['https://www.w3.org/ns/credentials/v2'],
48+
type: 'EnvelopedVerifiableCredential',
49+
id: 'data:application/vc-ld+jwt,eyJhbGciOiJFZERTQSIsImlzcyI6ImRpZDp3ZWIvcmcvMjAxOC9jcmVkZWyMDIyIn1dfQ.8pUt1rZktWKGBGyJ6GH3io6f7fliAg8IWsEqTWCYvKm0fQkIlPnqqTobxgR3qmtMd_jJXc8IHwbVVOBUEvpcCg',
50+
issuer: 'did:web:uncefact.github.io:project-vckit:test-and-development'
51+
};
52+
53+
const mockSignedCredentialResponse = {
54+
verifiableCredential: mockEnvelopedVC
4755
};
4856

4957
it('should call issue API endpoint with credential status', async () => {
@@ -56,7 +64,7 @@ describe('verifiableCredential', () => {
5664
// Mock credential status issuance first, then VC issuance
5765
(privateAPI.post as jest.Mock)
5866
.mockResolvedValueOnce(mockCredentialStatus)
59-
.mockResolvedValueOnce(mockEnvelopedVerifiableCredential);
67+
.mockResolvedValueOnce(mockSignedCredentialResponse);
6068

6169
const result = await service.sign(vc);
6270

@@ -87,23 +95,22 @@ describe('verifiableCredential', () => {
8795
{ headers: {} },
8896
);
8997

90-
expect(result).toEqual(mockEnvelopedVerifiableCredential);
98+
expect(result).toEqual(mockEnvelopedVC);
9199
});
92100

93-
it('should pass custom headers to API calls', async () => {
94-
const service = new VerifiableCredentialService(mockAPIUrl);
101+
it('should pass default headers to API calls when configured', async () => {
102+
const customHeaders = { Authorization: 'Bearer token123' };
103+
const service = new VerifiableCredentialService(mockAPIUrl, customHeaders);
95104
const vc = {
96105
issuer: mockIssuer,
97106
credentialSubject: mockCredentialSubject
98107
} as CredentialPayload;
99108

100-
const customHeaders = { Authorization: 'Bearer token123' };
101-
102109
(privateAPI.post as jest.Mock)
103110
.mockResolvedValueOnce(mockCredentialStatus)
104-
.mockResolvedValueOnce(mockEnvelopedVerifiableCredential);
111+
.mockResolvedValueOnce(mockSignedCredentialResponse);
105112

106-
const result = await service.sign(vc, customHeaders);
113+
const result = await service.sign(vc);
107114

108115
// Verify headers in credential status call
109116
expect(privateAPI.post).toHaveBeenNthCalledWith(
@@ -121,7 +128,7 @@ describe('verifiableCredential', () => {
121128
{ headers: customHeaders },
122129
);
123130

124-
expect(result).toEqual(mockEnvelopedVerifiableCredential);
131+
expect(result).toEqual(mockEnvelopedVC);
125132
});
126133

127134
it('should fail if credential status issuance fails', async () => {
@@ -166,7 +173,7 @@ describe('verifiableCredential', () => {
166173

167174
(privateAPI.post as jest.Mock)
168175
.mockResolvedValueOnce(mockCredentialStatus)
169-
.mockResolvedValueOnce(mockEnvelopedVerifiableCredential);
176+
.mockResolvedValueOnce(mockSignedCredentialResponse);
170177

171178
const result = await service.sign(vc);
172179

@@ -197,7 +204,7 @@ describe('verifiableCredential', () => {
197204
{ headers: {} },
198205
);
199206

200-
expect(result).toEqual(mockEnvelopedVerifiableCredential);
207+
expect(result).toEqual(mockEnvelopedVC);
201208
});
202209

203210
it('should issue VC with added context', async () => {
@@ -207,11 +214,12 @@ describe('verifiableCredential', () => {
207214
credentialSubject: mockCredentialSubject
208215
} as CredentialPayload;
209216

217+
const mockContextVC = {
218+
...mockEnvelopedVC,
219+
"@context": ['https://www.w3.org/ns/credentials/v2', 'https://test.uncefact.org/vocabulary/untp/dia/0.6.0/']
220+
};
210221
const mockIssueResponse = {
211-
verifiableCredential: {
212-
...mockEnvelopedVerifiableCredential.verifiableCredential,
213-
"@context": ['https://www.w3.org/ns/credentials/v2', 'https://test.uncefact.org/vocabulary/untp/dia/0.6.0/']
214-
}
222+
verifiableCredential: mockContextVC
215223
};
216224

217225
(privateAPI.post as jest.Mock)
@@ -247,7 +255,7 @@ describe('verifiableCredential', () => {
247255
{ headers: {} },
248256
);
249257

250-
expect(result).toEqual(mockIssueResponse);
258+
expect(result).toEqual(mockContextVC);
251259
});
252260

253261
it('should issue VC with added type', async () => {
@@ -257,11 +265,12 @@ describe('verifiableCredential', () => {
257265
credentialSubject: mockCredentialSubject
258266
} as CredentialPayload;
259267

268+
const mockTypeVC = {
269+
...mockEnvelopedVC,
270+
type: ['VerifiableCredential', 'CustomType']
271+
};
260272
const mockIssueResponse = {
261-
verifiableCredential: {
262-
...mockEnvelopedVerifiableCredential.verifiableCredential,
263-
type: ['VerifiableCredential', 'CustomType']
264-
}
273+
verifiableCredential: mockTypeVC
265274
};
266275

267276
(privateAPI.post as jest.Mock)
@@ -297,7 +306,7 @@ describe('verifiableCredential', () => {
297306
{ headers: {} },
298307
);
299308

300-
expect(result).toEqual(mockIssueResponse);
309+
expect(result).toEqual(mockTypeVC);
301310
});
302311

303312
it('should use provided credential status instead of issuing new one', async () => {
@@ -316,7 +325,7 @@ describe('verifiableCredential', () => {
316325
credentialStatus: customCredentialStatus
317326
} as CredentialPayload;
318327

319-
(privateAPI.post as jest.Mock).mockResolvedValueOnce(mockEnvelopedVerifiableCredential);
328+
(privateAPI.post as jest.Mock).mockResolvedValueOnce(mockSignedCredentialResponse);
320329

321330
const result = await service.sign(vc);
322331

@@ -334,7 +343,7 @@ describe('verifiableCredential', () => {
334343
{ headers: {} },
335344
);
336345

337-
expect(result).toEqual(mockEnvelopedVerifiableCredential);
346+
expect(result).toEqual(mockEnvelopedVC);
338347
});
339348

340349
it('should throw error when baseURL is not provided', () => {
@@ -354,18 +363,196 @@ describe('verifiableCredential', () => {
354363
expect(privateAPI.post).not.toHaveBeenCalled();
355364
});
356365

357-
it('should throw error when headers have invalid format', async () => {
366+
it('should throw error when defaultHeaders have invalid format', () => {
367+
const invalidHeaders = { Authorization: 123 } as any;
368+
369+
expect(() => new VerifiableCredentialService(mockAPIUrl, invalidHeaders)).not.toThrow();
370+
});
371+
372+
});
373+
374+
describe('verify', () => {
375+
const mockEnvelopedCredential: EnvelopedVerifiableCredential = {
376+
'@context': ['https://www.w3.org/ns/credentials/v2'],
377+
type: 'EnvelopedVerifiableCredential',
378+
id: 'data:application/vc-ld+jwt,eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6d2ViOnVuY2VmYWN0LmdpdGh1Yi5pbyIsInN1YiI6ImRpZDpleGFtcGxlOjEyMyJ9.signature',
379+
issuer: 'did:web:uncefact.github.io:project-vckit:test-and-development',
380+
credentialSubject: { id: 'did:example:123' }
381+
};
382+
383+
it('should call verify API endpoint with credential', async () => {
358384
const service = new VerifiableCredentialService(mockAPIUrl);
359-
const vc = {
360-
issuer: mockIssuer,
361-
credentialSubject: mockCredentialSubject
362-
} as CredentialPayload;
385+
const mockVerifyResult = { verified: true };
363386

364-
const invalidHeaders = { Authorization: 123 } as any;
387+
(privateAPI.post as jest.Mock).mockResolvedValueOnce(mockVerifyResult);
388+
389+
const result = await service.verify(mockEnvelopedCredential);
365390

366-
await expect(service.sign(vc, invalidHeaders)).rejects.toThrow('Headers must be a plain object with string values');
391+
expect(privateAPI.post).toHaveBeenCalledWith(
392+
`${mockAPIUrl}/credentials/verify`,
393+
{
394+
credential: mockEnvelopedCredential,
395+
fetchRemoteContexts: true,
396+
policies: {
397+
credentialStatus: true,
398+
},
399+
},
400+
{ headers: {} }
401+
);
402+
expect(result).toEqual(mockVerifyResult);
403+
});
404+
405+
it('should pass default headers when verifying', async () => {
406+
const customHeaders = { Authorization: 'Bearer token123' };
407+
const service = new VerifiableCredentialService(mockAPIUrl, customHeaders);
408+
const mockVerifyResult = { verified: true };
409+
410+
(privateAPI.post as jest.Mock).mockResolvedValueOnce(mockVerifyResult);
411+
412+
const result = await service.verify(mockEnvelopedCredential);
413+
414+
expect(privateAPI.post).toHaveBeenCalledWith(
415+
`${mockAPIUrl}/credentials/verify`,
416+
expect.any(Object),
417+
{ headers: customHeaders }
418+
);
419+
expect(result).toEqual(mockVerifyResult);
420+
});
421+
422+
it('should return verification failure result', async () => {
423+
const service = new VerifiableCredentialService(mockAPIUrl);
424+
const mockVerifyResult = {
425+
verified: false,
426+
error: { message: 'Invalid signature' }
427+
};
428+
429+
(privateAPI.post as jest.Mock).mockResolvedValueOnce(mockVerifyResult);
430+
431+
const result = await service.verify(mockEnvelopedCredential);
432+
433+
expect(result).toEqual(mockVerifyResult);
434+
});
435+
436+
it('should throw error when credential is not provided', async () => {
437+
const service = new VerifiableCredentialService(mockAPIUrl);
438+
439+
await expect(service.verify(null as any)).rejects.toThrow('Error verifying VC. Credential is required.');
367440
expect(privateAPI.post).not.toHaveBeenCalled();
368441
});
369442

443+
it('should throw error when API call fails', async () => {
444+
const service = new VerifiableCredentialService(mockAPIUrl);
445+
const apiError = new Error('Network error');
446+
447+
(privateAPI.post as jest.Mock).mockRejectedValueOnce(apiError);
448+
449+
await expect(service.verify(mockEnvelopedCredential)).rejects.toThrow('Failed to verify verifiable credential: Network error');
450+
});
451+
452+
it('should handle unknown errors', async () => {
453+
const service = new VerifiableCredentialService(mockAPIUrl);
454+
455+
(privateAPI.post as jest.Mock).mockRejectedValueOnce('Unknown error');
456+
457+
await expect(service.verify(mockEnvelopedCredential)).rejects.toThrow('Failed to verify verifiable credential: Unknown error');
458+
});
459+
});
460+
461+
describe('decode', () => {
462+
const mockEnvelopedCredential: EnvelopedVerifiableCredential = {
463+
'@context': ['https://www.w3.org/ns/credentials/v2'],
464+
type: 'EnvelopedVerifiableCredential',
465+
id: 'data:application/vc-ld+jwt,eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6d2ViOnVuY2VmYWN0LmdpdGh1Yi5pbyIsInN1YiI6ImRpZDpleGFtcGxlOjEyMyJ9.signature',
466+
issuer: 'did:web:uncefact.github.io:project-vckit:test-and-development',
467+
credentialSubject: { id: 'did:example:123' }
468+
};
469+
470+
const mockDecodedCredential = {
471+
'@context': ['https://www.w3.org/ns/credentials/v2'],
472+
type: ['VerifiableCredential'],
473+
issuer: 'did:web:uncefact.github.io',
474+
credentialSubject: { id: 'did:example:123', name: 'John Doe' }
475+
};
476+
477+
beforeEach(() => {
478+
(decodeJwt as jest.Mock).mockReset();
479+
});
480+
481+
it('should decode enveloped credential using jose', async () => {
482+
const service = new VerifiableCredentialService(mockAPIUrl);
483+
484+
(decodeJwt as jest.Mock).mockReturnValueOnce(mockDecodedCredential);
485+
486+
const result = await service.decode(mockEnvelopedCredential);
487+
488+
expect(decodeJwt).toHaveBeenCalledWith(
489+
'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6d2ViOnVuY2VmYWN0LmdpdGh1Yi5pbyIsInN1YiI6ImRpZDpleGFtcGxlOjEyMyJ9.signature'
490+
);
491+
expect(result).toEqual(mockDecodedCredential);
492+
});
493+
494+
it('should throw error when credential is not provided', async () => {
495+
const service = new VerifiableCredentialService(mockAPIUrl);
496+
497+
await expect(service.decode(null as any)).rejects.toThrow('Error decoding VC. Credential is required.');
498+
expect(decodeJwt).not.toHaveBeenCalled();
499+
});
500+
501+
it('should throw error when credential type is not EnvelopedVerifiableCredential', async () => {
502+
const service = new VerifiableCredentialService(mockAPIUrl);
503+
const invalidCredential = {
504+
'@context': ['https://www.w3.org/ns/credentials/v2'],
505+
type: 'VerifiableCredential',
506+
issuer: 'did:example:123',
507+
credentialSubject: { id: 'did:example:456' }
508+
} as any;
509+
510+
await expect(service.decode(invalidCredential)).rejects.toThrow('Failed to decode verifiable credential: Credential is not an EnvelopedVerifiableCredential');
511+
});
512+
513+
it('should throw error when credential id is missing encoded data', async () => {
514+
const service = new VerifiableCredentialService(mockAPIUrl);
515+
const invalidCredential: EnvelopedVerifiableCredential = {
516+
'@context': ['https://www.w3.org/ns/credentials/v2'],
517+
type: 'EnvelopedVerifiableCredential',
518+
id: 'data:application/vc-ld+jwt',
519+
issuer: 'did:example:123',
520+
credentialSubject: { id: 'did:example:456' }
521+
};
522+
523+
await expect(service.decode(invalidCredential)).rejects.toThrow('Failed to decode verifiable credential: Invalid enveloped credential format: missing encoded data');
524+
});
525+
526+
it('should throw error when credential id is undefined', async () => {
527+
const service = new VerifiableCredentialService(mockAPIUrl);
528+
const invalidCredential: EnvelopedVerifiableCredential = {
529+
'@context': ['https://www.w3.org/ns/credentials/v2'],
530+
type: 'EnvelopedVerifiableCredential',
531+
issuer: 'did:example:123',
532+
credentialSubject: { id: 'did:example:456' }
533+
};
534+
535+
await expect(service.decode(invalidCredential)).rejects.toThrow('Failed to decode verifiable credential: Invalid enveloped credential format: missing encoded data');
536+
});
537+
538+
it('should handle decodeJwt errors', async () => {
539+
const service = new VerifiableCredentialService(mockAPIUrl);
540+
541+
(decodeJwt as jest.Mock).mockImplementationOnce(() => {
542+
throw new Error('Invalid JWT format');
543+
});
544+
545+
await expect(service.decode(mockEnvelopedCredential)).rejects.toThrow('Failed to decode verifiable credential: Invalid JWT format');
546+
});
547+
548+
it('should handle unknown errors during decoding', async () => {
549+
const service = new VerifiableCredentialService(mockAPIUrl);
550+
551+
(decodeJwt as jest.Mock).mockImplementationOnce(() => {
552+
throw 'Unknown error';
553+
});
554+
555+
await expect(service.decode(mockEnvelopedCredential)).rejects.toThrow('Failed to decode verifiable credential: Unknown error');
556+
});
370557
});
371558
})

0 commit comments

Comments
 (0)