authorization module
This commit is contained in:
parent
ced57e35a6
commit
d78a065c54
|
@ -3,7 +3,7 @@ package USER.DELETE
|
||||||
default allow := false
|
default allow := false
|
||||||
|
|
||||||
allow {
|
allow {
|
||||||
input.uuid == input.owner
|
input.id == input.owner
|
||||||
}
|
}
|
||||||
|
|
||||||
allow {
|
allow {
|
||||||
|
|
|
@ -3,7 +3,7 @@ package USER.READ
|
||||||
default allow := false
|
default allow := false
|
||||||
|
|
||||||
allow {
|
allow {
|
||||||
input.uuid == input.owner
|
input.id == input.owner
|
||||||
}
|
}
|
||||||
|
|
||||||
allow {
|
allow {
|
||||||
|
|
|
@ -3,7 +3,7 @@ package USER.UPDATE
|
||||||
default allow := false
|
default allow := false
|
||||||
|
|
||||||
allow {
|
allow {
|
||||||
input.uuid == input.owner
|
input.id == input.owner
|
||||||
}
|
}
|
||||||
|
|
||||||
allow {
|
allow {
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
// import { classes } from '@automapper/classes';
|
|
||||||
// import { AutomapperModule } from '@automapper/nestjs';
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
// import { AuthorizationModule } from './modules/authorization/authorization.module';
|
|
||||||
// import { HealthModule } from './modules/health/health.module';
|
|
||||||
import { AuthenticationModule } from '@modules/authentication/authentication.module';
|
import { AuthenticationModule } from '@modules/authentication/authentication.module';
|
||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
import {
|
import {
|
||||||
MessageBrokerModule,
|
MessageBrokerModule,
|
||||||
MessageBrokerModuleOptions,
|
MessageBrokerModuleOptions,
|
||||||
} from '@mobicoop/message-broker-module';
|
} from '@mobicoop/message-broker-module';
|
||||||
|
import { HealthModule } from '@modules/health/health.module';
|
||||||
|
import { AuthorizationModule } from '@modules/authorization/authorization.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -36,10 +34,9 @@ import {
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
// AutomapperModule.forRoot({ strategyInitializer: classes() }),
|
|
||||||
AuthenticationModule,
|
AuthenticationModule,
|
||||||
// AuthorizationModule,
|
AuthorizationModule,
|
||||||
// HealthModule,
|
HealthModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|
|
@ -19,9 +19,12 @@ async function bootstrap() {
|
||||||
),
|
),
|
||||||
join(
|
join(
|
||||||
__dirname,
|
__dirname,
|
||||||
'modules/authorization/adapters/primaries/authorization.proto',
|
'modules/authorization/interface/grpc-controllers/authorization.proto',
|
||||||
|
),
|
||||||
|
join(
|
||||||
|
__dirname,
|
||||||
|
'modules/health/interface/grpc-controllers/health.proto',
|
||||||
),
|
),
|
||||||
join(__dirname, 'modules/health/adapters/primaries/health.proto'),
|
|
||||||
],
|
],
|
||||||
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
|
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
|
||||||
loader: { keepCase: true, enums: String },
|
loader: { keepCase: true, enums: String },
|
||||||
|
|
|
@ -88,6 +88,7 @@ const orms: Provider[] = [PrismaService];
|
||||||
exports: [
|
exports: [
|
||||||
PrismaService,
|
PrismaService,
|
||||||
AuthenticationMapper,
|
AuthenticationMapper,
|
||||||
|
UsernameMapper,
|
||||||
AUTHENTICATION_REPOSITORY,
|
AUTHENTICATION_REPOSITORY,
|
||||||
USERNAME_REPOSITORY,
|
USERNAME_REPOSITORY,
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
import { Mapper } from '@automapper/core';
|
|
||||||
import { InjectMapper } from '@automapper/nestjs';
|
|
||||||
import { Controller, UsePipes } from '@nestjs/common';
|
|
||||||
import { QueryBus } from '@nestjs/cqrs';
|
|
||||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
|
||||||
import { RpcValidationPipe } from 'src/utils/pipes/rpc.validation-pipe';
|
|
||||||
import { DecisionRequest } from '../../domain/dtos/decision.request';
|
|
||||||
import { Authorization } from '../../domain/entities/authorization';
|
|
||||||
import { DecisionQuery } from '../../queries/decision.query';
|
|
||||||
import { AuthorizationPresenter } from './authorization.presenter';
|
|
||||||
|
|
||||||
@UsePipes(
|
|
||||||
new RpcValidationPipe({
|
|
||||||
whitelist: true,
|
|
||||||
forbidUnknownValues: false,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
@Controller()
|
|
||||||
export class AuthorizationController {
|
|
||||||
constructor(
|
|
||||||
private readonly queryBus: QueryBus,
|
|
||||||
@InjectMapper() private readonly mapper: Mapper,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@GrpcMethod('AuthorizationService', 'Decide')
|
|
||||||
async decide(data: DecisionRequest): Promise<AuthorizationPresenter> {
|
|
||||||
try {
|
|
||||||
const authorization: Authorization = await this.queryBus.execute(
|
|
||||||
new DecisionQuery(data.domain, data.action, data.context),
|
|
||||||
);
|
|
||||||
return this.mapper.map(
|
|
||||||
authorization,
|
|
||||||
Authorization,
|
|
||||||
AuthorizationPresenter,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 7,
|
|
||||||
message: 'Permission denied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
|
|
||||||
export class AuthorizationPresenter {
|
|
||||||
@AutoMap()
|
|
||||||
allow: boolean;
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export class DecisionResult {
|
|
||||||
allow: boolean;
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { DecisionResult } from './decision-result';
|
|
||||||
|
|
||||||
export class Decision {
|
|
||||||
decision_id: string;
|
|
||||||
result: DecisionResult;
|
|
||||||
}
|
|
|
@ -0,0 +1 @@
|
||||||
|
export const DECISION_MAKER = Symbol('DECISION_MAKER');
|
|
@ -1,16 +1,26 @@
|
||||||
import { HttpModule } from '@nestjs/axios';
|
import { Module, Provider } from '@nestjs/common';
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { CqrsModule } from '@nestjs/cqrs';
|
import { CqrsModule } from '@nestjs/cqrs';
|
||||||
import { DatabaseModule } from '../database/database.module';
|
import { DecideGrpcController } from './interface/grpc-controllers/decide.grpc.controller';
|
||||||
import { AuthorizationController } from './adapters/primaries/authorization.controller';
|
import { DecisionQueryHandler } from './core/application/queries/decision/decision.query-handler';
|
||||||
import { OpaDecisionMaker } from './adapters/secondaries/opa.decision-maker';
|
import { DECISION_MAKER } from './authorization.di-tokens';
|
||||||
import { DecisionUseCase } from './domain/usecases/decision.usecase';
|
import { OpaDecisionMaker } from './infrastructure/opa.decision-maker';
|
||||||
import { AuthorizationProfile } from './mappers/authorization.profile';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
|
|
||||||
|
const grpcControllers = [DecideGrpcController];
|
||||||
|
|
||||||
|
const queryHandlers: Provider[] = [DecisionQueryHandler];
|
||||||
|
|
||||||
|
const adapters: Provider[] = [
|
||||||
|
{
|
||||||
|
provide: DECISION_MAKER,
|
||||||
|
useClass: OpaDecisionMaker,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DatabaseModule, CqrsModule, HttpModule],
|
imports: [CqrsModule, HttpModule],
|
||||||
|
controllers: [...grpcControllers],
|
||||||
|
providers: [...queryHandlers, ...adapters],
|
||||||
exports: [],
|
exports: [],
|
||||||
controllers: [AuthorizationController],
|
|
||||||
providers: [OpaDecisionMaker, DecisionUseCase, AuthorizationProfile],
|
|
||||||
})
|
})
|
||||||
export class AuthorizationModule {}
|
export class AuthorizationModule {}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Action, ContextItem, Domain } from '../../domain/authorization.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export abstract class DecisionMakerPort {
|
||||||
|
abstract decide(
|
||||||
|
domain: Domain,
|
||||||
|
action: Action,
|
||||||
|
context: ContextItem[],
|
||||||
|
): Promise<boolean>;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { DecisionQuery } from './decision.query';
|
||||||
|
import { DECISION_MAKER } from '@modules/authorization/authorization.di-tokens';
|
||||||
|
import { DecisionMakerPort } from '../../ports/decision-maker.port';
|
||||||
|
|
||||||
|
@QueryHandler(DecisionQuery)
|
||||||
|
export class DecisionQueryHandler implements IQueryHandler {
|
||||||
|
constructor(
|
||||||
|
@Inject(DECISION_MAKER)
|
||||||
|
private readonly decisionMaker: DecisionMakerPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
execute = async (decisionQuery: DecisionQuery): Promise<boolean> =>
|
||||||
|
this.decisionMaker.decide(
|
||||||
|
decisionQuery.domain,
|
||||||
|
decisionQuery.action,
|
||||||
|
decisionQuery.context,
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { QueryBase } from '@mobicoop/ddd-library';
|
||||||
|
import {
|
||||||
|
Action,
|
||||||
|
ContextItem,
|
||||||
|
Domain,
|
||||||
|
} from '@modules/authorization/core/domain/authorization.types';
|
||||||
|
|
||||||
|
export class DecisionQuery extends QueryBase {
|
||||||
|
readonly domain: Domain;
|
||||||
|
readonly action: Action;
|
||||||
|
readonly context?: ContextItem[];
|
||||||
|
|
||||||
|
constructor(domain: Domain, action: Action, context?: ContextItem[]) {
|
||||||
|
super();
|
||||||
|
this.domain = domain;
|
||||||
|
this.action = action;
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,14 @@
|
||||||
|
export interface ContextItem {
|
||||||
|
name: string;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Domain {
|
||||||
|
USER = 'USER',
|
||||||
|
ADMIN = 'ADMIN',
|
||||||
|
AD = 'AD',
|
||||||
|
}
|
||||||
|
|
||||||
export enum Action {
|
export enum Action {
|
||||||
CREATE = 'CREATE',
|
CREATE = 'CREATE',
|
||||||
READ = 'READ',
|
READ = 'READ',
|
|
@ -1,9 +0,0 @@
|
||||||
export class ContextItem {
|
|
||||||
name: string;
|
|
||||||
value: any;
|
|
||||||
|
|
||||||
constructor(name: string, value: any) {
|
|
||||||
this.name = name;
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { IsArray, IsEnum, IsNotEmpty } from 'class-validator';
|
|
||||||
import { ContextItem } from './context-item';
|
|
||||||
import { Action } from './action.enum';
|
|
||||||
import { Domain } from './domain.enum';
|
|
||||||
|
|
||||||
export class DecisionRequest {
|
|
||||||
@IsEnum(Domain)
|
|
||||||
@IsNotEmpty()
|
|
||||||
domain: Domain;
|
|
||||||
|
|
||||||
@IsEnum(Action)
|
|
||||||
@IsNotEmpty()
|
|
||||||
action: Action;
|
|
||||||
|
|
||||||
@IsArray()
|
|
||||||
context?: Array<ContextItem>;
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
export enum Domain {
|
|
||||||
USER = 'USER',
|
|
||||||
ADMIN = 'ADMIN',
|
|
||||||
AD = 'AD',
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
|
|
||||||
export class Authorization {
|
|
||||||
@AutoMap()
|
|
||||||
allow: boolean;
|
|
||||||
|
|
||||||
constructor(allow: boolean) {
|
|
||||||
this.allow = allow;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Action } from '../dtos/action.enum';
|
|
||||||
import { Domain } from '../dtos/domain.enum';
|
|
||||||
import { Authorization } from '../entities/authorization';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export abstract class IMakeDecision {
|
|
||||||
abstract decide(
|
|
||||||
domain: Domain,
|
|
||||||
action: Action,
|
|
||||||
context: Array<{ name: string; value: string }>,
|
|
||||||
): Promise<Authorization>;
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { QueryHandler } from '@nestjs/cqrs';
|
|
||||||
import { OpaDecisionMaker } from '../../adapters/secondaries/opa.decision-maker';
|
|
||||||
import { DecisionQuery } from '../../queries/decision.query';
|
|
||||||
import { Authorization } from '../entities/authorization';
|
|
||||||
|
|
||||||
@QueryHandler(DecisionQuery)
|
|
||||||
export class DecisionUseCase {
|
|
||||||
constructor(private readonly decisionMaker: OpaDecisionMaker) {}
|
|
||||||
|
|
||||||
execute = (decisionQuery: DecisionQuery): Promise<Authorization> =>
|
|
||||||
this.decisionMaker.decide(
|
|
||||||
decisionQuery.domain,
|
|
||||||
decisionQuery.action,
|
|
||||||
decisionQuery.context,
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -2,15 +2,15 @@ import { HttpService } from '@nestjs/axios';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { Action } from '../../domain/dtos/action.enum';
|
import { DecisionMakerPort } from '../core/application/ports/decision-maker.port';
|
||||||
import { Domain } from '../../domain/dtos/domain.enum';
|
import {
|
||||||
import { IMakeDecision } from '../../domain/interfaces/decision-maker';
|
Action,
|
||||||
import { ContextItem } from '../../domain/dtos/context-item';
|
ContextItem,
|
||||||
import { Decision } from './decision';
|
Domain,
|
||||||
import { Authorization } from '../../domain/entities/authorization';
|
} from '../core/domain/authorization.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OpaDecisionMaker extends IMakeDecision {
|
export class OpaDecisionMaker extends DecisionMakerPort {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
|
@ -21,8 +21,8 @@ export class OpaDecisionMaker extends IMakeDecision {
|
||||||
decide = async (
|
decide = async (
|
||||||
domain: Domain,
|
domain: Domain,
|
||||||
action: Action,
|
action: Action,
|
||||||
context: Array<ContextItem>,
|
context: ContextItem[],
|
||||||
): Promise<Authorization> => {
|
): Promise<boolean> => {
|
||||||
const reducedContext = context.reduce(
|
const reducedContext = context.reduce(
|
||||||
(obj, item) => Object.assign(obj, { [item.name]: item.value }),
|
(obj, item) => Object.assign(obj, { [item.name]: item.value }),
|
||||||
{},
|
{},
|
||||||
|
@ -30,7 +30,7 @@ export class OpaDecisionMaker extends IMakeDecision {
|
||||||
try {
|
try {
|
||||||
const { data } = await lastValueFrom(
|
const { data } = await lastValueFrom(
|
||||||
this.httpService.post<Decision>(
|
this.httpService.post<Decision>(
|
||||||
this.configService.get<string>('OPA_URL') + domain + '/' + action,
|
`${this.configService.get<string>('OPA_URL')}${domain}/${action}`,
|
||||||
{
|
{
|
||||||
input: {
|
input: {
|
||||||
...reducedContext,
|
...reducedContext,
|
||||||
|
@ -38,9 +38,16 @@ export class OpaDecisionMaker extends IMakeDecision {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return new Authorization(data.result.allow);
|
return data.result.allow;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return new Authorization(false);
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Decision = {
|
||||||
|
decision_id: string;
|
||||||
|
result: {
|
||||||
|
allow: boolean;
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
export class DecisionResponseDto {
|
||||||
|
readonly allow: boolean;
|
||||||
|
|
||||||
|
constructor(allow: boolean) {
|
||||||
|
this.allow = allow;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { RpcExceptionCode, RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||||
|
import { Controller, UsePipes } from '@nestjs/common';
|
||||||
|
import { QueryBus } from '@nestjs/cqrs';
|
||||||
|
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||||
|
import { DecisionRequestDto } from './dtos/decision.request.dto';
|
||||||
|
import { DecisionQuery } from '@modules/authorization/core/application/queries/decision/decision.query';
|
||||||
|
import { DecisionResponseDto } from '../dtos/decision.response.dto';
|
||||||
|
|
||||||
|
@UsePipes(
|
||||||
|
new RpcValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
forbidUnknownValues: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@Controller()
|
||||||
|
export class DecideGrpcController {
|
||||||
|
constructor(private readonly queryBus: QueryBus) {}
|
||||||
|
|
||||||
|
@GrpcMethod('AuthorizationService', 'Decide')
|
||||||
|
async decide(data: DecisionRequestDto): Promise<DecisionResponseDto> {
|
||||||
|
try {
|
||||||
|
const allow: boolean = await this.queryBus.execute(
|
||||||
|
new DecisionQuery(data.domain, data.action, data.context),
|
||||||
|
);
|
||||||
|
return new DecisionResponseDto(allow);
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcExceptionCode.PERMISSION_DENIED,
|
||||||
|
message: 'Permission denied',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import {
|
||||||
|
Action,
|
||||||
|
ContextItem,
|
||||||
|
Domain,
|
||||||
|
} from '@modules/authorization/core/domain/authorization.types';
|
||||||
|
import { IsArray, IsEnum, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class DecisionRequestDto {
|
||||||
|
@IsEnum(Domain)
|
||||||
|
@IsNotEmpty()
|
||||||
|
domain: Domain;
|
||||||
|
|
||||||
|
@IsEnum(Action)
|
||||||
|
@IsNotEmpty()
|
||||||
|
action: Action;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
context?: ContextItem[];
|
||||||
|
}
|
|
@ -1,18 +0,0 @@
|
||||||
import { createMap, Mapper } from '@automapper/core';
|
|
||||||
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { AuthorizationPresenter } from '../adapters/primaries/authorization.presenter';
|
|
||||||
import { Authorization } from '../domain/entities/authorization';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuthorizationProfile extends AutomapperProfile {
|
|
||||||
constructor(@InjectMapper() mapper: Mapper) {
|
|
||||||
super(mapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
override get profile() {
|
|
||||||
return (mapper: any) => {
|
|
||||||
createMap(mapper, Authorization, AuthorizationPresenter);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { ContextItem } from '../domain/dtos/context-item';
|
|
||||||
import { Action } from '../domain/dtos/action.enum';
|
|
||||||
import { Domain } from '../domain/dtos/domain.enum';
|
|
||||||
|
|
||||||
export class DecisionQuery {
|
|
||||||
readonly domain: Domain;
|
|
||||||
readonly action: Action;
|
|
||||||
readonly context: Array<ContextItem>;
|
|
||||||
|
|
||||||
constructor(domain: Domain, action: Action, context?: Array<ContextItem>) {
|
|
||||||
this.domain = domain;
|
|
||||||
this.action = action;
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { DECISION_MAKER } from '@modules/authorization/authorization.di-tokens';
|
||||||
|
import { DecisionQuery } from '@modules/authorization/core/application/queries/decision/decision.query';
|
||||||
|
import { DecisionQueryHandler } from '@modules/authorization/core/application/queries/decision/decision.query-handler';
|
||||||
|
import {
|
||||||
|
Action,
|
||||||
|
Domain,
|
||||||
|
} from '@modules/authorization/core/domain/authorization.types';
|
||||||
|
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
const mockDecisionMaker = {
|
||||||
|
decide: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() => true)
|
||||||
|
.mockImplementationOnce(() => false),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Decision Query Handler', () => {
|
||||||
|
let decisionQueryHandler: DecisionQueryHandler;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: DECISION_MAKER,
|
||||||
|
useValue: mockDecisionMaker,
|
||||||
|
},
|
||||||
|
DecisionQueryHandler,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
decisionQueryHandler =
|
||||||
|
module.get<DecisionQueryHandler>(DecisionQueryHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(decisionQueryHandler).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('execution', () => {
|
||||||
|
it('should return a positive decision', async () => {
|
||||||
|
const decisionQuery = new DecisionQuery(Domain.USER, Action.READ, [
|
||||||
|
{
|
||||||
|
name: 'owner',
|
||||||
|
value: '96d99d44-e0a6-458e-a656-de2a400d60a8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
value: '96d99d44-e0a6-458e-a656-de2a400d60a8',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const decision: boolean = await decisionQueryHandler.execute(
|
||||||
|
decisionQuery,
|
||||||
|
);
|
||||||
|
expect(decision).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return a negative decision', async () => {
|
||||||
|
const decisionQuery = new DecisionQuery(Domain.USER, Action.READ, [
|
||||||
|
{
|
||||||
|
name: 'owner',
|
||||||
|
value: '96d99d44-e0a6-458e-a656-de2a400d60a8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
value: '96d99d44-e0a6-458e-a656-de2a400d60a9',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const decision: boolean = await decisionQueryHandler.execute(
|
||||||
|
decisionQuery,
|
||||||
|
);
|
||||||
|
expect(decision).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,57 +0,0 @@
|
||||||
import { classes } from '@automapper/classes';
|
|
||||||
import { AutomapperModule } from '@automapper/nestjs';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { OpaDecisionMaker } from '../../adapters/secondaries/opa.decision-maker';
|
|
||||||
import { Action } from '../../domain/dtos/action.enum';
|
|
||||||
import { ContextItem } from '../../domain/dtos/context-item';
|
|
||||||
import { DecisionRequest } from '../../domain/dtos/decision.request';
|
|
||||||
import { Domain } from '../../domain/dtos/domain.enum';
|
|
||||||
import { DecisionUseCase } from '../../domain/usecases/decision.usecase';
|
|
||||||
import { AuthorizationProfile } from '../../mappers/authorization.profile';
|
|
||||||
import { DecisionQuery } from '../../queries/decision.query';
|
|
||||||
|
|
||||||
const mockOpaDecisionMaker = {
|
|
||||||
decide: jest.fn().mockResolvedValue(Promise.resolve(true)),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('DecisionUseCase', () => {
|
|
||||||
let decisionUseCase: DecisionUseCase;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: OpaDecisionMaker,
|
|
||||||
useValue: mockOpaDecisionMaker,
|
|
||||||
},
|
|
||||||
DecisionUseCase,
|
|
||||||
AuthorizationProfile,
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
decisionUseCase = module.get<DecisionUseCase>(DecisionUseCase);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(decisionUseCase).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('execute', () => {
|
|
||||||
it('should validate an authorization', async () => {
|
|
||||||
const decisionRequest: DecisionRequest = new DecisionRequest();
|
|
||||||
decisionRequest.domain = Domain.USER;
|
|
||||||
decisionRequest.action = Action.CREATE;
|
|
||||||
decisionRequest.context = [new ContextItem('context1', 'value1')];
|
|
||||||
expect(
|
|
||||||
decisionUseCase.execute(
|
|
||||||
new DecisionQuery(
|
|
||||||
decisionRequest.domain,
|
|
||||||
decisionRequest.action,
|
|
||||||
decisionRequest.context,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { OpaDecisionMaker } from '@modules/authorization/infrastructure/opa.decision-maker';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import {
|
||||||
|
Action,
|
||||||
|
Domain,
|
||||||
|
} from '@modules/authorization/core/domain/authorization.types';
|
||||||
|
|
||||||
|
const mockConfigService = {
|
||||||
|
get: jest.fn().mockImplementation(() => 'http://localhost:8181/v1/data/'),
|
||||||
|
};
|
||||||
|
const mockHttpService = {
|
||||||
|
post: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return of({
|
||||||
|
status: 200,
|
||||||
|
data: {
|
||||||
|
decision_id: 'b22965f0-f48a-4fcf-84db-b6f31cd07b8c',
|
||||||
|
result: {
|
||||||
|
allow: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return of({
|
||||||
|
status: 200,
|
||||||
|
data: {
|
||||||
|
decision_id: '5a648ea2-6790-4337-b63c-2ebdf225466f',
|
||||||
|
result: {
|
||||||
|
allow: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Error();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('OPA decision maker', () => {
|
||||||
|
let opaDecisonMaker: OpaDecisionMaker;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ConfigService,
|
||||||
|
useValue: mockConfigService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: HttpService,
|
||||||
|
useValue: mockHttpService,
|
||||||
|
},
|
||||||
|
OpaDecisionMaker,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
opaDecisonMaker = module.get<OpaDecisionMaker>(OpaDecisionMaker);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(opaDecisonMaker).toBeDefined();
|
||||||
|
});
|
||||||
|
it('should return a positive decision', async () => {
|
||||||
|
const decision: boolean = await opaDecisonMaker.decide(
|
||||||
|
Domain.USER,
|
||||||
|
Action.READ,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'owner',
|
||||||
|
value: '96d99d44-e0a6-458e-a656-de2a400d60a8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
value: '96d99d44-e0a6-458e-a656-de2a400d60a8',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(decision).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return a negative decision', async () => {
|
||||||
|
const decision: boolean = await opaDecisonMaker.decide(
|
||||||
|
Domain.USER,
|
||||||
|
Action.READ,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'owner',
|
||||||
|
value: '96d99d44-e0a6-458e-a656-de2a400d60a8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
value: '96d99d44-e0a6-458e-a656-de2a400d60a9',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(decision).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('should return a negative decision if an error occurs', async () => {
|
||||||
|
const decision: boolean = await opaDecisonMaker.decide(
|
||||||
|
Domain.USER,
|
||||||
|
Action.READ,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'owner',
|
||||||
|
value: '96d99d44-e0a6-458e-a656-de2a400d60a8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
value: '96d99d44-e0a6-458e-a656-de2a400d60a9',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(decision).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
|
import {
|
||||||
|
Action,
|
||||||
|
Domain,
|
||||||
|
} from '@modules/authorization/core/domain/authorization.types';
|
||||||
|
import { DecisionResponseDto } from '@modules/authorization/interface/dtos/decision.response.dto';
|
||||||
|
import { DecideGrpcController } from '@modules/authorization/interface/grpc-controllers/decide.grpc.controller';
|
||||||
|
import { DecisionRequestDto } from '@modules/authorization/interface/grpc-controllers/dtos/decision.request.dto';
|
||||||
|
import { QueryBus } from '@nestjs/cqrs';
|
||||||
|
import { RpcException } from '@nestjs/microservices';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
const decisionRequest: DecisionRequestDto = {
|
||||||
|
domain: Domain.USER,
|
||||||
|
action: Action.READ,
|
||||||
|
context: [
|
||||||
|
{
|
||||||
|
name: 'owner',
|
||||||
|
value: '96d99d44-e0a6-458e-a656-de2a400d60a8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
value: '96d99d44-e0a6-458e-a656-de2a400d60a8',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockQueryBus = {
|
||||||
|
execute: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() => true)
|
||||||
|
.mockImplementationOnce(() => false)
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Error();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Decide Grpc Controller', () => {
|
||||||
|
let decideGrpcController: DecideGrpcController;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: QueryBus,
|
||||||
|
useValue: mockQueryBus,
|
||||||
|
},
|
||||||
|
DecideGrpcController,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
decideGrpcController =
|
||||||
|
module.get<DecideGrpcController>(DecideGrpcController);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(decideGrpcController).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a positive decision response', async () => {
|
||||||
|
jest.spyOn(mockQueryBus, 'execute');
|
||||||
|
const decisionResponse: DecisionResponseDto =
|
||||||
|
await decideGrpcController.decide(decisionRequest);
|
||||||
|
expect(decisionResponse.allow).toBeTruthy();
|
||||||
|
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a negative decision response', async () => {
|
||||||
|
jest.spyOn(mockQueryBus, 'execute');
|
||||||
|
const decisionResponse: DecisionResponseDto =
|
||||||
|
await decideGrpcController.decide(decisionRequest);
|
||||||
|
expect(decisionResponse.allow).toBeFalsy();
|
||||||
|
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw a dedicated RpcException if authorization fails', async () => {
|
||||||
|
jest.spyOn(mockQueryBus, 'execute');
|
||||||
|
expect.assertions(3);
|
||||||
|
try {
|
||||||
|
await decideGrpcController.decide(decisionRequest);
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(RpcException);
|
||||||
|
expect(e.error.code).toBe(RpcExceptionCode.PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,97 +0,0 @@
|
||||||
import { HttpService } from '@nestjs/axios';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { of } from 'rxjs';
|
|
||||||
import { OpaDecisionMaker } from '../../adapters/secondaries/opa.decision-maker';
|
|
||||||
import { Action } from '../../domain/dtos/action.enum';
|
|
||||||
import { Domain } from '../../domain/dtos/domain.enum';
|
|
||||||
|
|
||||||
const mockHttpService = {
|
|
||||||
post: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return of({
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
decision_id: '96d99d44-e0a6-458e-a656-de2a400d60a8',
|
|
||||||
result: {
|
|
||||||
allow: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return of({
|
|
||||||
status: 200,
|
|
||||||
data: {
|
|
||||||
decision_id: '96d99d44-e0a6-458e-a656-de2a400d60a9',
|
|
||||||
result: {
|
|
||||||
allow: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new Error();
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockConfigService = {
|
|
||||||
get: jest.fn().mockResolvedValue({
|
|
||||||
OPA_URL: 'http://url/',
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('OpaDecisionMaker', () => {
|
|
||||||
let opaDecisionMaker: OpaDecisionMaker;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: HttpService,
|
|
||||||
useValue: mockHttpService,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ConfigService,
|
|
||||||
useValue: mockConfigService,
|
|
||||||
},
|
|
||||||
OpaDecisionMaker,
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
opaDecisionMaker = module.get<OpaDecisionMaker>(OpaDecisionMaker);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(opaDecisionMaker).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('execute', () => {
|
|
||||||
it('should return a truthy authorization', async () => {
|
|
||||||
const authorization = await opaDecisionMaker.decide(
|
|
||||||
Domain.USER,
|
|
||||||
Action.READ,
|
|
||||||
[{ name: 'uuid', value: 'bb281075-1b98-4456-89d6-c643d3044a91' }],
|
|
||||||
);
|
|
||||||
expect(authorization.allow).toBeTruthy();
|
|
||||||
});
|
|
||||||
it('should return a falsy authorization', async () => {
|
|
||||||
const authorization = await opaDecisionMaker.decide(
|
|
||||||
Domain.USER,
|
|
||||||
Action.READ,
|
|
||||||
[{ name: 'uuid', value: 'bb281075-1b98-4456-89d6-c643d3044a91' }],
|
|
||||||
);
|
|
||||||
expect(authorization.allow).toBeFalsy();
|
|
||||||
});
|
|
||||||
it('should return a falsy authorization when an error happens', async () => {
|
|
||||||
const authorization = await opaDecisionMaker.decide(
|
|
||||||
Domain.USER,
|
|
||||||
Action.READ,
|
|
||||||
[{ name: 'uuid', value: 'bb281075-1b98-4456-89d6-c643d3044a91' }],
|
|
||||||
);
|
|
||||||
expect(authorization.allow).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,259 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
import { DatabaseException } from '../../exceptions/database.exception';
|
|
||||||
import { ICollection } from '../../interfaces/collection.interface';
|
|
||||||
import { IRepository } from '../../interfaces/repository.interface';
|
|
||||||
import { PrismaService } from './prisma-service';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Child classes MUST redefined _model property with appropriate model name
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export abstract class PrismaRepository<T> implements IRepository<T> {
|
|
||||||
protected model: string;
|
|
||||||
|
|
||||||
constructor(protected readonly prisma: PrismaService) {}
|
|
||||||
|
|
||||||
findAll = async (
|
|
||||||
page = 1,
|
|
||||||
perPage = 10,
|
|
||||||
where?: any,
|
|
||||||
include?: any,
|
|
||||||
): Promise<ICollection<T>> => {
|
|
||||||
const [data, total] = await this.prisma.$transaction([
|
|
||||||
this.prisma[this.model].findMany({
|
|
||||||
where,
|
|
||||||
include,
|
|
||||||
skip: (page - 1) * perPage,
|
|
||||||
take: perPage,
|
|
||||||
}),
|
|
||||||
this.prisma[this.model].count({
|
|
||||||
where,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
return Promise.resolve({
|
|
||||||
data,
|
|
||||||
total,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
findOneByUuid = async (uuid: string): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const entity = await this.prisma[this.model].findUnique({
|
|
||||||
where: { uuid },
|
|
||||||
});
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
findOne = async (where: any, include?: any): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const entity = await this.prisma[this.model].findFirst({
|
|
||||||
where: where,
|
|
||||||
include: include,
|
|
||||||
});
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO : using any is not good, but needed for nested entities
|
|
||||||
// TODO : Refactor for good clean architecture ?
|
|
||||||
async create(entity: Partial<T> | any, include?: any): Promise<T> {
|
|
||||||
try {
|
|
||||||
const res = await this.prisma[this.model].create({
|
|
||||||
data: entity,
|
|
||||||
include: include,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update = async (uuid: string, entity: Partial<T>): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const updatedEntity = await this.prisma[this.model].update({
|
|
||||||
where: { uuid },
|
|
||||||
data: entity,
|
|
||||||
});
|
|
||||||
return updatedEntity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateWhere = async (
|
|
||||||
where: any,
|
|
||||||
entity: Partial<T> | any,
|
|
||||||
include?: any,
|
|
||||||
): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const updatedEntity = await this.prisma[this.model].update({
|
|
||||||
where: where,
|
|
||||||
data: entity,
|
|
||||||
include: include,
|
|
||||||
});
|
|
||||||
|
|
||||||
return updatedEntity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
delete = async (uuid: string): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const entity = await this.prisma[this.model].delete({
|
|
||||||
where: { uuid },
|
|
||||||
});
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteMany = async (where: any): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const entity = await this.prisma[this.model].deleteMany({
|
|
||||||
where: where,
|
|
||||||
});
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
findAllByQuery = async (
|
|
||||||
include: string[],
|
|
||||||
where: string[],
|
|
||||||
): Promise<ICollection<T>> => {
|
|
||||||
const query = `SELECT ${include.join(',')} FROM ${
|
|
||||||
this.model
|
|
||||||
} WHERE ${where.join(' AND ')}`;
|
|
||||||
const data: T[] = await this.prisma.$queryRawUnsafe(query);
|
|
||||||
return Promise.resolve({
|
|
||||||
data,
|
|
||||||
total: data.length,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
createWithFields = async (fields: object): Promise<number> => {
|
|
||||||
try {
|
|
||||||
const command = `INSERT INTO ${this.model} ("${Object.keys(fields).join(
|
|
||||||
'","',
|
|
||||||
)}") VALUES (${Object.values(fields).join(',')})`;
|
|
||||||
return await this.prisma.$executeRawUnsafe(command);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateWithFields = async (uuid: string, entity: object): Promise<number> => {
|
|
||||||
entity['"updatedAt"'] = `to_timestamp(${Date.now()} / 1000.0)`;
|
|
||||||
const values = Object.keys(entity).map((key) => `${key} = ${entity[key]}`);
|
|
||||||
try {
|
|
||||||
const command = `UPDATE ${this.model} SET ${values.join(
|
|
||||||
', ',
|
|
||||||
)} WHERE uuid = '${uuid}'`;
|
|
||||||
return await this.prisma.$executeRawUnsafe(command);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
healthCheck = async (): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
await this.prisma.$queryRaw`SELECT 1`;
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
|
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PrismaService extends PrismaClient implements OnModuleInit {
|
|
||||||
async onModuleInit() {
|
|
||||||
await this.$connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
async enableShutdownHooks(app: INestApplication) {
|
|
||||||
this.$on('beforeExit', async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { AuthenticationRepository } from '../oldauthentication/adapters/secondaries/authentication.repository';
|
|
||||||
import { UsernameRepository } from '../oldauthentication/adapters/secondaries/username.repository';
|
|
||||||
import { PrismaService } from './adapters/secondaries/prisma-service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [PrismaService, AuthenticationRepository, UsernameRepository],
|
|
||||||
exports: [PrismaService, AuthenticationRepository, UsernameRepository],
|
|
||||||
})
|
|
||||||
export class DatabaseModule {}
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { PrismaRepository } from '../adapters/secondaries/prisma-repository.abstract';
|
|
||||||
|
|
||||||
export class AuthRepository<T> extends PrismaRepository<T> {}
|
|
|
@ -1,24 +0,0 @@
|
||||||
export class DatabaseException implements Error {
|
|
||||||
name: string;
|
|
||||||
message: string;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private _type: string = 'unknown',
|
|
||||||
private _code: string = '',
|
|
||||||
message?: string,
|
|
||||||
) {
|
|
||||||
this.name = 'DatabaseException';
|
|
||||||
this.message = message ?? 'An error occured with the database.';
|
|
||||||
if (this.message.includes('Unique constraint failed')) {
|
|
||||||
this.message = 'Already exists.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get type(): string {
|
|
||||||
return this._type;
|
|
||||||
}
|
|
||||||
|
|
||||||
get code(): string {
|
|
||||||
return this._code;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export interface ICollection<T> {
|
|
||||||
data: T[];
|
|
||||||
total: number;
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { ICollection } from './collection.interface';
|
|
||||||
|
|
||||||
export interface IRepository<T> {
|
|
||||||
findAll(
|
|
||||||
page: number,
|
|
||||||
perPage: number,
|
|
||||||
params?: any,
|
|
||||||
include?: any,
|
|
||||||
): Promise<ICollection<T>>;
|
|
||||||
findOne(where: any, include?: any): Promise<T>;
|
|
||||||
findOneByUuid(uuid: string, include?: any): Promise<T>;
|
|
||||||
create(entity: Partial<T> | any, include?: any): Promise<T>;
|
|
||||||
update(uuid: string, entity: Partial<T>, include?: any): Promise<T>;
|
|
||||||
updateWhere(where: any, entity: Partial<T> | any, include?: any): Promise<T>;
|
|
||||||
delete(uuid: string): Promise<T>;
|
|
||||||
deleteMany(where: any): Promise<void>;
|
|
||||||
}
|
|
|
@ -1,571 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { PrismaService } from '../../adapters/secondaries/prisma-service';
|
|
||||||
import { PrismaRepository } from '../../adapters/secondaries/prisma-repository.abstract';
|
|
||||||
import { DatabaseException } from '../../exceptions/database.exception';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
|
|
||||||
class FakeEntity {
|
|
||||||
uuid?: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let entityId = 2;
|
|
||||||
const entityUuid = 'uuid-';
|
|
||||||
const entityName = 'name-';
|
|
||||||
|
|
||||||
const createRandomEntity = (): FakeEntity => {
|
|
||||||
const entity: FakeEntity = {
|
|
||||||
uuid: `${entityUuid}${entityId}`,
|
|
||||||
name: `${entityName}${entityId}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
entityId++;
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fakeEntityToCreate: FakeEntity = {
|
|
||||||
name: 'test',
|
|
||||||
};
|
|
||||||
|
|
||||||
const fakeEntityCreated: FakeEntity = {
|
|
||||||
...fakeEntityToCreate,
|
|
||||||
uuid: 'some-uuid',
|
|
||||||
};
|
|
||||||
|
|
||||||
const fakeEntities: FakeEntity[] = [];
|
|
||||||
Array.from({ length: 10 }).forEach(() => {
|
|
||||||
fakeEntities.push(createRandomEntity());
|
|
||||||
});
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
class FakePrismaRepository extends PrismaRepository<FakeEntity> {
|
|
||||||
protected model = 'fake';
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakePrismaService extends PrismaService {
|
|
||||||
fake: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockPrismaService = {
|
|
||||||
$transaction: jest.fn().mockImplementation(async (data: any) => {
|
|
||||||
const entities = await data[0];
|
|
||||||
if (entities.length == 1) {
|
|
||||||
return Promise.resolve([[fakeEntityCreated], 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve([fakeEntities, fakeEntities.length]);
|
|
||||||
}),
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
$queryRawUnsafe: jest.fn().mockImplementation((query?: string) => {
|
|
||||||
return Promise.resolve(fakeEntities);
|
|
||||||
}),
|
|
||||||
$executeRawUnsafe: jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce(fakeEntityCreated)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((fields: object) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((fields: object) => {
|
|
||||||
throw new Error('an unknown error');
|
|
||||||
})
|
|
||||||
.mockResolvedValueOnce(fakeEntityCreated)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((fields: object) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((fields: object) => {
|
|
||||||
throw new Error('an unknown error');
|
|
||||||
}),
|
|
||||||
$queryRaw: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.mockImplementation(() => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('Database unavailable', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
fake: {
|
|
||||||
create: jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce(fakeEntityCreated)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Error('an unknown error');
|
|
||||||
}),
|
|
||||||
|
|
||||||
findMany: jest.fn().mockImplementation((params?: any) => {
|
|
||||||
if (params?.where?.limit == 1) {
|
|
||||||
return Promise.resolve([fakeEntityCreated]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(fakeEntities);
|
|
||||||
}),
|
|
||||||
count: jest.fn().mockResolvedValue(fakeEntities.length),
|
|
||||||
|
|
||||||
findUnique: jest.fn().mockImplementation(async (params?: any) => {
|
|
||||||
let entity;
|
|
||||||
|
|
||||||
if (params?.where?.uuid) {
|
|
||||||
entity = fakeEntities.find(
|
|
||||||
(entity) => entity.uuid === params?.where?.uuid,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entity && params?.where?.uuid == 'unknown') {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
} else if (!entity) {
|
|
||||||
throw new Error('no entity');
|
|
||||||
}
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
}),
|
|
||||||
|
|
||||||
findFirst: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
if (params?.where?.name) {
|
|
||||||
return Promise.resolve(
|
|
||||||
fakeEntities.find((entity) => entity.name === params?.where?.name),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Error('an unknown error');
|
|
||||||
}),
|
|
||||||
|
|
||||||
update: jest
|
|
||||||
.fn()
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementationOnce((params: any) => {
|
|
||||||
const entity = fakeEntities.find(
|
|
||||||
(entity) => entity.name === params.where.name,
|
|
||||||
);
|
|
||||||
Object.entries(params.data).map(([key, value]) => {
|
|
||||||
entity[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.resolve(entity);
|
|
||||||
})
|
|
||||||
.mockImplementation((params: any) => {
|
|
||||||
const entity = fakeEntities.find(
|
|
||||||
(entity) => entity.uuid === params.where.uuid,
|
|
||||||
);
|
|
||||||
Object.entries(params.data).map(([key, value]) => {
|
|
||||||
entity[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.resolve(entity);
|
|
||||||
}),
|
|
||||||
|
|
||||||
delete: jest
|
|
||||||
.fn()
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementation((params: any) => {
|
|
||||||
let found = false;
|
|
||||||
|
|
||||||
fakeEntities.forEach((entity, index) => {
|
|
||||||
if (entity.uuid === params?.where?.uuid) {
|
|
||||||
found = true;
|
|
||||||
fakeEntities.splice(index, 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
deleteMany: jest
|
|
||||||
.fn()
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementation((params: any) => {
|
|
||||||
let found = false;
|
|
||||||
|
|
||||||
fakeEntities.forEach((entity, index) => {
|
|
||||||
if (entity.uuid === params?.where?.uuid) {
|
|
||||||
found = true;
|
|
||||||
fakeEntities.splice(index, 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('PrismaRepository', () => {
|
|
||||||
let fakeRepository: FakePrismaRepository;
|
|
||||||
let prisma: FakePrismaService;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
FakePrismaRepository,
|
|
||||||
{
|
|
||||||
provide: PrismaService,
|
|
||||||
useValue: mockPrismaService,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
fakeRepository = module.get<FakePrismaRepository>(FakePrismaRepository);
|
|
||||||
prisma = module.get<PrismaService>(PrismaService) as FakePrismaService;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(fakeRepository).toBeDefined();
|
|
||||||
expect(prisma).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findAll', () => {
|
|
||||||
it('should return an array of entities', async () => {
|
|
||||||
jest.spyOn(prisma.fake, 'findMany');
|
|
||||||
jest.spyOn(prisma.fake, 'count');
|
|
||||||
jest.spyOn(prisma, '$transaction');
|
|
||||||
|
|
||||||
const entities = await fakeRepository.findAll();
|
|
||||||
expect(entities).toStrictEqual({
|
|
||||||
data: fakeEntities,
|
|
||||||
total: fakeEntities.length,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return an array containing only one entity', async () => {
|
|
||||||
const entities = await fakeRepository.findAll(1, 10, { limit: 1 });
|
|
||||||
|
|
||||||
expect(prisma.fake.findMany).toHaveBeenCalledWith({
|
|
||||||
skip: 0,
|
|
||||||
take: 10,
|
|
||||||
where: { limit: 1 },
|
|
||||||
});
|
|
||||||
expect(entities).toEqual({
|
|
||||||
data: [fakeEntityCreated],
|
|
||||||
total: 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('create', () => {
|
|
||||||
it('should create an entity', async () => {
|
|
||||||
jest.spyOn(prisma.fake, 'create');
|
|
||||||
|
|
||||||
const newEntity = await fakeRepository.create(fakeEntityToCreate);
|
|
||||||
expect(newEntity).toBe(fakeEntityCreated);
|
|
||||||
expect(prisma.fake.create).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.create(fakeEntityToCreate),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.create(fakeEntityToCreate),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findOneByUuid', () => {
|
|
||||||
it('should find an entity by uuid', async () => {
|
|
||||||
const entity = await fakeRepository.findOneByUuid(fakeEntities[0].uuid);
|
|
||||||
expect(entity).toBe(fakeEntities[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.findOneByUuid('unknown'),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.findOneByUuid('wrong-uuid'),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findOne', () => {
|
|
||||||
it('should find one entity', async () => {
|
|
||||||
const entity = await fakeRepository.findOne({
|
|
||||||
name: fakeEntities[0].name,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(entity.name).toBe(fakeEntities[0].name);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.findOne({
|
|
||||||
name: fakeEntities[0].name,
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for unknown error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.findOne({
|
|
||||||
name: fakeEntities[0].name,
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('update', () => {
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.update('fake-uuid', { name: 'error' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
await expect(
|
|
||||||
fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update an entity with name', async () => {
|
|
||||||
const newName = 'new-random-name';
|
|
||||||
|
|
||||||
await fakeRepository.updateWhere(
|
|
||||||
{ name: fakeEntities[0].name },
|
|
||||||
{
|
|
||||||
name: newName,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(fakeEntities[0].name).toBe(newName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update an entity with uuid', async () => {
|
|
||||||
const newName = 'random-name';
|
|
||||||
|
|
||||||
await fakeRepository.update(fakeEntities[0].uuid, {
|
|
||||||
name: newName,
|
|
||||||
});
|
|
||||||
expect(fakeEntities[0].name).toBe(newName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw an exception if an entity doesn't exist", async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.update('fake-uuid', { name: 'error' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
await expect(
|
|
||||||
fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('delete', () => {
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf(
|
|
||||||
DatabaseException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete an entity', async () => {
|
|
||||||
const savedUuid = fakeEntities[0].uuid;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const res = await fakeRepository.delete(savedUuid);
|
|
||||||
|
|
||||||
const deletedEntity = fakeEntities.find(
|
|
||||||
(entity) => entity.uuid === savedUuid,
|
|
||||||
);
|
|
||||||
expect(deletedEntity).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw an exception if an entity doesn't exist", async () => {
|
|
||||||
await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf(
|
|
||||||
DatabaseException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('deleteMany', () => {
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.deleteMany({ uuid: 'fake-uuid' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete entities based on their uuid', async () => {
|
|
||||||
const savedUuid = fakeEntities[0].uuid;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const res = await fakeRepository.deleteMany({ uuid: savedUuid });
|
|
||||||
|
|
||||||
const deletedEntity = fakeEntities.find(
|
|
||||||
(entity) => entity.uuid === savedUuid,
|
|
||||||
);
|
|
||||||
expect(deletedEntity).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw an exception if an entity doesn't exist", async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.deleteMany({ uuid: 'fake-uuid' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findAllByquery', () => {
|
|
||||||
it('should return an array of entities', async () => {
|
|
||||||
const entities = await fakeRepository.findAllByQuery(
|
|
||||||
['uuid', 'name'],
|
|
||||||
['name is not null'],
|
|
||||||
);
|
|
||||||
expect(entities).toStrictEqual({
|
|
||||||
data: fakeEntities,
|
|
||||||
total: fakeEntities.length,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('createWithFields', () => {
|
|
||||||
it('should create an entity', async () => {
|
|
||||||
jest.spyOn(prisma, '$queryRawUnsafe');
|
|
||||||
|
|
||||||
const newEntity = await fakeRepository.createWithFields({
|
|
||||||
uuid: '804319b3-a09b-4491-9f82-7976bfce0aff',
|
|
||||||
name: 'my-name',
|
|
||||||
});
|
|
||||||
expect(newEntity).toBe(fakeEntityCreated);
|
|
||||||
expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.createWithFields({
|
|
||||||
uuid: '804319b3-a09b-4491-9f82-7976bfce0aff',
|
|
||||||
name: 'my-name',
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.createWithFields({
|
|
||||||
name: 'my-name',
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateWithFields', () => {
|
|
||||||
it('should update an entity', async () => {
|
|
||||||
jest.spyOn(prisma, '$queryRawUnsafe');
|
|
||||||
|
|
||||||
const updatedEntity = await fakeRepository.updateWithFields(
|
|
||||||
'804319b3-a09b-4491-9f82-7976bfce0aff',
|
|
||||||
{
|
|
||||||
name: 'my-name',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(updatedEntity).toBe(fakeEntityCreated);
|
|
||||||
expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.updateWithFields(
|
|
||||||
'804319b3-a09b-4491-9f82-7976bfce0aff',
|
|
||||||
{
|
|
||||||
name: 'my-name',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.updateWithFields(
|
|
||||||
'804319b3-a09b-4491-9f82-7976bfce0aff',
|
|
||||||
{
|
|
||||||
name: 'my-name',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('healthCheck', () => {
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
|
|
||||||
DatabaseException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a healthy result', async () => {
|
|
||||||
const res = await fakeRepository.healthCheck();
|
|
||||||
expect(res).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an exception if database is not available', async () => {
|
|
||||||
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
|
|
||||||
DatabaseException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { Controller, Get } from '@nestjs/common';
|
|
||||||
import {
|
|
||||||
HealthCheckService,
|
|
||||||
HealthCheck,
|
|
||||||
HealthCheckResult,
|
|
||||||
} from '@nestjs/terminus';
|
|
||||||
import { Messager } from '../secondaries/messager';
|
|
||||||
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
|
|
||||||
|
|
||||||
// this controller responds to rest GET /health
|
|
||||||
@Controller('health')
|
|
||||||
export class HealthController {
|
|
||||||
constructor(
|
|
||||||
private readonly prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
|
|
||||||
private healthCheckService: HealthCheckService,
|
|
||||||
private messager: Messager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@HealthCheck()
|
|
||||||
async check() {
|
|
||||||
try {
|
|
||||||
return await this.healthCheckService.check([
|
|
||||||
async () => this.prismaHealthIndicatorUseCase.isHealthy('prisma'),
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
|
||||||
const healthCheckResult: HealthCheckResult = error.response;
|
|
||||||
this.messager.publish(
|
|
||||||
'logging.auth.health.crit',
|
|
||||||
JSON.stringify(healthCheckResult.error),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export abstract class IMessageBroker {
|
|
||||||
exchange: string;
|
|
||||||
|
|
||||||
constructor(exchange: string) {
|
|
||||||
this.exchange = exchange;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract publish(routingKey: string, message: string): void;
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { IMessageBroker } from './message-broker';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class Messager extends IMessageBroker {
|
|
||||||
constructor(
|
|
||||||
private readonly amqpConnection: AmqpConnection,
|
|
||||||
configService: ConfigService,
|
|
||||||
) {
|
|
||||||
super(configService.get<string>('RMQ_EXCHANGE'));
|
|
||||||
}
|
|
||||||
|
|
||||||
publish = (routingKey: string, message: string): void => {
|
|
||||||
this.amqpConnection.publish(this.exchange, routingKey, message);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface CheckRepositoryPort {
|
||||||
|
healthCheck(): Promise<boolean>;
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
HealthCheckError,
|
||||||
|
HealthCheckResult,
|
||||||
|
HealthIndicator,
|
||||||
|
HealthIndicatorResult,
|
||||||
|
} from '@nestjs/terminus';
|
||||||
|
import { CheckRepositoryPort } from '../ports/check-repository.port';
|
||||||
|
import {
|
||||||
|
AUTHENTICATION_REPOSITORY,
|
||||||
|
USERNAME_REPOSITORY,
|
||||||
|
} from '@modules/health/health.di-tokens';
|
||||||
|
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
|
||||||
|
import { LOGGING_AUTHENTICATION_HEALTH_CRIT } from '@modules/health/health.constants';
|
||||||
|
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||||
|
import { AuthenticationRepositoryPort } from '@modules/authentication/core/application/ports/authentication.repository.port';
|
||||||
|
import { UsernameRepositoryPort } from '@modules/authentication/core/application/ports/username.repository.port';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RepositoriesHealthIndicatorUseCase extends HealthIndicator {
|
||||||
|
private _checkRepositories: CheckRepositoryPort[];
|
||||||
|
constructor(
|
||||||
|
@Inject(AUTHENTICATION_REPOSITORY)
|
||||||
|
private readonly authenticationRepository: AuthenticationRepositoryPort,
|
||||||
|
@Inject(USERNAME_REPOSITORY)
|
||||||
|
private readonly usernameRepository: UsernameRepositoryPort,
|
||||||
|
@Inject(MESSAGE_PUBLISHER)
|
||||||
|
private readonly messagePublisher: MessagePublisherPort,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this._checkRepositories = [authenticationRepository, usernameRepository];
|
||||||
|
}
|
||||||
|
isHealthy = async (key: string): Promise<HealthIndicatorResult> => {
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
this._checkRepositories.map(
|
||||||
|
async (checkRepository: CheckRepositoryPort) => {
|
||||||
|
await checkRepository.healthCheck();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return this.getStatus(key, true);
|
||||||
|
} catch (error) {
|
||||||
|
const healthCheckResult: HealthCheckResult = error;
|
||||||
|
this.messagePublisher.publish(
|
||||||
|
LOGGING_AUTHENTICATION_HEALTH_CRIT,
|
||||||
|
JSON.stringify(healthCheckResult.error),
|
||||||
|
);
|
||||||
|
throw new HealthCheckError('Repository', {
|
||||||
|
repository: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,25 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import {
|
|
||||||
HealthCheckError,
|
|
||||||
HealthIndicator,
|
|
||||||
HealthIndicatorResult,
|
|
||||||
} from '@nestjs/terminus';
|
|
||||||
import { AuthenticationRepository } from '../../../oldauthentication/adapters/secondaries/authentication.repository';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PrismaHealthIndicatorUseCase extends HealthIndicator {
|
|
||||||
constructor(private readonly repository: AuthenticationRepository) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
isHealthy = async (key: string): Promise<HealthIndicatorResult> => {
|
|
||||||
try {
|
|
||||||
await this.repository.healthCheck();
|
|
||||||
return this.getStatus(key, true);
|
|
||||||
} catch (e) {
|
|
||||||
throw new HealthCheckError('Prisma', {
|
|
||||||
prisma: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -0,0 +1 @@
|
||||||
|
export const LOGGING_AUTHENTICATION_HEALTH_CRIT = 'logging.auth.health.crit';
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const AUTHENTICATION_REPOSITORY = Symbol('AUTHENTICATION_REPOSITORY');
|
||||||
|
export const USERNAME_REPOSITORY = Symbol('USERNAME_REPOSITORY');
|
|
@ -1,34 +1,45 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module, Provider } from '@nestjs/common';
|
||||||
import { HealthServerController } from './adapters/primaries/health-server.controller';
|
import { HealthHttpController } from './interface/http-controllers/health.http.controller';
|
||||||
import { PrismaHealthIndicatorUseCase } from './domain/usecases/prisma.health-indicator.usecase';
|
|
||||||
import { AuthenticationRepository } from '../oldauthentication/adapters/secondaries/authentication.repository';
|
|
||||||
import { DatabaseModule } from '../database/database.module';
|
|
||||||
import { HealthController } from './adapters/primaries/health.controller';
|
|
||||||
import { TerminusModule } from '@nestjs/terminus';
|
import { TerminusModule } from '@nestjs/terminus';
|
||||||
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
|
import { RepositoriesHealthIndicatorUseCase } from './core/application/usecases/repositories.health-indicator.usecase';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { HealthGrpcController } from './interface/grpc-controllers/health.grpc.controller';
|
||||||
import { Messager } from './adapters/secondaries/messager';
|
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||||
|
import { AuthenticationRepository } from '@modules/authentication/infrastructure/authentication.repository';
|
||||||
|
import {
|
||||||
|
AUTHENTICATION_REPOSITORY,
|
||||||
|
USERNAME_REPOSITORY,
|
||||||
|
} from './health.di-tokens';
|
||||||
|
import { UsernameRepository } from '@modules/authentication/infrastructure/username.repository';
|
||||||
|
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
|
||||||
|
import { AuthenticationModule } from '@modules/authentication/authentication.module';
|
||||||
|
|
||||||
|
const grpcControllers = [HealthGrpcController];
|
||||||
|
|
||||||
|
const httpControllers = [HealthHttpController];
|
||||||
|
|
||||||
|
const useCases: Provider[] = [RepositoriesHealthIndicatorUseCase];
|
||||||
|
|
||||||
|
const repositories: Provider[] = [
|
||||||
|
{
|
||||||
|
provide: AUTHENTICATION_REPOSITORY,
|
||||||
|
useClass: AuthenticationRepository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: USERNAME_REPOSITORY,
|
||||||
|
useClass: UsernameRepository,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const messageBrokers: Provider[] = [
|
||||||
|
{
|
||||||
|
provide: MESSAGE_PUBLISHER,
|
||||||
|
useClass: MessageBrokerPublisher,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [TerminusModule, AuthenticationModule],
|
||||||
TerminusModule,
|
controllers: [...grpcControllers, ...httpControllers],
|
||||||
RabbitMQModule.forRootAsync(RabbitMQModule, {
|
providers: [...useCases, ...repositories, ...messageBrokers],
|
||||||
imports: [ConfigModule],
|
|
||||||
useFactory: async (configService: ConfigService) => ({
|
|
||||||
exchanges: [
|
|
||||||
{
|
|
||||||
name: configService.get<string>('RMQ_EXCHANGE'),
|
|
||||||
type: 'topic',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
uri: configService.get<string>('RMQ_URI'),
|
|
||||||
connectionInitOptions: { wait: false },
|
|
||||||
}),
|
|
||||||
inject: [ConfigService],
|
|
||||||
}),
|
|
||||||
DatabaseModule,
|
|
||||||
],
|
|
||||||
controllers: [HealthServerController, HealthController],
|
|
||||||
providers: [PrismaHealthIndicatorUseCase, AuthenticationRepository, Messager],
|
|
||||||
})
|
})
|
||||||
export class HealthModule {}
|
export class HealthModule {}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Controller } from '@nestjs/common';
|
import { Controller } from '@nestjs/common';
|
||||||
import { GrpcMethod } from '@nestjs/microservices';
|
import { GrpcMethod } from '@nestjs/microservices';
|
||||||
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
|
import { RepositoriesHealthIndicatorUseCase } from '../../core/application/usecases/repositories.health-indicator.usecase';
|
||||||
|
|
||||||
enum ServingStatus {
|
export enum ServingStatus {
|
||||||
UNKNOWN = 0,
|
UNKNOWN = 0,
|
||||||
SERVING = 1,
|
SERVING = 1,
|
||||||
NOT_SERVING = 2,
|
NOT_SERVING = 2,
|
||||||
|
@ -16,26 +16,25 @@ interface HealthCheckResponse {
|
||||||
status: ServingStatus;
|
status: ServingStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this controller responds to gRPC health check service
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class HealthServerController {
|
export class HealthGrpcController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
|
private readonly repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@GrpcMethod('Health', 'Check')
|
@GrpcMethod('Health', 'Check')
|
||||||
async check(
|
async check(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
data: HealthCheckRequest,
|
data?: HealthCheckRequest,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
metadata: any,
|
metadata?: any,
|
||||||
): Promise<HealthCheckResponse> {
|
): Promise<HealthCheckResponse> {
|
||||||
const healthCheck = await this.prismaHealthIndicatorUseCase.isHealthy(
|
const healthCheck = await this.repositoriesHealthIndicatorUseCase.isHealthy(
|
||||||
'prisma',
|
'repositories',
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
status:
|
status:
|
||||||
healthCheck['prisma'].status == 'up'
|
healthCheck['repositories'].status == 'up'
|
||||||
? ServingStatus.SERVING
|
? ServingStatus.SERVING
|
||||||
: ServingStatus.NOT_SERVING,
|
: ServingStatus.NOT_SERVING,
|
||||||
};
|
};
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { RepositoriesHealthIndicatorUseCase } from '@modules/health/core/application/usecases/repositories.health-indicator.usecase';
|
||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
HealthCheckService,
|
||||||
|
HealthCheck,
|
||||||
|
HealthCheckResult,
|
||||||
|
} from '@nestjs/terminus';
|
||||||
|
|
||||||
|
@Controller('health')
|
||||||
|
export class HealthHttpController {
|
||||||
|
constructor(
|
||||||
|
private readonly repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase,
|
||||||
|
private readonly healthCheckService: HealthCheckService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@HealthCheck()
|
||||||
|
async check(): Promise<HealthCheckResult> {
|
||||||
|
try {
|
||||||
|
return await this.healthCheckService.check([
|
||||||
|
async () =>
|
||||||
|
this.repositoriesHealthIndicatorUseCase.isHealthy('repositories'),
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { RepositoriesHealthIndicatorUseCase } from '@modules/health/core/application/usecases/repositories.health-indicator.usecase';
|
||||||
|
import {
|
||||||
|
HealthGrpcController,
|
||||||
|
ServingStatus,
|
||||||
|
} from '@modules/health/interface/grpc-controllers/health.grpc.controller';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
const mockRepositoriesHealthIndicatorUseCase = {
|
||||||
|
isHealthy: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() => ({
|
||||||
|
repositories: {
|
||||||
|
status: 'up',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.mockImplementationOnce(() => ({
|
||||||
|
repositories: {
|
||||||
|
status: 'down',
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Health Grpc Controller', () => {
|
||||||
|
let healthGrpcController: HealthGrpcController;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: RepositoriesHealthIndicatorUseCase,
|
||||||
|
useValue: mockRepositoriesHealthIndicatorUseCase,
|
||||||
|
},
|
||||||
|
HealthGrpcController,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
healthGrpcController =
|
||||||
|
module.get<HealthGrpcController>(HealthGrpcController);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(healthGrpcController).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a Serving status ', async () => {
|
||||||
|
jest.spyOn(mockRepositoriesHealthIndicatorUseCase, 'isHealthy');
|
||||||
|
const servingStatus: { status: ServingStatus } =
|
||||||
|
await healthGrpcController.check();
|
||||||
|
expect(servingStatus).toEqual({
|
||||||
|
status: ServingStatus.SERVING,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
mockRepositoriesHealthIndicatorUseCase.isHealthy,
|
||||||
|
).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a Not Serving status ', async () => {
|
||||||
|
jest.spyOn(mockRepositoriesHealthIndicatorUseCase, 'isHealthy');
|
||||||
|
const servingStatus: { status: ServingStatus } =
|
||||||
|
await healthGrpcController.check();
|
||||||
|
expect(servingStatus).toEqual({
|
||||||
|
status: ServingStatus.NOT_SERVING,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
mockRepositoriesHealthIndicatorUseCase.isHealthy,
|
||||||
|
).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { RepositoriesHealthIndicatorUseCase } from '@modules/health/core/application/usecases/repositories.health-indicator.usecase';
|
||||||
|
import { HealthHttpController } from '@modules/health/interface/http-controllers/health.http.controller';
|
||||||
|
import { HealthCheckResult, HealthCheckService } from '@nestjs/terminus';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
const mockHealthCheckService = {
|
||||||
|
check: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() => ({
|
||||||
|
status: 'ok',
|
||||||
|
info: {
|
||||||
|
repositories: {
|
||||||
|
status: 'up',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: {},
|
||||||
|
details: {
|
||||||
|
repositories: {
|
||||||
|
status: 'up',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.mockImplementationOnce(() => ({
|
||||||
|
status: 'error',
|
||||||
|
info: {},
|
||||||
|
error: {
|
||||||
|
repository:
|
||||||
|
"\nInvalid `prisma.$queryRaw()` invocation:\n\n\nCan't reach database server at `v3-db`:`5432`\n\nPlease make sure your database server is running at `v3-db`:`5432`.",
|
||||||
|
},
|
||||||
|
details: {
|
||||||
|
repository:
|
||||||
|
"\nInvalid `prisma.$queryRaw()` invocation:\n\n\nCan't reach database server at `v3-db`:`5432`\n\nPlease make sure your database server is running at `v3-db`:`5432`.",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockRepositoriesHealthIndicatorUseCase = {
|
||||||
|
isHealthy: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Health Http Controller', () => {
|
||||||
|
let healthHttpController: HealthHttpController;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: HealthCheckService,
|
||||||
|
useValue: mockHealthCheckService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: RepositoriesHealthIndicatorUseCase,
|
||||||
|
useValue: mockRepositoriesHealthIndicatorUseCase,
|
||||||
|
},
|
||||||
|
HealthHttpController,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
healthHttpController =
|
||||||
|
module.get<HealthHttpController>(HealthHttpController);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(healthHttpController).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an HealthCheckResult with Ok status ', async () => {
|
||||||
|
jest.spyOn(mockHealthCheckService, 'check');
|
||||||
|
jest.spyOn(mockRepositoriesHealthIndicatorUseCase, 'isHealthy');
|
||||||
|
|
||||||
|
const healthCheckResult: HealthCheckResult =
|
||||||
|
await healthHttpController.check();
|
||||||
|
expect(healthCheckResult.status).toBe('ok');
|
||||||
|
expect(mockHealthCheckService.check).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an HealthCheckResult with Error status ', async () => {
|
||||||
|
jest.spyOn(mockHealthCheckService, 'check');
|
||||||
|
jest.spyOn(mockRepositoriesHealthIndicatorUseCase, 'isHealthy');
|
||||||
|
|
||||||
|
const healthCheckResult: HealthCheckResult =
|
||||||
|
await healthHttpController.check();
|
||||||
|
expect(healthCheckResult.status).toBe('error');
|
||||||
|
expect(mockHealthCheckService.check).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,47 +0,0 @@
|
||||||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
|
|
||||||
const mockAmqpConnection = {
|
|
||||||
publish: jest.fn().mockImplementation(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockConfigService = {
|
|
||||||
get: jest.fn().mockResolvedValue({
|
|
||||||
RMQ_EXCHANGE: 'mobicoop',
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Messager', () => {
|
|
||||||
let messager: Messager;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [],
|
|
||||||
providers: [
|
|
||||||
Messager,
|
|
||||||
{
|
|
||||||
provide: AmqpConnection,
|
|
||||||
useValue: mockAmqpConnection,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ConfigService,
|
|
||||||
useValue: mockConfigService,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
messager = module.get<Messager>(Messager);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(messager).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should publish a message', async () => {
|
|
||||||
jest.spyOn(mockAmqpConnection, 'publish');
|
|
||||||
messager.publish('test.create.info', 'my-test');
|
|
||||||
expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,58 +0,0 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
|
|
||||||
import { AuthenticationRepository } from '../../../oldauthentication/adapters/secondaries/authentication.repository';
|
|
||||||
import { PrismaClientKnownRequestError } from '@prisma/client/runtime';
|
|
||||||
import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus';
|
|
||||||
|
|
||||||
const mockAuthenticationRepository = {
|
|
||||||
healthCheck: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return Promise.resolve(true);
|
|
||||||
})
|
|
||||||
.mockImplementation(() => {
|
|
||||||
throw new PrismaClientKnownRequestError('Service unavailable', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('PrismaHealthIndicatorUseCase', () => {
|
|
||||||
let prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: AuthenticationRepository,
|
|
||||||
useValue: mockAuthenticationRepository,
|
|
||||||
},
|
|
||||||
PrismaHealthIndicatorUseCase,
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
prismaHealthIndicatorUseCase = module.get<PrismaHealthIndicatorUseCase>(
|
|
||||||
PrismaHealthIndicatorUseCase,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(prismaHealthIndicatorUseCase).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('execute', () => {
|
|
||||||
it('should check health successfully', async () => {
|
|
||||||
const healthIndicatorResult: HealthIndicatorResult =
|
|
||||||
await prismaHealthIndicatorUseCase.isHealthy('prisma');
|
|
||||||
|
|
||||||
expect(healthIndicatorResult['prisma'].status).toBe('up');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if database is unavailable', async () => {
|
|
||||||
await expect(
|
|
||||||
prismaHealthIndicatorUseCase.isHealthy('prisma'),
|
|
||||||
).rejects.toBeInstanceOf(HealthCheckError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus';
|
||||||
|
import { RepositoriesHealthIndicatorUseCase } from '../../core/application/usecases/repositories.health-indicator.usecase';
|
||||||
|
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
|
||||||
|
import { DatabaseErrorException } from '@mobicoop/ddd-library';
|
||||||
|
import {
|
||||||
|
AUTHENTICATION_REPOSITORY,
|
||||||
|
USERNAME_REPOSITORY,
|
||||||
|
} from '@modules/health/health.di-tokens';
|
||||||
|
|
||||||
|
const mockAuthenticationRepository = {
|
||||||
|
healthCheck: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
})
|
||||||
|
.mockImplementation(() => {
|
||||||
|
throw new DatabaseErrorException('An error occured in the database');
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockUsernameRepository = {
|
||||||
|
healthCheck: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
})
|
||||||
|
.mockImplementation(() => {
|
||||||
|
throw new DatabaseErrorException('An error occured in the database');
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockMessagePublisher = {
|
||||||
|
publish: jest.fn().mockImplementation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('RepositoriesHealthIndicatorUseCase', () => {
|
||||||
|
let repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
RepositoriesHealthIndicatorUseCase,
|
||||||
|
{
|
||||||
|
provide: AUTHENTICATION_REPOSITORY,
|
||||||
|
useValue: mockAuthenticationRepository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: USERNAME_REPOSITORY,
|
||||||
|
useValue: mockUsernameRepository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: MESSAGE_PUBLISHER,
|
||||||
|
useValue: mockMessagePublisher,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
repositoriesHealthIndicatorUseCase =
|
||||||
|
module.get<RepositoriesHealthIndicatorUseCase>(
|
||||||
|
RepositoriesHealthIndicatorUseCase,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(repositoriesHealthIndicatorUseCase).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('execute', () => {
|
||||||
|
it('should check health successfully', async () => {
|
||||||
|
const healthIndicatorResult: HealthIndicatorResult =
|
||||||
|
await repositoriesHealthIndicatorUseCase.isHealthy('repositories');
|
||||||
|
expect(healthIndicatorResult['repositories'].status).toBe('up');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if database is unavailable', async () => {
|
||||||
|
jest.spyOn(mockMessagePublisher, 'publish');
|
||||||
|
await expect(
|
||||||
|
repositoriesHealthIndicatorUseCase.isHealthy('repositories'),
|
||||||
|
).rejects.toBeInstanceOf(HealthCheckError);
|
||||||
|
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,52 +0,0 @@
|
||||||
import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq';
|
|
||||||
import { Controller } from '@nestjs/common';
|
|
||||||
import { CommandBus } from '@nestjs/cqrs';
|
|
||||||
import { UpdateUsernameCommand } from '../../commands/update-username.command';
|
|
||||||
import { Type } from '../../domain/dtos/type.enum';
|
|
||||||
import { UpdateUsernameRequest } from '../../domain/dtos/update-username.request';
|
|
||||||
import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request';
|
|
||||||
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
|
|
||||||
|
|
||||||
@Controller()
|
|
||||||
export class AuthenticationMessagerController {
|
|
||||||
constructor(private readonly commandBus: CommandBus) {}
|
|
||||||
|
|
||||||
@RabbitSubscribe({
|
|
||||||
name: 'userUpdate',
|
|
||||||
})
|
|
||||||
public async userUpdatedHandler(message: string) {
|
|
||||||
const updatedUser = JSON.parse(message);
|
|
||||||
if (!updatedUser.hasOwnProperty('uuid')) throw new Error();
|
|
||||||
if (updatedUser.hasOwnProperty('email') && updatedUser.email) {
|
|
||||||
const updateUsernameRequest = new UpdateUsernameRequest();
|
|
||||||
updateUsernameRequest.uuid = updatedUser.uuid;
|
|
||||||
updateUsernameRequest.username = updatedUser.email;
|
|
||||||
updateUsernameRequest.type = Type.EMAIL;
|
|
||||||
await this.commandBus.execute(
|
|
||||||
new UpdateUsernameCommand(updateUsernameRequest),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (updatedUser.hasOwnProperty('phone') && updatedUser.phone) {
|
|
||||||
const updateUsernameRequest = new UpdateUsernameRequest();
|
|
||||||
updateUsernameRequest.uuid = updatedUser.uuid;
|
|
||||||
updateUsernameRequest.username = updatedUser.phone;
|
|
||||||
updateUsernameRequest.type = Type.PHONE;
|
|
||||||
await this.commandBus.execute(
|
|
||||||
new UpdateUsernameCommand(updateUsernameRequest),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RabbitSubscribe({
|
|
||||||
name: 'userDelete',
|
|
||||||
})
|
|
||||||
public async userDeletedHandler(message: string) {
|
|
||||||
const deletedUser = JSON.parse(message);
|
|
||||||
if (!deletedUser.hasOwnProperty('uuid')) throw new Error();
|
|
||||||
const deleteAuthenticationRequest = new DeleteAuthenticationRequest();
|
|
||||||
deleteAuthenticationRequest.uuid = deletedUser.uuid;
|
|
||||||
await this.commandBus.execute(
|
|
||||||
new DeleteAuthenticationCommand(deleteAuthenticationRequest),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,188 +0,0 @@
|
||||||
import { Mapper } from '@automapper/core';
|
|
||||||
import { InjectMapper } from '@automapper/nestjs';
|
|
||||||
import { Controller, UsePipes } from '@nestjs/common';
|
|
||||||
import { CommandBus, QueryBus } from '@nestjs/cqrs';
|
|
||||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
|
||||||
import { DatabaseException } from 'src/modules/database/exceptions/database.exception';
|
|
||||||
import { AddUsernameCommand } from '../../commands/add-username.command';
|
|
||||||
import { CreateAuthenticationCommand } from '../../commands/create-authentication.command';
|
|
||||||
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
|
|
||||||
import { DeleteUsernameCommand } from '../../commands/delete-username.command';
|
|
||||||
import { UpdatePasswordCommand } from '../../commands/update-password.command';
|
|
||||||
import { UpdateUsernameCommand } from '../../commands/update-username.command';
|
|
||||||
import { AddUsernameRequest } from '../../domain/dtos/add-username.request';
|
|
||||||
import { CreateAuthenticationRequest } from '../../domain/dtos/create-authentication.request';
|
|
||||||
import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request';
|
|
||||||
import { DeleteUsernameRequest } from '../../domain/dtos/delete-username.request';
|
|
||||||
import { UpdatePasswordRequest } from '../../domain/dtos/update-password.request';
|
|
||||||
import { UpdateUsernameRequest } from '../../domain/dtos/update-username.request';
|
|
||||||
import { ValidateAuthenticationRequest } from '../../domain/dtos/validate-authentication.request';
|
|
||||||
import { Authentication } from '../../domain/entities/authentication';
|
|
||||||
import { Username } from '../../domain/entities/username';
|
|
||||||
import { ValidateAuthenticationQuery } from '../../queries/validate-authentication.query';
|
|
||||||
import { AuthenticationPresenter } from './authentication.presenter';
|
|
||||||
import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
|
|
||||||
import { UsernamePresenter } from './username.presenter';
|
|
||||||
|
|
||||||
@UsePipes(
|
|
||||||
new RpcValidationPipe({
|
|
||||||
whitelist: true,
|
|
||||||
forbidUnknownValues: false,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
@Controller()
|
|
||||||
export class AuthenticationController {
|
|
||||||
constructor(
|
|
||||||
private readonly commandBus: CommandBus,
|
|
||||||
private readonly queryBus: QueryBus,
|
|
||||||
@InjectMapper() private readonly mapper: Mapper,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@GrpcMethod('AuthenticationService', 'Validate')
|
|
||||||
async validate(
|
|
||||||
data: ValidateAuthenticationRequest,
|
|
||||||
): Promise<AuthenticationPresenter> {
|
|
||||||
try {
|
|
||||||
const authentication: Authentication = await this.queryBus.execute(
|
|
||||||
new ValidateAuthenticationQuery(data.username, data.password),
|
|
||||||
);
|
|
||||||
return this.mapper.map(
|
|
||||||
authentication,
|
|
||||||
Authentication,
|
|
||||||
AuthenticationPresenter,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 7,
|
|
||||||
message: 'Permission denied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GrpcMethod('AuthenticationService', 'Create')
|
|
||||||
async createUser(
|
|
||||||
data: CreateAuthenticationRequest,
|
|
||||||
): Promise<AuthenticationPresenter> {
|
|
||||||
try {
|
|
||||||
const authentication: Authentication = await this.commandBus.execute(
|
|
||||||
new CreateAuthenticationCommand(data),
|
|
||||||
);
|
|
||||||
return this.mapper.map(
|
|
||||||
authentication,
|
|
||||||
Authentication,
|
|
||||||
AuthenticationPresenter,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof DatabaseException) {
|
|
||||||
if (e.message.includes('Already exists')) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 6,
|
|
||||||
message: 'Auth already exists',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new RpcException({
|
|
||||||
code: 7,
|
|
||||||
message: 'Permission denied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GrpcMethod('AuthenticationService', 'AddUsername')
|
|
||||||
async addUsername(data: AddUsernameRequest): Promise<UsernamePresenter> {
|
|
||||||
try {
|
|
||||||
const username: Username = await this.commandBus.execute(
|
|
||||||
new AddUsernameCommand(data),
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.mapper.map(username, Username, UsernamePresenter);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof DatabaseException) {
|
|
||||||
if (e.message.includes('Already exists')) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 6,
|
|
||||||
message: 'Username already exists',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new RpcException({
|
|
||||||
code: 7,
|
|
||||||
message: 'Permission denied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GrpcMethod('AuthenticationService', 'UpdateUsername')
|
|
||||||
async updateUsername(
|
|
||||||
data: UpdateUsernameRequest,
|
|
||||||
): Promise<UsernamePresenter> {
|
|
||||||
try {
|
|
||||||
const username: Username = await this.commandBus.execute(
|
|
||||||
new UpdateUsernameCommand(data),
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.mapper.map(username, Username, UsernamePresenter);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof DatabaseException) {
|
|
||||||
if (e.message.includes('Already exists')) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 6,
|
|
||||||
message: 'Username already exists',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new RpcException({
|
|
||||||
code: 7,
|
|
||||||
message: 'Permission denied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GrpcMethod('AuthenticationService', 'UpdatePassword')
|
|
||||||
async updatePassword(
|
|
||||||
data: UpdatePasswordRequest,
|
|
||||||
): Promise<AuthenticationPresenter> {
|
|
||||||
try {
|
|
||||||
const authentication: Authentication = await this.commandBus.execute(
|
|
||||||
new UpdatePasswordCommand(data),
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.mapper.map(
|
|
||||||
authentication,
|
|
||||||
Authentication,
|
|
||||||
AuthenticationPresenter,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 7,
|
|
||||||
message: 'Permission denied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GrpcMethod('AuthenticationService', 'DeleteUsername')
|
|
||||||
async deleteUsername(data: DeleteUsernameRequest) {
|
|
||||||
try {
|
|
||||||
return await this.commandBus.execute(new DeleteUsernameCommand(data));
|
|
||||||
} catch (e) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 7,
|
|
||||||
message: 'Permission denied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GrpcMethod('AuthenticationService', 'Delete')
|
|
||||||
async deleteAuthentication(data: DeleteAuthenticationRequest) {
|
|
||||||
try {
|
|
||||||
return await this.commandBus.execute(
|
|
||||||
new DeleteAuthenticationCommand(data),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
throw new RpcException({
|
|
||||||
code: 7,
|
|
||||||
message: 'Permission denied',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
|
|
||||||
export class AuthenticationPresenter {
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
package authentication;
|
|
||||||
|
|
||||||
service AuthenticationService {
|
|
||||||
rpc Validate(AuthenticationByUsernamePassword) returns (Uuid);
|
|
||||||
rpc Create(Authentication) returns (Uuid);
|
|
||||||
rpc AddUsername(Username) returns (Uuid);
|
|
||||||
rpc UpdatePassword(Password) returns (Uuid);
|
|
||||||
rpc UpdateUsername(Username) returns (Uuid);
|
|
||||||
rpc DeleteUsername(Username) returns (Uuid);
|
|
||||||
rpc Delete(Uuid) returns (Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
message AuthenticationByUsernamePassword {
|
|
||||||
string username = 1;
|
|
||||||
string password = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Authentication {
|
|
||||||
string uuid = 1;
|
|
||||||
string username = 2;
|
|
||||||
string password = 3;
|
|
||||||
string type = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Password {
|
|
||||||
string uuid = 1;
|
|
||||||
string password = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Username {
|
|
||||||
string uuid = 1;
|
|
||||||
string username = 2;
|
|
||||||
string type = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Uuid {
|
|
||||||
string uuid = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Empty {}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
|
|
||||||
export class UsernamePresenter {
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
username: string;
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { AuthRepository } from '../../../database/domain/auth-repository';
|
|
||||||
import { Authentication } from '../../domain/entities/authentication';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuthenticationRepository extends AuthRepository<Authentication> {
|
|
||||||
protected model = 'auth';
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { IMessageBroker } from '../../domain/interfaces/message-broker';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class Messager extends IMessageBroker {
|
|
||||||
constructor(
|
|
||||||
private readonly amqpConnection: AmqpConnection,
|
|
||||||
configService: ConfigService,
|
|
||||||
) {
|
|
||||||
super(configService.get<string>('RMQ_EXCHANGE'));
|
|
||||||
}
|
|
||||||
|
|
||||||
publish = (routingKey: string, message: string): void => {
|
|
||||||
this.amqpConnection.publish(this.exchange, routingKey, message);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { AuthRepository } from '../../../database/domain/auth-repository';
|
|
||||||
import { Username } from '../../domain/entities/username';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UsernameRepository extends AuthRepository<Username> {
|
|
||||||
protected model = 'username';
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { CqrsModule } from '@nestjs/cqrs';
|
|
||||||
import { DatabaseModule } from '../database/database.module';
|
|
||||||
import { AuthenticationController } from './adapters/primaries/authentication.controller';
|
|
||||||
import { CreateAuthenticationUseCase } from './domain/usecases/create-authentication.usecase';
|
|
||||||
import { ValidateAuthenticationUseCase } from './domain/usecases/validate-authentication.usecase';
|
|
||||||
import { AuthenticationProfile } from './mappers/authentication.profile';
|
|
||||||
import { AuthenticationRepository } from './adapters/secondaries/authentication.repository';
|
|
||||||
import { UpdateUsernameUseCase } from './domain/usecases/update-username.usecase';
|
|
||||||
import { UsernameProfile } from './mappers/username.profile';
|
|
||||||
import { AddUsernameUseCase } from './domain/usecases/add-username.usecase';
|
|
||||||
import { UpdatePasswordUseCase } from './domain/usecases/update-password.usecase';
|
|
||||||
import { DeleteUsernameUseCase } from './domain/usecases/delete-username.usecase';
|
|
||||||
import { DeleteAuthenticationUseCase } from './domain/usecases/delete-authentication.usecase';
|
|
||||||
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
|
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
||||||
import { AuthenticationMessagerController } from './adapters/primaries/authentication-messager.controller';
|
|
||||||
import { Messager } from './adapters/secondaries/messager';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
DatabaseModule,
|
|
||||||
CqrsModule,
|
|
||||||
RabbitMQModule.forRootAsync(RabbitMQModule, {
|
|
||||||
imports: [ConfigModule],
|
|
||||||
useFactory: async (configService: ConfigService) => ({
|
|
||||||
exchanges: [
|
|
||||||
{
|
|
||||||
name: configService.get<string>('RMQ_EXCHANGE'),
|
|
||||||
type: 'topic',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
handlers: {
|
|
||||||
userUpdate: {
|
|
||||||
exchange: configService.get<string>('RMQ_EXCHANGE'),
|
|
||||||
routingKey: 'user.update',
|
|
||||||
},
|
|
||||||
userDelete: {
|
|
||||||
exchange: configService.get<string>('RMQ_EXCHANGE'),
|
|
||||||
routingKey: 'user.delete',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
uri: configService.get<string>('RMQ_URI'),
|
|
||||||
connectionInitOptions: { wait: false },
|
|
||||||
enableControllerDiscovery: true,
|
|
||||||
}),
|
|
||||||
inject: [ConfigService],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
controllers: [AuthenticationController, AuthenticationMessagerController],
|
|
||||||
providers: [
|
|
||||||
AuthenticationProfile,
|
|
||||||
UsernameProfile,
|
|
||||||
AuthenticationRepository,
|
|
||||||
Messager,
|
|
||||||
ValidateAuthenticationUseCase,
|
|
||||||
CreateAuthenticationUseCase,
|
|
||||||
AddUsernameUseCase,
|
|
||||||
UpdateUsernameUseCase,
|
|
||||||
UpdatePasswordUseCase,
|
|
||||||
DeleteUsernameUseCase,
|
|
||||||
DeleteAuthenticationUseCase,
|
|
||||||
],
|
|
||||||
exports: [],
|
|
||||||
})
|
|
||||||
export class AuthenticationModule {}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { AddUsernameRequest } from '../domain/dtos/add-username.request';
|
|
||||||
|
|
||||||
export class AddUsernameCommand {
|
|
||||||
readonly addUsernameRequest: AddUsernameRequest;
|
|
||||||
|
|
||||||
constructor(request: AddUsernameRequest) {
|
|
||||||
this.addUsernameRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { CreateAuthenticationRequest } from '../domain/dtos/create-authentication.request';
|
|
||||||
|
|
||||||
export class CreateAuthenticationCommand {
|
|
||||||
readonly createAuthenticationRequest: CreateAuthenticationRequest;
|
|
||||||
|
|
||||||
constructor(request: CreateAuthenticationRequest) {
|
|
||||||
this.createAuthenticationRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { DeleteAuthenticationRequest } from '../domain/dtos/delete-authentication.request';
|
|
||||||
|
|
||||||
export class DeleteAuthenticationCommand {
|
|
||||||
readonly deleteAuthenticationRequest: DeleteAuthenticationRequest;
|
|
||||||
|
|
||||||
constructor(request: DeleteAuthenticationRequest) {
|
|
||||||
this.deleteAuthenticationRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { DeleteUsernameRequest } from '../domain/dtos/delete-username.request';
|
|
||||||
|
|
||||||
export class DeleteUsernameCommand {
|
|
||||||
readonly deleteUsernameRequest: DeleteUsernameRequest;
|
|
||||||
|
|
||||||
constructor(request: DeleteUsernameRequest) {
|
|
||||||
this.deleteUsernameRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { UpdatePasswordRequest } from '../domain/dtos/update-password.request';
|
|
||||||
|
|
||||||
export class UpdatePasswordCommand {
|
|
||||||
readonly updatePasswordRequest: UpdatePasswordRequest;
|
|
||||||
|
|
||||||
constructor(request: UpdatePasswordRequest) {
|
|
||||||
this.updatePasswordRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { UpdateUsernameRequest } from '../domain/dtos/update-username.request';
|
|
||||||
|
|
||||||
export class UpdateUsernameCommand {
|
|
||||||
readonly updateUsernameRequest: UpdateUsernameRequest;
|
|
||||||
|
|
||||||
constructor(request: UpdateUsernameRequest) {
|
|
||||||
this.updateUsernameRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
import { Type } from './type.enum';
|
|
||||||
|
|
||||||
export class AddUsernameRequest {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@IsEnum(Type)
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
type: Type;
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
import { Type } from './type.enum';
|
|
||||||
|
|
||||||
export class CreateAuthenticationRequest {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
password: string;
|
|
||||||
|
|
||||||
@IsEnum(Type)
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
type: Type;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class DeleteAuthenticationRequest {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class DeleteUsernameRequest {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
username: string;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export enum Type {
|
|
||||||
EMAIL = 'EMAIL',
|
|
||||||
PHONE = 'PHONE',
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class UpdatePasswordRequest {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
password: string;
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
import { Type } from './type.enum';
|
|
||||||
|
|
||||||
export class UpdateUsernameRequest {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@IsEnum(Type)
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
type: Type;
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class ValidateAuthenticationRequest {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
password: string;
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
|
|
||||||
export class Authentication {
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
password: string;
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { Type } from '../dtos/type.enum';
|
|
||||||
|
|
||||||
export class Username {
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
type: Type;
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export abstract class IMessageBroker {
|
|
||||||
exchange: string;
|
|
||||||
|
|
||||||
constructor(exchange: string) {
|
|
||||||
this.exchange = exchange;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract publish(routingKey: string, message: string): void;
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import { CommandHandler } from '@nestjs/cqrs';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { AddUsernameCommand } from '../../commands/add-username.command';
|
|
||||||
import { Username } from '../entities/username';
|
|
||||||
|
|
||||||
@CommandHandler(AddUsernameCommand)
|
|
||||||
export class AddUsernameUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly usernameRepository: UsernameRepository,
|
|
||||||
private readonly messager: Messager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
execute = async (command: AddUsernameCommand): Promise<Username> => {
|
|
||||||
const { uuid, username, type } = command.addUsernameRequest;
|
|
||||||
try {
|
|
||||||
return await this.usernameRepository.create({
|
|
||||||
uuid,
|
|
||||||
type,
|
|
||||||
username,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.messager.publish(
|
|
||||||
'logging.auth.username.add.warning',
|
|
||||||
JSON.stringify({
|
|
||||||
command,
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { CommandHandler } from '@nestjs/cqrs';
|
|
||||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
|
||||||
import { CreateAuthenticationCommand } from '../../commands/create-authentication.command';
|
|
||||||
import { Authentication } from '../entities/authentication';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
|
|
||||||
@CommandHandler(CreateAuthenticationCommand)
|
|
||||||
export class CreateAuthenticationUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly authenticationRepository: AuthenticationRepository,
|
|
||||||
private readonly usernameRepository: UsernameRepository,
|
|
||||||
private readonly messager: Messager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
execute = async (
|
|
||||||
command: CreateAuthenticationCommand,
|
|
||||||
): Promise<Authentication> => {
|
|
||||||
const { uuid, password, ...username } = command.createAuthenticationRequest;
|
|
||||||
const hash = await bcrypt.hash(password, 10);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const auth = await this.authenticationRepository.create({
|
|
||||||
uuid,
|
|
||||||
password: hash,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.usernameRepository.create({
|
|
||||||
uuid,
|
|
||||||
...username,
|
|
||||||
});
|
|
||||||
|
|
||||||
return auth;
|
|
||||||
} catch (error) {
|
|
||||||
this.messager.publish(
|
|
||||||
'logging.auth.create.crit',
|
|
||||||
JSON.stringify({
|
|
||||||
command,
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { CommandHandler } from '@nestjs/cqrs';
|
|
||||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
|
|
||||||
import { Authentication } from '../entities/authentication';
|
|
||||||
|
|
||||||
@CommandHandler(DeleteAuthenticationCommand)
|
|
||||||
export class DeleteAuthenticationUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly authenticationRepository: AuthenticationRepository,
|
|
||||||
private readonly usernameRepository: UsernameRepository,
|
|
||||||
private readonly messager: Messager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
execute = async (
|
|
||||||
command: DeleteAuthenticationCommand,
|
|
||||||
): Promise<Authentication> => {
|
|
||||||
try {
|
|
||||||
await this.usernameRepository.deleteMany({
|
|
||||||
uuid: command.deleteAuthenticationRequest.uuid,
|
|
||||||
});
|
|
||||||
return await this.authenticationRepository.delete(
|
|
||||||
command.deleteAuthenticationRequest.uuid,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.messager.publish(
|
|
||||||
'logging.auth.delete.crit',
|
|
||||||
JSON.stringify({
|
|
||||||
command,
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { UnauthorizedException } from '@nestjs/common';
|
|
||||||
import { CommandHandler } from '@nestjs/cqrs';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { DeleteUsernameCommand } from '../../commands/delete-username.command';
|
|
||||||
|
|
||||||
@CommandHandler(DeleteUsernameCommand)
|
|
||||||
export class DeleteUsernameUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly usernameRepository: UsernameRepository,
|
|
||||||
private readonly messager: Messager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
execute = async (command: DeleteUsernameCommand): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { username } = command.deleteUsernameRequest;
|
|
||||||
const usernameFound = await this.usernameRepository.findOne({
|
|
||||||
username,
|
|
||||||
});
|
|
||||||
const usernames = await this.usernameRepository.findAll(1, 1, {
|
|
||||||
uuid: usernameFound.uuid,
|
|
||||||
});
|
|
||||||
if (usernames.total > 1) {
|
|
||||||
return await this.usernameRepository.deleteMany({ username });
|
|
||||||
}
|
|
||||||
throw new UnauthorizedException();
|
|
||||||
} catch (error) {
|
|
||||||
this.messager.publish(
|
|
||||||
'logging.auth.username.delete.warning',
|
|
||||||
JSON.stringify({
|
|
||||||
command,
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { CommandHandler } from '@nestjs/cqrs';
|
|
||||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
|
||||||
import { Authentication } from '../entities/authentication';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
import { UpdatePasswordCommand } from '../../commands/update-password.command';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
|
|
||||||
@CommandHandler(UpdatePasswordCommand)
|
|
||||||
export class UpdatePasswordUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly authenticationRepository: AuthenticationRepository,
|
|
||||||
private readonly messager: Messager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
execute = async (command: UpdatePasswordCommand): Promise<Authentication> => {
|
|
||||||
const { uuid, password } = command.updatePasswordRequest;
|
|
||||||
const hash = await bcrypt.hash(password, 10);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await this.authenticationRepository.update(uuid, {
|
|
||||||
password: hash,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.messager.publish(
|
|
||||||
'logging.auth.password.update.warning',
|
|
||||||
JSON.stringify({
|
|
||||||
command,
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
import { Mapper } from '@automapper/core';
|
|
||||||
import { InjectMapper } from '@automapper/nestjs';
|
|
||||||
import { BadRequestException } from '@nestjs/common';
|
|
||||||
import { CommandBus, CommandHandler } from '@nestjs/cqrs';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { AddUsernameCommand } from '../../commands/add-username.command';
|
|
||||||
import { UpdateUsernameCommand } from '../../commands/update-username.command';
|
|
||||||
import { AddUsernameRequest } from '../dtos/add-username.request';
|
|
||||||
import { UpdateUsernameRequest } from '../dtos/update-username.request';
|
|
||||||
import { Username } from '../entities/username';
|
|
||||||
|
|
||||||
@CommandHandler(UpdateUsernameCommand)
|
|
||||||
export class UpdateUsernameUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly usernameRepository: UsernameRepository,
|
|
||||||
private readonly commandBus: CommandBus,
|
|
||||||
@InjectMapper() private readonly mapper: Mapper,
|
|
||||||
private readonly messager: Messager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
execute = async (command: UpdateUsernameCommand): Promise<Username> => {
|
|
||||||
const { uuid, username, type } = command.updateUsernameRequest;
|
|
||||||
if (!username) throw new BadRequestException();
|
|
||||||
// update username if it exists, otherwise create it
|
|
||||||
const existingUsername = await this.usernameRepository.findOne({
|
|
||||||
uuid,
|
|
||||||
type,
|
|
||||||
});
|
|
||||||
if (existingUsername) {
|
|
||||||
try {
|
|
||||||
return await this.usernameRepository.updateWhere(
|
|
||||||
{
|
|
||||||
uuid_type: {
|
|
||||||
uuid,
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
username,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.messager.publish(
|
|
||||||
'logging.auth.username.update.warning',
|
|
||||||
JSON.stringify({
|
|
||||||
command,
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const addUsernameRequest = this.mapper.map(
|
|
||||||
command.updateUsernameRequest,
|
|
||||||
UpdateUsernameRequest,
|
|
||||||
AddUsernameRequest,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
return await this.commandBus.execute(
|
|
||||||
new AddUsernameCommand(addUsernameRequest),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { QueryHandler } from '@nestjs/cqrs';
|
|
||||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
|
||||||
import { ValidateAuthenticationQuery as ValidateAuthenticationQuery } from '../../queries/validate-authentication.query';
|
|
||||||
import { Authentication } from '../entities/authentication';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
import { NotFoundException, UnauthorizedException } from '@nestjs/common';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { Username } from '../entities/username';
|
|
||||||
|
|
||||||
@QueryHandler(ValidateAuthenticationQuery)
|
|
||||||
export class ValidateAuthenticationUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly authenticationRepository: AuthenticationRepository,
|
|
||||||
private readonly usernameRepository: UsernameRepository,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
execute = async (
|
|
||||||
validate: ValidateAuthenticationQuery,
|
|
||||||
): Promise<Authentication> => {
|
|
||||||
let username = new Username();
|
|
||||||
try {
|
|
||||||
username = await this.usernameRepository.findOne({
|
|
||||||
username: validate.username,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const auth = await this.authenticationRepository.findOne({
|
|
||||||
uuid: username.uuid,
|
|
||||||
});
|
|
||||||
if (auth) {
|
|
||||||
const isMatch = await bcrypt.compare(validate.password, auth.password);
|
|
||||||
if (isMatch) return auth;
|
|
||||||
}
|
|
||||||
throw new UnauthorizedException();
|
|
||||||
} catch (e) {
|
|
||||||
throw new UnauthorizedException();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { createMap, Mapper } from '@automapper/core';
|
|
||||||
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { AuthenticationPresenter } from '../adapters/primaries/authentication.presenter';
|
|
||||||
import { Authentication } from '../domain/entities/authentication';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuthenticationProfile extends AutomapperProfile {
|
|
||||||
constructor(@InjectMapper() mapper: Mapper) {
|
|
||||||
super(mapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
override get profile() {
|
|
||||||
return (mapper: any) => {
|
|
||||||
createMap(mapper, Authentication, AuthenticationPresenter);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { createMap, Mapper } from '@automapper/core';
|
|
||||||
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { UsernamePresenter } from '../adapters/primaries/username.presenter';
|
|
||||||
import { AddUsernameRequest } from '../domain/dtos/add-username.request';
|
|
||||||
import { UpdateUsernameRequest } from '../domain/dtos/update-username.request';
|
|
||||||
import { Username } from '../domain/entities/username';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UsernameProfile extends AutomapperProfile {
|
|
||||||
constructor(@InjectMapper() mapper: Mapper) {
|
|
||||||
super(mapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
override get profile() {
|
|
||||||
return (mapper: any) => {
|
|
||||||
createMap(mapper, Username, UsernamePresenter);
|
|
||||||
createMap(mapper, UpdateUsernameRequest, AddUsernameRequest);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
export class ValidateAuthenticationQuery {
|
|
||||||
readonly username: string;
|
|
||||||
readonly password: string;
|
|
||||||
|
|
||||||
constructor(username: string, password: string) {
|
|
||||||
this.username = username;
|
|
||||||
this.password = password;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,162 +0,0 @@
|
||||||
import { TestingModule, Test } from '@nestjs/testing';
|
|
||||||
import { DatabaseModule } from '../../../database/database.module';
|
|
||||||
import { PrismaService } from '../../../database/adapters/secondaries/prisma-service';
|
|
||||||
import { DatabaseException } from '../../../database/exceptions/database.exception';
|
|
||||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
import { Authentication } from '../../domain/entities/authentication';
|
|
||||||
|
|
||||||
describe('AuthenticationRepository', () => {
|
|
||||||
let prismaService: PrismaService;
|
|
||||||
let authenticationRepository: AuthenticationRepository;
|
|
||||||
|
|
||||||
const createAuthentications = async (nbToCreate = 10) => {
|
|
||||||
for (let i = 0; i < nbToCreate; i++) {
|
|
||||||
await prismaService.auth.create({
|
|
||||||
data: {
|
|
||||||
uuid: v4(),
|
|
||||||
password: bcrypt.hashSync(`password-${i}`, 10),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [DatabaseModule],
|
|
||||||
providers: [AuthenticationRepository, PrismaService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
prismaService = module.get<PrismaService>(PrismaService);
|
|
||||||
authenticationRepository = module.get<AuthenticationRepository>(
|
|
||||||
AuthenticationRepository,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await prismaService.$disconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await prismaService.auth.deleteMany();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findAll', () => {
|
|
||||||
it('should return an empty data array', async () => {
|
|
||||||
const res = await authenticationRepository.findAll();
|
|
||||||
expect(res).toEqual({
|
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a data array with 8 auths', async () => {
|
|
||||||
await createAuthentications(8);
|
|
||||||
const auths = await authenticationRepository.findAll();
|
|
||||||
expect(auths.data.length).toBe(8);
|
|
||||||
expect(auths.total).toBe(8);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a data array limited to 10 authentications', async () => {
|
|
||||||
await createAuthentications(20);
|
|
||||||
const auths = await authenticationRepository.findAll();
|
|
||||||
expect(auths.data.length).toBe(10);
|
|
||||||
expect(auths.total).toBe(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findOneByUuid', () => {
|
|
||||||
it('should return an authentication', async () => {
|
|
||||||
const authToFind = await prismaService.auth.create({
|
|
||||||
data: {
|
|
||||||
uuid: v4(),
|
|
||||||
password: bcrypt.hashSync(`password`, 10),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const auth = await authenticationRepository.findOneByUuid(
|
|
||||||
authToFind.uuid,
|
|
||||||
);
|
|
||||||
expect(auth.uuid).toBe(authToFind.uuid);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null', async () => {
|
|
||||||
const auth = await authenticationRepository.findOneByUuid(
|
|
||||||
'544572be-11fb-4244-8235-587221fc9104',
|
|
||||||
);
|
|
||||||
expect(auth).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('create', () => {
|
|
||||||
it('should create an authentication', async () => {
|
|
||||||
const beforeCount = await prismaService.auth.count();
|
|
||||||
|
|
||||||
const authenticationToCreate: Authentication = new Authentication();
|
|
||||||
authenticationToCreate.uuid = v4();
|
|
||||||
authenticationToCreate.password = bcrypt.hashSync(`password`, 10);
|
|
||||||
const authentication = await authenticationRepository.create(
|
|
||||||
authenticationToCreate,
|
|
||||||
);
|
|
||||||
|
|
||||||
const afterCount = await prismaService.auth.count();
|
|
||||||
|
|
||||||
expect(afterCount - beforeCount).toBe(1);
|
|
||||||
expect(authentication.uuid).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('update', () => {
|
|
||||||
it('should update authentication password', async () => {
|
|
||||||
const authenticationToUpdate = await prismaService.auth.create({
|
|
||||||
data: {
|
|
||||||
uuid: v4(),
|
|
||||||
password: bcrypt.hashSync(`password`, 10),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const toUpdate: Authentication = new Authentication();
|
|
||||||
toUpdate.password = bcrypt.hashSync(`newPassword`, 10);
|
|
||||||
const updatedAuthentication = await authenticationRepository.update(
|
|
||||||
authenticationToUpdate.uuid,
|
|
||||||
toUpdate,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(updatedAuthentication.uuid).toBe(authenticationToUpdate.uuid);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw DatabaseException', async () => {
|
|
||||||
const toUpdate: Authentication = new Authentication();
|
|
||||||
toUpdate.password = bcrypt.hashSync(`newPassword`, 10);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
authenticationRepository.update(
|
|
||||||
'544572be-11fb-4244-8235-587221fc9104',
|
|
||||||
toUpdate,
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('delete', () => {
|
|
||||||
it('should delete an authentication', async () => {
|
|
||||||
const authenticationToRemove = await prismaService.auth.create({
|
|
||||||
data: {
|
|
||||||
uuid: v4(),
|
|
||||||
password: bcrypt.hashSync(`password`, 10),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await authenticationRepository.delete(authenticationToRemove.uuid);
|
|
||||||
|
|
||||||
const count = await prismaService.auth.count();
|
|
||||||
expect(count).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw DatabaseException', async () => {
|
|
||||||
await expect(
|
|
||||||
authenticationRepository.delete('544572be-11fb-4244-8235-587221fc9104'),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,282 +0,0 @@
|
||||||
import { TestingModule, Test } from '@nestjs/testing';
|
|
||||||
import { DatabaseModule } from '../../../database/database.module';
|
|
||||||
import { PrismaService } from '../../../database/adapters/secondaries/prisma-service';
|
|
||||||
import { DatabaseException } from '../../../database/exceptions/database.exception';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
import { Type } from '../../domain/dtos/type.enum';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { Username } from '../../domain/entities/username';
|
|
||||||
|
|
||||||
describe('UsernameRepository', () => {
|
|
||||||
let prismaService: PrismaService;
|
|
||||||
let usernameRepository: UsernameRepository;
|
|
||||||
|
|
||||||
const createUsernames = async (nbToCreate = 10) => {
|
|
||||||
for (let i = 0; i < nbToCreate; i++) {
|
|
||||||
await prismaService.username.create({
|
|
||||||
data: {
|
|
||||||
uuid: v4(),
|
|
||||||
username: `john.doe.${i}@email.com`,
|
|
||||||
type: Type.EMAIL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [DatabaseModule],
|
|
||||||
providers: [UsernameRepository, PrismaService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
prismaService = module.get<PrismaService>(PrismaService);
|
|
||||||
usernameRepository = module.get<UsernameRepository>(UsernameRepository);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await prismaService.$disconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await prismaService.username.deleteMany();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findAll', () => {
|
|
||||||
it('should return an empty data array', async () => {
|
|
||||||
const res = await usernameRepository.findAll();
|
|
||||||
expect(res).toEqual({
|
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a data array with 8 usernames', async () => {
|
|
||||||
await createUsernames(8);
|
|
||||||
const usernames = await usernameRepository.findAll();
|
|
||||||
expect(usernames.data.length).toBe(8);
|
|
||||||
expect(usernames.total).toBe(8);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a data array limited to 10 usernames', async () => {
|
|
||||||
await createUsernames(20);
|
|
||||||
const usernames = await usernameRepository.findAll();
|
|
||||||
expect(usernames.data.length).toBe(10);
|
|
||||||
expect(usernames.total).toBe(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findOne', () => {
|
|
||||||
it('should return a username with uuid and email', async () => {
|
|
||||||
const usernameToFind = await prismaService.username.create({
|
|
||||||
data: {
|
|
||||||
uuid: v4(),
|
|
||||||
username: 'john.doe@email.com',
|
|
||||||
type: Type.EMAIL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const username = await usernameRepository.findOne({
|
|
||||||
username: 'john.doe@email.com',
|
|
||||||
type: Type.EMAIL,
|
|
||||||
});
|
|
||||||
expect(username.uuid).toBe(usernameToFind.uuid);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null', async () => {
|
|
||||||
const username = await usernameRepository.findOne({
|
|
||||||
username: 'jane.doe@email.com',
|
|
||||||
type: Type.EMAIL,
|
|
||||||
});
|
|
||||||
expect(username).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('create', () => {
|
|
||||||
it('should create a username with an email', async () => {
|
|
||||||
const beforeCount = await prismaService.username.count();
|
|
||||||
|
|
||||||
const usernameToCreate: Username = new Username();
|
|
||||||
usernameToCreate.uuid = v4();
|
|
||||||
usernameToCreate.username = 'john.doe@email.com';
|
|
||||||
usernameToCreate.type = Type.EMAIL;
|
|
||||||
const username = await usernameRepository.create(usernameToCreate);
|
|
||||||
|
|
||||||
const afterCount = await prismaService.username.count();
|
|
||||||
|
|
||||||
expect(afterCount - beforeCount).toBe(1);
|
|
||||||
expect(username.uuid).toBeDefined();
|
|
||||||
});
|
|
||||||
it('should create a username with a phone number', async () => {
|
|
||||||
const beforeCount = await prismaService.username.count();
|
|
||||||
|
|
||||||
const usernameToCreate: Username = new Username();
|
|
||||||
usernameToCreate.uuid = v4();
|
|
||||||
usernameToCreate.username = '+33611223344';
|
|
||||||
usernameToCreate.type = Type.PHONE;
|
|
||||||
const username = await usernameRepository.create(usernameToCreate);
|
|
||||||
|
|
||||||
const afterCount = await prismaService.username.count();
|
|
||||||
|
|
||||||
expect(afterCount - beforeCount).toBe(1);
|
|
||||||
expect(username.uuid).toBeDefined();
|
|
||||||
});
|
|
||||||
it('should create a username with an email for an existing uuid', async () => {
|
|
||||||
const beforeCount = await prismaService.username.count();
|
|
||||||
|
|
||||||
const uuid = v4();
|
|
||||||
|
|
||||||
const firstUsernameToCreate: Username = new Username();
|
|
||||||
firstUsernameToCreate.uuid = uuid;
|
|
||||||
firstUsernameToCreate.username = '+33611223344';
|
|
||||||
firstUsernameToCreate.type = Type.PHONE;
|
|
||||||
const firstUsername = await usernameRepository.create(
|
|
||||||
firstUsernameToCreate,
|
|
||||||
);
|
|
||||||
|
|
||||||
const secondUsernameToCreate: Username = new Username();
|
|
||||||
secondUsernameToCreate.uuid = uuid;
|
|
||||||
secondUsernameToCreate.username = 'john.doe@email.com';
|
|
||||||
secondUsernameToCreate.type = Type.EMAIL;
|
|
||||||
const secondUsername = await usernameRepository.create(
|
|
||||||
secondUsernameToCreate,
|
|
||||||
);
|
|
||||||
|
|
||||||
const afterCount = await prismaService.username.count();
|
|
||||||
|
|
||||||
expect(afterCount - beforeCount).toBe(2);
|
|
||||||
expect(firstUsername.uuid).toEqual(secondUsername.uuid);
|
|
||||||
});
|
|
||||||
it('should throw DatabaseException if username already exists for a given type', async () => {
|
|
||||||
const uuid = v4();
|
|
||||||
|
|
||||||
const firstUsernameToCreate: Username = new Username();
|
|
||||||
firstUsernameToCreate.uuid = uuid;
|
|
||||||
firstUsernameToCreate.username = 'john.doe@email.com';
|
|
||||||
firstUsernameToCreate.type = Type.EMAIL;
|
|
||||||
await usernameRepository.create(firstUsernameToCreate);
|
|
||||||
|
|
||||||
const secondUsernameToCreate: Username = new Username();
|
|
||||||
secondUsernameToCreate.uuid = uuid;
|
|
||||||
secondUsernameToCreate.username = 'jane.doe@email.com';
|
|
||||||
secondUsernameToCreate.type = Type.EMAIL;
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
usernameRepository.create(secondUsernameToCreate),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('update', () => {
|
|
||||||
it('should update username email', async () => {
|
|
||||||
const usernameToUpdate = await prismaService.username.create({
|
|
||||||
data: {
|
|
||||||
uuid: v4(),
|
|
||||||
username: `john.doe@email.com`,
|
|
||||||
type: Type.EMAIL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const toUpdate: Username = new Username();
|
|
||||||
toUpdate.username = 'jane.doe@email.com';
|
|
||||||
const updatedUsername = await usernameRepository.updateWhere(
|
|
||||||
{
|
|
||||||
uuid_type: {
|
|
||||||
uuid: usernameToUpdate.uuid,
|
|
||||||
type: usernameToUpdate.type,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
username: toUpdate.username,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(updatedUsername.uuid).toBe(usernameToUpdate.uuid);
|
|
||||||
expect(updatedUsername.username).toBe('jane.doe@email.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update username phone', async () => {
|
|
||||||
const usernameToUpdate = await prismaService.username.create({
|
|
||||||
data: {
|
|
||||||
uuid: v4(),
|
|
||||||
username: `+33611223344`,
|
|
||||||
type: Type.PHONE,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const toUpdate: Username = new Username();
|
|
||||||
toUpdate.username = '+33622334455';
|
|
||||||
const updatedUsername = await usernameRepository.updateWhere(
|
|
||||||
{
|
|
||||||
uuid_type: {
|
|
||||||
uuid: usernameToUpdate.uuid,
|
|
||||||
type: usernameToUpdate.type,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
username: toUpdate.username,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(updatedUsername.uuid).toBe(usernameToUpdate.uuid);
|
|
||||||
expect(updatedUsername.username).toBe('+33622334455');
|
|
||||||
});
|
|
||||||
it('should throw DatabaseException if email not found', async () => {
|
|
||||||
const toUpdate: Username = new Username();
|
|
||||||
toUpdate.username = 'jane.doe@email.com';
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
usernameRepository.updateWhere(
|
|
||||||
{
|
|
||||||
uuid_type: {
|
|
||||||
uuid: '544572be-11fb-4244-8235-587221fc9104',
|
|
||||||
type: Type.EMAIL,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
username: toUpdate.username,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
it('should throw DatabaseException if phone not found', async () => {
|
|
||||||
const toUpdate: Username = new Username();
|
|
||||||
toUpdate.username = '+33611223344';
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
usernameRepository.updateWhere(
|
|
||||||
{
|
|
||||||
uuid_type: {
|
|
||||||
uuid: '544572be-11fb-4244-8235-587221fc9104',
|
|
||||||
type: Type.PHONE,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
username: toUpdate.username,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('delete', () => {
|
|
||||||
it('should delete a username', async () => {
|
|
||||||
const usernameToRemove = await prismaService.username.create({
|
|
||||||
data: {
|
|
||||||
uuid: v4(),
|
|
||||||
username: `+33611223344`,
|
|
||||||
type: Type.PHONE,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await usernameRepository.deleteMany({ uuid: usernameToRemove.uuid });
|
|
||||||
|
|
||||||
const count = await prismaService.username.count();
|
|
||||||
expect(count).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw DatabaseException', async () => {
|
|
||||||
await expect(
|
|
||||||
usernameRepository.delete('544572be-11fb-4244-8235-587221fc9104'),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,79 +0,0 @@
|
||||||
import { classes } from '@automapper/classes';
|
|
||||||
import { AutomapperModule } from '@automapper/nestjs';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { AuthenticationProfile } from '../../mappers/authentication.profile';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { Username } from '../../domain/entities/username';
|
|
||||||
import { Type } from '../../domain/dtos/type.enum';
|
|
||||||
import { AddUsernameRequest } from '../../domain/dtos/add-username.request';
|
|
||||||
import { AddUsernameCommand } from '../../commands/add-username.command';
|
|
||||||
import { AddUsernameUseCase } from '../../domain/usecases/add-username.usecase';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
|
|
||||||
const addUsernameRequest: AddUsernameRequest = {
|
|
||||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
|
||||||
username: '0611223344',
|
|
||||||
type: Type.PHONE,
|
|
||||||
};
|
|
||||||
const addUsernameCommand: AddUsernameCommand = new AddUsernameCommand(
|
|
||||||
addUsernameRequest,
|
|
||||||
);
|
|
||||||
|
|
||||||
const mockUsernameRepository = {
|
|
||||||
create: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return Promise.resolve(addUsernameRequest);
|
|
||||||
})
|
|
||||||
.mockImplementation(() => {
|
|
||||||
throw new Error('Already exists');
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMessager = {
|
|
||||||
publish: jest.fn().mockImplementation(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('AddUsernameUseCase', () => {
|
|
||||||
let addUsernameUseCase: AddUsernameUseCase;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: UsernameRepository,
|
|
||||||
useValue: mockUsernameRepository,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: Messager,
|
|
||||||
useValue: mockMessager,
|
|
||||||
},
|
|
||||||
AddUsernameUseCase,
|
|
||||||
AuthenticationProfile,
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
addUsernameUseCase = module.get<AddUsernameUseCase>(AddUsernameUseCase);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(addUsernameUseCase).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('execute', () => {
|
|
||||||
it('should add a username for phone type', async () => {
|
|
||||||
const addedUsername: Username = await addUsernameUseCase.execute(
|
|
||||||
addUsernameCommand,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(addedUsername.username).toBe(addUsernameRequest.username);
|
|
||||||
expect(addedUsername.type).toBe(addUsernameRequest.type);
|
|
||||||
});
|
|
||||||
it('should throw an error if user already exists', async () => {
|
|
||||||
await expect(
|
|
||||||
addUsernameUseCase.execute(addUsernameCommand),
|
|
||||||
).rejects.toBeInstanceOf(Error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,99 +0,0 @@
|
||||||
import { classes } from '@automapper/classes';
|
|
||||||
import { AutomapperModule } from '@automapper/nestjs';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
|
||||||
import { CreateAuthenticationCommand } from '../../commands/create-authentication.command';
|
|
||||||
import { CreateAuthenticationRequest } from '../../domain/dtos/create-authentication.request';
|
|
||||||
import { Authentication } from '../../domain/entities/authentication';
|
|
||||||
import { CreateAuthenticationUseCase } from '../../domain/usecases/create-authentication.usecase';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { Type } from '../../domain/dtos/type.enum';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
|
|
||||||
const newAuthenticationRequest: CreateAuthenticationRequest =
|
|
||||||
new CreateAuthenticationRequest();
|
|
||||||
newAuthenticationRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
|
|
||||||
newAuthenticationRequest.username = 'john.doe@email.com';
|
|
||||||
newAuthenticationRequest.password = 'John123';
|
|
||||||
newAuthenticationRequest.type = Type.EMAIL;
|
|
||||||
const newAuthCommand: CreateAuthenticationCommand =
|
|
||||||
new CreateAuthenticationCommand(newAuthenticationRequest);
|
|
||||||
|
|
||||||
const mockAuthenticationRepository = {
|
|
||||||
create: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return Promise.resolve({
|
|
||||||
uuid: newAuthenticationRequest.uuid,
|
|
||||||
password: bcrypt.hashSync(newAuthenticationRequest.password, 10),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementation(() => {
|
|
||||||
throw new Error('Already exists');
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockUsernameRepository = {
|
|
||||||
create: jest.fn().mockResolvedValue({
|
|
||||||
uuid: newAuthenticationRequest.uuid,
|
|
||||||
username: newAuthenticationRequest.username,
|
|
||||||
type: newAuthenticationRequest.type,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMessager = {
|
|
||||||
publish: jest.fn().mockImplementation(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('CreateAuthenticationUseCase', () => {
|
|
||||||
let createAuthenticationUseCase: CreateAuthenticationUseCase;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: AuthenticationRepository,
|
|
||||||
useValue: mockAuthenticationRepository,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: UsernameRepository,
|
|
||||||
useValue: mockUsernameRepository,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: Messager,
|
|
||||||
useValue: mockMessager,
|
|
||||||
},
|
|
||||||
CreateAuthenticationUseCase,
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
createAuthenticationUseCase = module.get<CreateAuthenticationUseCase>(
|
|
||||||
CreateAuthenticationUseCase,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(createAuthenticationUseCase).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('execute', () => {
|
|
||||||
it('should create an authentication with an encrypted password', async () => {
|
|
||||||
const newAuthentication: Authentication =
|
|
||||||
await createAuthenticationUseCase.execute(newAuthCommand);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
bcrypt.compareSync(
|
|
||||||
newAuthenticationRequest.password,
|
|
||||||
newAuthentication.password,
|
|
||||||
),
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
|
||||||
it('should throw an error if user already exists', async () => {
|
|
||||||
await expect(
|
|
||||||
createAuthenticationUseCase.execute(newAuthCommand),
|
|
||||||
).rejects.toBeInstanceOf(Error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,102 +0,0 @@
|
||||||
import { classes } from '@automapper/classes';
|
|
||||||
import { AutomapperModule } from '@automapper/nestjs';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
|
|
||||||
import { Messager } from '../../adapters/secondaries/messager';
|
|
||||||
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
|
|
||||||
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
|
|
||||||
import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request';
|
|
||||||
import { Type } from '../../domain/dtos/type.enum';
|
|
||||||
import { DeleteAuthenticationUseCase } from '../../domain/usecases/delete-authentication.usecase';
|
|
||||||
|
|
||||||
const usernames = {
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
uuid: 'cf76af29-f75d-4f6e-bb40-4ecbcfa3356e',
|
|
||||||
username: 'john.doe@email.com',
|
|
||||||
type: Type.EMAIL,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: 'cf76af29-f75d-4f6e-bb40-4ecbcfa3356e',
|
|
||||||
username: '0611223344',
|
|
||||||
type: Type.PHONE,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
total: 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteAuthenticationRequest: DeleteAuthenticationRequest =
|
|
||||||
new DeleteAuthenticationRequest();
|
|
||||||
deleteAuthenticationRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
|
|
||||||
const deleteAuthenticationCommand: DeleteAuthenticationCommand =
|
|
||||||
new DeleteAuthenticationCommand(deleteAuthenticationRequest);
|
|
||||||
|
|
||||||
const mockAuthenticationRepository = {
|
|
||||||
delete: jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce(undefined)
|
|
||||||
.mockImplementation(() => {
|
|
||||||
throw new Error('Error');
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockUsernameRepository = {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
findAll: jest.fn().mockImplementation((page, perPage, query) => {
|
|
||||||
return Promise.resolve(usernames);
|
|
||||||
}),
|
|
||||||
delete: jest.fn().mockResolvedValue(undefined),
|
|
||||||
deleteMany: jest.fn().mockResolvedValue(undefined),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMessager = {
|
|
||||||
publish: jest.fn().mockImplementation(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('DeleteAuthenticationUseCase', () => {
|
|
||||||
let deleteAuthenticationUseCase: DeleteAuthenticationUseCase;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: AuthenticationRepository,
|
|
||||||
useValue: mockAuthenticationRepository,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: UsernameRepository,
|
|
||||||
useValue: mockUsernameRepository,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: Messager,
|
|
||||||
useValue: mockMessager,
|
|
||||||
},
|
|
||||||
DeleteAuthenticationUseCase,
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
deleteAuthenticationUseCase = module.get<DeleteAuthenticationUseCase>(
|
|
||||||
DeleteAuthenticationUseCase,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(deleteAuthenticationUseCase).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('execute', () => {
|
|
||||||
it('should delete an authentication and its usernames', async () => {
|
|
||||||
const deletedAuthentication = await deleteAuthenticationUseCase.execute(
|
|
||||||
deleteAuthenticationCommand,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(deletedAuthentication).toBe(undefined);
|
|
||||||
});
|
|
||||||
it('should throw an error if authentication does not exist', async () => {
|
|
||||||
await expect(
|
|
||||||
deleteAuthenticationUseCase.execute(deleteAuthenticationCommand),
|
|
||||||
).rejects.toBeInstanceOf(Error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue