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

Merge branch 'feat/credential-offers' into 'main'

Credential offers API.

See merge request eclipse/xfsc/ocm/ocm-engine!18
parents c9021414 1e9a8c4b
No related branches found
No related tags found
No related merge requests found
Showing
with 514 additions and 1 deletion
...@@ -3,12 +3,15 @@ import type { ConfigType } from '@nestjs/config'; ...@@ -3,12 +3,15 @@ import type { ConfigType } from '@nestjs/config';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { RouterModule } from '@nestjs/core'; import { RouterModule } from '@nestjs/core';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { HealthModule } from '@ocm/shared'; import { HealthModule } from '@ocm/shared';
import { NATS_CLIENT } from './common/constants.js';
import { httpConfig } from './config/http.config.js'; import { httpConfig } from './config/http.config.js';
import { natsConfig } from './config/nats.config.js'; import { natsConfig } from './config/nats.config.js';
import { ssiConfig } from './config/ssi.config.js'; import { ssiConfig } from './config/ssi.config.js';
import { validationSchema } from './config/validation.js'; import { validationSchema } from './config/validation.js';
import { CredentialOffersModule } from './credential-offers/credential-offers.module.js';
@Module({ @Module({
imports: [ imports: [
...@@ -24,6 +27,22 @@ import { validationSchema } from './config/validation.js'; ...@@ -24,6 +27,22 @@ import { validationSchema } from './config/validation.js';
}, },
}), }),
ClientsModule.registerAsync({
isGlobal: true,
clients: [
{
name: NATS_CLIENT,
inject: [natsConfig.KEY],
useFactory: (config: ConfigType<typeof natsConfig>) => ({
transport: Transport.NATS,
options: {
url: config.url as string,
},
}),
},
],
}),
HealthModule.registerAsync({ HealthModule.registerAsync({
inject: [natsConfig.KEY], inject: [natsConfig.KEY],
useFactory: (config: ConfigType<typeof natsConfig>) => { useFactory: (config: ConfigType<typeof natsConfig>) => {
...@@ -39,7 +58,12 @@ import { validationSchema } from './config/validation.js'; ...@@ -39,7 +58,12 @@ import { validationSchema } from './config/validation.js';
}, },
}), }),
RouterModule.register([{ module: HealthModule, path: '/health' }]), CredentialOffersModule,
RouterModule.register([
{ module: HealthModule, path: '/health' },
{ module: CredentialOffersModule, path: '/credential-offers' },
]),
], ],
}) })
export class Application {} export class Application {}
export const SERVICE_NAME = 'CREDENTIAL_MANAGER_SERVICE'; export const SERVICE_NAME = 'CREDENTIAL_MANAGER_SERVICE';
export const NATS_CLIENT = Symbol('NATS_CLIENT');
import type { TestingModule } from '@nestjs/testing';
import type {
EventAnonCredsCredentialOfferGetAll,
EventAnonCredsCredentialOfferGetById,
} from '@ocm/shared';
import { Test } from '@nestjs/testing';
import { Subject, of, takeUntil } from 'rxjs';
import { NATS_CLIENT } from '../../common/constants.js';
import { CredentialOffersController } from '../credential-offers.controller.js';
import { CredentialOffersService } from '../credential-offers.service.js';
describe('CredentialOffersController', () => {
const natsClientMock = {};
let controller: CredentialOffersController;
let service: CredentialOffersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CredentialOffersController],
providers: [
{ provide: NATS_CLIENT, useValue: natsClientMock },
CredentialOffersService,
],
}).compile();
controller = module.get<CredentialOffersController>(
CredentialOffersController,
);
service = module.get<CredentialOffersService>(CredentialOffersService);
});
describe('find', () => {
it('should return a list of credential offers', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'exampleTenantId';
const expectedResult: EventAnonCredsCredentialOfferGetAll['data'] = [];
jest
.spyOn(service, 'findCredentialOffers')
.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 offer', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'exampleTenantId';
const credentialOfferId = 'exampleCredentialOfferId';
const expectedResult: EventAnonCredsCredentialOfferGetById['data'] = {
cred_def_id: 'exampleCredDefId',
key_correctness_proof: {},
nonce: 'exampleNonce',
schema_id: 'exampleSchemaId',
};
jest
.spyOn(service, 'getCredentialOfferById')
.mockReturnValueOnce(of(expectedResult));
controller
.getById({ credentialOfferId }, { 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 { CredentialOffersController } from '../credential-offers.controller.js';
import { CredentialOffersModule } from '../credential-offers.module.js';
import { CredentialOffersService } from '../credential-offers.service.js';
describe('CredentialOffersModule', () => {
let credentialOffersController: CredentialOffersController;
let credentialOffersService: CredentialOffersService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
ClientsModule.registerAsync({
isGlobal: true,
clients: [{ name: NATS_CLIENT, useFactory: () => ({}) }],
}),
CredentialOffersModule,
],
}).compile();
credentialOffersController = moduleRef.get<CredentialOffersController>(
CredentialOffersController,
);
credentialOffersService = moduleRef.get<CredentialOffersService>(
CredentialOffersService,
);
});
it('should be defined', () => {
expect(credentialOffersController).toBeDefined();
expect(credentialOffersController).toBeInstanceOf(
CredentialOffersController,
);
expect(credentialOffersService).toBeDefined();
expect(credentialOffersService).toBeInstanceOf(CredentialOffersService);
});
});
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import {
EventAnonCredsCredentialOfferGetAll,
EventAnonCredsCredentialOfferGetById,
} from '@ocm/shared';
import { Subject, of, takeUntil } from 'rxjs';
import { NATS_CLIENT } from '../../common/constants.js';
import { CredentialOffersService } from '../credential-offers.service.js';
describe('CredentialOffersService', () => {
const natsClientMock = { send: jest.fn() };
let service: CredentialOffersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{ provide: NATS_CLIENT, useValue: natsClientMock },
CredentialOffersService,
],
}).compile();
service = module.get<CredentialOffersService>(CredentialOffersService);
});
describe('findCredentialOffers', () => {
it('should call natsClient.send with the correct arguments', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'tenantId';
const expectedResult: EventAnonCredsCredentialOfferGetAll['data'] = [];
natsClientMock.send.mockReturnValueOnce(
of(new EventAnonCredsCredentialOfferGetAll(expectedResult, tenantId)),
);
service
.findCredentialOffers(tenantId)
.pipe(takeUntil(unsubscribe$))
.subscribe((result) => {
expect(natsClientMock.send).toHaveBeenCalledWith(
EventAnonCredsCredentialOfferGetAll.token,
{ tenantId },
);
expect(result).toStrictEqual(expectedResult);
unsubscribe$.next();
unsubscribe$.complete();
done();
});
});
});
describe('getCredentialOfferById', () => {
it('should call natsClient.send with the correct arguments', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'tenantId';
const credentialOfferId = 'id';
const expectedResult: EventAnonCredsCredentialOfferGetById['data'] = {
cred_def_id: 'exampleCredDefId',
key_correctness_proof: {},
nonce: 'exampleNonce',
schema_id: 'exampleSchemaId',
};
natsClientMock.send.mockReturnValueOnce(
of(new EventAnonCredsCredentialOfferGetById(expectedResult, tenantId)),
);
service
.getCredentialOfferById(tenantId, credentialOfferId)
.pipe(takeUntil(unsubscribe$))
.subscribe((result) => {
expect(natsClientMock.send).toHaveBeenCalledWith(
EventAnonCredsCredentialOfferGetById.token,
{ tenantId, credentialOfferId },
);
expect(result).toStrictEqual(expectedResult);
unsubscribe$.next();
unsubscribe$.complete();
done();
});
});
});
});
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 { CredentialOffersService } from './credential-offers.service.js';
import { GetByIdParams } from './dto/get-by-id.dto.js';
@Controller()
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
@UseInterceptors(ResponseFormatInterceptor)
@ApiTags('Credential Offers')
export class CredentialOffersController {
public constructor(private readonly service: CredentialOffersService) {}
@Get()
@ApiOperation({
summary: 'Fetch a list of credential offers',
description:
'This call provides a list of credential offers for a given tenant',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Credential offers fetched successfully',
content: {
'application/json': {
schema: {},
examples: {
'Credential offers fetched successfully': {
value: {
statusCode: 200,
message: 'Credential offers fetched successfully',
data: [
{
id: '71b784a3',
},
],
},
},
'Tenant not found': {
value: {
statusCode: 404,
message: 'Tenant 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 find(@Query() { tenantId }: MultitenancyParams) {
return this.service.findCredentialOffers(tenantId);
}
@Get(':credentialOfferId')
@ApiOperation({
summary: 'Fetch a credential offer by ID',
description: 'This call provides a credential offer for a given ID',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Credential offer fetched successfully',
content: {
'application/json': {
schema: {},
examples: {
'Credential offer fetched successfully': {
value: {
statusCode: 200,
message: 'Credential offer fetched 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,
},
},
},
},
},
})
@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 getById(
@Param() { credentialOfferId }: GetByIdParams,
@Query() { tenantId }: MultitenancyParams,
) {
return this.service.getCredentialOfferById(tenantId, credentialOfferId);
}
}
import { Module } from '@nestjs/common';
import { CredentialOffersController } from './credential-offers.controller.js';
import { CredentialOffersService } from './credential-offers.service.js';
@Module({
providers: [CredentialOffersService],
controllers: [CredentialOffersController],
})
export class CredentialOffersModule {}
import type {
EventAnonCredsCredentialOfferGetAllInput,
EventAnonCredsCredentialOfferGetByIdInput,
} from '@ocm/shared';
import { Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import {
EventAnonCredsCredentialOfferGetAll,
EventAnonCredsCredentialOfferGetById,
} from '@ocm/shared';
import { map } from 'rxjs';
import { NATS_CLIENT } from '../common/constants.js';
@Injectable()
export class CredentialOffersService {
public constructor(
@Inject(NATS_CLIENT) private readonly natsClient: ClientProxy,
) {}
public findCredentialOffers(tenantId: string) {
return this.natsClient
.send<
EventAnonCredsCredentialOfferGetAll,
EventAnonCredsCredentialOfferGetAllInput
>(EventAnonCredsCredentialOfferGetAll.token, { tenantId })
.pipe(map(({ data }) => data));
}
public getCredentialOfferById(tenantId: string, credentialOfferId: string) {
return this.natsClient
.send<
EventAnonCredsCredentialOfferGetById,
EventAnonCredsCredentialOfferGetByIdInput
>(EventAnonCredsCredentialOfferGetById.token, {
tenantId,
credentialOfferId,
})
.pipe(map(({ data }) => data));
}
}
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class GetByIdParams {
@IsString()
@IsNotEmpty()
@ApiProperty({
description: 'The credential offer ID to retrieve',
format: 'string',
})
public credentialOfferId: string;
}
import type { BaseEventInput } from './baseEvents.js';
import type { AnonCredsCredentialOffer } from '@aries-framework/anoncreds';
import { BaseEvent } from './baseEvents.js';
export type EventAnonCredsCredentialOfferGetAllInput = BaseEventInput;
export class EventAnonCredsCredentialOfferGetAll extends BaseEvent<
Array<AnonCredsCredentialOffer>
> {
public static token = 'anoncreds.credentialOffers.getAll';
public get instance() {
return this.data;
}
public static fromEvent(e: EventAnonCredsCredentialOfferGetAll) {
return new EventAnonCredsCredentialOfferGetAll(
e.data,
e.tenantId,
e.id,
e.type,
e.timestamp,
);
}
}
export type EventAnonCredsCredentialOfferGetByIdInput = BaseEventInput & {
credentialOfferId: string;
};
export class EventAnonCredsCredentialOfferGetById extends BaseEvent<AnonCredsCredentialOffer | null> {
public static token = 'anoncreds.credentialOffers.getById';
public get instance() {
return this.data;
}
public static fromEvent(e: EventAnonCredsCredentialOfferGetById) {
return new EventAnonCredsCredentialOfferGetById(
e.data,
e.tenantId,
e.id,
e.type,
e.timestamp,
);
}
}
...@@ -10,6 +10,7 @@ export * from './events/tenantEvents.js'; ...@@ -10,6 +10,7 @@ export * from './events/tenantEvents.js';
export * from './events/schemaEvents.js'; export * from './events/schemaEvents.js';
export * from './events/credentialDefinitionEvents.js'; export * from './events/credentialDefinitionEvents.js';
export * from './events/credentialEvents.js'; export * from './events/credentialEvents.js';
export * from './events/credentialOfferEvents.js';
export * from './dto/pagination-params.dto.js'; export * from './dto/pagination-params.dto.js';
export * from './dto/multitenancy-params.dto.js'; export * from './dto/multitenancy-params.dto.js';
......
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