add opa, refactor auth to authentication

This commit is contained in:
Gsk54
2023-01-16 15:03:58 +01:00
parent 0a2a44bc15
commit 6802cd3620
61 changed files with 456 additions and 381 deletions

View File

@@ -0,0 +1,56 @@
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({
exchange: 'user',
routingKey: 'update',
queue: 'auth-user-update',
})
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({
exchange: 'user',
routingKey: 'delete',
queue: 'auth-user-delete',
})
public async userDeletedHandler(message: string) {
const deletedUser = JSON.parse(message);
if (!deletedUser.hasOwnProperty('uuid')) throw new Error();
const deleteAuthRequest = new DeleteAuthenticationRequest();
deleteAuthRequest.uuid = deletedUser.uuid;
await this._commandBus.execute(
new DeleteAuthenticationCommand(deleteAuthRequest),
);
}
}

View File

@@ -0,0 +1,174 @@
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/src/exceptions/DatabaseException';
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 { ValidateAuthRequest } 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 './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('AuthService', 'Validate')
async validate(data: ValidateAuthRequest): Promise<AuthenticationPresenter> {
try {
const auth: Authentication = await this._queryBus.execute(
new ValidateAuthenticationQuery(data.username, data.password),
);
return this._mapper.map(auth, Authentication, AuthenticationPresenter);
} catch (e) {
throw new RpcException({
code: 7,
message: 'Permission denied',
});
}
}
@GrpcMethod('AuthService', 'Create')
async createUser(
data: CreateAuthenticationRequest,
): Promise<AuthenticationPresenter> {
try {
const auth: Authentication = await this._commandBus.execute(
new CreateAuthenticationCommand(data),
);
return this._mapper.map(auth, 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('AuthService', 'AddUsername')
async addUsername(data: AddUsernameRequest): Promise<UsernamePresenter> {
try {
const username: Username = await this._commandBus.execute(
new AddUsernameCommand(data),
);
return this._mapper.map(username, Username, UsernamePresenter);
} catch (e) {
if (e instanceof DatabaseException) {
if (e.message.includes('Already exists')) {
throw new RpcException({
code: 6,
message: 'Username already exists',
});
}
}
throw new RpcException({
code: 7,
message: 'Permission denied',
});
}
}
@GrpcMethod('AuthService', 'UpdateUsername')
async updateUsername(
data: UpdateUsernameRequest,
): Promise<UsernamePresenter> {
try {
const username: Username = await this._commandBus.execute(
new UpdateUsernameCommand(data),
);
return this._mapper.map(username, Username, UsernamePresenter);
} catch (e) {
if (e instanceof DatabaseException) {
if (e.message.includes('Already exists')) {
throw new RpcException({
code: 6,
message: 'Username already exists',
});
}
}
throw new RpcException({
code: 7,
message: 'Permission denied',
});
}
}
@GrpcMethod('AuthService', 'UpdatePassword')
async updatePassword(
data: UpdatePasswordRequest,
): Promise<AuthenticationPresenter> {
try {
const auth: Authentication = await this._commandBus.execute(
new UpdatePasswordCommand(data),
);
return this._mapper.map(auth, Authentication, AuthenticationPresenter);
} catch (e) {
throw new RpcException({
code: 7,
message: 'Permission denied',
});
}
}
@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: DeleteAuthenticationRequest) {
try {
return await this._commandBus.execute(
new DeleteAuthenticationCommand(data),
);
} catch (e) {
throw new RpcException({
code: 7,
message: 'Permission denied',
});
}
}
}

View File

@@ -0,0 +1,6 @@
import { AutoMap } from '@automapper/classes';
export class AuthenticationPresenter {
@AutoMap()
uuid: string;
}

View File

@@ -0,0 +1,47 @@
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;
}
enum Type {
EMAIL = 0;
PHONE = 1;
}
message Authentication {
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 {}

View File

@@ -0,0 +1,14 @@
import { Injectable, ValidationPipe } from '@nestjs/common';
import { RpcException } from '@nestjs/microservices';
@Injectable()
export class RpcValidationPipe extends ValidationPipe {
createExceptionFactory() {
return (validationErrors = []) => {
return new RpcException({
code: 3,
message: this.flattenValidationErrors(validationErrors),
});
};
}
}

View File

@@ -0,0 +1,9 @@
import { AutoMap } from '@automapper/classes';
export class UsernamePresenter {
@AutoMap()
uuid: string;
@AutoMap()
username: string;
}

View File

@@ -0,0 +1,14 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Injectable } from '@nestjs/common';
import { IMessageBroker } from '../../domain/interfaces/message-broker';
@Injectable()
export class AuthenticationMessager extends IMessageBroker {
constructor(private readonly _amqpConnection: AmqpConnection) {
super('auth');
}
publish(routingKey: string, message: string): void {
this._amqpConnection.publish(this.exchange, routingKey, message);
}
}

View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
import { AuthRepository } from '../../../database/src/domain/auth-repository';
import { Authentication } from '../../domain/entities/authentication';
@Injectable()
export class AuthenticationRepository extends AuthRepository<Authentication> {
protected _model = 'auth';
}

View File

@@ -0,0 +1,14 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Injectable } from '@nestjs/common';
import { IMessageBroker } from '../../domain/interfaces/message-broker';
@Injectable()
export class LoggingMessager extends IMessageBroker {
constructor(private readonly _amqpConnection: AmqpConnection) {
super('logging');
}
publish(routingKey: string, message: string): void {
this._amqpConnection.publish(this.exchange, routingKey, message);
}
}

View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
import { AuthRepository } from '../../../database/src/domain/auth-repository';
import { Username } from '../../domain/entities/username';
@Injectable()
export class UsernameRepository extends AuthRepository<Username> {
protected _model = 'username';
}

View File

@@ -0,0 +1,60 @@
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 { LoggingMessager } from './adapters/secondaries/logging.messager';
@Module({
imports: [
DatabaseModule,
CqrsModule,
RabbitMQModule.forRootAsync(RabbitMQModule, {
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
exchanges: [
{
name: 'user',
type: 'topic',
},
{
name: 'logging',
type: 'topic',
},
],
uri: configService.get<string>('RMQ_URI'),
connectionInitOptions: { wait: false },
enableControllerDiscovery: true,
}),
inject: [ConfigService],
}),
],
controllers: [AuthenticationController, AuthenticationMessagerController],
providers: [
AuthenticationProfile,
UsernameProfile,
AuthenticationRepository,
LoggingMessager,
ValidateAuthenticationUseCase,
CreateAuthenticationUseCase,
AddUsernameUseCase,
UpdateUsernameUseCase,
UpdatePasswordUseCase,
DeleteUsernameUseCase,
DeleteAuthenticationUseCase,
],
exports: [],
})
export class AuthenticationModule {}

View File

@@ -0,0 +1,9 @@
import { AddUsernameRequest } from '../domain/dtos/add-username.request';
export class AddUsernameCommand {
readonly addUsernameRequest: AddUsernameRequest;
constructor(request: AddUsernameRequest) {
this.addUsernameRequest = request;
}
}

View File

@@ -0,0 +1,9 @@
import { CreateAuthenticationRequest } from '../domain/dtos/create-authentication.request';
export class CreateAuthenticationCommand {
readonly createAuthenticationRequest: CreateAuthenticationRequest;
constructor(request: CreateAuthenticationRequest) {
this.createAuthenticationRequest = request;
}
}

View File

@@ -0,0 +1,9 @@
import { DeleteAuthenticationRequest } from '../domain/dtos/delete-authentication.request';
export class DeleteAuthenticationCommand {
readonly deleteAuthenticationRequest: DeleteAuthenticationRequest;
constructor(request: DeleteAuthenticationRequest) {
this.deleteAuthenticationRequest = request;
}
}

View File

@@ -0,0 +1,9 @@
import { DeleteUsernameRequest } from '../domain/dtos/delete-username.request';
export class DeleteUsernameCommand {
readonly deleteUsernameRequest: DeleteUsernameRequest;
constructor(request: DeleteUsernameRequest) {
this.deleteUsernameRequest = request;
}
}

View File

@@ -0,0 +1,9 @@
import { UpdatePasswordRequest } from '../domain/dtos/update-password.request';
export class UpdatePasswordCommand {
readonly updatePasswordRequest: UpdatePasswordRequest;
constructor(request: UpdatePasswordRequest) {
this.updatePasswordRequest = request;
}
}

View File

@@ -0,0 +1,9 @@
import { UpdateUsernameRequest } from '../domain/dtos/update-username.request';
export class UpdateUsernameCommand {
readonly updateUsernameRequest: UpdateUsernameRequest;
constructor(request: UpdateUsernameRequest) {
this.updateUsernameRequest = request;
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,25 @@
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;
}

View File

@@ -0,0 +1,9 @@
import { AutoMap } from '@automapper/classes';
import { IsNotEmpty, IsString } from 'class-validator';
export class DeleteAuthenticationRequest {
@IsString()
@IsNotEmpty()
@AutoMap()
uuid: string;
}

View File

@@ -0,0 +1,9 @@
import { AutoMap } from '@automapper/classes';
import { IsNotEmpty, IsString } from 'class-validator';
export class DeleteUsernameRequest {
@IsString()
@IsNotEmpty()
@AutoMap()
username: string;
}

View File

@@ -0,0 +1,4 @@
export enum Type {
EMAIL = 'EMAIL',
PHONE = 'PHONE',
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,11 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class ValidateAuthenticationRequest {
@IsString()
@IsNotEmpty()
username: string;
@IsString()
@IsNotEmpty()
password: string;
}

View File

@@ -0,0 +1,8 @@
import { AutoMap } from '@automapper/classes';
export class Authentication {
@AutoMap()
uuid: string;
password: string;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,12 @@
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;
}

View File

@@ -0,0 +1,33 @@
import { CommandHandler } from '@nestjs/cqrs';
import { LoggingMessager } from '../../adapters/secondaries/logging.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 _loggingMessager: LoggingMessager,
) {}
async execute(command: AddUsernameCommand): Promise<Username> {
const { uuid, username, type } = command.addUsernameRequest;
try {
return await this._usernameRepository.create({
uuid,
type,
username,
});
} catch (error) {
this._loggingMessager.publish(
'auth.username.add.warning',
JSON.stringify({
command,
error,
}),
);
throw error;
}
}
}

View File

@@ -0,0 +1,44 @@
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 { LoggingMessager } from '../../adapters/secondaries/logging.messager';
@CommandHandler(CreateAuthenticationCommand)
export class CreateAuthenticationUseCase {
constructor(
private readonly _authenticationRepository: AuthenticationRepository,
private readonly _usernameRepository: UsernameRepository,
private readonly _loggingMessager: LoggingMessager,
) {}
async execute(command: CreateAuthenticationCommand): Promise<Authentication> {
const { uuid, password, ...username } = command.createAuthenticationRequest;
const hash = await bcrypt.hash(password, 10);
try {
const auth = await this._authenticationRepository.create({
uuid,
password: hash,
});
await this._usernameRepository.create({
uuid,
...username,
});
return auth;
} catch (error) {
this._loggingMessager.publish(
'auth.create.crit',
JSON.stringify({
command,
error,
}),
);
throw error;
}
}
}

View File

@@ -0,0 +1,34 @@
import { CommandHandler } from '@nestjs/cqrs';
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
import { LoggingMessager } from '../../adapters/secondaries/logging.messager';
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command';
@CommandHandler(DeleteAuthenticationCommand)
export class DeleteAuthenticationUseCase {
constructor(
private readonly _authenticationRepository: AuthenticationRepository,
private readonly _usernameRepository: UsernameRepository,
private readonly _loggingMessager: LoggingMessager,
) {}
async execute(command: DeleteAuthenticationCommand) {
try {
await this._usernameRepository.deleteMany({
uuid: command.deleteAuthenticationRequest.uuid,
});
return await this._authenticationRepository.delete(
command.deleteAuthenticationRequest.uuid,
);
} catch (error) {
this._loggingMessager.publish(
'auth.delete.crit',
JSON.stringify({
command,
error,
}),
);
throw error;
}
}
}

View File

@@ -0,0 +1,38 @@
import { UnauthorizedException } from '@nestjs/common';
import { CommandHandler } from '@nestjs/cqrs';
import { LoggingMessager } from '../../adapters/secondaries/logging.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 _loggingMessager: LoggingMessager,
) {}
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.deleteMany({ username });
}
throw new UnauthorizedException();
} catch (error) {
this._loggingMessager.publish(
'auth.username.delete.warning',
JSON.stringify({
command,
error,
}),
);
throw error;
}
}
}

View File

@@ -0,0 +1,34 @@
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 { LoggingMessager } from '../../adapters/secondaries/logging.messager';
@CommandHandler(UpdatePasswordCommand)
export class UpdatePasswordUseCase {
constructor(
private readonly _authenticationRepository: AuthenticationRepository,
private readonly _loggingMessager: LoggingMessager,
) {}
async execute(command: UpdatePasswordCommand): Promise<Authentication> {
const { uuid, password } = command.updatePasswordRequest;
const hash = await bcrypt.hash(password, 10);
try {
return await this._authenticationRepository.update(uuid, {
password: hash,
});
} catch (error) {
this._loggingMessager.publish(
'auth.password.update.warning',
JSON.stringify({
command,
error,
}),
);
throw error;
}
}
}

View File

@@ -0,0 +1,67 @@
import { Mapper } from '@automapper/core';
import { InjectMapper } from '@automapper/nestjs';
import { BadRequestException } from '@nestjs/common';
import { CommandBus, CommandHandler } from '@nestjs/cqrs';
import { LoggingMessager } from '../../adapters/secondaries/logging.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 _loggingMessager: LoggingMessager,
) {}
async execute(command: UpdateUsernameCommand): Promise<Username> {
const { uuid, username, type } = command.updateUsernameRequest;
if (!username) throw new BadRequestException();
// update username if it exists, otherwise create it
const existingUsername = await this._usernameRepository.findOne({
uuid,
type,
});
if (existingUsername) {
try {
return await this._usernameRepository.updateWhere(
{
uuid_type: {
uuid,
type,
},
},
{
username,
},
);
} catch (error) {
this._loggingMessager.publish(
'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;
}
}
}

View File

@@ -0,0 +1,41 @@
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,
) {}
async execute(
validate: ValidateAuthenticationQuery,
): Promise<Authentication> {
let username = new Username();
try {
username = await this._usernameRepository.findOne({
username: validate.username,
});
} catch (e) {
throw new NotFoundException();
}
try {
const auth = await this._authenticationRepository.findOne({
uuid: username.uuid,
});
if (auth) {
const isMatch = await bcrypt.compare(validate.password, auth.password);
if (isMatch) return auth;
}
throw new UnauthorizedException();
} catch (e) {
throw new UnauthorizedException();
}
}
}

View File

@@ -0,0 +1,18 @@
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);
};
}
}

View File

@@ -0,0 +1,21 @@
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);
};
}
}

View File

@@ -0,0 +1,9 @@
export class ValidateAuthenticationQuery {
readonly username: string;
readonly password: string;
constructor(username: string, password: string) {
this.username = username;
this.password = password;
}
}

View File

@@ -0,0 +1,162 @@
import { TestingModule, Test } from '@nestjs/testing';
import { DatabaseModule } from '../../../database/database.module';
import { PrismaService } from '../../../database/src/adapters/secondaries/prisma-service';
import { DatabaseException } from '../../../database/src/exceptions/DatabaseException';
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 createAuths = async (nbToCreate = 10) => {
for (let i = 0; i < nbToCreate; i++) {
await prismaService.auth.create({
data: {
uuid: v4(),
password: bcrypt.hashSync(`password-${i}`, 10),
},
});
}
};
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [DatabaseModule],
providers: [AuthenticationRepository, PrismaService],
}).compile();
prismaService = module.get<PrismaService>(PrismaService);
authenticationRepository = module.get<AuthenticationRepository>(
AuthenticationRepository,
);
});
afterAll(async () => {
await prismaService.$disconnect();
});
beforeEach(async () => {
await prismaService.auth.deleteMany();
});
describe('findAll', () => {
it('should return an empty data array', async () => {
const res = await authenticationRepository.findAll();
expect(res).toEqual({
data: [],
total: 0,
});
});
it('should return a data array with 8 auths', async () => {
await createAuths(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 createAuths(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);
});
});
});

View File

@@ -0,0 +1,282 @@
import { TestingModule, Test } from '@nestjs/testing';
import { DatabaseModule } from '../../../database/database.module';
import { PrismaService } from '../../../database/src/adapters/secondaries/prisma-service';
import { DatabaseException } from '../../../database/src/exceptions/DatabaseException';
import { v4 } from 'uuid';
import { Type } from '../../domain/dtos/type.enum';
import { UsernameRepository } from '../../adapters/secondaries/username.repository';
import { Username } from '../../domain/entities/username';
describe('UsernameRepository', () => {
let prismaService: PrismaService;
let usernameRepository: UsernameRepository;
const createUsernames = async (nbToCreate = 10) => {
for (let i = 0; i < nbToCreate; i++) {
await prismaService.username.create({
data: {
uuid: v4(),
username: `john.doe.${i}@email.com`,
type: Type.EMAIL,
},
});
}
};
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [DatabaseModule],
providers: [UsernameRepository, PrismaService],
}).compile();
prismaService = module.get<PrismaService>(PrismaService);
usernameRepository = module.get<UsernameRepository>(UsernameRepository);
});
afterAll(async () => {
await prismaService.$disconnect();
});
beforeEach(async () => {
await prismaService.username.deleteMany();
});
describe('findAll', () => {
it('should return an empty data array', async () => {
const res = await usernameRepository.findAll();
expect(res).toEqual({
data: [],
total: 0,
});
});
it('should return a data array with 8 usernames', async () => {
await createUsernames(8);
const usernames = await usernameRepository.findAll();
expect(usernames.data.length).toBe(8);
expect(usernames.total).toBe(8);
});
it('should return a data array limited to 10 usernames', async () => {
await createUsernames(20);
const usernames = await usernameRepository.findAll();
expect(usernames.data.length).toBe(10);
expect(usernames.total).toBe(20);
});
});
describe('findOne', () => {
it('should return a username with uuid and email', async () => {
const usernameToFind = await prismaService.username.create({
data: {
uuid: v4(),
username: 'john.doe@email.com',
type: Type.EMAIL,
},
});
const username = await usernameRepository.findOne({
username: 'john.doe@email.com',
type: Type.EMAIL,
});
expect(username.uuid).toBe(usernameToFind.uuid);
});
it('should return null', async () => {
const username = await usernameRepository.findOne({
username: 'jane.doe@email.com',
type: Type.EMAIL,
});
expect(username).toBeNull();
});
});
describe('create', () => {
it('should create a username with an email', async () => {
const beforeCount = await prismaService.username.count();
const usernameToCreate: Username = new Username();
usernameToCreate.uuid = v4();
usernameToCreate.username = 'john.doe@email.com';
usernameToCreate.type = Type.EMAIL;
const username = await usernameRepository.create(usernameToCreate);
const afterCount = await prismaService.username.count();
expect(afterCount - beforeCount).toBe(1);
expect(username.uuid).toBeDefined();
});
it('should create a username with a phone number', async () => {
const beforeCount = await prismaService.username.count();
const usernameToCreate: Username = new Username();
usernameToCreate.uuid = v4();
usernameToCreate.username = '+33611223344';
usernameToCreate.type = Type.PHONE;
const username = await usernameRepository.create(usernameToCreate);
const afterCount = await prismaService.username.count();
expect(afterCount - beforeCount).toBe(1);
expect(username.uuid).toBeDefined();
});
it('should create a username with an email for an existing uuid', async () => {
const beforeCount = await prismaService.username.count();
const uuid = v4();
const firstUsernameToCreate: Username = new Username();
firstUsernameToCreate.uuid = uuid;
firstUsernameToCreate.username = '+33611223344';
firstUsernameToCreate.type = Type.PHONE;
const firstUsername = await usernameRepository.create(
firstUsernameToCreate,
);
const secondUsernameToCreate: Username = new Username();
secondUsernameToCreate.uuid = uuid;
secondUsernameToCreate.username = 'john.doe@email.com';
secondUsernameToCreate.type = Type.EMAIL;
const secondUsername = await usernameRepository.create(
secondUsernameToCreate,
);
const afterCount = await prismaService.username.count();
expect(afterCount - beforeCount).toBe(2);
expect(firstUsername.uuid).toEqual(secondUsername.uuid);
});
it('should throw DatabaseException if username already exists for a given type', async () => {
const uuid = v4();
const firstUsernameToCreate: Username = new Username();
firstUsernameToCreate.uuid = uuid;
firstUsernameToCreate.username = 'john.doe@email.com';
firstUsernameToCreate.type = Type.EMAIL;
await usernameRepository.create(firstUsernameToCreate);
const secondUsernameToCreate: Username = new Username();
secondUsernameToCreate.uuid = uuid;
secondUsernameToCreate.username = 'jane.doe@email.com';
secondUsernameToCreate.type = Type.EMAIL;
await expect(
usernameRepository.create(secondUsernameToCreate),
).rejects.toBeInstanceOf(DatabaseException);
});
});
describe('update', () => {
it('should update username email', async () => {
const usernameToUpdate = await prismaService.username.create({
data: {
uuid: v4(),
username: `john.doe@email.com`,
type: Type.EMAIL,
},
});
const toUpdate: Username = new Username();
toUpdate.username = 'jane.doe@email.com';
const updatedUsername = await usernameRepository.updateWhere(
{
uuid_type: {
uuid: usernameToUpdate.uuid,
type: usernameToUpdate.type,
},
},
{
username: toUpdate.username,
},
);
expect(updatedUsername.uuid).toBe(usernameToUpdate.uuid);
expect(updatedUsername.username).toBe('jane.doe@email.com');
});
it('should update username phone', async () => {
const usernameToUpdate = await prismaService.username.create({
data: {
uuid: v4(),
username: `+33611223344`,
type: Type.PHONE,
},
});
const toUpdate: Username = new Username();
toUpdate.username = '+33622334455';
const updatedUsername = await usernameRepository.updateWhere(
{
uuid_type: {
uuid: usernameToUpdate.uuid,
type: usernameToUpdate.type,
},
},
{
username: toUpdate.username,
},
);
expect(updatedUsername.uuid).toBe(usernameToUpdate.uuid);
expect(updatedUsername.username).toBe('+33622334455');
});
it('should throw DatabaseException if email not found', async () => {
const toUpdate: Username = new Username();
toUpdate.username = 'jane.doe@email.com';
await expect(
usernameRepository.updateWhere(
{
uuid_type: {
uuid: '544572be-11fb-4244-8235-587221fc9104',
type: Type.EMAIL,
},
},
{
username: toUpdate.username,
},
),
).rejects.toBeInstanceOf(DatabaseException);
});
it('should throw DatabaseException if phone not found', async () => {
const toUpdate: Username = new Username();
toUpdate.username = '+33611223344';
await expect(
usernameRepository.updateWhere(
{
uuid_type: {
uuid: '544572be-11fb-4244-8235-587221fc9104',
type: Type.PHONE,
},
},
{
username: toUpdate.username,
},
),
).rejects.toBeInstanceOf(DatabaseException);
});
});
describe('delete', () => {
it('should delete a username', async () => {
const usernameToRemove = await prismaService.username.create({
data: {
uuid: v4(),
username: `+33611223344`,
type: Type.PHONE,
},
});
await usernameRepository.deleteMany({ uuid: usernameToRemove.uuid });
const count = await prismaService.username.count();
expect(count).toBe(0);
});
it('should throw DatabaseException', async () => {
await expect(
usernameRepository.delete('544572be-11fb-4244-8235-587221fc9104'),
).rejects.toBeInstanceOf(DatabaseException);
});
});
});

View File

@@ -0,0 +1,79 @@
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 { LoggingMessager } from '../../adapters/secondaries/logging.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: LoggingMessager,
useValue: mockMessager,
},
AddUsernameUseCase,
AuthenticationProfile,
],
}).compile();
addUsernameUseCase = module.get<AddUsernameUseCase>(AddUsernameUseCase);
});
it('should be defined', () => {
expect(addUsernameUseCase).toBeDefined();
});
describe('execute', () => {
it('should add a username for phone type', async () => {
const addedUsername: Username = await addUsernameUseCase.execute(
addUsernameCommand,
);
expect(addedUsername.username).toBe(addUsernameRequest.username);
expect(addedUsername.type).toBe(addUsernameRequest.type);
});
it('should throw an error if user already exists', async () => {
await expect(
addUsernameUseCase.execute(addUsernameCommand),
).rejects.toBeInstanceOf(Error);
});
});
});

View File

@@ -0,0 +1,99 @@
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 { LoggingMessager } from '../../adapters/secondaries/logging.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: LoggingMessager,
useValue: mockMessager,
},
CreateAuthenticationUseCase,
],
}).compile();
createAuthenticationUseCase = module.get<CreateAuthenticationUseCase>(
CreateAuthenticationUseCase,
);
});
it('should be defined', () => {
expect(createAuthenticationUseCase).toBeDefined();
});
describe('execute', () => {
it('should create an authentication with an encrypted password', async () => {
const newAuthentication: Authentication =
await createAuthenticationUseCase.execute(newAuthCommand);
expect(
bcrypt.compareSync(
newAuthenticationRequest.password,
newAuthentication.password,
),
).toBeTruthy();
});
it('should throw an error if user already exists', async () => {
await expect(
createAuthenticationUseCase.execute(newAuthCommand),
).rejects.toBeInstanceOf(Error);
});
});
});

View File

@@ -0,0 +1,102 @@
import { classes } from '@automapper/classes';
import { AutomapperModule } from '@automapper/nestjs';
import { Test, TestingModule } from '@nestjs/testing';
import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository';
import { LoggingMessager } from '../../adapters/secondaries/logging.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: LoggingMessager,
useValue: mockMessager,
},
DeleteAuthenticationUseCase,
],
}).compile();
deleteAuthenticationUseCase = module.get<DeleteAuthenticationUseCase>(
DeleteAuthenticationUseCase,
);
});
it('should be defined', () => {
expect(deleteAuthenticationUseCase).toBeDefined();
});
describe('execute', () => {
it('should delete an authentication and its usernames', async () => {
const deletedAuthentication = await deleteAuthenticationUseCase.execute(
deleteAuthenticationCommand,
);
expect(deletedAuthentication).toBe(undefined);
});
it('should throw an error if authentication does not exist', async () => {
await expect(
deleteAuthenticationUseCase.execute(deleteAuthenticationCommand),
).rejects.toBeInstanceOf(Error);
});
});
});

View File

@@ -0,0 +1,115 @@
import { classes } from '@automapper/classes';
import { AutomapperModule } from '@automapper/nestjs';
import { UnauthorizedException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { LoggingMessager } from '../../adapters/secondaries/logging.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: LoggingMessager,
useValue: mockMessager,
},
DeleteUsernameUseCase,
],
}).compile();
deleteUsernameUseCase = module.get<DeleteUsernameUseCase>(
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);
});
});
});

View File

@@ -0,0 +1,81 @@
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 { LoggingMessager } from '../../adapters/secondaries/logging.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: LoggingMessager,
useValue: mockMessager,
},
UpdatePasswordUseCase,
],
}).compile();
updatePasswordUseCase = module.get<UpdatePasswordUseCase>(
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);
});
});
});

View File

@@ -0,0 +1,148 @@
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 { LoggingMessager } from '../../adapters/secondaries/logging.messager';
const existingUsername = {
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
username: 'john.doe@email.com',
type: Type.EMAIL,
};
const updateUsernameRequest: UpdateUsernameRequest =
new UpdateUsernameRequest();
updateUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
updateUsernameRequest.username = 'johnny.doe@email.com';
updateUsernameRequest.type = Type.EMAIL;
const newUsernameRequest: UpdateUsernameRequest = new UpdateUsernameRequest();
newUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
newUsernameRequest.username = '+33611223344';
newUsernameRequest.type = Type.PHONE;
const invalidUpdateUsernameRequest: UpdateUsernameRequest =
new UpdateUsernameRequest();
invalidUpdateUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
invalidUpdateUsernameRequest.username = '';
invalidUpdateUsernameRequest.type = Type.EMAIL;
const unknownUsernameRequest: UpdateUsernameRequest =
new UpdateUsernameRequest();
unknownUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
unknownUsernameRequest.username = 'unknown@email.com';
unknownUsernameRequest.type = Type.EMAIL;
const updateUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand(
updateUsernameRequest,
);
const newUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand(
newUsernameRequest,
);
const invalidUpdateUsernameCommand: UpdateUsernameCommand =
new UpdateUsernameCommand(invalidUpdateUsernameRequest);
const unknownUpdateUsernameCommand: UpdateUsernameCommand =
new UpdateUsernameCommand(unknownUsernameRequest);
const mockUsernameRepository = {
findOne: jest.fn().mockResolvedValue(existingUsername),
updateWhere: jest
.fn()
.mockImplementationOnce(() => {
return Promise.resolve(updateUsernameRequest);
})
.mockImplementationOnce(() => {
return Promise.resolve(newUsernameRequest);
})
.mockImplementationOnce(() => {
throw new Error('Error');
})
.mockImplementationOnce(() => {
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: LoggingMessager,
useValue: mockMessager,
},
UpdateUsernameUseCase,
UsernameProfile,
],
}).compile();
updateUsernameUseCase = module.get<UpdateUsernameUseCase>(
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);
});
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 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);
});
});
});

View File

@@ -0,0 +1,107 @@
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/src/exceptions/DatabaseException';
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>(
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);
});
});
});