Skip to content
Snippets Groups Projects
Commit 617a7fa4 authored by Steffen Schulze's avatar Steffen Schulze
Browse files

Merge branch 'feat/credentials' into 'main'

Credentials

See merge request !35
parents 955494fd 04072dae
No related branches found
No related tags found
1 merge request!35Credentials
Pipeline #40085 passed
Showing
with 912 additions and 0 deletions
......@@ -13,6 +13,7 @@ import { natsConfig } from './config/nats.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';
import { CredentialsModule } from './credentials/credentials.module.js';
@Module({
imports: [
......@@ -68,11 +69,13 @@ import { CredentialRequestsModule } from './credential-requests/credential-reque
},
}),
CredentialsModule,
CredentialOffersModule,
CredentialRequestsModule,
RouterModule.register([
{ module: HealthModule, path: '/health' },
{ module: CredentialsModule, path: '/credentials' },
{ module: CredentialOffersModule, path: '/credential-offers' },
{ module: CredentialRequestsModule, path: '/credential-requests' },
]),
......
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();
});
});
});
});
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,
);
}
}
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));
}
}
export class OfferPayload {
public connectionId: string;
public credentialDefinitionId: string;
public attributes: Array<{
name: string;
value: string;
mimeType?: string;
}>;
}
import type {
EventAnonCredsCredentialsDeleteById,
EventAnonCredsCredentialsGetAll,
EventAnonCredsCredentialsGetById,
} from '@ocm/shared';
import { Test } from '@nestjs/testing';
import { Subject, of, takeUntil } from 'rxjs';
import { NATS_CLIENT } from '../../common/constants.js';
import { CredentialsController } from '../credentials.controller.js';
import { CredentialsService } from '../credentials.service.js';
describe('CredentialsController', () => {
const natsClientMock = {};
let controller: CredentialsController;
let service: CredentialsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CredentialsController],
providers: [
{ provide: NATS_CLIENT, useValue: natsClientMock },
CredentialsService,
],
}).compile();
controller = moduleRef.get<CredentialsController>(CredentialsController);
service = moduleRef.get<CredentialsService>(CredentialsService);
});
describe('find', () => {
it('should return a list of credentials', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'exampleTenantId';
const expectedResult: EventAnonCredsCredentialsGetAll['data'] = [];
jest
.spyOn(service, 'find')
.mockImplementationOnce(() => 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', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'exampleTenantId';
const credentialRecordId = 'exampleCredentialRecordId';
const expectedResult = {} as EventAnonCredsCredentialsGetById['data'];
jest
.spyOn(service, 'get')
.mockImplementationOnce(() => of(expectedResult));
controller
.get({ credentialRecordId }, { tenantId })
.pipe(takeUntil(unsubscribe$))
.subscribe((result) => {
expect(result).toStrictEqual(expectedResult);
unsubscribe$.next();
unsubscribe$.complete();
done();
});
});
});
describe('delete', () => {
it('should delete a credential', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'exampleTenantId';
const credentialRecordId = 'exampleCredentialRecordId';
const expectedResult = {} as EventAnonCredsCredentialsDeleteById['data'];
jest
.spyOn(service, 'delete')
.mockImplementationOnce(() => of(expectedResult));
controller
.delete({ credentialRecordId }, { tenantId })
.pipe(takeUntil(unsubscribe$))
.subscribe((result) => {
expect(result).toStrictEqual(expectedResult);
unsubscribe$.next();
unsubscribe$.complete();
done();
});
});
});
});
import { ClientsModule } from '@nestjs/microservices';
import { Test } from '@nestjs/testing';
import { NATS_CLIENT } from '../../common/constants.js';
import { CredentialsController } from '../credentials.controller.js';
import { CredentialsModule } from '../credentials.module.js';
import { CredentialsService } from '../credentials.service.js';
describe('CredentialsModule', () => {
let credentialsController: CredentialsController;
let credentialsService: CredentialsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
ClientsModule.registerAsync({
isGlobal: true,
clients: [{ name: NATS_CLIENT, useFactory: () => ({}) }],
}),
CredentialsModule,
],
}).compile();
credentialsController = moduleRef.get<CredentialsController>(
CredentialsController,
);
credentialsService = moduleRef.get<CredentialsService>(CredentialsService);
});
it('should be defined', () => {
expect(credentialsController).toBeDefined();
expect(credentialsController).toBeInstanceOf(CredentialsController);
expect(credentialsService).toBeDefined();
expect(credentialsService).toBeInstanceOf(CredentialsService);
});
});
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import {
EventAnonCredsCredentialsDeleteById,
EventAnonCredsCredentialsGetAll,
EventAnonCredsCredentialsGetById,
} from '@ocm/shared';
import { Subject, of, takeUntil } from 'rxjs';
import { NATS_CLIENT } from '../../common/constants.js';
import { CredentialsService } from '../credentials.service.js';
describe('CredentialsService', () => {
const natsClientMock = { send: jest.fn() };
let service: CredentialsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{ provide: NATS_CLIENT, useValue: natsClientMock },
CredentialsService,
],
}).compile();
service = module.get<CredentialsService>(CredentialsService);
});
describe('find', () => {
it('should call the natsClient send method with the correct arguments', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'tenantId';
const expectedResult: EventAnonCredsCredentialsGetAll['data'] = [];
natsClientMock.send.mockReturnValueOnce(
of(new EventAnonCredsCredentialsGetAll(expectedResult, tenantId)),
);
service
.find(tenantId)
.pipe(takeUntil(unsubscribe$))
.subscribe((result) => {
expect(natsClientMock.send).toHaveBeenCalledWith(
EventAnonCredsCredentialsGetAll.token,
{ tenantId },
);
expect(result).toStrictEqual(expectedResult);
unsubscribe$.next();
unsubscribe$.complete();
done();
});
});
});
describe('get', () => {
it('should call the natsClient send method with the correct arguments', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'tenantId';
const credentialRecordId = 'credentialRecordId';
const expectedResult = {} as EventAnonCredsCredentialsGetById['data'];
natsClientMock.send.mockReturnValueOnce(
of(new EventAnonCredsCredentialsGetById(expectedResult, tenantId)),
);
service
.get(tenantId, credentialRecordId)
.pipe(takeUntil(unsubscribe$))
.subscribe((result) => {
expect(natsClientMock.send).toHaveBeenCalledWith(
EventAnonCredsCredentialsGetById.token,
{ tenantId, credentialRecordId },
);
expect(result).toStrictEqual(expectedResult);
unsubscribe$.next();
unsubscribe$.complete();
done();
});
});
});
describe('delete', () => {
it('should call the natsClient send method with the correct arguments', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'tenantId';
const credentialRecordId = 'credentialRecordId';
const expectedResult = {} as EventAnonCredsCredentialsDeleteById['data'];
natsClientMock.send.mockReturnValueOnce(
of(new EventAnonCredsCredentialsDeleteById(expectedResult, tenantId)),
);
service
.delete(tenantId, credentialRecordId)
.pipe(takeUntil(unsubscribe$))
.subscribe((result) => {
expect(natsClientMock.send).toHaveBeenCalledWith(
EventAnonCredsCredentialsDeleteById.token,
{ tenantId, credentialRecordId },
);
expect(result).toStrictEqual(expectedResult);
unsubscribe$.next();
unsubscribe$.complete();
done();
});
});
});
});
import {
Controller,
Delete,
Get,
HttpStatus,
Param,
Query,
UseInterceptors,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { MultitenancyParams, ResponseFormatInterceptor } from '@ocm/shared';
import { CredentialsService } from './credentials.service.js';
import { DeleteParams } from './dto/delete.dto.js';
import { GetParams } from './dto/get.dto.js';
@Controller()
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
@UseInterceptors(ResponseFormatInterceptor)
@ApiTags('Credentials')
export class CredentialsController {
public constructor(private readonly service: CredentialsService) {}
@Get()
@ApiOperation({
summary: 'Fetch a list of credentials',
description: 'This call provides a list of credentials for a given tenant',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Credentials fetched successfully',
content: {
'application/json': {
schema: {},
examples: {
'Credentials fetched successfully': {
value: {
statusCode: 200,
message: 'Credentials fetched successfully',
data: [
{
id: '71b784a3',
},
],
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
content: {
'application/json': {
schema: {},
examples: {
'Tenant not found': {
value: {
statusCode: 404,
message: 'Tenant not found',
data: null,
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.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.find(tenantId);
}
@Get(':credentialId')
@ApiOperation({
summary: 'Fetch a credential',
description: 'This call provides a credential for a given tenant',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Credential fetched successfully',
content: {
'application/json': {
schema: {},
examples: {
'Credential fetched successfully': {
value: {
statusCode: 200,
message: 'Credential fetched successfully',
data: {
id: '71b784a3',
},
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
content: {
'application/json': {
schema: {},
examples: {
'Credential not found': {
value: {
statusCode: 404,
message: 'Credential not found',
data: null,
},
},
'Tenant not found': {
value: {
statusCode: 404,
message: 'Tenant not found',
data: null,
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
content: {
'application/json': {
schema: {},
examples: {
'Internal server error': {
value: {
statusCode: 500,
message: 'Internal server error',
data: null,
},
},
},
},
},
})
public get(
@Param() { credentialRecordId }: GetParams,
@Query() { tenantId }: MultitenancyParams,
) {
return this.service.get(tenantId, credentialRecordId);
}
@Delete(':credentialId')
@ApiOperation({
summary: 'Delete a credential',
description: 'This call deletes a credential for a given tenant',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Credential deleted successfully',
content: {
'application/json': {
schema: {},
examples: {
'Credential deleted successfully': {
value: {
statusCode: 200,
message: 'Credential deleted successfully',
data: null,
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
content: {
'application/json': {
schema: {},
examples: {
'Credential not found': {
value: {
statusCode: 404,
message: 'Credential not found',
data: null,
},
},
'Tenant not found': {
value: {
statusCode: 404,
message: 'Tenant not found',
data: null,
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
content: {
'application/json': {
schema: {},
examples: {
'Internal server error': {
value: {
statusCode: 500,
message: 'Internal server error',
data: null,
},
},
},
},
},
})
public delete(
@Param() { credentialRecordId }: DeleteParams,
@Query() { tenantId }: MultitenancyParams,
) {
return this.service.delete(tenantId, credentialRecordId);
}
}
import { Module } from '@nestjs/common';
import { CredentialsController } from './credentials.controller.js';
import { CredentialsService } from './credentials.service.js';
@Module({
providers: [CredentialsService],
controllers: [CredentialsController],
})
export class CredentialsModule {}
import type {
EventAnonCredsCredentialsDeleteByIdInput,
EventAnonCredsCredentialsGetAllInput,
EventAnonCredsCredentialsGetByIdInput,
} from '@ocm/shared';
import type { Observable } from 'rxjs';
import { Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import {
EventAnonCredsCredentialsDeleteById,
EventAnonCredsCredentialsGetAll,
EventAnonCredsCredentialsGetById,
} from '@ocm/shared';
import { map } from 'rxjs';
import { NATS_CLIENT } from '../common/constants.js';
@Injectable()
export class CredentialsService {
public constructor(
@Inject(NATS_CLIENT) private readonly natsClient: ClientProxy,
) {}
public find(
tenantId: string,
): Observable<EventAnonCredsCredentialsGetAll['data']> {
return this.natsClient
.send<
EventAnonCredsCredentialsGetAll,
EventAnonCredsCredentialsGetAllInput
>(EventAnonCredsCredentialsGetAll.token, {
tenantId,
})
.pipe(map(({ data }) => data));
}
public get(
tenantId: string,
credentialRecordId: string,
): Observable<EventAnonCredsCredentialsGetById['data']> {
return this.natsClient
.send<
EventAnonCredsCredentialsGetById,
EventAnonCredsCredentialsGetByIdInput
>(EventAnonCredsCredentialsGetById.token, {
tenantId,
credentialRecordId,
})
.pipe(map(({ data }) => data));
}
public delete(
tenantId: string,
credentialRecordId: string,
): Observable<EventAnonCredsCredentialsDeleteById['data']> {
return this.natsClient
.send<
EventAnonCredsCredentialsDeleteById,
EventAnonCredsCredentialsDeleteByIdInput
>(EventAnonCredsCredentialsDeleteById.token, {
tenantId,
credentialRecordId,
})
.pipe(map(({ data }) => data));
}
}
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class DeleteParams {
@IsString()
@IsNotEmpty()
@ApiProperty({
description: 'The credential record ID to delete',
format: 'string',
})
public credentialRecordId: string;
}
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class GetParams {
@IsString()
@IsNotEmpty()
@ApiProperty({
description: 'The credential record ID to retrieve',
format: 'string',
})
public credentialRecordId: string;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment