Skip to content
Snippets Groups Projects
Commit e71ec0d1 authored by Steffen Schulze's avatar Steffen Schulze Committed by Berend Sliedrecht
Browse files

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

Credential requests API. Refs #10

See merge request eclipse/xfsc/ocm/ocm-engine!19
parents d00284ce a8f40bc5
No related branches found
No related tags found
No related merge requests found
Showing
with 692 additions and 11 deletions
......@@ -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' },
]),
],
})
......
......@@ -45,6 +45,23 @@ export class CredentialOffersController {
],
},
},
},
},
},
})
@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,
......
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();
});
});
});
});
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);
});
});
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();
});
});
});
});
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);
}
}
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 {}
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));
}
}
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;
}
......@@ -97,3 +97,20 @@ export class EventDidcommAnonCredsCredentialsOfferToSelf extends BaseEvent<Crede
);
}
}
export type EventAnonCredsCredentialsDeleteByIdInput = BaseEventInput<{
credentialRecordId: string;
}>;
export class EventAnonCredsCredentialsDeleteById extends BaseEvent {
public static token = 'anoncreds.credentials.offerToSelf.deleteById';
public static fromEvent(e: EventAnonCredsCredentialsDeleteById) {
return new EventAnonCredsCredentialsDeleteById(
e.data,
e.tenantId,
e.id,
e.type,
e.timestamp,
);
}
}
import type { BaseEventInput } from './baseEvents.js';
import type { AnonCredsCredentialOffer } from '@aries-framework/anoncreds';
import type { CredentialExchangeRecord } from '@aries-framework/core';
import { BaseEvent } from './baseEvents.js';
export type EventAnonCredsCredentialOfferGetAllInput = BaseEventInput;
export class EventAnonCredsCredentialOfferGetAll extends BaseEvent<
Array<AnonCredsCredentialOffer>
Array<CredentialExchangeRecord>
> {
public static token = 'anoncreds.credentialOffers.getAll';
......@@ -29,7 +29,7 @@ export type EventAnonCredsCredentialOfferGetByIdInput = BaseEventInput & {
credentialOfferId: string;
};
export class EventAnonCredsCredentialOfferGetById extends BaseEvent<AnonCredsCredentialOffer | null> {
export class EventAnonCredsCredentialOfferGetById extends BaseEvent<CredentialExchangeRecord | null> {
public static token = 'anoncreds.credentialOffers.getById';
public get instance() {
......
import type { BaseEventInput } from './baseEvents.js';
import type { CredentialExchangeRecord } from '@aries-framework/core';
import { BaseEvent } from './baseEvents.js';
export type EventAnonCredsCredentialRequestGetAllInput = BaseEventInput;
export class EventAnonCredsCredentialRequestGetAll extends BaseEvent<
Array<CredentialExchangeRecord>
> {
public static token = 'anoncreds.credentialRequests.getAll';
public get instance() {
return this.data;
}
public static fromEvent(e: EventAnonCredsCredentialRequestGetAll) {
return new EventAnonCredsCredentialRequestGetAll(
e.data,
e.tenantId,
e.id,
e.type,
e.timestamp,
);
}
}
export type EventAnonCredsCredentialRequestGetByIdInput = BaseEventInput & {
credentialRequestId: string;
};
export class EventAnonCredsCredentialRequestGetById extends BaseEvent<CredentialExchangeRecord | null> {
public static token = 'anoncreds.credentialRequests.getById';
public get instance() {
return this.data;
}
public static fromEvent(e: EventAnonCredsCredentialRequestGetById) {
return new EventAnonCredsCredentialRequestGetById(
e.data,
e.tenantId,
e.id,
e.type,
e.timestamp,
);
}
}
......@@ -11,6 +11,7 @@ export * from './events/schemaEvents.js';
export * from './events/credentialDefinitionEvents.js';
export * from './events/credentialEvents.js';
export * from './events/credentialOfferEvents.js';
export * from './events/credentialRequestEvents.js';
export * from './dto/pagination-params.dto.js';
export * from './dto/multitenancy-params.dto.js';
......
......@@ -9,6 +9,10 @@ import {
EventDidcommAnonCredsCredentialsOfferInput,
EventDidcommAnonCredsCredentialsOfferToSelfInput,
EventDidcommAnonCredsCredentialsOfferToSelf,
EventAnonCredsCredentialOfferGetAll,
EventAnonCredsCredentialOfferGetAllInput,
EventAnonCredsCredentialRequestGetAll,
EventAnonCredsCredentialRequestGetAllInput,
} from '@ocm/shared';
import { AnonCredsCredentialsService } from './anoncredsCredentials.service.js';
......@@ -27,6 +31,26 @@ export class AnonCredsCredentialsController {
);
}
@MessagePattern(EventAnonCredsCredentialOfferGetAll.token)
public async getAllOffers(
options: EventAnonCredsCredentialOfferGetAllInput,
): Promise<EventAnonCredsCredentialOfferGetAll> {
return new EventAnonCredsCredentialOfferGetAll(
await this.credentialsService.getAllOffers(options),
options.tenantId,
);
}
@MessagePattern(EventAnonCredsCredentialRequestGetAll.token)
public async getAllRequests(
options: EventAnonCredsCredentialRequestGetAllInput,
): Promise<EventAnonCredsCredentialRequestGetAll> {
return new EventAnonCredsCredentialRequestGetAll(
await this.credentialsService.getAllRequests(options),
options.tenantId,
);
}
@MessagePattern(EventDidcommAnonCredsCredentialsGetById.token)
public async getById(
options: EventDidcommAnonCredsCredentialsGetByIdInput,
......
import type {
EventAnonCredsCredentialRequestGetByIdInput,
EventAnonCredsCredentialOfferGetAllInput,
EventAnonCredsCredentialOfferGetById,
EventAnonCredsCredentialOfferGetByIdInput,
EventAnonCredsCredentialRequestGetAllInput,
EventDidcommAnonCredsCredentialsGetAllInput,
EventDidcommAnonCredsCredentialsGetByIdInput,
EventDidcommAnonCredsCredentialsOfferInput,
EventDidcommAnonCredsCredentialsOfferToSelfInput,
EventAnonCredsCredentialRequestGetById,
EventDidcommAnonCredsCredentialsOffer,
EventDidcommAnonCredsCredentialsOfferToSelf,
EventDidcommAnonCredsCredentialsGetById,
EventAnonCredsCredentialRequestGetAll,
EventAnonCredsCredentialOfferGetAll,
EventDidcommAnonCredsCredentialsGetAll,
EventAnonCredsCredentialsDeleteByIdInput,
EventAnonCredsCredentialsDeleteById,
} from '@ocm/shared';
import {
AutoAcceptCredential,
type CredentialExchangeRecord,
} from '@aries-framework/core';
import { AutoAcceptCredential, CredentialState } from '@aries-framework/core';
import { Injectable } from '@nestjs/common';
import { logger } from '@ocm/shared';
import { MetadataTokens } from '../../common/constants.js';
import { WithTenantService } from '../withTenantService.js';
......@@ -21,28 +33,120 @@ export class AnonCredsCredentialsService {
public async getAll({
tenantId,
}: EventDidcommAnonCredsCredentialsGetAllInput): Promise<
Array<CredentialExchangeRecord>
EventDidcommAnonCredsCredentialsGetAll['data']
> {
return this.withTenantService.invoke(tenantId, (t) =>
t.credentials.getAll(),
);
}
public async getAllOffers({
tenantId,
}: EventAnonCredsCredentialOfferGetAllInput): Promise<
EventAnonCredsCredentialOfferGetAll['data']
> {
return this.withTenantService.invoke(tenantId, (t) =>
t.credentials.findAllByQuery({
$or: [
{ state: CredentialState.OfferSent },
{ state: CredentialState.OfferReceived },
],
}),
);
}
public async getAllRequests({
tenantId,
}: EventAnonCredsCredentialRequestGetAllInput): Promise<
EventAnonCredsCredentialRequestGetAll['data']
> {
return this.withTenantService.invoke(tenantId, (t) =>
t.credentials.findAllByQuery({
$or: [
{ state: CredentialState.RequestSent },
{ state: CredentialState.RequestReceived },
],
}),
);
}
public async deleteById({
tenantId,
credentialRecordId,
}: EventAnonCredsCredentialsDeleteByIdInput): Promise<
EventAnonCredsCredentialsDeleteById['data']
> {
return this.withTenantService.invoke(tenantId, async (t) => {
await t.credentials.deleteById(credentialRecordId);
return {};
});
}
public async getById({
tenantId,
credentialRecordId,
}: EventDidcommAnonCredsCredentialsGetByIdInput): Promise<CredentialExchangeRecord | null> {
}: EventDidcommAnonCredsCredentialsGetByIdInput): Promise<
EventDidcommAnonCredsCredentialsGetById['data']
> {
return this.withTenantService.invoke(tenantId, (t) =>
t.credentials.findById(credentialRecordId),
);
}
public async getOfferById({
tenantId,
credentialOfferId,
}: EventAnonCredsCredentialOfferGetByIdInput): Promise<
EventAnonCredsCredentialOfferGetById['data']
> {
return this.withTenantService.invoke(tenantId, async (t) => {
const credential = await t.credentials.findById(credentialOfferId);
if (
credential &&
credential.state !== CredentialState.OfferSent &&
credential.state !== CredentialState.OfferReceived
) {
logger.warn(
`Credential '${credentialOfferId}' does exist, but is not in offer state. Actual state: ${credential.state}`,
);
}
return credential;
});
}
public async getRequestById({
tenantId,
credentialRequestId,
}: EventAnonCredsCredentialRequestGetByIdInput): Promise<
EventAnonCredsCredentialRequestGetById['data']
> {
return this.withTenantService.invoke(tenantId, async (t) => {
const credential = await t.credentials.findById(credentialRequestId);
if (
credential &&
credential.state !== CredentialState.RequestSent &&
credential.state !== CredentialState.RequestReceived
) {
logger.warn(
`Credential '${credentialRequestId}' does exist, but is not in a request state. Actual state: ${credential.state}`,
);
}
return credential;
});
}
public async offer({
tenantId,
connectionId,
credentialDefinitionId,
attributes,
}: EventDidcommAnonCredsCredentialsOfferInput): Promise<CredentialExchangeRecord> {
}: EventDidcommAnonCredsCredentialsOfferInput): Promise<
EventDidcommAnonCredsCredentialsOffer['data']
> {
return this.withTenantService.invoke(tenantId, (t) =>
t.credentials.offerCredential({
protocolVersion: 'v2',
......@@ -58,7 +162,9 @@ export class AnonCredsCredentialsService {
tenantId,
credentialDefinitionId,
attributes,
}: EventDidcommAnonCredsCredentialsOfferToSelfInput): Promise<CredentialExchangeRecord> {
}: EventDidcommAnonCredsCredentialsOfferToSelfInput): Promise<
EventDidcommAnonCredsCredentialsOfferToSelf['data']
> {
return this.withTenantService.invoke(tenantId, async (t) => {
const connections = await t.connections.getAll();
const connection = connections.find((c) => {
......
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