diff --git a/apps/credential-manager/src/application.ts b/apps/credential-manager/src/application.ts index c0ad214c9e168df74c7ff3f31a15253f0cbafa39..f60686bfdfb1359cffcd9c418d60ee6bd4459be6 100644 --- a/apps/credential-manager/src/application.ts +++ b/apps/credential-manager/src/application.ts @@ -12,6 +12,7 @@ import { natsConfig } from './config/nats.config.js'; import { ssiConfig } from './config/ssi.config.js'; import { validationSchema } from './config/validation.js'; import { CredentialOffersModule } from './credential-offers/credential-offers.module.js'; +import { CredentialRequestsModule } from './credential-requests/credential-requests.module.js'; @Module({ imports: [ @@ -59,10 +60,12 @@ import { CredentialOffersModule } from './credential-offers/credential-offers.mo }), CredentialOffersModule, + CredentialRequestsModule, RouterModule.register([ { module: HealthModule, path: '/health' }, { module: CredentialOffersModule, path: '/credential-offers' }, + { module: CredentialRequestsModule, path: '/credential-requests' }, ]), ], }) diff --git a/apps/credential-manager/src/credential-requests/__tests__/credential-requests.controller.spec.ts b/apps/credential-manager/src/credential-requests/__tests__/credential-requests.controller.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..302cc4058798f0e1bb4cb257d2d53d22b47aa914 --- /dev/null +++ b/apps/credential-manager/src/credential-requests/__tests__/credential-requests.controller.spec.ts @@ -0,0 +1,90 @@ +import type { TestingModule } from '@nestjs/testing'; +import type { + EventAnonCredsCredentialRequestGetAll, + EventAnonCredsCredentialRequestGetById, +} from '@ocm/shared'; + +import { Test } from '@nestjs/testing'; +import { Subject, of, takeUntil } from 'rxjs'; + +import { NATS_CLIENT } from '../../common/constants.js'; +import { CredentialRequestsController } from '../credential-requests.controller.js'; +import { CredentialRequestsService } from '../credential-requests.service.js'; + +describe('CredentialRequestsController', () => { + const natsClientMock = {}; + + let controller: CredentialRequestsController; + let service: CredentialRequestsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [CredentialRequestsController], + providers: [ + { provide: NATS_CLIENT, useValue: natsClientMock }, + CredentialRequestsService, + ], + }).compile(); + + controller = module.get<CredentialRequestsController>( + CredentialRequestsController, + ); + service = module.get<CredentialRequestsService>(CredentialRequestsService); + }); + + describe('find', () => { + it('should return a list of credential requests', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'exampleTenantId'; + const expectedResult: EventAnonCredsCredentialRequestGetAll['data'] = []; + + jest + .spyOn(service, 'findCredentialRequests') + .mockReturnValueOnce(of(expectedResult)); + + controller + .find({ tenantId }) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); + + describe('getById', () => { + it('should return a credential request', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'exampleTenantId'; + const credentialRequestId = 'exampleCredentialRequestId'; + const expectedResult: EventAnonCredsCredentialRequestGetById['data'] = { + blinded_ms: {}, + blinded_ms_correctness_proof: {}, + cred_def_id: 'cred_def_id', + nonce: 'nonce', + entropy: 'entropy', + prover_did: 'prover_did', + }; + + jest + .spyOn(service, 'getCredentialRequestById') + .mockReturnValueOnce(of(expectedResult)); + + controller + .getById({ credentialRequestId }, { tenantId }) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); +}); diff --git a/apps/credential-manager/src/credential-requests/__tests__/credential-requests.module.spec.ts b/apps/credential-manager/src/credential-requests/__tests__/credential-requests.module.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..7fcd088315f73bfcf6cfc81f4e3c895705b58092 --- /dev/null +++ b/apps/credential-manager/src/credential-requests/__tests__/credential-requests.module.spec.ts @@ -0,0 +1,41 @@ +import { ClientsModule } from '@nestjs/microservices'; +import { Test } from '@nestjs/testing'; + +import { NATS_CLIENT } from '../../common/constants.js'; +import { CredentialRequestsController } from '../credential-requests.controller.js'; +import { CredentialRequestsModule } from '../credential-requests.module.js'; +import { CredentialRequestsService } from '../credential-requests.service.js'; + +describe('CredentialRequestsModule', () => { + let credentialRequestsController: CredentialRequestsController; + let credentialRequestsService: CredentialRequestsService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ClientsModule.registerAsync({ + isGlobal: true, + clients: [{ name: NATS_CLIENT, useFactory: () => ({}) }], + }), + CredentialRequestsModule, + ], + }).compile(); + + credentialRequestsController = moduleRef.get<CredentialRequestsController>( + CredentialRequestsController, + ); + credentialRequestsService = moduleRef.get<CredentialRequestsService>( + CredentialRequestsService, + ); + }); + + it('should be defined', () => { + expect(credentialRequestsController).toBeDefined(); + expect(credentialRequestsController).toBeInstanceOf( + CredentialRequestsController, + ); + + expect(credentialRequestsService).toBeDefined(); + expect(credentialRequestsService).toBeInstanceOf(CredentialRequestsService); + }); +}); diff --git a/apps/credential-manager/src/credential-requests/__tests__/credential-requests.service.spec.ts b/apps/credential-manager/src/credential-requests/__tests__/credential-requests.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a9a8d844eb55d25548ccb150edac1d6b5a42dcbc --- /dev/null +++ b/apps/credential-manager/src/credential-requests/__tests__/credential-requests.service.spec.ts @@ -0,0 +1,83 @@ +import type { TestingModule } from '@nestjs/testing'; + +import { Test } from '@nestjs/testing'; +import { EventAnonCredsCredentialRequestGetAll } from '@ocm/shared'; +import { Subject, of, takeUntil } from 'rxjs'; + +import { NATS_CLIENT } from '../../common/constants.js'; +import { CredentialRequestsService } from '../credential-requests.service.js'; + +describe('CredentialRequestsService', () => { + const natsClientMock = { send: jest.fn() }; + let service: CredentialRequestsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { provide: NATS_CLIENT, useValue: natsClientMock }, + CredentialRequestsService, + ], + }).compile(); + + service = module.get<CredentialRequestsService>(CredentialRequestsService); + }); + + describe('findCredentialRequests', () => { + it('should call the natsClient send method with the correct arguments', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'tenantId'; + const expectedResult: EventAnonCredsCredentialRequestGetAll['data'] = []; + + natsClientMock.send.mockReturnValueOnce( + of(new EventAnonCredsCredentialRequestGetAll(expectedResult, tenantId)), + ); + + service + .findCredentialRequests(tenantId) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(natsClientMock.send).toHaveBeenCalledWith( + EventAnonCredsCredentialRequestGetAll.token, + { tenantId }, + ); + + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); + + describe('getCredentialRequestById', () => { + it('should call the natsClient send method with the correct arguments', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'tenantId'; + const credentialRequestId = 'credentialRequestId'; + const expectedResult: EventAnonCredsCredentialRequestGetAll['data'] = []; + + natsClientMock.send.mockReturnValueOnce( + of(new EventAnonCredsCredentialRequestGetAll(expectedResult, tenantId)), + ); + + service + .getCredentialRequestById(tenantId, credentialRequestId) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(natsClientMock.send).toHaveBeenCalledWith( + EventAnonCredsCredentialRequestGetAll.token, + { tenantId, credentialRequestId }, + ); + + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); +}); diff --git a/apps/credential-manager/src/credential-requests/credential-requests.controller.ts b/apps/credential-manager/src/credential-requests/credential-requests.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..99297fdf950fcc017bdb1330a356a6f6e07f8ee9 --- /dev/null +++ b/apps/credential-manager/src/credential-requests/credential-requests.controller.ts @@ -0,0 +1,186 @@ +import { + Controller, + Get, + HttpStatus, + Param, + Query, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { MultitenancyParams, ResponseFormatInterceptor } from '@ocm/shared'; + +import { CredentialRequestsService } from './credential-requests.service.js'; +import { GetByIdParams } from './dto/get-by-id.dto.js'; + +@Controller() +@UsePipes(new ValidationPipe({ transform: true, whitelist: true })) +@UseInterceptors(ResponseFormatInterceptor) +@ApiTags('Credential Requests') +export class CredentialRequestsController { + public constructor(private readonly service: CredentialRequestsService) {} + + @Get() + @ApiOperation({ + summary: 'Fetch a list of credential requests', + description: + 'This call provides a list of credential requests for a given tenant', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Credential requests fetched successfully', + content: { + 'application/json': { + schema: {}, + examples: { + 'Credential requests fetched successfully': { + value: { + statusCode: 200, + message: 'Credential requests fetched successfully', + data: [ + { + id: '71b784a3', + }, + ], + }, + }, + 'Tenant not found': { + value: { + statusCode: 404, + message: 'Tenant not found', + data: null, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + content: { + 'application/json': { + schema: {}, + examples: { + 'Credential request not found': { + value: { + statusCode: 404, + message: 'Credential request not found', + data: null, + }, + }, + 'Tenant not found': { + value: { + statusCode: 404, + message: 'Tenant not found', + data: null, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: 'Internal server error', + content: { + 'application/json': { + schema: {}, + examples: { + 'Internal server error': { + value: { + statusCode: 500, + message: 'Internal server error', + data: null, + }, + }, + }, + }, + }, + }) + public find(@Query() { tenantId }: MultitenancyParams) { + return this.service.findCredentialRequests(tenantId); + } + + @Get(':id') + @ApiOperation({ + summary: 'Fetch a credential request by id', + description: + 'This call provides a credential request for a given tenant by id', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Credential request fetched successfully', + content: { + 'application/json': { + schema: {}, + examples: { + 'Credential request fetched successfully': { + value: { + statusCode: 200, + message: 'Credential request fetched successfully', + data: { + id: '71b784a3', + }, + }, + }, + 'Tenant not found': { + value: { + statusCode: 404, + message: 'Tenant not found', + data: null, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + content: { + 'application/json': { + schema: {}, + examples: { + 'Credential request not found': { + value: { + statusCode: 404, + message: 'Credential request not found', + data: null, + }, + }, + 'Tenant not found': { + value: { + statusCode: 404, + message: 'Tenant not found', + data: null, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: 'Internal server error', + content: { + 'application/json': { + schema: {}, + examples: { + 'Internal server error': { + value: { + statusCode: 500, + message: 'Internal server error', + data: null, + }, + }, + }, + }, + }, + }) + public getById( + @Param() { credentialRequestId }: GetByIdParams, + @Query() { tenantId }: MultitenancyParams, + ) { + return this.service.getCredentialRequestById(tenantId, credentialRequestId); + } +} diff --git a/apps/credential-manager/src/credential-requests/credential-requests.module.ts b/apps/credential-manager/src/credential-requests/credential-requests.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..af559d79bd371d0d0b848ae77f27c89f087b8091 --- /dev/null +++ b/apps/credential-manager/src/credential-requests/credential-requests.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { CredentialRequestsController } from './credential-requests.controller.js'; +import { CredentialRequestsService } from './credential-requests.service.js'; + +@Module({ + providers: [CredentialRequestsService], + controllers: [CredentialRequestsController], +}) +export class CredentialRequestsModule {} diff --git a/apps/credential-manager/src/credential-requests/credential-requests.service.ts b/apps/credential-manager/src/credential-requests/credential-requests.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..81675508bb0135a679f2136b031a7bb2eba64d0f --- /dev/null +++ b/apps/credential-manager/src/credential-requests/credential-requests.service.ts @@ -0,0 +1,43 @@ +import type { + EventAnonCredsCredentialRequestGetAllInput, + EventAnonCredsCredentialRequestGetById, + EventAnonCredsCredentialRequestGetByIdInput, +} from '@ocm/shared'; + +import { Inject, Injectable } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { EventAnonCredsCredentialRequestGetAll } from '@ocm/shared'; +import { map } from 'rxjs'; + +import { NATS_CLIENT } from '../common/constants.js'; + +@Injectable() +export class CredentialRequestsService { + public constructor( + @Inject(NATS_CLIENT) private readonly natsClient: ClientProxy, + ) {} + + public findCredentialRequests(tenantId: string) { + return this.natsClient + .send< + EventAnonCredsCredentialRequestGetAll, + EventAnonCredsCredentialRequestGetAllInput + >(EventAnonCredsCredentialRequestGetAll.token, { tenantId }) + .pipe(map(({ data }) => data)); + } + + public getCredentialRequestById( + tenantId: string, + credentialRequestId: string, + ) { + return this.natsClient + .send< + EventAnonCredsCredentialRequestGetById, + EventAnonCredsCredentialRequestGetByIdInput + >(EventAnonCredsCredentialRequestGetAll.token, { + tenantId, + credentialRequestId, + }) + .pipe(map(({ data }) => data)); + } +} diff --git a/apps/credential-manager/src/credential-requests/dto/get-by-id.dto.ts b/apps/credential-manager/src/credential-requests/dto/get-by-id.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..9c3e323437fe7c0cfaffb4b295d30540f8581623 --- /dev/null +++ b/apps/credential-manager/src/credential-requests/dto/get-by-id.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class GetByIdParams { + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'The credential request ID to retrieve', + format: 'string', + }) + public credentialRequestId: string; +}