|
1 | 1 | import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' |
2 | 2 | import { createMock } from '@golevelup/ts-vitest' |
3 | | -import { UnprocessableEntityException } from '@nestjs/common' |
| 3 | +import { BadRequestException, UnprocessableEntityException } from '@nestjs/common' |
4 | 4 | import { ConfigType } from '@nestjs/config' |
5 | 5 |
|
6 | 6 | import { TenantAgent } from 'common/agent' |
@@ -348,6 +348,15 @@ describe('OpenId4VcIssuanceSessionService', () => { |
348 | 348 | ) |
349 | 349 | expect(result.credentialOffer).toBe('openid-credential-offer://jwt') |
350 | 350 | expect(statusListService.addItems).toHaveBeenCalledWith(authInfo, 'sl-1', [5]) |
| 351 | + // The index reserved in the status list must be the one the issued credential carries, |
| 352 | + // otherwise the credential points at a bit that is never set on revocation. |
| 353 | + expect(tenantAgent.openid4vc.issuer.createCredentialOffer).toHaveBeenCalledWith( |
| 354 | + expect.objectContaining({ |
| 355 | + issuanceMetadata: expect.objectContaining({ |
| 356 | + credentials: [expect.objectContaining({ credentialStatus: expect.objectContaining({ index: 5 }) })], |
| 357 | + }), |
| 358 | + }), |
| 359 | + ) |
351 | 360 | }) |
352 | 361 |
|
353 | 362 | test('should create issuance session for JwtVcJsonLd format WITH credentialStatus', async () => { |
@@ -402,6 +411,13 @@ describe('OpenId4VcIssuanceSessionService', () => { |
402 | 411 | await service.offer(authInfo, tenantAgent, req) |
403 | 412 |
|
404 | 413 | expect(statusListService.addItems).toHaveBeenCalledWith(authInfo, 'sl-2', [10]) |
| 414 | + expect(tenantAgent.openid4vc.issuer.createCredentialOffer).toHaveBeenCalledWith( |
| 415 | + expect.objectContaining({ |
| 416 | + issuanceMetadata: expect.objectContaining({ |
| 417 | + credentials: [expect.objectContaining({ credentialStatus: expect.objectContaining({ index: 10 }) })], |
| 418 | + }), |
| 419 | + }), |
| 420 | + ) |
405 | 421 | }) |
406 | 422 |
|
407 | 423 | test('should create issuance session for LdpVc format WITH credentialStatus', async () => { |
@@ -456,6 +472,61 @@ describe('OpenId4VcIssuanceSessionService', () => { |
456 | 472 | await service.offer(authInfo, tenantAgent, req) |
457 | 473 |
|
458 | 474 | expect(statusListService.addItems).toHaveBeenCalledWith(authInfo, 'sl-3', [0]) |
| 475 | + expect(tenantAgent.openid4vc.issuer.createCredentialOffer).toHaveBeenCalledWith( |
| 476 | + expect.objectContaining({ |
| 477 | + issuanceMetadata: expect.objectContaining({ |
| 478 | + credentials: [expect.objectContaining({ credentialStatus: expect.objectContaining({ index: 0 }) })], |
| 479 | + }), |
| 480 | + }), |
| 481 | + ) |
| 482 | + }) |
| 483 | + |
| 484 | + test('should reject an over-capacity batch before creating the credential offer', async () => { |
| 485 | + const mockIssuer = issuerRecordStub({ |
| 486 | + issuerId: 'issuer-1', |
| 487 | + credentialConfigurationsSupported: { |
| 488 | + 'cred-jwt-1': { |
| 489 | + format: 'jwt_vc_json', |
| 490 | + credential_definition: { type: ['VerifiableCredential'] }, |
| 491 | + }, |
| 492 | + }, |
| 493 | + }) |
| 494 | + |
| 495 | + vi.mocked(tenantAgent.openid4vc.issuer.getIssuerByIssuerId).mockResolvedValue(mockIssuer) |
| 496 | + // One free slot (lastIndex 99 of size 100) but two revocable credentials requested |
| 497 | + vi.mocked(statusListService.getOrCreate).mockResolvedValue({ id: 'sl-4', lastIndex: 99, size: 100 } as any) |
| 498 | + vi.mocked(statusListService.location).mockReturnValue('https://example.com/status-lists/sl-4') |
| 499 | + vi.mocked(tenantAgent.dids.resolve).mockResolvedValue( |
| 500 | + didResolutionResultStub({ |
| 501 | + didDocument: { verificationMethod: [{ id: 'did:key:z6MkJwt#key-1' }] }, |
| 502 | + }), |
| 503 | + ) |
| 504 | + vi.mocked(statusListService.assertHasFreeIndexes).mockImplementation(() => { |
| 505 | + throw new BadRequestException('Status list does not have enough free indexes') |
| 506 | + }) |
| 507 | + |
| 508 | + const req = { |
| 509 | + publicIssuerId: 'issuer-1', |
| 510 | + credentials: [ |
| 511 | + { |
| 512 | + credentialSupportedId: 'cred-jwt-1', |
| 513 | + format: OpenId4VciCredentialFormatProfile.JwtVcJson, |
| 514 | + issuer: { did: 'did:key:z6MkJwt' }, |
| 515 | + }, |
| 516 | + { |
| 517 | + credentialSupportedId: 'cred-jwt-1', |
| 518 | + format: OpenId4VciCredentialFormatProfile.JwtVcJson, |
| 519 | + issuer: { did: 'did:key:z6MkJwt' }, |
| 520 | + }, |
| 521 | + ], |
| 522 | + baseUri: 'https://example.com', |
| 523 | + } as any |
| 524 | + |
| 525 | + await expect(service.offer(authInfo, tenantAgent, req)).rejects.toThrow(BadRequestException) |
| 526 | + |
| 527 | + // The point of the preflight: nothing irreversible may happen once capacity is known to be short |
| 528 | + expect(tenantAgent.openid4vc.issuer.createCredentialOffer).not.toHaveBeenCalled() |
| 529 | + expect(statusListService.addItems).not.toHaveBeenCalled() |
459 | 530 | }) |
460 | 531 |
|
461 | 532 | test('should create issuance session for MsoMdoc format without DID resolution or credentialStatus', async () => { |
|
0 commit comments