From 57d41725805b4acd490f8af8e3b0df67b500df9b Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Tue, 20 Dec 2022 17:37:59 +0100 Subject: [PATCH] multiple usernames --- .../20221215102603_init/migration.sql | 10 -- .../20221220135616_init/migration.sql | 26 ++++ prisma/schema.prisma | 17 ++- src/main.ts | 2 +- .../adapters/primaries/auth.controller.ts | 107 ++++++++++++++-- .../auth/adapters/primaries/auth.presenter.ts | 3 - .../auth/adapters/primaries/auth.proto | 39 ++++-- .../adapters/primaries/username.presenter.ts | 9 ++ .../secondaries/username.repository.ts | 8 ++ src/modules/auth/auth.module.ts | 14 ++- .../auth/commands/add-username.command.ts | 9 ++ .../auth/commands/delete-auth.command.ts | 9 ++ .../auth/commands/delete-username.command.ts | 9 ++ .../auth/commands/update-auth.command.ts | 9 -- .../auth/commands/update-password.command.ts | 9 ++ .../auth/commands/update-username.command.ts | 9 ++ .../auth/domain/dtos/add-username.request.ts | 20 +++ .../auth/domain/dtos/create-auth.request.ts | 9 +- .../auth/domain/dtos/delete-auth.request.ts | 9 ++ .../domain/dtos/delete-username.request.ts | 9 ++ src/modules/auth/domain/dtos/type.enum.ts | 4 + .../auth/domain/dtos/update-auth.request.ts | 16 --- .../domain/dtos/update-password.request.ts | 14 +++ .../domain/dtos/update-username.request.ts | 20 +++ src/modules/auth/domain/entities/auth.ts | 4 - src/modules/auth/domain/entities/username.ts | 13 ++ .../domain/usecases/add-username.usecase.ts | 22 ++++ .../domain/usecases/create-auth.usecase.ts | 27 +++- .../domain/usecases/delete-auth.usecase.ts | 31 +++++ .../usecases/delete-username.usecase.ts | 27 ++++ .../domain/usecases/update-auth.usecase.ts | 29 ----- .../usecases/update-password.usecase.ts | 23 ++++ .../usecases/update-username.usecase.ts | 28 +++++ .../domain/usecases/validate-auth.usecase.ts | 27 ++-- src/modules/auth/mappers/auth.profile.ts | 2 +- src/modules/auth/mappers/username.profile.ts | 18 +++ .../tests/unit/add-username.usecase.spec.ts | 58 +++++++++ .../tests/unit/create-auth.usecase.spec.ts | 22 +++- .../tests/unit/delete-auth.usecase.spec.ts | 78 ++++++++++++ .../unit/delete-username.usecase.spec.ts | 106 ++++++++++++++++ .../tests/unit/update-auth.usecase.spec.ts | 119 ------------------ .../unit/update-password.usecase.spec.ts | 61 +++++++++ .../unit/update-username.usecase.spec.ts | 58 +++++++++ .../tests/unit/validate-auth.usecase.spec.ts | 18 ++- src/modules/database/database.module.ts | 5 +- .../secondaries/prisma-repository.abstract.ts | 5 +- .../tests/unit/prisma-repository.spec.ts | 2 +- 47 files changed, 928 insertions(+), 245 deletions(-) delete mode 100644 prisma/migrations/20221215102603_init/migration.sql create mode 100644 prisma/migrations/20221220135616_init/migration.sql create mode 100644 src/modules/auth/adapters/primaries/username.presenter.ts create mode 100644 src/modules/auth/adapters/secondaries/username.repository.ts create mode 100644 src/modules/auth/commands/add-username.command.ts create mode 100644 src/modules/auth/commands/delete-auth.command.ts create mode 100644 src/modules/auth/commands/delete-username.command.ts delete mode 100644 src/modules/auth/commands/update-auth.command.ts create mode 100644 src/modules/auth/commands/update-password.command.ts create mode 100644 src/modules/auth/commands/update-username.command.ts create mode 100644 src/modules/auth/domain/dtos/add-username.request.ts create mode 100644 src/modules/auth/domain/dtos/delete-auth.request.ts create mode 100644 src/modules/auth/domain/dtos/delete-username.request.ts create mode 100644 src/modules/auth/domain/dtos/type.enum.ts delete mode 100644 src/modules/auth/domain/dtos/update-auth.request.ts create mode 100644 src/modules/auth/domain/dtos/update-password.request.ts create mode 100644 src/modules/auth/domain/dtos/update-username.request.ts create mode 100644 src/modules/auth/domain/entities/username.ts create mode 100644 src/modules/auth/domain/usecases/add-username.usecase.ts create mode 100644 src/modules/auth/domain/usecases/delete-auth.usecase.ts create mode 100644 src/modules/auth/domain/usecases/delete-username.usecase.ts delete mode 100644 src/modules/auth/domain/usecases/update-auth.usecase.ts create mode 100644 src/modules/auth/domain/usecases/update-password.usecase.ts create mode 100644 src/modules/auth/domain/usecases/update-username.usecase.ts create mode 100644 src/modules/auth/mappers/username.profile.ts create mode 100644 src/modules/auth/tests/unit/add-username.usecase.spec.ts create mode 100644 src/modules/auth/tests/unit/delete-auth.usecase.spec.ts create mode 100644 src/modules/auth/tests/unit/delete-username.usecase.spec.ts delete mode 100644 src/modules/auth/tests/unit/update-auth.usecase.spec.ts create mode 100644 src/modules/auth/tests/unit/update-password.usecase.spec.ts create mode 100644 src/modules/auth/tests/unit/update-username.usecase.spec.ts diff --git a/prisma/migrations/20221215102603_init/migration.sql b/prisma/migrations/20221215102603_init/migration.sql deleted file mode 100644 index fbf3303..0000000 --- a/prisma/migrations/20221215102603_init/migration.sql +++ /dev/null @@ -1,10 +0,0 @@ --- CreateTable -CREATE TABLE "auth" ( - "uuid" UUID NOT NULL, - "username" TEXT NOT NULL, - "password" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "auth_pkey" PRIMARY KEY ("uuid") -); diff --git a/prisma/migrations/20221220135616_init/migration.sql b/prisma/migrations/20221220135616_init/migration.sql new file mode 100644 index 0000000..eedb8ce --- /dev/null +++ b/prisma/migrations/20221220135616_init/migration.sql @@ -0,0 +1,26 @@ +-- CreateEnum +CREATE TYPE "Type" AS ENUM ('EMAIL', 'PHONE'); + +-- CreateTable +CREATE TABLE "auth" ( + "uuid" UUID NOT NULL, + "password" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "auth_pkey" PRIMARY KEY ("uuid") +); + +-- CreateTable +CREATE TABLE "username" ( + "username" TEXT NOT NULL, + "uuid" UUID NOT NULL, + "type" "Type" NOT NULL DEFAULT 'EMAIL', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "username_pkey" PRIMARY KEY ("username") +); + +-- CreateIndex +CREATE UNIQUE INDEX "username_uuid_type_key" ON "username"("uuid", "type"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 069ce7c..49c2ce7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,10 +12,25 @@ datasource db { model Auth { uuid String @id @db.Uuid - username String password String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("auth") } + +model Username { + username String @id + uuid String @db.Uuid + type Type @default(EMAIL) // type is needed in case of username update + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([uuid, type]) + @@map("username") +} + +enum Type { + EMAIL + PHONE +} diff --git a/src/main.ts b/src/main.ts index a0bb799..92a84a4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,7 +15,7 @@ async function bootstrap() { 'modules/auth/adapters/primaries/auth.proto', ), url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT, - loader: { keepCase: true }, + loader: { keepCase: true, enums: String }, }, }, ); diff --git a/src/modules/auth/adapters/primaries/auth.controller.ts b/src/modules/auth/adapters/primaries/auth.controller.ts index 97a3d6d..161f123 100644 --- a/src/modules/auth/adapters/primaries/auth.controller.ts +++ b/src/modules/auth/adapters/primaries/auth.controller.ts @@ -4,14 +4,24 @@ import { Controller } from '@nestjs/common'; import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; import { DatabaseException } from 'src/modules/database/src/exceptions/DatabaseException'; +import { AddUsernameCommand } from '../../commands/add-username.command'; import { CreateAuthCommand } from '../../commands/create-auth.command'; -import { UpdateAuthCommand } from '../../commands/update-auth.command'; +import { DeleteAuthCommand } from '../../commands/delete-auth.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 { CreateAuthRequest } from '../../domain/dtos/create-auth.request'; -import { UpdateAuthRequest } from '../../domain/dtos/update-auth.request'; +import { DeleteAuthRequest } from '../../domain/dtos/delete-auth.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 { ValidateAuthRequest } from '../../domain/dtos/validate-auth.request'; import { Auth } from '../../domain/entities/auth'; +import { Username } from '../../domain/entities/username'; import { ValidateAuthQuery } from '../../queries/validate-auth.query'; import { AuthPresenter } from './auth.presenter'; +import { UsernamePresenter } from './username.presenter'; @Controller() export class AuthController { @@ -24,7 +34,7 @@ export class AuthController { @GrpcMethod('AuthService', 'Validate') async validate(data: ValidateAuthRequest): Promise { try { - const auth = await this._queryBus.execute( + const auth: Auth = await this._queryBus.execute( new ValidateAuthQuery(data.username, data.password), ); return this._mapper.map(auth, Auth, AuthPresenter); @@ -39,7 +49,9 @@ export class AuthController { @GrpcMethod('AuthService', 'Create') async createUser(data: CreateAuthRequest): Promise { try { - const auth = await this._commandBus.execute(new CreateAuthCommand(data)); + const auth: Auth = await this._commandBus.execute( + new CreateAuthCommand(data), + ); return this._mapper.map(auth, Auth, AuthPresenter); } catch (e) { if (e instanceof DatabaseException) { @@ -50,14 +62,69 @@ export class AuthController { }); } } - throw new RpcException({}); + throw new RpcException({ + code: 7, + message: 'Permission denied', + }); } } - @GrpcMethod('AuthService', 'Update') - async updateAuth(data: UpdateAuthRequest): Promise { + @GrpcMethod('AuthService', 'AddUsername') + async addUsername(data: AddUsernameRequest): Promise { try { - const auth = await this._commandBus.execute(new UpdateAuthCommand(data)); + 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('AuthService', '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('AuthService', 'UpdatePassword') + async updatePassword(data: UpdatePasswordRequest): Promise { + try { + const auth: Auth = await this._commandBus.execute( + new UpdatePasswordCommand(data), + ); return this._mapper.map(auth, Auth, AuthPresenter); } catch (e) { @@ -67,4 +134,28 @@ export class AuthController { }); } } + + @GrpcMethod('AuthService', '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('AuthService', 'Delete') + async deleteAuth(data: DeleteAuthRequest) { + try { + return await this._commandBus.execute(new DeleteAuthCommand(data)); + } catch (e) { + throw new RpcException({ + code: 7, + message: 'Permission denied', + }); + } + } } diff --git a/src/modules/auth/adapters/primaries/auth.presenter.ts b/src/modules/auth/adapters/primaries/auth.presenter.ts index ebb3f6b..b780968 100644 --- a/src/modules/auth/adapters/primaries/auth.presenter.ts +++ b/src/modules/auth/adapters/primaries/auth.presenter.ts @@ -3,7 +3,4 @@ import { AutoMap } from '@automapper/classes'; export class AuthPresenter { @AutoMap() uuid: string; - - @AutoMap() - username: string; } diff --git a/src/modules/auth/adapters/primaries/auth.proto b/src/modules/auth/adapters/primaries/auth.proto index 00898e4..587dd66 100644 --- a/src/modules/auth/adapters/primaries/auth.proto +++ b/src/modules/auth/adapters/primaries/auth.proto @@ -3,10 +3,13 @@ syntax = "proto3"; package auth; service AuthService { - rpc Validate(AuthByUsernamePassword) returns (AuthByUuid); - rpc Create(AuthWithPassword) returns (Auth); - rpc Update(AuthWithPassword) returns (Auth); - rpc Delete(AuthByUuid) returns (Empty); + rpc Validate(AuthByUsernamePassword) returns (Uuid); + rpc Create(Auth) 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 AuthByUsernamePassword { @@ -14,19 +17,31 @@ message AuthByUsernamePassword { string password = 2; } -message AuthWithPassword { - string uuid = 1; - string username = 2; - string password = 3; -} - -message AuthByUuid { - string uuid = 1; +enum Type { + EMAIL = 0; + PHONE = 1; } message Auth { string uuid = 1; string username = 2; + string password = 3; + Type type = 4; +} + +message Password { + string uuid = 1; + string password = 2; +} + +message Username { + string uuid = 1; + string username = 2; + Type type = 3; +} + +message Uuid { + string uuid = 1; } message Empty {} diff --git a/src/modules/auth/adapters/primaries/username.presenter.ts b/src/modules/auth/adapters/primaries/username.presenter.ts new file mode 100644 index 0000000..167e25a --- /dev/null +++ b/src/modules/auth/adapters/primaries/username.presenter.ts @@ -0,0 +1,9 @@ +import { AutoMap } from '@automapper/classes'; + +export class UsernamePresenter { + @AutoMap() + uuid: string; + + @AutoMap() + username: string; +} diff --git a/src/modules/auth/adapters/secondaries/username.repository.ts b/src/modules/auth/adapters/secondaries/username.repository.ts new file mode 100644 index 0000000..fe39acd --- /dev/null +++ b/src/modules/auth/adapters/secondaries/username.repository.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; +import { AuthNZRepository } from '../../../database/src/domain/authnz-repository'; +import { Username } from '../../domain/entities/username'; + +@Injectable() +export class UsernameRepository extends AuthNZRepository { + protected _model = 'username'; +} diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index a5d1dfe..f703a3e 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -6,17 +6,27 @@ import { CreateAuthUseCase } from './domain/usecases/create-auth.usecase'; import { ValidateAuthUseCase } from './domain/usecases/validate-auth.usecase'; import { AuthProfile } from './mappers/auth.profile'; import { AuthRepository } from './adapters/secondaries/auth.repository'; -import { UpdateAuthUseCase } from './domain/usecases/update-auth.usecase'; +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 { DeleteAuthUseCase } from './domain/usecases/delete-auth.usecase'; @Module({ imports: [DatabaseModule, CqrsModule], controllers: [AuthController], providers: [ AuthProfile, + UsernameProfile, AuthRepository, ValidateAuthUseCase, CreateAuthUseCase, - UpdateAuthUseCase, + AddUsernameUseCase, + UpdateUsernameUseCase, + UpdatePasswordUseCase, + DeleteUsernameUseCase, + DeleteAuthUseCase, ], exports: [], }) diff --git a/src/modules/auth/commands/add-username.command.ts b/src/modules/auth/commands/add-username.command.ts new file mode 100644 index 0000000..2036a23 --- /dev/null +++ b/src/modules/auth/commands/add-username.command.ts @@ -0,0 +1,9 @@ +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/auth/commands/delete-auth.command.ts b/src/modules/auth/commands/delete-auth.command.ts new file mode 100644 index 0000000..fb3034f --- /dev/null +++ b/src/modules/auth/commands/delete-auth.command.ts @@ -0,0 +1,9 @@ +import { DeleteAuthRequest } from '../domain/dtos/delete-auth.request'; + +export class DeleteAuthCommand { + readonly deleteAuthRequest: DeleteAuthRequest; + + constructor(request: DeleteAuthRequest) { + this.deleteAuthRequest = request; + } +} diff --git a/src/modules/auth/commands/delete-username.command.ts b/src/modules/auth/commands/delete-username.command.ts new file mode 100644 index 0000000..c46a3bd --- /dev/null +++ b/src/modules/auth/commands/delete-username.command.ts @@ -0,0 +1,9 @@ +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/auth/commands/update-auth.command.ts b/src/modules/auth/commands/update-auth.command.ts deleted file mode 100644 index a1faebe..0000000 --- a/src/modules/auth/commands/update-auth.command.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { UpdateAuthRequest } from '../domain/dtos/update-auth.request'; - -export class UpdateAuthCommand { - readonly updateAuthRequest: UpdateAuthRequest; - - constructor(request: UpdateAuthRequest) { - this.updateAuthRequest = request; - } -} diff --git a/src/modules/auth/commands/update-password.command.ts b/src/modules/auth/commands/update-password.command.ts new file mode 100644 index 0000000..2120df1 --- /dev/null +++ b/src/modules/auth/commands/update-password.command.ts @@ -0,0 +1,9 @@ +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/auth/commands/update-username.command.ts b/src/modules/auth/commands/update-username.command.ts new file mode 100644 index 0000000..54a8415 --- /dev/null +++ b/src/modules/auth/commands/update-username.command.ts @@ -0,0 +1,9 @@ +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/auth/domain/dtos/add-username.request.ts b/src/modules/auth/domain/dtos/add-username.request.ts new file mode 100644 index 0000000..85a3f25 --- /dev/null +++ b/src/modules/auth/domain/dtos/add-username.request.ts @@ -0,0 +1,20 @@ +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/auth/domain/dtos/create-auth.request.ts b/src/modules/auth/domain/dtos/create-auth.request.ts index b472ea8..91423cb 100644 --- a/src/modules/auth/domain/dtos/create-auth.request.ts +++ b/src/modules/auth/domain/dtos/create-auth.request.ts @@ -1,8 +1,10 @@ import { AutoMap } from '@automapper/classes'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { Type } from './type.enum'; export class CreateAuthRequest { @IsString() + @IsNotEmpty() @AutoMap() uuid: string; @@ -15,4 +17,9 @@ export class CreateAuthRequest { @IsNotEmpty() @AutoMap() password: string; + + @IsEnum(Type) + @IsNotEmpty() + @AutoMap() + type: Type; } diff --git a/src/modules/auth/domain/dtos/delete-auth.request.ts b/src/modules/auth/domain/dtos/delete-auth.request.ts new file mode 100644 index 0000000..ac92806 --- /dev/null +++ b/src/modules/auth/domain/dtos/delete-auth.request.ts @@ -0,0 +1,9 @@ +import { AutoMap } from '@automapper/classes'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DeleteAuthRequest { + @IsString() + @IsNotEmpty() + @AutoMap() + uuid: string; +} diff --git a/src/modules/auth/domain/dtos/delete-username.request.ts b/src/modules/auth/domain/dtos/delete-username.request.ts new file mode 100644 index 0000000..3b2a221 --- /dev/null +++ b/src/modules/auth/domain/dtos/delete-username.request.ts @@ -0,0 +1,9 @@ +import { AutoMap } from '@automapper/classes'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DeleteUsernameRequest { + @IsString() + @IsNotEmpty() + @AutoMap() + username: string; +} diff --git a/src/modules/auth/domain/dtos/type.enum.ts b/src/modules/auth/domain/dtos/type.enum.ts new file mode 100644 index 0000000..2ef59cd --- /dev/null +++ b/src/modules/auth/domain/dtos/type.enum.ts @@ -0,0 +1,4 @@ +export enum Type { + EMAIL = 'EMAIL', + PHONE = 'PHONE', +} diff --git a/src/modules/auth/domain/dtos/update-auth.request.ts b/src/modules/auth/domain/dtos/update-auth.request.ts deleted file mode 100644 index c12eac4..0000000 --- a/src/modules/auth/domain/dtos/update-auth.request.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AutoMap } from '@automapper/classes'; -import { IsString } from 'class-validator'; - -export class UpdateAuthRequest { - @IsString() - @AutoMap() - uuid: string; - - @IsString() - @AutoMap() - username?: string; - - @IsString() - @AutoMap() - password?: string; -} diff --git a/src/modules/auth/domain/dtos/update-password.request.ts b/src/modules/auth/domain/dtos/update-password.request.ts new file mode 100644 index 0000000..bf4b6e4 --- /dev/null +++ b/src/modules/auth/domain/dtos/update-password.request.ts @@ -0,0 +1,14 @@ +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/auth/domain/dtos/update-username.request.ts b/src/modules/auth/domain/dtos/update-username.request.ts new file mode 100644 index 0000000..47ccf9d --- /dev/null +++ b/src/modules/auth/domain/dtos/update-username.request.ts @@ -0,0 +1,20 @@ +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/auth/domain/entities/auth.ts b/src/modules/auth/domain/entities/auth.ts index 23d1807..4bf327d 100644 --- a/src/modules/auth/domain/entities/auth.ts +++ b/src/modules/auth/domain/entities/auth.ts @@ -4,9 +4,5 @@ export class Auth { @AutoMap() uuid: string; - @AutoMap() - username: string; - - @AutoMap() password: string; } diff --git a/src/modules/auth/domain/entities/username.ts b/src/modules/auth/domain/entities/username.ts new file mode 100644 index 0000000..333c190 --- /dev/null +++ b/src/modules/auth/domain/entities/username.ts @@ -0,0 +1,13 @@ +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/auth/domain/usecases/add-username.usecase.ts b/src/modules/auth/domain/usecases/add-username.usecase.ts new file mode 100644 index 0000000..2469d51 --- /dev/null +++ b/src/modules/auth/domain/usecases/add-username.usecase.ts @@ -0,0 +1,22 @@ +import { CommandHandler } from '@nestjs/cqrs'; +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) {} + + async execute(command: AddUsernameCommand): Promise { + const { uuid, username, type } = command.addUsernameRequest; + try { + return await this._usernameRepository.create({ + uuid, + type, + username, + }); + } catch (e) { + throw e; + } + } +} diff --git a/src/modules/auth/domain/usecases/create-auth.usecase.ts b/src/modules/auth/domain/usecases/create-auth.usecase.ts index 5dfa667..978b7be 100644 --- a/src/modules/auth/domain/usecases/create-auth.usecase.ts +++ b/src/modules/auth/domain/usecases/create-auth.usecase.ts @@ -3,18 +3,33 @@ import { AuthRepository } from '../../adapters/secondaries/auth.repository'; import { CreateAuthCommand } from '../../commands/create-auth.command'; import { Auth } from '../entities/auth'; import * as bcrypt from 'bcrypt'; +import { UsernameRepository } from '../../adapters/secondaries/username.repository'; @CommandHandler(CreateAuthCommand) export class CreateAuthUseCase { - constructor(private readonly _repository: AuthRepository) {} + constructor( + private readonly _authRepository: AuthRepository, + private readonly _usernameRepository: UsernameRepository, + ) {} async execute(command: CreateAuthCommand): Promise { - const { password, ...authWithoutPassword } = command.createAuthRequest; + const { uuid, password, ...username } = command.createAuthRequest; const hash = await bcrypt.hash(password, 10); - return this._repository.create({ - password: hash, - ...authWithoutPassword, - }); + try { + const auth = await this._authRepository.create({ + uuid, + password: hash, + }); + + await this._usernameRepository.create({ + uuid, + ...username, + }); + + return auth; + } catch (e) { + throw e; + } } } diff --git a/src/modules/auth/domain/usecases/delete-auth.usecase.ts b/src/modules/auth/domain/usecases/delete-auth.usecase.ts new file mode 100644 index 0000000..e8637f0 --- /dev/null +++ b/src/modules/auth/domain/usecases/delete-auth.usecase.ts @@ -0,0 +1,31 @@ +import { CommandHandler } from '@nestjs/cqrs'; +import { AuthRepository } from '../../adapters/secondaries/auth.repository'; +import { UsernameRepository } from '../../adapters/secondaries/username.repository'; +import { DeleteAuthCommand } from '../../commands/delete-auth.command'; + +@CommandHandler(DeleteAuthCommand) +export class DeleteAuthUseCase { + constructor( + private readonly _authRepository: AuthRepository, + private readonly _usernameRepository: UsernameRepository, + ) {} + + async execute(command: DeleteAuthCommand) { + try { + const usernames = await this._usernameRepository.findAll(1, 99, { + uuid: command.deleteAuthRequest.uuid, + }); + usernames.data.map( + async (username) => + await this._usernameRepository.delete({ + username: username.username, + }), + ); + return await this._authRepository.delete({ + uuid: command.deleteAuthRequest.uuid, + }); + } catch (e) { + throw e; + } + } +} diff --git a/src/modules/auth/domain/usecases/delete-username.usecase.ts b/src/modules/auth/domain/usecases/delete-username.usecase.ts new file mode 100644 index 0000000..8d5250a --- /dev/null +++ b/src/modules/auth/domain/usecases/delete-username.usecase.ts @@ -0,0 +1,27 @@ +import { UnauthorizedException } from '@nestjs/common'; +import { CommandHandler } from '@nestjs/cqrs'; +import { UsernameRepository } from '../../adapters/secondaries/username.repository'; +import { DeleteUsernameCommand } from '../../commands/delete-username.command'; + +@CommandHandler(DeleteUsernameCommand) +export class DeleteUsernameUseCase { + constructor(private readonly _usernameRepository: UsernameRepository) {} + + async execute(command: DeleteUsernameCommand) { + 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.delete({ username }); + } + throw new UnauthorizedException(); + } catch (e) { + throw e; + } + } +} diff --git a/src/modules/auth/domain/usecases/update-auth.usecase.ts b/src/modules/auth/domain/usecases/update-auth.usecase.ts deleted file mode 100644 index 57eff89..0000000 --- a/src/modules/auth/domain/usecases/update-auth.usecase.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Mapper } from '@automapper/core'; -import { InjectMapper } from '@automapper/nestjs'; -import { CommandHandler } from '@nestjs/cqrs'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; -import { UpdateAuthCommand } from '../../commands/update-auth.command'; -import { Auth } from '../entities/auth'; -import * as bcrypt from 'bcrypt'; - -@CommandHandler(UpdateAuthCommand) -export class UpdateAuthUseCase { - constructor( - private readonly _repository: AuthRepository, - @InjectMapper() private readonly _mapper: Mapper, - ) {} - - async execute(command: UpdateAuthCommand): Promise { - const { uuid, username, password } = command.updateAuthRequest; - const request = {}; - if (username) { - request['username'] = username; - } - if (password) { - const hash = await bcrypt.hash(password, 10); - request['password'] = hash; - } - - return this._repository.update(uuid, request); - } -} diff --git a/src/modules/auth/domain/usecases/update-password.usecase.ts b/src/modules/auth/domain/usecases/update-password.usecase.ts new file mode 100644 index 0000000..41074ba --- /dev/null +++ b/src/modules/auth/domain/usecases/update-password.usecase.ts @@ -0,0 +1,23 @@ +import { CommandHandler } from '@nestjs/cqrs'; +import { AuthRepository } from '../../adapters/secondaries/auth.repository'; +import { Auth } from '../entities/auth'; +import * as bcrypt from 'bcrypt'; +import { UpdatePasswordCommand } from '../../commands/update-password.command'; + +@CommandHandler(UpdatePasswordCommand) +export class UpdatePasswordUseCase { + constructor(private readonly _authRepository: AuthRepository) {} + + async execute(command: UpdatePasswordCommand): Promise { + const { uuid, password } = command.updatePasswordRequest; + const hash = await bcrypt.hash(password, 10); + + try { + return await this._authRepository.update(uuid, { + password: hash, + }); + } catch (e) { + throw e; + } + } +} diff --git a/src/modules/auth/domain/usecases/update-username.usecase.ts b/src/modules/auth/domain/usecases/update-username.usecase.ts new file mode 100644 index 0000000..f18b912 --- /dev/null +++ b/src/modules/auth/domain/usecases/update-username.usecase.ts @@ -0,0 +1,28 @@ +import { CommandHandler } from '@nestjs/cqrs'; +import { UsernameRepository } from '../../adapters/secondaries/username.repository'; +import { UpdateUsernameCommand } from '../../commands/update-username.command'; +import { Username } from '../entities/username'; + +@CommandHandler(UpdateUsernameCommand) +export class UpdateUsernameUseCase { + constructor(private readonly _usernameRepository: UsernameRepository) {} + + async execute(command: UpdateUsernameCommand): Promise { + const { uuid, username, type } = command.updateUsernameRequest; + try { + return await this._usernameRepository.updateWhere( + { + uuid_type: { + uuid, + type, + }, + }, + { + username, + }, + ); + } catch (e) { + throw e; + } + } +} diff --git a/src/modules/auth/domain/usecases/validate-auth.usecase.ts b/src/modules/auth/domain/usecases/validate-auth.usecase.ts index 7a1528c..e5a4484 100644 --- a/src/modules/auth/domain/usecases/validate-auth.usecase.ts +++ b/src/modules/auth/domain/usecases/validate-auth.usecase.ts @@ -4,20 +4,31 @@ import { ValidateAuthQuery } from '../../queries/validate-auth.query'; import { Auth } from '../entities/auth'; import * as bcrypt from 'bcrypt'; import { NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { UsernameRepository } from '../../adapters/secondaries/username.repository'; @QueryHandler(ValidateAuthQuery) export class ValidateAuthUseCase { - constructor(private readonly _authRepository: AuthRepository) {} + constructor( + private readonly _authRepository: AuthRepository, + private readonly _usernameRepository: UsernameRepository, + ) {} async execute(validate: ValidateAuthQuery): Promise { - const auth = await this._authRepository.findOne({ - username: validate.username, - }); - if (auth) { - const isMatch = await bcrypt.compare(validate.password, auth.password); - if (isMatch) return auth; + try { + const username = await this._usernameRepository.findOne({ + username: validate.username, + }); + const auth = await this._authRepository.findOne({ + uuid: username.uuid, + }); + if (auth) { + const isMatch = await bcrypt.compare(validate.password, auth.password); + if (isMatch) return auth; + throw new UnauthorizedException(); + } + throw new NotFoundException(); + } catch (e) { throw new UnauthorizedException(); } - throw new NotFoundException(); } } diff --git a/src/modules/auth/mappers/auth.profile.ts b/src/modules/auth/mappers/auth.profile.ts index 569ad92..2ceaf03 100644 --- a/src/modules/auth/mappers/auth.profile.ts +++ b/src/modules/auth/mappers/auth.profile.ts @@ -11,7 +11,7 @@ export class AuthProfile extends AutomapperProfile { } override get profile() { - return (mapper) => { + return (mapper: any) => { createMap(mapper, Auth, AuthPresenter); }; } diff --git a/src/modules/auth/mappers/username.profile.ts b/src/modules/auth/mappers/username.profile.ts new file mode 100644 index 0000000..9eeb8b2 --- /dev/null +++ b/src/modules/auth/mappers/username.profile.ts @@ -0,0 +1,18 @@ +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 { 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); + }; + } +} diff --git a/src/modules/auth/tests/unit/add-username.usecase.spec.ts b/src/modules/auth/tests/unit/add-username.usecase.spec.ts new file mode 100644 index 0000000..6062794 --- /dev/null +++ b/src/modules/auth/tests/unit/add-username.usecase.spec.ts @@ -0,0 +1,58 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthProfile } from '../../mappers/auth.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'; + +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().mockResolvedValue(addUsernameRequest), +}; + +describe('AddUsernameUseCase', () => { + let addUsernameUseCase: AddUsernameUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: UsernameRepository, + useValue: mockUsernameRepository, + }, + AddUsernameUseCase, + AuthProfile, + ], + }).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); + }); + }); +}); diff --git a/src/modules/auth/tests/unit/create-auth.usecase.spec.ts b/src/modules/auth/tests/unit/create-auth.usecase.spec.ts index 8ced70e..f536b9c 100644 --- a/src/modules/auth/tests/unit/create-auth.usecase.spec.ts +++ b/src/modules/auth/tests/unit/create-auth.usecase.spec.ts @@ -6,24 +6,33 @@ import { CreateAuthCommand } from '../../commands/create-auth.command'; import { CreateAuthRequest } from '../../domain/dtos/create-auth.request'; import { Auth } from '../../domain/entities/auth'; import { CreateAuthUseCase } from '../../domain/usecases/create-auth.usecase'; -import { AuthProfile } from '../../mappers/auth.profile'; import * as bcrypt from 'bcrypt'; +import { UsernameRepository } from '../../adapters/secondaries/username.repository'; +import { Type } from '../../domain/dtos/type.enum'; const newAuthRequest: CreateAuthRequest = { uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', username: 'john.doe@email.com', password: 'John123', + type: Type.EMAIL, }; const newAuthCommand: CreateAuthCommand = new CreateAuthCommand(newAuthRequest); const mockAuthRepository = { create: jest.fn().mockResolvedValue({ uuid: newAuthRequest.uuid, - username: newAuthRequest.username, password: bcrypt.hashSync(newAuthRequest.password, 10), }), }; +const mockUsernameRepository = { + create: jest.fn().mockResolvedValue({ + uuid: newAuthRequest.uuid, + username: newAuthRequest.username, + type: newAuthRequest.type, + }), +}; + describe('CreateAuthUseCase', () => { let createAuthUseCase: CreateAuthUseCase; @@ -35,8 +44,11 @@ describe('CreateAuthUseCase', () => { provide: AuthRepository, useValue: mockAuthRepository, }, + { + provide: UsernameRepository, + useValue: mockUsernameRepository, + }, CreateAuthUseCase, - AuthProfile, ], }).compile(); @@ -48,11 +60,9 @@ describe('CreateAuthUseCase', () => { }); describe('execute', () => { - it('should create an auth and returns new entity object', async () => { + it('should create an auth with an encrypted password', async () => { const newAuth: Auth = await createAuthUseCase.execute(newAuthCommand); - expect(newAuth.username).toBe(newAuthRequest.username); - expect( bcrypt.compareSync(newAuthRequest.password, newAuth.password), ).toBeTruthy(); diff --git a/src/modules/auth/tests/unit/delete-auth.usecase.spec.ts b/src/modules/auth/tests/unit/delete-auth.usecase.spec.ts new file mode 100644 index 0000000..45e2028 --- /dev/null +++ b/src/modules/auth/tests/unit/delete-auth.usecase.spec.ts @@ -0,0 +1,78 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthRepository } from '../../adapters/secondaries/auth.repository'; +import { UsernameRepository } from '../../adapters/secondaries/username.repository'; +import { DeleteAuthCommand } from '../../commands/delete-auth.command'; +import { DeleteAuthRequest } from '../../domain/dtos/delete-auth.request'; +import { Type } from '../../domain/dtos/type.enum'; +import { DeleteAuthUseCase } from '../../domain/usecases/delete-auth.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 deleteAuthRequest: DeleteAuthRequest = { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', +}; +const deleteAuthCommand: DeleteAuthCommand = new DeleteAuthCommand( + deleteAuthRequest, +); + +const mockAuthRepository = { + delete: jest.fn().mockResolvedValue(undefined), +}; + +const mockUsernameRepository = { + findAll: jest.fn().mockImplementation((page, perPage, query) => { + return Promise.resolve(usernames); + }), + delete: jest.fn().mockResolvedValue(undefined), +}; + +describe('DeleteAuthUseCase', () => { + let deleteAuthUseCase: DeleteAuthUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: AuthRepository, + useValue: mockAuthRepository, + }, + { + provide: UsernameRepository, + useValue: mockUsernameRepository, + }, + DeleteAuthUseCase, + ], + }).compile(); + + deleteAuthUseCase = module.get(DeleteAuthUseCase); + }); + + it('should be defined', () => { + expect(deleteAuthUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should delete an auth and its usernames', async () => { + const deletedAuth = await deleteAuthUseCase.execute(deleteAuthCommand); + + expect(deletedAuth).toBe(undefined); + }); + }); +}); diff --git a/src/modules/auth/tests/unit/delete-username.usecase.spec.ts b/src/modules/auth/tests/unit/delete-username.usecase.spec.ts new file mode 100644 index 0000000..3e8cdd1 --- /dev/null +++ b/src/modules/auth/tests/unit/delete-username.usecase.spec.ts @@ -0,0 +1,106 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { UnauthorizedException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +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 = { + username: 'john.doe@email.com', +}; + +const deleteUsernamePhoneRequest: DeleteUsernameRequest = { + 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); + }), + delete: jest.fn().mockResolvedValue(undefined), +}; + +describe('DeleteUsernameUseCase', () => { + let deleteUsernameUseCase: DeleteUsernameUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: UsernameRepository, + useValue: mockUsernameRepository, + }, + 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/auth/tests/unit/update-auth.usecase.spec.ts b/src/modules/auth/tests/unit/update-auth.usecase.spec.ts deleted file mode 100644 index 8476f37..0000000 --- a/src/modules/auth/tests/unit/update-auth.usecase.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { classes } from '@automapper/classes'; -import { AutomapperModule } from '@automapper/nestjs'; -import { Test, TestingModule } from '@nestjs/testing'; -import { UpdateAuthCommand } from '../../commands/update-auth.command'; -import { UpdateAuthRequest } from '../../domain/dtos/update-auth.request'; -import { Auth } from '../../domain/entities/auth'; -import * as bcrypt from 'bcrypt'; -import { UpdateAuthUseCase } from '../../domain/usecases/update-auth.usecase'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; -import { AuthProfile } from '../../mappers/auth.profile'; - -const originalAuth: Auth = new Auth(); -originalAuth.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; -originalAuth.username = 'john.doe@email.com'; -originalAuth.password = 'encrypted_password'; - -const updateUsernameAuthRequest: UpdateAuthRequest = { - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - username: 'johnny.doe@email.com', -}; - -const updatePasswordAuthRequest: UpdateAuthRequest = { - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - password: 'John1234', -}; - -const updateAuthRequest: UpdateAuthRequest = { - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - username: 'johnny.doe@email.com', - password: 'John1234', -}; - -const updateUsernameAuthCommand: UpdateAuthCommand = new UpdateAuthCommand( - updateUsernameAuthRequest, -); - -const updatePasswordAuthCommand: UpdateAuthCommand = new UpdateAuthCommand( - updatePasswordAuthRequest, -); - -const updateAuthCommand: UpdateAuthCommand = new UpdateAuthCommand( - updateAuthRequest, -); - -const mockAuthRepository = { - update: jest.fn().mockImplementation((uuid: string, params: any) => { - if (params.username && params.password) { - const auth: Auth = { ...originalAuth }; - auth.username = params.username; - auth.password = params.password; - return Promise.resolve(auth); - } - if (params.username) { - const auth: Auth = { ...originalAuth }; - auth.username = params.username; - return Promise.resolve(auth); - } - if (params.password) { - const auth: Auth = { ...originalAuth }; - auth.password = params.password; - return Promise.resolve(auth); - } - }), -}; - -describe('UpdateAuthUseCase', () => { - let updateAuthUseCase: UpdateAuthUseCase; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], - - providers: [ - { - provide: AuthRepository, - useValue: mockAuthRepository, - }, - UpdateAuthUseCase, - AuthProfile, - ], - }).compile(); - - updateAuthUseCase = module.get(UpdateAuthUseCase); - }); - - it('should be defined', () => { - expect(updateAuthUseCase).toBeDefined(); - }); - - describe('execute', () => { - it('should update the username of an Auth', async () => { - const updatedAuth: Auth = await updateAuthUseCase.execute( - updateUsernameAuthCommand, - ); - - expect(updatedAuth.username).toBe(updateUsernameAuthRequest.username); - }); - it('should update the password of an Auth', async () => { - const updatedAuth: Auth = await updateAuthUseCase.execute( - updatePasswordAuthCommand, - ); - expect( - bcrypt.compareSync( - updatePasswordAuthRequest.password, - updatedAuth.password, - ), - ).toBeTruthy(); - }); - it('should update the username and the password of an Auth', async () => { - const updatedAuth: Auth = await updateAuthUseCase.execute( - updateAuthCommand, - ); - expect(updatedAuth.username).toBe(updateAuthRequest.username); - expect( - bcrypt.compareSync(updateAuthRequest.password, updatedAuth.password), - ).toBeTruthy(); - }); - }); -}); diff --git a/src/modules/auth/tests/unit/update-password.usecase.spec.ts b/src/modules/auth/tests/unit/update-password.usecase.spec.ts new file mode 100644 index 0000000..bd388c2 --- /dev/null +++ b/src/modules/auth/tests/unit/update-password.usecase.spec.ts @@ -0,0 +1,61 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthRepository } from '../../adapters/secondaries/auth.repository'; +import { Auth } from '../../domain/entities/auth'; +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'; + +const updatePasswordRequest: UpdatePasswordRequest = { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + password: 'John123', +}; +const updatePasswordCommand: UpdatePasswordCommand = new UpdatePasswordCommand( + updatePasswordRequest, +); + +const mockAuthRepository = { + update: jest.fn().mockResolvedValue({ + uuid: updatePasswordRequest.uuid, + password: bcrypt.hashSync(updatePasswordRequest.password, 10), + }), +}; + +describe('UpdatePasswordUseCase', () => { + let updatePasswordUseCase: UpdatePasswordUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: AuthRepository, + useValue: mockAuthRepository, + }, + 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: Auth = await updatePasswordUseCase.execute( + updatePasswordCommand, + ); + + expect( + bcrypt.compareSync(updatePasswordRequest.password, newAuth.password), + ).toBeTruthy(); + }); + }); +}); diff --git a/src/modules/auth/tests/unit/update-username.usecase.spec.ts b/src/modules/auth/tests/unit/update-username.usecase.spec.ts new file mode 100644 index 0000000..1866eea --- /dev/null +++ b/src/modules/auth/tests/unit/update-username.usecase.spec.ts @@ -0,0 +1,58 @@ +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'; + +const updateUsernameRequest: UpdateUsernameRequest = { + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + username: 'johnny.doe@email.com', + type: Type.EMAIL, +}; +const updateUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand( + updateUsernameRequest, +); + +const mockUsernameRepository = { + updateWhere: jest.fn().mockResolvedValue(updateUsernameRequest), +}; + +describe('UpdateUsernameUseCase', () => { + let updateUsernameUseCase: UpdateUsernameUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: UsernameRepository, + useValue: mockUsernameRepository, + }, + UpdateUsernameUseCase, + ], + }).compile(); + + updateUsernameUseCase = module.get( + UpdateUsernameUseCase, + ); + }); + + it('should be defined', () => { + expect(updateUsernameUseCase).toBeDefined(); + }); + + describe('execute', () => { + 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); + }); + }); +}); diff --git a/src/modules/auth/tests/unit/validate-auth.usecase.spec.ts b/src/modules/auth/tests/unit/validate-auth.usecase.spec.ts index 4d0a62d..f6830d5 100644 --- a/src/modules/auth/tests/unit/validate-auth.usecase.spec.ts +++ b/src/modules/auth/tests/unit/validate-auth.usecase.spec.ts @@ -5,17 +5,25 @@ import { AuthRepository } from '../../adapters/secondaries/auth.repository'; import { Auth } from '../../domain/entities/auth'; import * as bcrypt from 'bcrypt'; import { ValidateAuthUseCase } from '../../domain/usecases/validate-auth.usecase'; -import { AuthProfile } from '../../mappers/auth.profile'; import { ValidateAuthQuery } from '../../queries/validate-auth.query'; +import { UsernameRepository } from '../../adapters/secondaries/username.repository'; +import { Type } from '../../domain/dtos/type.enum'; const mockAuthRepository = { findOne: jest.fn().mockResolvedValue({ uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - username: 'john.doe@email.com', password: bcrypt.hashSync('John123', 10), }), }; +const mockUsernameRepository = { + findOne: jest.fn().mockResolvedValue({ + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + username: 'john.doe@email.com', + type: Type.EMAIL, + }), +}; + describe('ValidateAuthUseCase', () => { let validateAuthUseCase: ValidateAuthUseCase; @@ -27,8 +35,11 @@ describe('ValidateAuthUseCase', () => { provide: AuthRepository, useValue: mockAuthRepository, }, + { + provide: UsernameRepository, + useValue: mockUsernameRepository, + }, ValidateAuthUseCase, - AuthProfile, ], }).compile(); @@ -45,7 +56,6 @@ describe('ValidateAuthUseCase', () => { new ValidateAuthQuery('john.doe@email.com', 'John123'), ); - expect(auth.username).toBe('john.doe@email.com'); expect(bcrypt.compareSync('John123', auth.password)).toBeTruthy(); }); }); diff --git a/src/modules/database/database.module.ts b/src/modules/database/database.module.ts index 0315576..ab31158 100644 --- a/src/modules/database/database.module.ts +++ b/src/modules/database/database.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; import { AuthRepository } from '../auth/adapters/secondaries/auth.repository'; +import { UsernameRepository } from '../auth/adapters/secondaries/username.repository'; import { PrismaService } from './src/adapters/secondaries/prisma-service'; @Module({ - providers: [PrismaService, AuthRepository], - exports: [PrismaService, AuthRepository], + providers: [PrismaService, AuthRepository, UsernameRepository], + exports: [PrismaService, AuthRepository, UsernameRepository], }) export class DatabaseModule {} diff --git a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts b/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts index 7cb5446..118587a 100644 --- a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts +++ b/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts @@ -143,14 +143,15 @@ export abstract class PrismaRepository implements IRepository { } } - async delete(uuid: string): Promise { + async delete(where: any): Promise { try { const entity = await this._prisma[this._model].delete({ - where: { uuid }, + where: where, }); return entity; } catch (e) { + console.log(e); if (e instanceof PrismaClientKnownRequestError) { throw new DatabaseException( PrismaClientKnownRequestError.name, diff --git a/src/modules/database/tests/unit/prisma-repository.spec.ts b/src/modules/database/tests/unit/prisma-repository.spec.ts index 39b9d0a..4381965 100644 --- a/src/modules/database/tests/unit/prisma-repository.spec.ts +++ b/src/modules/database/tests/unit/prisma-repository.spec.ts @@ -217,7 +217,7 @@ describe('PrismaRepository', () => { const savedUuid = fakeEntities[0].uuid; // eslint-disable-next-line @typescript-eslint/no-unused-vars - const res = await fakeRepository.delete(savedUuid); + const res = await fakeRepository.delete({ uuid: savedUuid }); const deletedEntity = fakeEntities.find( (entity) => entity.uuid === savedUuid,