diff --git a/apps/schema-manager/src/credential-definitions/__tests__/credential-definitions.controller.spec.ts b/apps/schema-manager/src/credential-definitions/__tests__/credential-definitions.controller.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3980056858bf8cea92149e54e81d3bc8b6f1b694 --- /dev/null +++ b/apps/schema-manager/src/credential-definitions/__tests__/credential-definitions.controller.spec.ts @@ -0,0 +1,138 @@ +import type { CreateCredentialDefinitionPayload } from '../dto/create-credential-definition.dto.js'; +import type { TestingModule } from '@nestjs/testing'; +import type { + EventAnonCredsCredentialDefinitionsGetAll, + EventAnonCredsCredentialDefinitionsGetById, + EventAnonCredsCredentialDefinitionsRegister, +} from '@ocm/shared'; + +import { Test } from '@nestjs/testing'; +import { Subject, of, takeUntil } from 'rxjs'; + +import { NATS_CLIENT } from '../../common/constants.js'; +import { CredentialDefinitionsController } from '../credential-definitions.controller.js'; +import { CredentialDefinitionsService } from '../credential-definitions.service.js'; + +describe('CredentialDefinitionsController', () => { + const natsClientMock = {}; + + let controller: CredentialDefinitionsController; + let service: CredentialDefinitionsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [CredentialDefinitionsController], + providers: [ + { provide: NATS_CLIENT, useValue: natsClientMock }, + CredentialDefinitionsService, + ], + }).compile(); + + controller = module.get<CredentialDefinitionsController>( + CredentialDefinitionsController, + ); + service = module.get<CredentialDefinitionsService>( + CredentialDefinitionsService, + ); + }); + + describe('find', () => { + it('should return a list of credential definitions', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'exampleTenantId'; + const expectedResult: EventAnonCredsCredentialDefinitionsGetAll['data'] = + []; + + jest + .spyOn(service, 'findCredentialDefinitions') + .mockReturnValueOnce(of(expectedResult)); + + controller + .find({ tenantId }) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); + + describe('get', () => { + it('should return a credential definition', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'exampleTenantId'; + const credentialDefinitionId = 'exampleCredentialDefinitionId'; + const expectedResult: EventAnonCredsCredentialDefinitionsGetById['data'] = + { + credentialDefinitionId: 'exampleCredentialDefinitionId', + issuerId: 'exampleIssuerId', + schemaId: 'exampleSchemaId', + tag: 'exampleTag', + type: 'CL', + value: { + primary: {}, + revocation: {}, + }, + }; + + jest + .spyOn(service, 'getCredentialDefinitionById') + .mockReturnValueOnce(of(expectedResult)); + + controller + .get({ tenantId }, credentialDefinitionId) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); + + describe('register', () => { + it('should return a credential definition', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'exampleTenantId'; + const payload: CreateCredentialDefinitionPayload = { + schemaId: 'exampleSchemaId', + tag: 'exampleTag', + }; + const expectedResult: EventAnonCredsCredentialDefinitionsRegister['data'] = + { + credentialDefinitionId: 'exampleCredentialDefinitionId', + issuerId: 'exampleIssuerId', + schemaId: 'exampleSchemaId', + tag: 'exampleTag', + type: 'CL', + value: { + primary: {}, + revocation: {}, + }, + }; + + jest + .spyOn(service, 'registerCredentialDefinition') + .mockReturnValueOnce(of(expectedResult)); + + controller + .register({ tenantId }, payload) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); +}); diff --git a/apps/schema-manager/src/credential-definitions/__tests__/credential-definitions.module.spec.ts b/apps/schema-manager/src/credential-definitions/__tests__/credential-definitions.module.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..2df90a796c49d0c0f523616582fee65a517091a3 --- /dev/null +++ b/apps/schema-manager/src/credential-definitions/__tests__/credential-definitions.module.spec.ts @@ -0,0 +1,33 @@ +import { ClientsModule } from '@nestjs/microservices'; +import { Test } from '@nestjs/testing'; + +import { NATS_CLIENT } from '../../common/constants.js'; +import { CredentialDefinitionsController } from '../credential-definitions.controller.js'; +import { CredentialDefinitionsModule } from '../credential-definitions.module.js'; +import { CredentialDefinitionsService } from '../credential-definitions.service.js'; + +describe('CredentialDefinitionsModule', () => { + let credentialDefinitionsModule: CredentialDefinitionsModule; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ClientsModule.registerAsync({ + isGlobal: true, + clients: [{ name: NATS_CLIENT, useFactory: () => ({}) }], + }), + CredentialDefinitionsModule, + ], + controllers: [CredentialDefinitionsController], + providers: [CredentialDefinitionsService], + }).compile(); + + credentialDefinitionsModule = moduleRef.get<CredentialDefinitionsModule>( + CredentialDefinitionsModule, + ); + }); + + it('should be defined', () => { + expect(credentialDefinitionsModule).toBeDefined(); + }); +}); diff --git a/apps/schema-manager/src/credential-definitions/__tests__/credential-definitions.service.spec.ts b/apps/schema-manager/src/credential-definitions/__tests__/credential-definitions.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..be86f4634ad8dedd0775b491f22a2260cfe054c0 --- /dev/null +++ b/apps/schema-manager/src/credential-definitions/__tests__/credential-definitions.service.spec.ts @@ -0,0 +1,152 @@ +import type { TestingModule } from '@nestjs/testing'; + +import { Test } from '@nestjs/testing'; +import { + EventAnonCredsCredentialDefinitionsGetAll, + EventAnonCredsCredentialDefinitionsGetById, + EventAnonCredsCredentialDefinitionsRegister, +} from '@ocm/shared'; +import { Subject, of, takeUntil } from 'rxjs'; + +import { NATS_CLIENT } from '../../common/constants.js'; +import { CredentialDefinitionsService } from '../credential-definitions.service.js'; + +describe('CredentialDefinitionsService', () => { + let service: CredentialDefinitionsService; + const natsClientMock = { send: jest.fn() }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { provide: NATS_CLIENT, useValue: natsClientMock }, + CredentialDefinitionsService, + ], + }).compile(); + + service = module.get<CredentialDefinitionsService>( + CredentialDefinitionsService, + ); + + jest.resetAllMocks(); + }); + + describe('findCredentialDefinitions', () => { + it('should call natsClient.send with the correct pattern and payload', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'testTenantId'; + const expectedResult: EventAnonCredsCredentialDefinitionsGetAll['data'] = + []; + + natsClientMock.send.mockReturnValueOnce( + of(new EventAnonCredsCredentialDefinitionsGetAll([], tenantId)), + ); + + service + .findCredentialDefinitions(tenantId) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(natsClientMock.send).toHaveBeenCalledWith( + EventAnonCredsCredentialDefinitionsGetAll.token, + { tenantId }, + ); + + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); + + describe('getCredentialDefinitionById', () => { + it('should call natsClient.send with the correct pattern and payload', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'testTenantId'; + const credentialDefinitionId = 'testCredentialDefinitionId'; + const expectedResult: EventAnonCredsCredentialDefinitionsGetById['data'] = + { + credentialDefinitionId: 'testCredentialDefinitionId', + issuerId: 'testIssuerId', + schemaId: 'testSchemaId', + tag: 'testTag', + type: 'CL', + value: { + primary: {}, + revocation: {}, + }, + }; + + natsClientMock.send.mockReturnValueOnce( + of( + new EventAnonCredsCredentialDefinitionsGetById( + expectedResult, + tenantId, + ), + ), + ); + + service + .getCredentialDefinitionById(tenantId, credentialDefinitionId) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(natsClientMock.send).toHaveBeenCalledWith( + EventAnonCredsCredentialDefinitionsGetById.token, + { tenantId, credentialDefinitionId }, + ); + + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); + + describe('createCredentialDefinition', () => { + it('should call natsClient.send with the correct pattern and payload', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'testTenantId'; + const payload = { test: 'payload' }; + const expectedResult: EventAnonCredsCredentialDefinitionsRegister['data'] = + { + credentialDefinitionId: 'testCredentialDefinitionId', + issuerId: 'testIssuerId', + schemaId: 'testSchemaId', + tag: 'testTag', + type: 'CL', + value: { + primary: {}, + revocation: {}, + }, + }; + + natsClientMock.send.mockReturnValueOnce( + of( + new EventAnonCredsCredentialDefinitionsRegister( + expectedResult, + tenantId, + ), + ), + ); + + service + .registerCredentialDefinition(tenantId, payload) + .pipe(takeUntil(unsubscribe$)) + .subscribe(() => { + expect(natsClientMock.send).toHaveBeenCalledWith( + EventAnonCredsCredentialDefinitionsRegister.token, + { tenantId, payload }, + ); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); +}); diff --git a/apps/schema-manager/src/credential-definitions/credential-definitions.controller.ts b/apps/schema-manager/src/credential-definitions/credential-definitions.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..099cb60782fdf8749ed7b15d7c24df351e262941 --- /dev/null +++ b/apps/schema-manager/src/credential-definitions/credential-definitions.controller.ts @@ -0,0 +1,292 @@ +import { + Body, + Controller, + Get, + HttpStatus, + Param, + Post, + Query, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { MultitenancyParams } from '@ocm/shared'; + +import { ResponseFormatInterceptor } from '../common/response-format.interceptor.js'; + +import { CredentialDefinitionsService } from './credential-definitions.service.js'; +import { CreateCredentialDefinitionPayload } from './dto/create-credential-definition.dto.js'; + +@Controller() +@UsePipes(new ValidationPipe({ transform: true, whitelist: true })) +@UseInterceptors(ResponseFormatInterceptor) +@ApiTags('Credential Definitions') +export class CredentialDefinitionsController { + public constructor(private readonly service: CredentialDefinitionsService) {} + + @Get() + @ApiOperation({ + summary: 'Fetch a list of credential definitions', + description: + 'This call provides a list of credential definitions for a given tenant', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Credential definitions fetched successfully', + content: { + 'application/json': { + schema: {}, + examples: { + 'Credential definitions fetched successfully': { + value: { + statusCode: 200, + message: 'Credential definitions fetched successfully', + data: [ + { + id: '71b784a3', + }, + ], + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Tenant not found', + content: { + 'application/json': { + schema: {}, + examples: { + 'Tenant not found': { + value: { + statusCode: 404, + message: 'Tenant not found', + error: 'Not Found', + }, + }, + }, + }, + }, + }) + @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', + error: 'Internal Server Error', + }, + }, + }, + }, + }, + }) + public find( + @Query() { tenantId }: MultitenancyParams, + ): ReturnType<CredentialDefinitionsService['findCredentialDefinitions']> { + return this.service.findCredentialDefinitions(tenantId); + } + + @Get(':credentialDefinitionId') + @ApiOperation({ + summary: 'Fetch a credential definition by ID', + description: + 'This call provides a credential definition for a given tenant', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Credential definition fetched successfully', + content: { + 'application/json': { + schema: {}, + examples: { + 'Credential definition fetched successfully': { + value: { + statusCode: 200, + message: 'Credential definition fetched successfully', + data: { + id: '71b784a3', + }, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Credential definition not found', + content: { + 'application/json': { + schema: {}, + examples: { + 'Credential definition not found': { + value: { + statusCode: 404, + message: 'Credential definition not found', + error: 'Not Found', + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Tenant not found', + content: { + 'application/json': { + schema: {}, + examples: { + 'Tenant not found': { + value: { + statusCode: 404, + message: 'Tenant not found', + error: 'Not Found', + }, + }, + }, + }, + }, + }) + @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', + error: 'Internal Server Error', + }, + }, + }, + }, + }, + }) + public get( + @Query() { tenantId }: MultitenancyParams, + @Param('credentialDefinitionId') credentialDefinitionId: string, + ): ReturnType<CredentialDefinitionsService['getCredentialDefinitionById']> { + return this.service.getCredentialDefinitionById( + tenantId, + credentialDefinitionId, + ); + } + + @Post() + @ApiOperation({ + summary: 'Create a credential definition', + description: + 'This call allows you to create a credential definition for a given tenant', + }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Credential definition created successfully', + content: { + 'application/json': { + schema: {}, + examples: { + 'Credential definition created successfully': { + value: { + statusCode: 201, + message: 'Credential definition created successfully', + data: { + id: '71b784a3', + }, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Tenant not found', + content: { + 'application/json': { + schema: {}, + examples: { + 'Tenant not found': { + value: { + statusCode: 404, + message: 'Tenant not found', + error: 'Not Found', + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid request', + content: { + 'application/json': { + schema: {}, + examples: { + 'Invalid request': { + value: { + statusCode: 400, + message: 'Invalid request', + error: 'Bad Request', + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.CONFLICT, + description: 'Credential definition already exists', + content: { + 'application/json': { + schema: {}, + examples: { + 'Credential definition already exists': { + value: { + statusCode: 409, + message: 'Credential definition already exists', + error: 'Conflict', + }, + }, + }, + }, + }, + }) + @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', + error: 'Internal Server Error', + }, + }, + }, + }, + }, + }) + public register( + @Query() { tenantId }: MultitenancyParams, + @Body() payload: CreateCredentialDefinitionPayload, + ): ReturnType<CredentialDefinitionsService['registerCredentialDefinition']> { + return this.service.registerCredentialDefinition(tenantId, payload); + } +} diff --git a/apps/schema-manager/src/credential-definitions/credential-definitions.module.ts b/apps/schema-manager/src/credential-definitions/credential-definitions.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..e53667ea6eb6c1a5fa3ea1c23d73170666d66ac5 --- /dev/null +++ b/apps/schema-manager/src/credential-definitions/credential-definitions.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { CredentialDefinitionsController } from './credential-definitions.controller.js'; +import { CredentialDefinitionsService } from './credential-definitions.service.js'; + +@Module({ + providers: [CredentialDefinitionsService], + controllers: [CredentialDefinitionsController], +}) +export class CredentialDefinitionsModule {} diff --git a/apps/schema-manager/src/credential-definitions/credential-definitions.service.ts b/apps/schema-manager/src/credential-definitions/credential-definitions.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..a24b0dc8f98aabcb0c2d940892a824c042cd4798 --- /dev/null +++ b/apps/schema-manager/src/credential-definitions/credential-definitions.service.ts @@ -0,0 +1,55 @@ +import type { EventAnonCredsCredentialDefinitionsGetAllInput } from '@ocm/shared'; +import type { Observable } from 'rxjs'; + +import { Inject, Injectable } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { + EventAnonCredsCredentialDefinitionsGetAll, + EventAnonCredsCredentialDefinitionsGetById, + EventAnonCredsCredentialDefinitionsRegister, +} from '@ocm/shared'; +import { map } from 'rxjs'; + +import { NATS_CLIENT } from '../common/constants.js'; + +@Injectable() +export class CredentialDefinitionsService { + public constructor( + @Inject(NATS_CLIENT) private readonly natsClient: ClientProxy, + ) {} + + public findCredentialDefinitions( + tenantId: string, + ): Observable<EventAnonCredsCredentialDefinitionsGetAll['data']> { + return this.natsClient + .send< + EventAnonCredsCredentialDefinitionsGetAll, + EventAnonCredsCredentialDefinitionsGetAllInput + >(EventAnonCredsCredentialDefinitionsGetAll.token, { tenantId }) + .pipe(map((result) => result.data)); + } + + public getCredentialDefinitionById( + tenantId: string, + credentialDefinitionId: string, + ): Observable<EventAnonCredsCredentialDefinitionsGetById['data']> { + return this.natsClient + .send(EventAnonCredsCredentialDefinitionsGetById.token, { + tenantId, + credentialDefinitionId, + }) + .pipe(map((result) => result.data)); + } + + public registerCredentialDefinition( + tenantId: string, + payload: unknown, + ): Observable<EventAnonCredsCredentialDefinitionsRegister['data']> { + return this.natsClient + .send(EventAnonCredsCredentialDefinitionsRegister.token, { + tenantId, + payload, + }) + .pipe(map((result) => result.data)); + } +} diff --git a/apps/schema-manager/src/credential-definitions/dto/create-credential-definition.dto.ts b/apps/schema-manager/src/credential-definitions/dto/create-credential-definition.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..02c3612cf31ef82f23201347de2e87e1dec44bb4 --- /dev/null +++ b/apps/schema-manager/src/credential-definitions/dto/create-credential-definition.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class CreateCredentialDefinitionPayload { + @IsString() + @IsNotEmpty() + @ApiProperty() + public schemaId: string; + + @IsString() + @IsNotEmpty() + @ApiProperty() + public tag: string; +}