diff --git a/apps/credential-manager/src/credential-offers/__tests__/credential-offers.service.spec.ts b/apps/credential-manager/src/credential-offers/__tests__/credential-offers.service.spec.ts index d568df68c66fb269a33d79f80df960f2e5a3df0d..06493aca2578b39fca79e2db0b3eacc58948fad0 100644 --- a/apps/credential-manager/src/credential-offers/__tests__/credential-offers.service.spec.ts +++ b/apps/credential-manager/src/credential-offers/__tests__/credential-offers.service.spec.ts @@ -1,9 +1,15 @@ import type { TestingModule } from '@nestjs/testing'; +import type { + EventDidcommAnonCredsCredentialsOfferInput, + EventDidcommAnonCredsCredentialsOfferToSelfInput, +} from '@ocm/shared'; import { Test } from '@nestjs/testing'; import { EventAnonCredsCredentialOfferGetAll, EventAnonCredsCredentialOfferGetById, + EventDidcommAnonCredsCredentialsOffer, + EventDidcommAnonCredsCredentialsOfferToSelf, } from '@ocm/shared'; import { Subject, of, takeUntil } from 'rxjs'; @@ -88,4 +94,85 @@ describe('CredentialOffersService', () => { }); }); }); + + describe('createCredentialOffer', () => { + it('should call natsClient.send with the correct arguments', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'tenantId'; + const connectionId = 'connectionId'; + const credentialDefinitionId = 'credentialDefinitionId'; + const attributes: EventDidcommAnonCredsCredentialsOfferInput['attributes'] = + []; + const expectedResult = + {} as EventDidcommAnonCredsCredentialsOffer['data']; + + natsClientMock.send.mockReturnValueOnce( + of(new EventDidcommAnonCredsCredentialsOffer(expectedResult, tenantId)), + ); + + service + .offer(tenantId, connectionId, credentialDefinitionId, attributes) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(natsClientMock.send).toHaveBeenCalledWith( + EventDidcommAnonCredsCredentialsOffer.token, + { + tenantId, + connectionId, + credentialDefinitionId, + attributes, + }, + ); + + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); + + describe('createCredentialOfferToSelf', () => { + it('should call natsClient.send with the correct arguments', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'tenantId'; + const credentialDefinitionId = 'credentialDefinitionId'; + const attributes: EventDidcommAnonCredsCredentialsOfferToSelfInput['attributes'] = + []; + const expectedResult = + {} as EventDidcommAnonCredsCredentialsOfferToSelf['data']; + + natsClientMock.send.mockReturnValueOnce( + of( + new EventDidcommAnonCredsCredentialsOfferToSelf( + expectedResult, + tenantId, + ), + ), + ); + + service + .offerToSelf(tenantId, credentialDefinitionId, attributes) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(natsClientMock.send).toHaveBeenCalledWith( + EventDidcommAnonCredsCredentialsOfferToSelf.token, + { + tenantId, + credentialDefinitionId, + attributes, + }, + ); + + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); }); diff --git a/apps/credential-manager/src/credential-offers/credential-offers.controller.ts b/apps/credential-manager/src/credential-offers/credential-offers.controller.ts index 1bd6ae85733d36b6c1bd08a77a40e9b00b16ccf7..8a00d0165af0252e9cc36a7116c04d3be9114086 100644 --- a/apps/credential-manager/src/credential-offers/credential-offers.controller.ts +++ b/apps/credential-manager/src/credential-offers/credential-offers.controller.ts @@ -1,8 +1,10 @@ import { + Body, Controller, Get, HttpStatus, Param, + Post, Query, UseInterceptors, UsePipes, @@ -13,6 +15,7 @@ import { MultitenancyParams, ResponseFormatInterceptor } from '@ocm/shared'; import { CredentialOffersService } from './credential-offers.service.js'; import { GetByIdParams } from './dto/get-by-id.dto.js'; +import { OfferPayload } from './dto/offer.dto.js'; @Controller() @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) @@ -168,4 +171,178 @@ export class CredentialOffersController { ) { return this.service.getCredentialOfferById(tenantId, credentialOfferId); } + + @Post() + @ApiOperation({ + summary: 'Create a credential offer', + description: + 'This call creates a credential offer for a given connection ID and credential definition ID', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Credential offer created successfully', + content: { + 'application/json': { + schema: {}, + examples: { + 'Credential offer created successfully': { + value: { + statusCode: 200, + message: 'Credential offer created successfully', + data: { + id: '71b784a3', + }, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + content: { + 'application/json': { + schema: {}, + examples: { + 'Credential offer not found': { + value: { + statusCode: 404, + message: 'Credential offer not found', + data: null, + }, + }, + 'Tenant not found': { + value: { + statusCode: 404, + message: 'Tenant not found', + data: null, + }, + }, + 'Credential definition not found': { + value: { + statusCode: 404, + message: 'Credential definition not found', + data: null, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: 'Something went wrong', + content: { + 'application/json': { + schema: {}, + examples: { + 'Something went wrong': { + value: { + statusCode: 500, + message: 'Something went wrong', + error: 'Internal Server Error', + }, + }, + }, + }, + }, + }) + public offer( + @Query() { tenantId }: MultitenancyParams, + @Body() { connectionId, credentialDefinitionId, attributes }: OfferPayload, + ) { + return this.service.offer( + tenantId, + connectionId, + credentialDefinitionId, + attributes, + ); + } + + @Post('self') + @ApiOperation({ + summary: 'Create a credential offer to self', + description: + 'This call creates a credential offer for a given credential definition ID', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Credential offer created successfully', + content: { + 'application/json': { + schema: {}, + examples: { + 'Credential offer created successfully': { + value: { + statusCode: 200, + message: 'Credential offer created successfully', + data: { + id: '71b784a3', + }, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + content: { + 'application/json': { + schema: {}, + examples: { + 'Credential offer not found': { + value: { + statusCode: 404, + message: 'Credential offer not found', + data: null, + }, + }, + 'Tenant not found': { + value: { + statusCode: 404, + message: 'Tenant not found', + data: null, + }, + }, + 'Credential definition not found': { + value: { + statusCode: 404, + message: 'Credential definition not found', + data: null, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: 'Something went wrong', + content: { + 'application/json': { + schema: {}, + examples: { + 'Something went wrong': { + value: { + statusCode: 500, + message: 'Something went wrong', + error: 'Internal Server Error', + }, + }, + }, + }, + }, + }) + public offerToSelf( + @Query() { tenantId }: MultitenancyParams, + @Body() + { credentialDefinitionId, attributes }: Omit<OfferPayload, 'connectionId'>, + ) { + return this.service.offerToSelf( + tenantId, + credentialDefinitionId, + attributes, + ); + } } diff --git a/apps/credential-manager/src/credential-offers/credential-offers.service.ts b/apps/credential-manager/src/credential-offers/credential-offers.service.ts index 30c3ff6903b58a8134f532607515d3f7b9f094f2..6a5426067b428a878a3ba5d89035175f857f8ff0 100644 --- a/apps/credential-manager/src/credential-offers/credential-offers.service.ts +++ b/apps/credential-manager/src/credential-offers/credential-offers.service.ts @@ -1,11 +1,16 @@ import type { EventAnonCredsCredentialOfferGetAllInput, EventAnonCredsCredentialOfferGetByIdInput, + EventDidcommAnonCredsCredentialsOfferInput, + EventDidcommAnonCredsCredentialsOfferToSelfInput, } from '@ocm/shared'; +import type { Observable } from 'rxjs'; import { Inject, Injectable } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; import { + EventDidcommAnonCredsCredentialsOffer, + EventDidcommAnonCredsCredentialsOfferToSelf, EventAnonCredsCredentialOfferGetAll, EventAnonCredsCredentialOfferGetById, } from '@ocm/shared'; @@ -39,4 +44,40 @@ export class CredentialOffersService { }) .pipe(map(({ data }) => data)); } + + public offer( + tenantId: string, + connectionId: string, + credentialDefinitionId: string, + attributes: EventDidcommAnonCredsCredentialsOfferInput['attributes'], + ): Observable<EventDidcommAnonCredsCredentialsOffer['data']> { + return this.natsClient + .send< + EventDidcommAnonCredsCredentialsOffer, + EventDidcommAnonCredsCredentialsOfferInput + >(EventDidcommAnonCredsCredentialsOffer.token, { + tenantId, + connectionId, + credentialDefinitionId, + attributes, + }) + .pipe(map(({ data }) => data)); + } + + public offerToSelf( + tenantId: string, + credentialDefinitionId: string, + attributes: EventDidcommAnonCredsCredentialsOfferToSelfInput['attributes'], + ): Observable<EventDidcommAnonCredsCredentialsOfferToSelf['data']> { + return this.natsClient + .send< + EventDidcommAnonCredsCredentialsOfferToSelf, + EventDidcommAnonCredsCredentialsOfferToSelfInput + >(EventDidcommAnonCredsCredentialsOfferToSelf.token, { + tenantId, + credentialDefinitionId, + attributes, + }) + .pipe(map(({ data }) => data)); + } } diff --git a/apps/credential-manager/src/credential-offers/dto/offer.dto.ts b/apps/credential-manager/src/credential-offers/dto/offer.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..618ac356f7fa9fac4ac0fef9e4ea3bf237a4237e --- /dev/null +++ b/apps/credential-manager/src/credential-offers/dto/offer.dto.ts @@ -0,0 +1,9 @@ +export class OfferPayload { + public connectionId: string; + public credentialDefinitionId: string; + public attributes: Array<{ + name: string; + value: string; + mimeType?: string; + }>; +}