From d78a065c54f54d32931c4db1d0a511ee9cc67d89 Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 12 Jul 2023 10:39:55 +0200 Subject: [PATCH] authorization module --- opa/user/delete.rego | 2 +- opa/user/read.rego | 2 +- opa/user/update.rego | 2 +- src/app.module.ts | 11 +- src/main.ts | 7 +- .../authentication/authentication.module.ts | 1 + .../primaries/authorization.controller.ts | 43 -- .../primaries/authorization.presenter.ts | 6 - .../adapters/secondaries/decision-result.ts | 3 - .../adapters/secondaries/decision.ts | 6 - .../authorization/authorization.di-tokens.ts | 1 + .../authorization/authorization.module.ts | 30 +- .../application/ports/decision-maker.port.ts | 11 + .../decision/decision.query-handler.ts | 20 + .../queries/decision/decision.query.ts | 19 + .../domain/authorization.types.ts} | 11 + .../authorization/domain/dtos/context-item.ts | 9 - .../domain/dtos/decision.request.ts | 17 - .../authorization/domain/dtos/domain.enum.ts | 5 - .../domain/entities/authorization.ts | 10 - .../domain/interfaces/decision-maker.ts | 13 - .../domain/usecases/decision.usecase.ts | 16 - .../opa.decision-maker.ts | 31 +- .../interface/dtos/decision.response.dto.ts | 7 + .../grpc-controllers}/authorization.proto | 0 .../decide.grpc.controller.ts | 33 + .../dtos/decision.request.dto.ts | 19 + .../mappers/authorization.profile.ts | 18 - .../authorization/queries/decision.query.ts | 15 - .../unit/core/decision.query-handler.spec.ts | 74 +++ .../tests/unit/decision.usecase.spec.ts | 57 -- .../infrastructure/opa.decision-maker.spec.ts | 123 ++++ .../interface/decide.grpc.controller.spec.ts | 91 +++ .../tests/unit/opa.decision-maker.spec.ts | 97 --- .../secondaries/prisma-repository.abstract.ts | 259 -------- .../adapters/secondaries/prisma-service.ts | 15 - src/modules/database/database.module.ts | 10 - .../database/domain/auth-repository.ts | 3 - .../database/exceptions/database.exception.ts | 24 - .../interfaces/collection.interface.ts | 4 - .../interfaces/repository.interface.ts | 17 - .../tests/unit/prisma-repository.spec.ts | 571 ------------------ .../adapters/primaries/health.controller.ts | 35 -- .../adapters/secondaries/message-broker.ts | 12 - .../health/adapters/secondaries/messager.ts | 18 - .../ports/check-repository.port.ts | 3 + .../repositories.health-indicator.usecase.ts | 54 ++ .../prisma.health-indicator.usecase.ts | 25 - src/modules/health/health.constants.ts | 1 + src/modules/health/health.di-tokens.ts | 2 + src/modules/health/health.module.ts | 69 ++- .../health.grpc.controller.ts} | 19 +- .../grpc-controllers}/health.proto | 0 .../health.http.controller.ts | 28 + .../tests/unit/health.grpc.controller.spec.ts | 72 +++ .../tests/unit/health.http.controller.spec.ts | 90 +++ .../health/tests/unit/messager.spec.ts | 47 -- .../prisma.health-indicator.usecase.spec.ts | 58 -- ...ositories.health-indicator.usecase.spec.ts | 84 +++ .../authentication-messager.controller.ts | 52 -- .../primaries/authentication.controller.ts | 188 ------ .../primaries/authentication.presenter.ts | 6 - .../adapters/primaries/authentication.proto | 42 -- .../adapters/primaries/username.presenter.ts | 9 - .../secondaries/authentication.repository.ts | 8 - .../adapters/secondaries/messager.ts | 18 - .../secondaries/username.repository.ts | 8 - .../authentication.module.ts | 66 -- .../commands/add-username.command.ts | 9 - .../commands/create-authentication.command.ts | 9 - .../commands/delete-authentication.command.ts | 9 - .../commands/delete-username.command.ts | 9 - .../commands/update-password.command.ts | 9 - .../commands/update-username.command.ts | 9 - .../domain/dtos/add-username.request.ts | 20 - .../dtos/create-authentication.request.ts | 25 - .../dtos/delete-authentication.request.ts | 9 - .../domain/dtos/delete-username.request.ts | 9 - .../domain/dtos/type.enum.ts | 4 - .../domain/dtos/update-password.request.ts | 14 - .../domain/dtos/update-username.request.ts | 20 - .../dtos/validate-authentication.request.ts | 11 - .../domain/entities/authentication.ts | 8 - .../domain/entities/username.ts | 13 - .../domain/interfaces/message-broker.ts | 12 - .../domain/usecases/add-username.usecase.ts | 33 - .../usecases/create-authentication.usecase.ts | 46 -- .../usecases/delete-authentication.usecase.ts | 37 -- .../usecases/delete-username.usecase.ts | 38 -- .../usecases/update-password.usecase.ts | 34 -- .../usecases/update-username.usecase.ts | 67 -- .../validate-authentication.usecase.ts | 41 -- .../mappers/authentication.profile.ts | 18 - .../mappers/username.profile.ts | 21 - .../queries/validate-authentication.query.ts | 9 - .../authentication.repository.spec.ts | 162 ----- .../integration/username.repository.spec.ts | 282 --------- .../tests/unit/add-username.usecase.spec.ts | 79 --- .../create-authentication.usecase.spec.ts | 99 --- .../delete-authentication.usecase.spec.ts | 102 ---- .../unit/delete-username.usecase.spec.ts | 115 ---- .../tests/unit/messager.spec.ts | 47 -- .../unit/update-password.usecase.spec.ts | 81 --- .../unit/update-username.usecase.spec.ts | 151 ----- .../validate-authentication.usecase.spec.ts | 107 ---- .../unit/rpc-validation-pipe.usecase.spec.ts | 6 +- 106 files changed, 847 insertions(+), 3654 deletions(-) delete mode 100644 src/modules/authorization/adapters/primaries/authorization.controller.ts delete mode 100644 src/modules/authorization/adapters/primaries/authorization.presenter.ts delete mode 100644 src/modules/authorization/adapters/secondaries/decision-result.ts delete mode 100644 src/modules/authorization/adapters/secondaries/decision.ts create mode 100644 src/modules/authorization/authorization.di-tokens.ts create mode 100644 src/modules/authorization/core/application/ports/decision-maker.port.ts create mode 100644 src/modules/authorization/core/application/queries/decision/decision.query-handler.ts create mode 100644 src/modules/authorization/core/application/queries/decision/decision.query.ts rename src/modules/authorization/{domain/dtos/action.enum.ts => core/domain/authorization.types.ts} (50%) delete mode 100644 src/modules/authorization/domain/dtos/context-item.ts delete mode 100644 src/modules/authorization/domain/dtos/decision.request.ts delete mode 100644 src/modules/authorization/domain/dtos/domain.enum.ts delete mode 100644 src/modules/authorization/domain/entities/authorization.ts delete mode 100644 src/modules/authorization/domain/interfaces/decision-maker.ts delete mode 100644 src/modules/authorization/domain/usecases/decision.usecase.ts rename src/modules/authorization/{adapters/secondaries => infrastructure}/opa.decision-maker.ts (54%) create mode 100644 src/modules/authorization/interface/dtos/decision.response.dto.ts rename src/modules/authorization/{adapters/primaries => interface/grpc-controllers}/authorization.proto (100%) create mode 100644 src/modules/authorization/interface/grpc-controllers/decide.grpc.controller.ts create mode 100644 src/modules/authorization/interface/grpc-controllers/dtos/decision.request.dto.ts delete mode 100644 src/modules/authorization/mappers/authorization.profile.ts delete mode 100644 src/modules/authorization/queries/decision.query.ts create mode 100644 src/modules/authorization/tests/unit/core/decision.query-handler.spec.ts delete mode 100644 src/modules/authorization/tests/unit/decision.usecase.spec.ts create mode 100644 src/modules/authorization/tests/unit/infrastructure/opa.decision-maker.spec.ts create mode 100644 src/modules/authorization/tests/unit/interface/decide.grpc.controller.spec.ts delete mode 100644 src/modules/authorization/tests/unit/opa.decision-maker.spec.ts delete mode 100644 src/modules/database/adapters/secondaries/prisma-repository.abstract.ts delete mode 100644 src/modules/database/adapters/secondaries/prisma-service.ts delete mode 100644 src/modules/database/database.module.ts delete mode 100644 src/modules/database/domain/auth-repository.ts delete mode 100644 src/modules/database/exceptions/database.exception.ts delete mode 100644 src/modules/database/interfaces/collection.interface.ts delete mode 100644 src/modules/database/interfaces/repository.interface.ts delete mode 100644 src/modules/database/tests/unit/prisma-repository.spec.ts delete mode 100644 src/modules/health/adapters/primaries/health.controller.ts delete mode 100644 src/modules/health/adapters/secondaries/message-broker.ts delete mode 100644 src/modules/health/adapters/secondaries/messager.ts create mode 100644 src/modules/health/core/application/ports/check-repository.port.ts create mode 100644 src/modules/health/core/application/usecases/repositories.health-indicator.usecase.ts delete mode 100644 src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts create mode 100644 src/modules/health/health.constants.ts create mode 100644 src/modules/health/health.di-tokens.ts rename src/modules/health/{adapters/primaries/health-server.controller.ts => interface/grpc-controllers/health.grpc.controller.ts} (56%) rename src/modules/health/{adapters/primaries => interface/grpc-controllers}/health.proto (100%) create mode 100644 src/modules/health/interface/http-controllers/health.http.controller.ts create mode 100644 src/modules/health/tests/unit/health.grpc.controller.spec.ts create mode 100644 src/modules/health/tests/unit/health.http.controller.spec.ts delete mode 100644 src/modules/health/tests/unit/messager.spec.ts delete mode 100644 src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts create mode 100644 src/modules/health/tests/unit/repositories.health-indicator.usecase.spec.ts delete mode 100644 src/modules/oldauthentication/adapters/primaries/authentication-messager.controller.ts delete mode 100644 src/modules/oldauthentication/adapters/primaries/authentication.controller.ts delete mode 100644 src/modules/oldauthentication/adapters/primaries/authentication.presenter.ts delete mode 100644 src/modules/oldauthentication/adapters/primaries/authentication.proto delete mode 100644 src/modules/oldauthentication/adapters/primaries/username.presenter.ts delete mode 100644 src/modules/oldauthentication/adapters/secondaries/authentication.repository.ts delete mode 100644 src/modules/oldauthentication/adapters/secondaries/messager.ts delete mode 100644 src/modules/oldauthentication/adapters/secondaries/username.repository.ts delete mode 100644 src/modules/oldauthentication/authentication.module.ts delete mode 100644 src/modules/oldauthentication/commands/add-username.command.ts delete mode 100644 src/modules/oldauthentication/commands/create-authentication.command.ts delete mode 100644 src/modules/oldauthentication/commands/delete-authentication.command.ts delete mode 100644 src/modules/oldauthentication/commands/delete-username.command.ts delete mode 100644 src/modules/oldauthentication/commands/update-password.command.ts delete mode 100644 src/modules/oldauthentication/commands/update-username.command.ts delete mode 100644 src/modules/oldauthentication/domain/dtos/add-username.request.ts delete mode 100644 src/modules/oldauthentication/domain/dtos/create-authentication.request.ts delete mode 100644 src/modules/oldauthentication/domain/dtos/delete-authentication.request.ts delete mode 100644 src/modules/oldauthentication/domain/dtos/delete-username.request.ts delete mode 100644 src/modules/oldauthentication/domain/dtos/type.enum.ts delete mode 100644 src/modules/oldauthentication/domain/dtos/update-password.request.ts delete mode 100644 src/modules/oldauthentication/domain/dtos/update-username.request.ts delete mode 100644 src/modules/oldauthentication/domain/dtos/validate-authentication.request.ts delete mode 100644 src/modules/oldauthentication/domain/entities/authentication.ts delete mode 100644 src/modules/oldauthentication/domain/entities/username.ts delete mode 100644 src/modules/oldauthentication/domain/interfaces/message-broker.ts delete mode 100644 src/modules/oldauthentication/domain/usecases/add-username.usecase.ts delete mode 100644 src/modules/oldauthentication/domain/usecases/create-authentication.usecase.ts delete mode 100644 src/modules/oldauthentication/domain/usecases/delete-authentication.usecase.ts delete mode 100644 src/modules/oldauthentication/domain/usecases/delete-username.usecase.ts delete mode 100644 src/modules/oldauthentication/domain/usecases/update-password.usecase.ts delete mode 100644 src/modules/oldauthentication/domain/usecases/update-username.usecase.ts delete mode 100644 src/modules/oldauthentication/domain/usecases/validate-authentication.usecase.ts delete mode 100644 src/modules/oldauthentication/mappers/authentication.profile.ts delete mode 100644 src/modules/oldauthentication/mappers/username.profile.ts delete mode 100644 src/modules/oldauthentication/queries/validate-authentication.query.ts delete mode 100644 src/modules/oldauthentication/tests/integration/authentication.repository.spec.ts delete mode 100644 src/modules/oldauthentication/tests/integration/username.repository.spec.ts delete mode 100644 src/modules/oldauthentication/tests/unit/add-username.usecase.spec.ts delete mode 100644 src/modules/oldauthentication/tests/unit/create-authentication.usecase.spec.ts delete mode 100644 src/modules/oldauthentication/tests/unit/delete-authentication.usecase.spec.ts delete mode 100644 src/modules/oldauthentication/tests/unit/delete-username.usecase.spec.ts delete mode 100644 src/modules/oldauthentication/tests/unit/messager.spec.ts delete mode 100644 src/modules/oldauthentication/tests/unit/update-password.usecase.spec.ts delete mode 100644 src/modules/oldauthentication/tests/unit/update-username.usecase.spec.ts delete mode 100644 src/modules/oldauthentication/tests/unit/validate-authentication.usecase.spec.ts diff --git a/opa/user/delete.rego b/opa/user/delete.rego index 541466e..1a22dbc 100644 --- a/opa/user/delete.rego +++ b/opa/user/delete.rego @@ -3,7 +3,7 @@ package USER.DELETE default allow := false allow { - input.uuid == input.owner + input.id == input.owner } allow { diff --git a/opa/user/read.rego b/opa/user/read.rego index 843d31b..ac4418c 100644 --- a/opa/user/read.rego +++ b/opa/user/read.rego @@ -3,7 +3,7 @@ package USER.READ default allow := false allow { - input.uuid == input.owner + input.id == input.owner } allow { diff --git a/opa/user/update.rego b/opa/user/update.rego index 6cde81b..cb2ed6c 100644 --- a/opa/user/update.rego +++ b/opa/user/update.rego @@ -3,7 +3,7 @@ package USER.UPDATE default allow := false allow { - input.uuid == input.owner + input.id == input.owner } allow { diff --git a/src/app.module.ts b/src/app.module.ts index 96860ca..52594b7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,15 +1,13 @@ -// import { classes } from '@automapper/classes'; -// import { AutomapperModule } from '@automapper/nestjs'; import { Module } from '@nestjs/common'; 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 { EventEmitterModule } from '@nestjs/event-emitter'; import { MessageBrokerModule, MessageBrokerModuleOptions, } from '@mobicoop/message-broker-module'; +import { HealthModule } from '@modules/health/health.module'; +import { AuthorizationModule } from '@modules/authorization/authorization.module'; @Module({ imports: [ @@ -36,10 +34,9 @@ import { }, }), }), - // AutomapperModule.forRoot({ strategyInitializer: classes() }), AuthenticationModule, - // AuthorizationModule, - // HealthModule, + AuthorizationModule, + HealthModule, ], controllers: [], providers: [], diff --git a/src/main.ts b/src/main.ts index 82277f9..9e21a07 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,9 +19,12 @@ async function bootstrap() { ), join( __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, loader: { keepCase: true, enums: String }, diff --git a/src/modules/authentication/authentication.module.ts b/src/modules/authentication/authentication.module.ts index 9b5ad69..688efcb 100644 --- a/src/modules/authentication/authentication.module.ts +++ b/src/modules/authentication/authentication.module.ts @@ -88,6 +88,7 @@ const orms: Provider[] = [PrismaService]; exports: [ PrismaService, AuthenticationMapper, + UsernameMapper, AUTHENTICATION_REPOSITORY, USERNAME_REPOSITORY, ], diff --git a/src/modules/authorization/adapters/primaries/authorization.controller.ts b/src/modules/authorization/adapters/primaries/authorization.controller.ts deleted file mode 100644 index a3eacd3..0000000 --- a/src/modules/authorization/adapters/primaries/authorization.controller.ts +++ /dev/null @@ -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 { - 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', - }); - } - } -} diff --git a/src/modules/authorization/adapters/primaries/authorization.presenter.ts b/src/modules/authorization/adapters/primaries/authorization.presenter.ts deleted file mode 100644 index c6f3733..0000000 --- a/src/modules/authorization/adapters/primaries/authorization.presenter.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { AutoMap } from '@automapper/classes'; - -export class AuthorizationPresenter { - @AutoMap() - allow: boolean; -} diff --git a/src/modules/authorization/adapters/secondaries/decision-result.ts b/src/modules/authorization/adapters/secondaries/decision-result.ts deleted file mode 100644 index 547205b..0000000 --- a/src/modules/authorization/adapters/secondaries/decision-result.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class DecisionResult { - allow: boolean; -} diff --git a/src/modules/authorization/adapters/secondaries/decision.ts b/src/modules/authorization/adapters/secondaries/decision.ts deleted file mode 100644 index ccc5e92..0000000 --- a/src/modules/authorization/adapters/secondaries/decision.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { DecisionResult } from './decision-result'; - -export class Decision { - decision_id: string; - result: DecisionResult; -} diff --git a/src/modules/authorization/authorization.di-tokens.ts b/src/modules/authorization/authorization.di-tokens.ts new file mode 100644 index 0000000..4bda93e --- /dev/null +++ b/src/modules/authorization/authorization.di-tokens.ts @@ -0,0 +1 @@ +export const DECISION_MAKER = Symbol('DECISION_MAKER'); diff --git a/src/modules/authorization/authorization.module.ts b/src/modules/authorization/authorization.module.ts index c66d76d..50480a9 100644 --- a/src/modules/authorization/authorization.module.ts +++ b/src/modules/authorization/authorization.module.ts @@ -1,16 +1,26 @@ -import { HttpModule } from '@nestjs/axios'; -import { Module } from '@nestjs/common'; +import { Module, Provider } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; -import { DatabaseModule } from '../database/database.module'; -import { AuthorizationController } from './adapters/primaries/authorization.controller'; -import { OpaDecisionMaker } from './adapters/secondaries/opa.decision-maker'; -import { DecisionUseCase } from './domain/usecases/decision.usecase'; -import { AuthorizationProfile } from './mappers/authorization.profile'; +import { DecideGrpcController } from './interface/grpc-controllers/decide.grpc.controller'; +import { DecisionQueryHandler } from './core/application/queries/decision/decision.query-handler'; +import { DECISION_MAKER } from './authorization.di-tokens'; +import { OpaDecisionMaker } from './infrastructure/opa.decision-maker'; +import { HttpModule } from '@nestjs/axios'; + +const grpcControllers = [DecideGrpcController]; + +const queryHandlers: Provider[] = [DecisionQueryHandler]; + +const adapters: Provider[] = [ + { + provide: DECISION_MAKER, + useClass: OpaDecisionMaker, + }, +]; @Module({ - imports: [DatabaseModule, CqrsModule, HttpModule], + imports: [CqrsModule, HttpModule], + controllers: [...grpcControllers], + providers: [...queryHandlers, ...adapters], exports: [], - controllers: [AuthorizationController], - providers: [OpaDecisionMaker, DecisionUseCase, AuthorizationProfile], }) export class AuthorizationModule {} diff --git a/src/modules/authorization/core/application/ports/decision-maker.port.ts b/src/modules/authorization/core/application/ports/decision-maker.port.ts new file mode 100644 index 0000000..0fb094d --- /dev/null +++ b/src/modules/authorization/core/application/ports/decision-maker.port.ts @@ -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; +} diff --git a/src/modules/authorization/core/application/queries/decision/decision.query-handler.ts b/src/modules/authorization/core/application/queries/decision/decision.query-handler.ts new file mode 100644 index 0000000..cc16468 --- /dev/null +++ b/src/modules/authorization/core/application/queries/decision/decision.query-handler.ts @@ -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 => + this.decisionMaker.decide( + decisionQuery.domain, + decisionQuery.action, + decisionQuery.context, + ); +} diff --git a/src/modules/authorization/core/application/queries/decision/decision.query.ts b/src/modules/authorization/core/application/queries/decision/decision.query.ts new file mode 100644 index 0000000..0e7a821 --- /dev/null +++ b/src/modules/authorization/core/application/queries/decision/decision.query.ts @@ -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; + } +} diff --git a/src/modules/authorization/domain/dtos/action.enum.ts b/src/modules/authorization/core/domain/authorization.types.ts similarity index 50% rename from src/modules/authorization/domain/dtos/action.enum.ts rename to src/modules/authorization/core/domain/authorization.types.ts index 6e08612..f79dae8 100644 --- a/src/modules/authorization/domain/dtos/action.enum.ts +++ b/src/modules/authorization/core/domain/authorization.types.ts @@ -1,3 +1,14 @@ +export interface ContextItem { + name: string; + value: any; +} + +export enum Domain { + USER = 'USER', + ADMIN = 'ADMIN', + AD = 'AD', +} + export enum Action { CREATE = 'CREATE', READ = 'READ', diff --git a/src/modules/authorization/domain/dtos/context-item.ts b/src/modules/authorization/domain/dtos/context-item.ts deleted file mode 100644 index b9b95dc..0000000 --- a/src/modules/authorization/domain/dtos/context-item.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class ContextItem { - name: string; - value: any; - - constructor(name: string, value: any) { - this.name = name; - this.value = value; - } -} diff --git a/src/modules/authorization/domain/dtos/decision.request.ts b/src/modules/authorization/domain/dtos/decision.request.ts deleted file mode 100644 index 7ab1da6..0000000 --- a/src/modules/authorization/domain/dtos/decision.request.ts +++ /dev/null @@ -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; -} diff --git a/src/modules/authorization/domain/dtos/domain.enum.ts b/src/modules/authorization/domain/dtos/domain.enum.ts deleted file mode 100644 index 9d24c49..0000000 --- a/src/modules/authorization/domain/dtos/domain.enum.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum Domain { - USER = 'USER', - ADMIN = 'ADMIN', - AD = 'AD', -} diff --git a/src/modules/authorization/domain/entities/authorization.ts b/src/modules/authorization/domain/entities/authorization.ts deleted file mode 100644 index 3869a3b..0000000 --- a/src/modules/authorization/domain/entities/authorization.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AutoMap } from '@automapper/classes'; - -export class Authorization { - @AutoMap() - allow: boolean; - - constructor(allow: boolean) { - this.allow = allow; - } -} diff --git a/src/modules/authorization/domain/interfaces/decision-maker.ts b/src/modules/authorization/domain/interfaces/decision-maker.ts deleted file mode 100644 index 4492f27..0000000 --- a/src/modules/authorization/domain/interfaces/decision-maker.ts +++ /dev/null @@ -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; -} diff --git a/src/modules/authorization/domain/usecases/decision.usecase.ts b/src/modules/authorization/domain/usecases/decision.usecase.ts deleted file mode 100644 index a1799fd..0000000 --- a/src/modules/authorization/domain/usecases/decision.usecase.ts +++ /dev/null @@ -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 => - this.decisionMaker.decide( - decisionQuery.domain, - decisionQuery.action, - decisionQuery.context, - ); -} diff --git a/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts b/src/modules/authorization/infrastructure/opa.decision-maker.ts similarity index 54% rename from src/modules/authorization/adapters/secondaries/opa.decision-maker.ts rename to src/modules/authorization/infrastructure/opa.decision-maker.ts index 9a012fa..160828e 100644 --- a/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts +++ b/src/modules/authorization/infrastructure/opa.decision-maker.ts @@ -2,15 +2,15 @@ import { HttpService } from '@nestjs/axios'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { lastValueFrom } from 'rxjs'; -import { Action } from '../../domain/dtos/action.enum'; -import { Domain } from '../../domain/dtos/domain.enum'; -import { IMakeDecision } from '../../domain/interfaces/decision-maker'; -import { ContextItem } from '../../domain/dtos/context-item'; -import { Decision } from './decision'; -import { Authorization } from '../../domain/entities/authorization'; +import { DecisionMakerPort } from '../core/application/ports/decision-maker.port'; +import { + Action, + ContextItem, + Domain, +} from '../core/domain/authorization.types'; @Injectable() -export class OpaDecisionMaker extends IMakeDecision { +export class OpaDecisionMaker extends DecisionMakerPort { constructor( private readonly configService: ConfigService, private readonly httpService: HttpService, @@ -21,8 +21,8 @@ export class OpaDecisionMaker extends IMakeDecision { decide = async ( domain: Domain, action: Action, - context: Array, - ): Promise => { + context: ContextItem[], + ): Promise => { const reducedContext = context.reduce( (obj, item) => Object.assign(obj, { [item.name]: item.value }), {}, @@ -30,7 +30,7 @@ export class OpaDecisionMaker extends IMakeDecision { try { const { data } = await lastValueFrom( this.httpService.post( - this.configService.get('OPA_URL') + domain + '/' + action, + `${this.configService.get('OPA_URL')}${domain}/${action}`, { input: { ...reducedContext, @@ -38,9 +38,16 @@ export class OpaDecisionMaker extends IMakeDecision { }, ), ); - return new Authorization(data.result.allow); + return data.result.allow; } catch (e) { - return new Authorization(false); + return false; } }; } + +type Decision = { + decision_id: string; + result: { + allow: boolean; + }; +}; diff --git a/src/modules/authorization/interface/dtos/decision.response.dto.ts b/src/modules/authorization/interface/dtos/decision.response.dto.ts new file mode 100644 index 0000000..ac1f398 --- /dev/null +++ b/src/modules/authorization/interface/dtos/decision.response.dto.ts @@ -0,0 +1,7 @@ +export class DecisionResponseDto { + readonly allow: boolean; + + constructor(allow: boolean) { + this.allow = allow; + } +} diff --git a/src/modules/authorization/adapters/primaries/authorization.proto b/src/modules/authorization/interface/grpc-controllers/authorization.proto similarity index 100% rename from src/modules/authorization/adapters/primaries/authorization.proto rename to src/modules/authorization/interface/grpc-controllers/authorization.proto diff --git a/src/modules/authorization/interface/grpc-controllers/decide.grpc.controller.ts b/src/modules/authorization/interface/grpc-controllers/decide.grpc.controller.ts new file mode 100644 index 0000000..a450bac --- /dev/null +++ b/src/modules/authorization/interface/grpc-controllers/decide.grpc.controller.ts @@ -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 { + 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', + }); + } + } +} diff --git a/src/modules/authorization/interface/grpc-controllers/dtos/decision.request.dto.ts b/src/modules/authorization/interface/grpc-controllers/dtos/decision.request.dto.ts new file mode 100644 index 0000000..13a60f2 --- /dev/null +++ b/src/modules/authorization/interface/grpc-controllers/dtos/decision.request.dto.ts @@ -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[]; +} diff --git a/src/modules/authorization/mappers/authorization.profile.ts b/src/modules/authorization/mappers/authorization.profile.ts deleted file mode 100644 index db4419d..0000000 --- a/src/modules/authorization/mappers/authorization.profile.ts +++ /dev/null @@ -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); - }; - } -} diff --git a/src/modules/authorization/queries/decision.query.ts b/src/modules/authorization/queries/decision.query.ts deleted file mode 100644 index 16a5875..0000000 --- a/src/modules/authorization/queries/decision.query.ts +++ /dev/null @@ -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; - - constructor(domain: Domain, action: Action, context?: Array) { - this.domain = domain; - this.action = action; - this.context = context; - } -} diff --git a/src/modules/authorization/tests/unit/core/decision.query-handler.spec.ts b/src/modules/authorization/tests/unit/core/decision.query-handler.spec.ts new file mode 100644 index 0000000..c0c7bbc --- /dev/null +++ b/src/modules/authorization/tests/unit/core/decision.query-handler.spec.ts @@ -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); + }); + + 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(); + }); + }); +}); diff --git a/src/modules/authorization/tests/unit/decision.usecase.spec.ts b/src/modules/authorization/tests/unit/decision.usecase.spec.ts deleted file mode 100644 index 1e2cc07..0000000 --- a/src/modules/authorization/tests/unit/decision.usecase.spec.ts +++ /dev/null @@ -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); - }); - - 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(); - }); - }); -}); diff --git a/src/modules/authorization/tests/unit/infrastructure/opa.decision-maker.spec.ts b/src/modules/authorization/tests/unit/infrastructure/opa.decision-maker.spec.ts new file mode 100644 index 0000000..811d6c9 --- /dev/null +++ b/src/modules/authorization/tests/unit/infrastructure/opa.decision-maker.spec.ts @@ -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); + }); + + 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(); + }); +}); diff --git a/src/modules/authorization/tests/unit/interface/decide.grpc.controller.spec.ts b/src/modules/authorization/tests/unit/interface/decide.grpc.controller.spec.ts new file mode 100644 index 0000000..ac64327 --- /dev/null +++ b/src/modules/authorization/tests/unit/interface/decide.grpc.controller.spec.ts @@ -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); + }); + + 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); + }); +}); diff --git a/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts b/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts deleted file mode 100644 index 4874ae9..0000000 --- a/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts +++ /dev/null @@ -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); - }); - - 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(); - }); - }); -}); diff --git a/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts b/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts deleted file mode 100644 index 02c5cf9..0000000 --- a/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts +++ /dev/null @@ -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 implements IRepository { - protected model: string; - - constructor(protected readonly prisma: PrismaService) {} - - findAll = async ( - page = 1, - perPage = 10, - where?: any, - include?: any, - ): Promise> => { - 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 => { - 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 => { - 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 | any, include?: any): Promise { - 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): Promise => { - 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 | any, - include?: any, - ): Promise => { - 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 => { - 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 => { - 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> => { - 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 => { - 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 => { - 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 => { - 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(); - } - } - }; -} diff --git a/src/modules/database/adapters/secondaries/prisma-service.ts b/src/modules/database/adapters/secondaries/prisma-service.ts deleted file mode 100644 index edf6532..0000000 --- a/src/modules/database/adapters/secondaries/prisma-service.ts +++ /dev/null @@ -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(); - }); - } -} diff --git a/src/modules/database/database.module.ts b/src/modules/database/database.module.ts deleted file mode 100644 index 40d3fc7..0000000 --- a/src/modules/database/database.module.ts +++ /dev/null @@ -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 {} diff --git a/src/modules/database/domain/auth-repository.ts b/src/modules/database/domain/auth-repository.ts deleted file mode 100644 index e20e282..0000000 --- a/src/modules/database/domain/auth-repository.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { PrismaRepository } from '../adapters/secondaries/prisma-repository.abstract'; - -export class AuthRepository extends PrismaRepository {} diff --git a/src/modules/database/exceptions/database.exception.ts b/src/modules/database/exceptions/database.exception.ts deleted file mode 100644 index b0782a6..0000000 --- a/src/modules/database/exceptions/database.exception.ts +++ /dev/null @@ -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; - } -} diff --git a/src/modules/database/interfaces/collection.interface.ts b/src/modules/database/interfaces/collection.interface.ts deleted file mode 100644 index 6e9a96d..0000000 --- a/src/modules/database/interfaces/collection.interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ICollection { - data: T[]; - total: number; -} diff --git a/src/modules/database/interfaces/repository.interface.ts b/src/modules/database/interfaces/repository.interface.ts deleted file mode 100644 index 8912545..0000000 --- a/src/modules/database/interfaces/repository.interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ICollection } from './collection.interface'; - -export interface IRepository { - findAll( - page: number, - perPage: number, - params?: any, - include?: any, - ): Promise>; - findOne(where: any, include?: any): Promise; - findOneByUuid(uuid: string, include?: any): Promise; - create(entity: Partial | any, include?: any): Promise; - update(uuid: string, entity: Partial, include?: any): Promise; - updateWhere(where: any, entity: Partial | any, include?: any): Promise; - delete(uuid: string): Promise; - deleteMany(where: any): Promise; -} diff --git a/src/modules/database/tests/unit/prisma-repository.spec.ts b/src/modules/database/tests/unit/prisma-repository.spec.ts deleted file mode 100644 index eb3bad0..0000000 --- a/src/modules/database/tests/unit/prisma-repository.spec.ts +++ /dev/null @@ -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 { - 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); - prisma = module.get(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, - ); - }); - }); -}); diff --git a/src/modules/health/adapters/primaries/health.controller.ts b/src/modules/health/adapters/primaries/health.controller.ts deleted file mode 100644 index caf3cf3..0000000 --- a/src/modules/health/adapters/primaries/health.controller.ts +++ /dev/null @@ -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; - } - } -} diff --git a/src/modules/health/adapters/secondaries/message-broker.ts b/src/modules/health/adapters/secondaries/message-broker.ts deleted file mode 100644 index 594aa43..0000000 --- a/src/modules/health/adapters/secondaries/message-broker.ts +++ /dev/null @@ -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; -} diff --git a/src/modules/health/adapters/secondaries/messager.ts b/src/modules/health/adapters/secondaries/messager.ts deleted file mode 100644 index cd7e7ef..0000000 --- a/src/modules/health/adapters/secondaries/messager.ts +++ /dev/null @@ -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('RMQ_EXCHANGE')); - } - - publish = (routingKey: string, message: string): void => { - this.amqpConnection.publish(this.exchange, routingKey, message); - }; -} diff --git a/src/modules/health/core/application/ports/check-repository.port.ts b/src/modules/health/core/application/ports/check-repository.port.ts new file mode 100644 index 0000000..64d8980 --- /dev/null +++ b/src/modules/health/core/application/ports/check-repository.port.ts @@ -0,0 +1,3 @@ +export interface CheckRepositoryPort { + healthCheck(): Promise; +} diff --git a/src/modules/health/core/application/usecases/repositories.health-indicator.usecase.ts b/src/modules/health/core/application/usecases/repositories.health-indicator.usecase.ts new file mode 100644 index 0000000..300af4b --- /dev/null +++ b/src/modules/health/core/application/usecases/repositories.health-indicator.usecase.ts @@ -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 => { + 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, + }); + } + }; +} diff --git a/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts b/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts deleted file mode 100644 index 2d77a7b..0000000 --- a/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts +++ /dev/null @@ -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 => { - try { - await this.repository.healthCheck(); - return this.getStatus(key, true); - } catch (e) { - throw new HealthCheckError('Prisma', { - prisma: e.message, - }); - } - }; -} diff --git a/src/modules/health/health.constants.ts b/src/modules/health/health.constants.ts new file mode 100644 index 0000000..53384ab --- /dev/null +++ b/src/modules/health/health.constants.ts @@ -0,0 +1 @@ +export const LOGGING_AUTHENTICATION_HEALTH_CRIT = 'logging.auth.health.crit'; diff --git a/src/modules/health/health.di-tokens.ts b/src/modules/health/health.di-tokens.ts new file mode 100644 index 0000000..5160269 --- /dev/null +++ b/src/modules/health/health.di-tokens.ts @@ -0,0 +1,2 @@ +export const AUTHENTICATION_REPOSITORY = Symbol('AUTHENTICATION_REPOSITORY'); +export const USERNAME_REPOSITORY = Symbol('USERNAME_REPOSITORY'); diff --git a/src/modules/health/health.module.ts b/src/modules/health/health.module.ts index 374fe2f..6d8706f 100644 --- a/src/modules/health/health.module.ts +++ b/src/modules/health/health.module.ts @@ -1,34 +1,45 @@ -import { Module } from '@nestjs/common'; -import { HealthServerController } from './adapters/primaries/health-server.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 { Module, Provider } from '@nestjs/common'; +import { HealthHttpController } from './interface/http-controllers/health.http.controller'; import { TerminusModule } from '@nestjs/terminus'; -import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { Messager } from './adapters/secondaries/messager'; +import { RepositoriesHealthIndicatorUseCase } from './core/application/usecases/repositories.health-indicator.usecase'; +import { HealthGrpcController } from './interface/grpc-controllers/health.grpc.controller'; +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({ - imports: [ - TerminusModule, - RabbitMQModule.forRootAsync(RabbitMQModule, { - imports: [ConfigModule], - useFactory: async (configService: ConfigService) => ({ - exchanges: [ - { - name: configService.get('RMQ_EXCHANGE'), - type: 'topic', - }, - ], - uri: configService.get('RMQ_URI'), - connectionInitOptions: { wait: false }, - }), - inject: [ConfigService], - }), - DatabaseModule, - ], - controllers: [HealthServerController, HealthController], - providers: [PrismaHealthIndicatorUseCase, AuthenticationRepository, Messager], + imports: [TerminusModule, AuthenticationModule], + controllers: [...grpcControllers, ...httpControllers], + providers: [...useCases, ...repositories, ...messageBrokers], }) export class HealthModule {} diff --git a/src/modules/health/adapters/primaries/health-server.controller.ts b/src/modules/health/interface/grpc-controllers/health.grpc.controller.ts similarity index 56% rename from src/modules/health/adapters/primaries/health-server.controller.ts rename to src/modules/health/interface/grpc-controllers/health.grpc.controller.ts index 858f705..a016ee4 100644 --- a/src/modules/health/adapters/primaries/health-server.controller.ts +++ b/src/modules/health/interface/grpc-controllers/health.grpc.controller.ts @@ -1,8 +1,8 @@ import { Controller } from '@nestjs/common'; 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, SERVING = 1, NOT_SERVING = 2, @@ -16,26 +16,25 @@ interface HealthCheckResponse { status: ServingStatus; } -// this controller responds to gRPC health check service @Controller() -export class HealthServerController { +export class HealthGrpcController { constructor( - private readonly prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase, + private readonly repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase, ) {} @GrpcMethod('Health', 'Check') async check( // eslint-disable-next-line @typescript-eslint/no-unused-vars - data: HealthCheckRequest, + data?: HealthCheckRequest, // eslint-disable-next-line @typescript-eslint/no-unused-vars - metadata: any, + metadata?: any, ): Promise { - const healthCheck = await this.prismaHealthIndicatorUseCase.isHealthy( - 'prisma', + const healthCheck = await this.repositoriesHealthIndicatorUseCase.isHealthy( + 'repositories', ); return { status: - healthCheck['prisma'].status == 'up' + healthCheck['repositories'].status == 'up' ? ServingStatus.SERVING : ServingStatus.NOT_SERVING, }; diff --git a/src/modules/health/adapters/primaries/health.proto b/src/modules/health/interface/grpc-controllers/health.proto similarity index 100% rename from src/modules/health/adapters/primaries/health.proto rename to src/modules/health/interface/grpc-controllers/health.proto diff --git a/src/modules/health/interface/http-controllers/health.http.controller.ts b/src/modules/health/interface/http-controllers/health.http.controller.ts new file mode 100644 index 0000000..32428e9 --- /dev/null +++ b/src/modules/health/interface/http-controllers/health.http.controller.ts @@ -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 { + try { + return await this.healthCheckService.check([ + async () => + this.repositoriesHealthIndicatorUseCase.isHealthy('repositories'), + ]); + } catch (error) { + throw error; + } + } +} diff --git a/src/modules/health/tests/unit/health.grpc.controller.spec.ts b/src/modules/health/tests/unit/health.grpc.controller.spec.ts new file mode 100644 index 0000000..25434ad --- /dev/null +++ b/src/modules/health/tests/unit/health.grpc.controller.spec.ts @@ -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); + }); + + 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); + }); +}); diff --git a/src/modules/health/tests/unit/health.http.controller.spec.ts b/src/modules/health/tests/unit/health.http.controller.spec.ts new file mode 100644 index 0000000..3fe40bb --- /dev/null +++ b/src/modules/health/tests/unit/health.http.controller.spec.ts @@ -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); + }); + + 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); + }); +}); diff --git a/src/modules/health/tests/unit/messager.spec.ts b/src/modules/health/tests/unit/messager.spec.ts deleted file mode 100644 index 0331332..0000000 --- a/src/modules/health/tests/unit/messager.spec.ts +++ /dev/null @@ -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); - }); - - 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); - }); -}); diff --git a/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts b/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts deleted file mode 100644 index d7d9971..0000000 --- a/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts +++ /dev/null @@ -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, - ); - }); - - 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); - }); - }); -}); diff --git a/src/modules/health/tests/unit/repositories.health-indicator.usecase.spec.ts b/src/modules/health/tests/unit/repositories.health-indicator.usecase.spec.ts new file mode 100644 index 0000000..eab6723 --- /dev/null +++ b/src/modules/health/tests/unit/repositories.health-indicator.usecase.spec.ts @@ -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, + ); + }); + + 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); + }); + }); +}); diff --git a/src/modules/oldauthentication/adapters/primaries/authentication-messager.controller.ts b/src/modules/oldauthentication/adapters/primaries/authentication-messager.controller.ts deleted file mode 100644 index 1c0cba2..0000000 --- a/src/modules/oldauthentication/adapters/primaries/authentication-messager.controller.ts +++ /dev/null @@ -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), - ); - } -} diff --git a/src/modules/oldauthentication/adapters/primaries/authentication.controller.ts b/src/modules/oldauthentication/adapters/primaries/authentication.controller.ts deleted file mode 100644 index af72330..0000000 --- a/src/modules/oldauthentication/adapters/primaries/authentication.controller.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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', - }); - } - } -} diff --git a/src/modules/oldauthentication/adapters/primaries/authentication.presenter.ts b/src/modules/oldauthentication/adapters/primaries/authentication.presenter.ts deleted file mode 100644 index 080c7eb..0000000 --- a/src/modules/oldauthentication/adapters/primaries/authentication.presenter.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { AutoMap } from '@automapper/classes'; - -export class AuthenticationPresenter { - @AutoMap() - uuid: string; -} diff --git a/src/modules/oldauthentication/adapters/primaries/authentication.proto b/src/modules/oldauthentication/adapters/primaries/authentication.proto deleted file mode 100644 index 9b16d3a..0000000 --- a/src/modules/oldauthentication/adapters/primaries/authentication.proto +++ /dev/null @@ -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 {} diff --git a/src/modules/oldauthentication/adapters/primaries/username.presenter.ts b/src/modules/oldauthentication/adapters/primaries/username.presenter.ts deleted file mode 100644 index 167e25a..0000000 --- a/src/modules/oldauthentication/adapters/primaries/username.presenter.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AutoMap } from '@automapper/classes'; - -export class UsernamePresenter { - @AutoMap() - uuid: string; - - @AutoMap() - username: string; -} diff --git a/src/modules/oldauthentication/adapters/secondaries/authentication.repository.ts b/src/modules/oldauthentication/adapters/secondaries/authentication.repository.ts deleted file mode 100644 index e8fe067..0000000 --- a/src/modules/oldauthentication/adapters/secondaries/authentication.repository.ts +++ /dev/null @@ -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 { - protected model = 'auth'; -} diff --git a/src/modules/oldauthentication/adapters/secondaries/messager.ts b/src/modules/oldauthentication/adapters/secondaries/messager.ts deleted file mode 100644 index afbbcb4..0000000 --- a/src/modules/oldauthentication/adapters/secondaries/messager.ts +++ /dev/null @@ -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('RMQ_EXCHANGE')); - } - - publish = (routingKey: string, message: string): void => { - this.amqpConnection.publish(this.exchange, routingKey, message); - }; -} diff --git a/src/modules/oldauthentication/adapters/secondaries/username.repository.ts b/src/modules/oldauthentication/adapters/secondaries/username.repository.ts deleted file mode 100644 index 53a5bbe..0000000 --- a/src/modules/oldauthentication/adapters/secondaries/username.repository.ts +++ /dev/null @@ -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 { - protected model = 'username'; -} diff --git a/src/modules/oldauthentication/authentication.module.ts b/src/modules/oldauthentication/authentication.module.ts deleted file mode 100644 index 3b7ed79..0000000 --- a/src/modules/oldauthentication/authentication.module.ts +++ /dev/null @@ -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('RMQ_EXCHANGE'), - type: 'topic', - }, - ], - handlers: { - userUpdate: { - exchange: configService.get('RMQ_EXCHANGE'), - routingKey: 'user.update', - }, - userDelete: { - exchange: configService.get('RMQ_EXCHANGE'), - routingKey: 'user.delete', - }, - }, - uri: configService.get('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 {} diff --git a/src/modules/oldauthentication/commands/add-username.command.ts b/src/modules/oldauthentication/commands/add-username.command.ts deleted file mode 100644 index 2036a23..0000000 --- a/src/modules/oldauthentication/commands/add-username.command.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AddUsernameRequest } from '../domain/dtos/add-username.request'; - -export class AddUsernameCommand { - readonly addUsernameRequest: AddUsernameRequest; - - constructor(request: AddUsernameRequest) { - this.addUsernameRequest = request; - } -} diff --git a/src/modules/oldauthentication/commands/create-authentication.command.ts b/src/modules/oldauthentication/commands/create-authentication.command.ts deleted file mode 100644 index 1d06deb..0000000 --- a/src/modules/oldauthentication/commands/create-authentication.command.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CreateAuthenticationRequest } from '../domain/dtos/create-authentication.request'; - -export class CreateAuthenticationCommand { - readonly createAuthenticationRequest: CreateAuthenticationRequest; - - constructor(request: CreateAuthenticationRequest) { - this.createAuthenticationRequest = request; - } -} diff --git a/src/modules/oldauthentication/commands/delete-authentication.command.ts b/src/modules/oldauthentication/commands/delete-authentication.command.ts deleted file mode 100644 index bea3cd2..0000000 --- a/src/modules/oldauthentication/commands/delete-authentication.command.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DeleteAuthenticationRequest } from '../domain/dtos/delete-authentication.request'; - -export class DeleteAuthenticationCommand { - readonly deleteAuthenticationRequest: DeleteAuthenticationRequest; - - constructor(request: DeleteAuthenticationRequest) { - this.deleteAuthenticationRequest = request; - } -} diff --git a/src/modules/oldauthentication/commands/delete-username.command.ts b/src/modules/oldauthentication/commands/delete-username.command.ts deleted file mode 100644 index c46a3bd..0000000 --- a/src/modules/oldauthentication/commands/delete-username.command.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DeleteUsernameRequest } from '../domain/dtos/delete-username.request'; - -export class DeleteUsernameCommand { - readonly deleteUsernameRequest: DeleteUsernameRequest; - - constructor(request: DeleteUsernameRequest) { - this.deleteUsernameRequest = request; - } -} diff --git a/src/modules/oldauthentication/commands/update-password.command.ts b/src/modules/oldauthentication/commands/update-password.command.ts deleted file mode 100644 index 2120df1..0000000 --- a/src/modules/oldauthentication/commands/update-password.command.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { UpdatePasswordRequest } from '../domain/dtos/update-password.request'; - -export class UpdatePasswordCommand { - readonly updatePasswordRequest: UpdatePasswordRequest; - - constructor(request: UpdatePasswordRequest) { - this.updatePasswordRequest = request; - } -} diff --git a/src/modules/oldauthentication/commands/update-username.command.ts b/src/modules/oldauthentication/commands/update-username.command.ts deleted file mode 100644 index 54a8415..0000000 --- a/src/modules/oldauthentication/commands/update-username.command.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { UpdateUsernameRequest } from '../domain/dtos/update-username.request'; - -export class UpdateUsernameCommand { - readonly updateUsernameRequest: UpdateUsernameRequest; - - constructor(request: UpdateUsernameRequest) { - this.updateUsernameRequest = request; - } -} diff --git a/src/modules/oldauthentication/domain/dtos/add-username.request.ts b/src/modules/oldauthentication/domain/dtos/add-username.request.ts deleted file mode 100644 index 85a3f25..0000000 --- a/src/modules/oldauthentication/domain/dtos/add-username.request.ts +++ /dev/null @@ -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; -} diff --git a/src/modules/oldauthentication/domain/dtos/create-authentication.request.ts b/src/modules/oldauthentication/domain/dtos/create-authentication.request.ts deleted file mode 100644 index 7ca6562..0000000 --- a/src/modules/oldauthentication/domain/dtos/create-authentication.request.ts +++ /dev/null @@ -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; -} diff --git a/src/modules/oldauthentication/domain/dtos/delete-authentication.request.ts b/src/modules/oldauthentication/domain/dtos/delete-authentication.request.ts deleted file mode 100644 index 09ecb56..0000000 --- a/src/modules/oldauthentication/domain/dtos/delete-authentication.request.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AutoMap } from '@automapper/classes'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class DeleteAuthenticationRequest { - @IsString() - @IsNotEmpty() - @AutoMap() - uuid: string; -} diff --git a/src/modules/oldauthentication/domain/dtos/delete-username.request.ts b/src/modules/oldauthentication/domain/dtos/delete-username.request.ts deleted file mode 100644 index 3b2a221..0000000 --- a/src/modules/oldauthentication/domain/dtos/delete-username.request.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AutoMap } from '@automapper/classes'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class DeleteUsernameRequest { - @IsString() - @IsNotEmpty() - @AutoMap() - username: string; -} diff --git a/src/modules/oldauthentication/domain/dtos/type.enum.ts b/src/modules/oldauthentication/domain/dtos/type.enum.ts deleted file mode 100644 index 2ef59cd..0000000 --- a/src/modules/oldauthentication/domain/dtos/type.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum Type { - EMAIL = 'EMAIL', - PHONE = 'PHONE', -} diff --git a/src/modules/oldauthentication/domain/dtos/update-password.request.ts b/src/modules/oldauthentication/domain/dtos/update-password.request.ts deleted file mode 100644 index bf4b6e4..0000000 --- a/src/modules/oldauthentication/domain/dtos/update-password.request.ts +++ /dev/null @@ -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; -} diff --git a/src/modules/oldauthentication/domain/dtos/update-username.request.ts b/src/modules/oldauthentication/domain/dtos/update-username.request.ts deleted file mode 100644 index 47ccf9d..0000000 --- a/src/modules/oldauthentication/domain/dtos/update-username.request.ts +++ /dev/null @@ -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; -} diff --git a/src/modules/oldauthentication/domain/dtos/validate-authentication.request.ts b/src/modules/oldauthentication/domain/dtos/validate-authentication.request.ts deleted file mode 100644 index 997d67e..0000000 --- a/src/modules/oldauthentication/domain/dtos/validate-authentication.request.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IsNotEmpty, IsString } from 'class-validator'; - -export class ValidateAuthenticationRequest { - @IsString() - @IsNotEmpty() - username: string; - - @IsString() - @IsNotEmpty() - password: string; -} diff --git a/src/modules/oldauthentication/domain/entities/authentication.ts b/src/modules/oldauthentication/domain/entities/authentication.ts deleted file mode 100644 index 6f00964..0000000 --- a/src/modules/oldauthentication/domain/entities/authentication.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { AutoMap } from '@automapper/classes'; - -export class Authentication { - @AutoMap() - uuid: string; - - password: string; -} diff --git a/src/modules/oldauthentication/domain/entities/username.ts b/src/modules/oldauthentication/domain/entities/username.ts deleted file mode 100644 index 333c190..0000000 --- a/src/modules/oldauthentication/domain/entities/username.ts +++ /dev/null @@ -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; -} diff --git a/src/modules/oldauthentication/domain/interfaces/message-broker.ts b/src/modules/oldauthentication/domain/interfaces/message-broker.ts deleted file mode 100644 index 594aa43..0000000 --- a/src/modules/oldauthentication/domain/interfaces/message-broker.ts +++ /dev/null @@ -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; -} diff --git a/src/modules/oldauthentication/domain/usecases/add-username.usecase.ts b/src/modules/oldauthentication/domain/usecases/add-username.usecase.ts deleted file mode 100644 index a8d5489..0000000 --- a/src/modules/oldauthentication/domain/usecases/add-username.usecase.ts +++ /dev/null @@ -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 => { - 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; - } - }; -} diff --git a/src/modules/oldauthentication/domain/usecases/create-authentication.usecase.ts b/src/modules/oldauthentication/domain/usecases/create-authentication.usecase.ts deleted file mode 100644 index 9ca07e4..0000000 --- a/src/modules/oldauthentication/domain/usecases/create-authentication.usecase.ts +++ /dev/null @@ -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 => { - 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; - } - }; -} diff --git a/src/modules/oldauthentication/domain/usecases/delete-authentication.usecase.ts b/src/modules/oldauthentication/domain/usecases/delete-authentication.usecase.ts deleted file mode 100644 index e429a5d..0000000 --- a/src/modules/oldauthentication/domain/usecases/delete-authentication.usecase.ts +++ /dev/null @@ -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 => { - 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; - } - }; -} diff --git a/src/modules/oldauthentication/domain/usecases/delete-username.usecase.ts b/src/modules/oldauthentication/domain/usecases/delete-username.usecase.ts deleted file mode 100644 index 76d3430..0000000 --- a/src/modules/oldauthentication/domain/usecases/delete-username.usecase.ts +++ /dev/null @@ -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 => { - 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; - } - }; -} diff --git a/src/modules/oldauthentication/domain/usecases/update-password.usecase.ts b/src/modules/oldauthentication/domain/usecases/update-password.usecase.ts deleted file mode 100644 index 5809f54..0000000 --- a/src/modules/oldauthentication/domain/usecases/update-password.usecase.ts +++ /dev/null @@ -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 => { - 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; - } - }; -} diff --git a/src/modules/oldauthentication/domain/usecases/update-username.usecase.ts b/src/modules/oldauthentication/domain/usecases/update-username.usecase.ts deleted file mode 100644 index 7870bf0..0000000 --- a/src/modules/oldauthentication/domain/usecases/update-username.usecase.ts +++ /dev/null @@ -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 => { - 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; - } - }; -} diff --git a/src/modules/oldauthentication/domain/usecases/validate-authentication.usecase.ts b/src/modules/oldauthentication/domain/usecases/validate-authentication.usecase.ts deleted file mode 100644 index 56829e9..0000000 --- a/src/modules/oldauthentication/domain/usecases/validate-authentication.usecase.ts +++ /dev/null @@ -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 => { - 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(); - } - }; -} diff --git a/src/modules/oldauthentication/mappers/authentication.profile.ts b/src/modules/oldauthentication/mappers/authentication.profile.ts deleted file mode 100644 index ab9156b..0000000 --- a/src/modules/oldauthentication/mappers/authentication.profile.ts +++ /dev/null @@ -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); - }; - } -} diff --git a/src/modules/oldauthentication/mappers/username.profile.ts b/src/modules/oldauthentication/mappers/username.profile.ts deleted file mode 100644 index af2cc76..0000000 --- a/src/modules/oldauthentication/mappers/username.profile.ts +++ /dev/null @@ -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); - }; - } -} diff --git a/src/modules/oldauthentication/queries/validate-authentication.query.ts b/src/modules/oldauthentication/queries/validate-authentication.query.ts deleted file mode 100644 index 32bb41c..0000000 --- a/src/modules/oldauthentication/queries/validate-authentication.query.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class ValidateAuthenticationQuery { - readonly username: string; - readonly password: string; - - constructor(username: string, password: string) { - this.username = username; - this.password = password; - } -} diff --git a/src/modules/oldauthentication/tests/integration/authentication.repository.spec.ts b/src/modules/oldauthentication/tests/integration/authentication.repository.spec.ts deleted file mode 100644 index ec29f55..0000000 --- a/src/modules/oldauthentication/tests/integration/authentication.repository.spec.ts +++ /dev/null @@ -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); - authenticationRepository = module.get( - 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); - }); - }); -}); diff --git a/src/modules/oldauthentication/tests/integration/username.repository.spec.ts b/src/modules/oldauthentication/tests/integration/username.repository.spec.ts deleted file mode 100644 index ccb1e3c..0000000 --- a/src/modules/oldauthentication/tests/integration/username.repository.spec.ts +++ /dev/null @@ -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); - usernameRepository = module.get(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); - }); - }); -}); diff --git a/src/modules/oldauthentication/tests/unit/add-username.usecase.spec.ts b/src/modules/oldauthentication/tests/unit/add-username.usecase.spec.ts deleted file mode 100644 index 4c5ddde..0000000 --- a/src/modules/oldauthentication/tests/unit/add-username.usecase.spec.ts +++ /dev/null @@ -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); - }); - - 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); - }); - }); -}); diff --git a/src/modules/oldauthentication/tests/unit/create-authentication.usecase.spec.ts b/src/modules/oldauthentication/tests/unit/create-authentication.usecase.spec.ts deleted file mode 100644 index 2ee6e9c..0000000 --- a/src/modules/oldauthentication/tests/unit/create-authentication.usecase.spec.ts +++ /dev/null @@ -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, - ); - }); - - 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); - }); - }); -}); diff --git a/src/modules/oldauthentication/tests/unit/delete-authentication.usecase.spec.ts b/src/modules/oldauthentication/tests/unit/delete-authentication.usecase.spec.ts deleted file mode 100644 index f3bda36..0000000 --- a/src/modules/oldauthentication/tests/unit/delete-authentication.usecase.spec.ts +++ /dev/null @@ -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, - ); - }); - - 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); - }); - }); -}); diff --git a/src/modules/oldauthentication/tests/unit/delete-username.usecase.spec.ts b/src/modules/oldauthentication/tests/unit/delete-username.usecase.spec.ts deleted file mode 100644 index 854479e..0000000 --- a/src/modules/oldauthentication/tests/unit/delete-username.usecase.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { classes } from '@automapper/classes'; -import { AutomapperModule } from '@automapper/nestjs'; -import { UnauthorizedException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { Messager } from '../../adapters/secondaries/messager'; -import { UsernameRepository } from '../../adapters/secondaries/username.repository'; -import { DeleteUsernameCommand } from '../../commands/delete-username.command'; -import { DeleteUsernameRequest } from '../../domain/dtos/delete-username.request'; -import { Type } from '../../domain/dtos/type.enum'; -import { DeleteUsernameUseCase } from '../../domain/usecases/delete-username.usecase'; - -const usernamesEmail = { - 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 usernamesPhone = { - data: [ - { - uuid: 'a7fa221f-dd77-481c-bb77-ae89da662c87', - username: '0611223344', - type: Type.PHONE, - }, - ], - total: 1, -}; - -const deleteUsernameEmailRequest: DeleteUsernameRequest = - new DeleteUsernameRequest(); -deleteUsernameEmailRequest.username = 'john.doe@email.com'; - -const deleteUsernamePhoneRequest: DeleteUsernameRequest = - new DeleteUsernameRequest(); -deleteUsernamePhoneRequest.username = '0611223344'; - -const deleteUsernameEmailCommand: DeleteUsernameCommand = - new DeleteUsernameCommand(deleteUsernameEmailRequest); - -const deleteUsernamePhoneCommand: DeleteUsernameCommand = - new DeleteUsernameCommand(deleteUsernamePhoneRequest); - -const mockUsernameRepository = { - findOne: jest.fn().mockImplementation((where) => { - if (where.username == 'john.doe@email.com') { - return { uuid: 'cf76af29-f75d-4f6e-bb40-4ecbcfa3356e' }; - } - return { uuid: 'a7fa221f-dd77-481c-bb77-ae89da662c87' }; - }), - findAll: jest.fn().mockImplementation((page, perPage, query) => { - if (query.uuid == 'cf76af29-f75d-4f6e-bb40-4ecbcfa3356e') { - return Promise.resolve(usernamesEmail); - } - return Promise.resolve(usernamesPhone); - }), - deleteMany: jest.fn().mockResolvedValue(undefined), -}; - -const mockMessager = { - publish: jest.fn().mockImplementation(), -}; - -describe('DeleteUsernameUseCase', () => { - let deleteUsernameUseCase: DeleteUsernameUseCase; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], - providers: [ - { - provide: UsernameRepository, - useValue: mockUsernameRepository, - }, - { - provide: Messager, - useValue: mockMessager, - }, - DeleteUsernameUseCase, - ], - }).compile(); - - deleteUsernameUseCase = module.get( - DeleteUsernameUseCase, - ); - }); - - it('should be defined', () => { - expect(deleteUsernameUseCase).toBeDefined(); - }); - - describe('execute', () => { - it('should delete a username', async () => { - const deletedEmailUsername = await deleteUsernameUseCase.execute( - deleteUsernameEmailCommand, - ); - expect(deletedEmailUsername).toBe(undefined); - }); - - it('should throw an exception if auth has only one username', async () => { - await expect( - deleteUsernameUseCase.execute(deleteUsernamePhoneCommand), - ).rejects.toBeInstanceOf(UnauthorizedException); - }); - }); -}); diff --git a/src/modules/oldauthentication/tests/unit/messager.spec.ts b/src/modules/oldauthentication/tests/unit/messager.spec.ts deleted file mode 100644 index 97107d1..0000000 --- a/src/modules/oldauthentication/tests/unit/messager.spec.ts +++ /dev/null @@ -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); - }); - - it('should be defined', () => { - expect(messager).toBeDefined(); - }); - - it('should publish a message', async () => { - jest.spyOn(mockAmqpConnection, 'publish'); - messager.publish('authentication.create.info', 'my-test'); - expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/modules/oldauthentication/tests/unit/update-password.usecase.spec.ts b/src/modules/oldauthentication/tests/unit/update-password.usecase.spec.ts deleted file mode 100644 index 982ea1e..0000000 --- a/src/modules/oldauthentication/tests/unit/update-password.usecase.spec.ts +++ /dev/null @@ -1,81 +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 { Authentication } from '../../domain/entities/authentication'; -import * as bcrypt from 'bcrypt'; -import { UpdatePasswordRequest } from '../../domain/dtos/update-password.request'; -import { UpdatePasswordCommand } from '../../commands/update-password.command'; -import { UpdatePasswordUseCase } from '../../domain/usecases/update-password.usecase'; -import { Messager } from '../../adapters/secondaries/messager'; - -const updatePasswordRequest: UpdatePasswordRequest = - new UpdatePasswordRequest(); -updatePasswordRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; -updatePasswordRequest.password = 'John123'; - -const updatePasswordCommand: UpdatePasswordCommand = new UpdatePasswordCommand( - updatePasswordRequest, -); - -const mockAuthenticationRepository = { - update: jest - .fn() - .mockResolvedValueOnce({ - uuid: updatePasswordRequest.uuid, - password: bcrypt.hashSync(updatePasswordRequest.password, 10), - }) - .mockImplementation(() => { - throw new Error('Error'); - }), -}; - -const mockMessager = { - publish: jest.fn().mockImplementation(), -}; - -describe('UpdatePasswordUseCase', () => { - let updatePasswordUseCase: UpdatePasswordUseCase; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], - providers: [ - { - provide: AuthenticationRepository, - useValue: mockAuthenticationRepository, - }, - { - provide: Messager, - useValue: mockMessager, - }, - UpdatePasswordUseCase, - ], - }).compile(); - - updatePasswordUseCase = module.get( - UpdatePasswordUseCase, - ); - }); - - it('should be defined', () => { - expect(updatePasswordUseCase).toBeDefined(); - }); - - describe('execute', () => { - it('should update an auth with an new encrypted password', async () => { - const newAuth: Authentication = await updatePasswordUseCase.execute( - updatePasswordCommand, - ); - - expect( - bcrypt.compareSync(updatePasswordRequest.password, newAuth.password), - ).toBeTruthy(); - }); - it('should throw an error if auth does not exist', async () => { - await expect( - updatePasswordUseCase.execute(updatePasswordCommand), - ).rejects.toBeInstanceOf(Error); - }); - }); -}); diff --git a/src/modules/oldauthentication/tests/unit/update-username.usecase.spec.ts b/src/modules/oldauthentication/tests/unit/update-username.usecase.spec.ts deleted file mode 100644 index ca3f8b3..0000000 --- a/src/modules/oldauthentication/tests/unit/update-username.usecase.spec.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { classes } from '@automapper/classes'; -import { AutomapperModule } from '@automapper/nestjs'; -import { Test, TestingModule } from '@nestjs/testing'; -import { UsernameRepository } from '../../adapters/secondaries/username.repository'; -import { Username } from '../../domain/entities/username'; -import { UpdateUsernameRequest } from '../../domain/dtos/update-username.request'; -import { UpdateUsernameCommand } from '../../commands/update-username.command'; -import { Type } from '../../domain/dtos/type.enum'; -import { UpdateUsernameUseCase } from '../../domain/usecases/update-username.usecase'; -import { CommandBus } from '@nestjs/cqrs'; -import { UsernameProfile } from '../../mappers/username.profile'; -import { BadRequestException } from '@nestjs/common'; -import { Messager } from '../../adapters/secondaries/messager'; - -const existingUsername = { - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - username: 'john.doe@email.com', - type: Type.EMAIL, -}; - -const newUsernameRequest: UpdateUsernameRequest = new UpdateUsernameRequest(); -newUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a90'; -newUsernameRequest.username = '+33611223344'; -newUsernameRequest.type = Type.PHONE; - -const updateUsernameRequest: UpdateUsernameRequest = - new UpdateUsernameRequest(); -updateUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; -updateUsernameRequest.username = 'johnny.doe@email.com'; -updateUsernameRequest.type = Type.EMAIL; - -const unknownUsernameRequest: UpdateUsernameRequest = - new UpdateUsernameRequest(); -unknownUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a92'; -unknownUsernameRequest.username = 'unknown@email.com'; -unknownUsernameRequest.type = Type.EMAIL; - -const invalidUpdateUsernameRequest: UpdateUsernameRequest = - new UpdateUsernameRequest(); -invalidUpdateUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a93'; -invalidUpdateUsernameRequest.username = ''; -invalidUpdateUsernameRequest.type = Type.EMAIL; - -const newUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand( - newUsernameRequest, -); - -const updateUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand( - updateUsernameRequest, -); - -const invalidUpdateUsernameCommand: UpdateUsernameCommand = - new UpdateUsernameCommand(invalidUpdateUsernameRequest); - -const unknownUpdateUsernameCommand: UpdateUsernameCommand = - new UpdateUsernameCommand(unknownUsernameRequest); - -const mockUsernameRepository = { - findOne: jest.fn().mockImplementation((request) => { - if (request.uuid == 'bb281075-1b98-4456-89d6-c643d3044a90') { - return Promise.resolve(null); - } - return Promise.resolve(existingUsername); - }), - updateWhere: jest.fn().mockImplementation((request) => { - if (request.uuid_type.uuid == 'bb281075-1b98-4456-89d6-c643d3044a90') { - return Promise.resolve(newUsernameRequest); - } - if (request.uuid_type.uuid == 'bb281075-1b98-4456-89d6-c643d3044a91') { - return Promise.resolve(updateUsernameRequest); - } - if (request.uuid_type.uuid == 'bb281075-1b98-4456-89d6-c643d3044a92') { - throw new Error('Error'); - } - return Promise.resolve(invalidUpdateUsernameRequest); - }), -}; - -const mockAddUsernameCommand = { - execute: jest.fn().mockResolvedValue(newUsernameRequest), -}; - -const mockMessager = { - publish: jest.fn().mockImplementation(), -}; - -describe('UpdateUsernameUseCase', () => { - let updateUsernameUseCase: UpdateUsernameUseCase; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], - providers: [ - { - provide: UsernameRepository, - useValue: mockUsernameRepository, - }, - { - provide: CommandBus, - useValue: mockAddUsernameCommand, - }, - { - provide: Messager, - useValue: mockMessager, - }, - UpdateUsernameUseCase, - UsernameProfile, - ], - }).compile(); - - updateUsernameUseCase = module.get( - UpdateUsernameUseCase, - ); - }); - - it('should be defined', () => { - expect(updateUsernameUseCase).toBeDefined(); - }); - - describe('execute', () => { - it('should create a new username', async () => { - const newUsername: Username = await updateUsernameUseCase.execute( - newUsernameCommand, - ); - - expect(newUsername.username).toBe(newUsernameRequest.username); - expect(newUsername.type).toBe(newUsernameRequest.type); - }); - - it('should update a username for email type', async () => { - const updatedUsername: Username = await updateUsernameUseCase.execute( - updateUsernameCommand, - ); - - expect(updatedUsername.username).toBe(updateUsernameRequest.username); - expect(updatedUsername.type).toBe(updateUsernameRequest.type); - }); - - it('should throw an error if username does not exist', async () => { - await expect( - updateUsernameUseCase.execute(unknownUpdateUsernameCommand), - ).rejects.toBeInstanceOf(Error); - }); - - it('should throw an exception if username is invalid', async () => { - await expect( - updateUsernameUseCase.execute(invalidUpdateUsernameCommand), - ).rejects.toBeInstanceOf(BadRequestException); - }); - }); -}); diff --git a/src/modules/oldauthentication/tests/unit/validate-authentication.usecase.spec.ts b/src/modules/oldauthentication/tests/unit/validate-authentication.usecase.spec.ts deleted file mode 100644 index 2e75726..0000000 --- a/src/modules/oldauthentication/tests/unit/validate-authentication.usecase.spec.ts +++ /dev/null @@ -1,107 +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 { Authentication } from '../../domain/entities/authentication'; -import * as bcrypt from 'bcrypt'; -import { ValidateAuthenticationUseCase } from '../../domain/usecases/validate-authentication.usecase'; -import { ValidateAuthenticationQuery } from '../../queries/validate-authentication.query'; -import { UsernameRepository } from '../../adapters/secondaries/username.repository'; -import { Type } from '../../domain/dtos/type.enum'; -import { NotFoundException, UnauthorizedException } from '@nestjs/common'; -import { DatabaseException } from '../../../database/exceptions/database.exception'; -import { ValidateAuthenticationRequest } from '../../domain/dtos/validate-authentication.request'; - -const mockAuthenticationRepository = { - findOne: jest - .fn() - .mockImplementationOnce(() => ({ - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - password: bcrypt.hashSync('John123', 10), - })) - .mockImplementationOnce(() => ({ - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - password: bcrypt.hashSync('John123', 10), - })), -}; - -const mockUsernameRepository = { - findOne: jest - .fn() - .mockImplementationOnce(() => ({ - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - username: 'john.doe@email.com', - type: Type.EMAIL, - })) - .mockImplementationOnce(() => { - throw new DatabaseException(); - }) - .mockImplementationOnce(() => ({ - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - username: 'john.doe@email.com', - type: Type.EMAIL, - })), -}; - -describe('ValidateAuthenticationUseCase', () => { - let validateAuthenticationUseCase: ValidateAuthenticationUseCase; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], - providers: [ - { - provide: AuthenticationRepository, - useValue: mockAuthenticationRepository, - }, - { - provide: UsernameRepository, - useValue: mockUsernameRepository, - }, - ValidateAuthenticationUseCase, - ], - }).compile(); - - validateAuthenticationUseCase = module.get( - ValidateAuthenticationUseCase, - ); - }); - - it('should be defined', () => { - expect(validateAuthenticationUseCase).toBeDefined(); - }); - - describe('execute', () => { - it('should validate an authentication and returns entity object', async () => { - const validateAuthenticationRequest: ValidateAuthenticationRequest = - new ValidateAuthenticationRequest(); - validateAuthenticationRequest.username = 'john.doe@email.com'; - validateAuthenticationRequest.password = 'John123'; - const authentication: Authentication = - await validateAuthenticationUseCase.execute( - new ValidateAuthenticationQuery( - validateAuthenticationRequest.username, - validateAuthenticationRequest.password, - ), - ); - - expect(authentication.uuid).toBe('bb281075-1b98-4456-89d6-c643d3044a91'); - }); - - it('should not validate an authentication with unknown username and returns not found exception', async () => { - await expect( - validateAuthenticationUseCase.execute( - new ValidateAuthenticationQuery('jane.doe@email.com', 'Jane123'), - ), - ).rejects.toBeInstanceOf(NotFoundException); - }); - - it('should not validate an authentication with wrong password and returns unauthorized exception', async () => { - await expect( - validateAuthenticationUseCase.execute( - new ValidateAuthenticationQuery('john.doe@email.com', 'John1234'), - ), - ).rejects.toBeInstanceOf(UnauthorizedException); - }); - }); -}); diff --git a/src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts b/src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts index 3f5af01..510e9c4 100644 --- a/src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts +++ b/src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts @@ -1,6 +1,6 @@ import { ArgumentMetadata } from '@nestjs/common'; -import { ValidateAuthenticationRequest } from '../../../modules/oldauthentication/domain/dtos/validate-authentication.request'; import { RpcValidationPipe } from '../../pipes/rpc.validation-pipe'; +import { ValidateAuthenticationRequestDto } from '@modules/authentication/interface/grpc-controllers/dtos/validate-authentication.request.dto'; describe('RpcValidationPipe', () => { it('should not validate request', async () => { @@ -10,11 +10,11 @@ describe('RpcValidationPipe', () => { }); const metadata: ArgumentMetadata = { type: 'body', - metatype: ValidateAuthenticationRequest, + metatype: ValidateAuthenticationRequestDto, data: '', }; await target - .transform({}, metadata) + .transform({}, metadata) .catch((err) => { expect(err.message).toEqual('Rpc Exception'); });