delete username usecase

This commit is contained in:
sbriat 2023-07-07 11:11:36 +02:00
parent a95caefaf2
commit 28c6ca0f63
12 changed files with 274 additions and 1 deletions

View File

@ -17,17 +17,21 @@ import { UsernameRepository } from './infrastructure/username.repository';
import { UsernameMapper } from './username.mapper';
import { AddUsernameGrpcController } from './interface/grpc-controllers/add-username.grpc.controller';
import { AddUsernameService } from './core/application/commands/add-username/add-username.service';
import { DeleteUsernameGrpcController } from './interface/grpc-controllers/delete-username.grpc.controller';
import { DeleteUsernameService } from './core/application/commands/delete-username/delete-username.service';
const grpcControllers = [
CreateAuthenticationGrpcController,
DeleteAuthenticationGrpcController,
AddUsernameGrpcController,
DeleteUsernameGrpcController,
];
const commandHandlers: Provider[] = [
CreateAuthenticationService,
DeleteAuthenticationService,
AddUsernameService,
DeleteUsernameService,
];
const mappers: Provider[] = [AuthenticationMapper, UsernameMapper];

View File

@ -0,0 +1,10 @@
import { Command, CommandProps } from '@mobicoop/ddd-library';
export class DeleteUsernameCommand extends Command {
readonly name: string;
constructor(props: CommandProps<DeleteUsernameCommand>) {
super(props);
this.name = props.name;
}
}

View File

@ -0,0 +1,23 @@
import { Inject } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { DeleteUsernameCommand } from './delete-username.command';
import { USERNAME_REPOSITORY } from '@modules/authentication/authentication.di-tokens';
import { UsernameRepositoryPort } from '../../ports/username.repository.port';
import { UsernameEntity } from '@modules/authentication/core/domain/username.entity';
@CommandHandler(DeleteUsernameCommand)
export class DeleteUsernameService implements ICommandHandler {
constructor(
@Inject(USERNAME_REPOSITORY)
private readonly usernameRepository: UsernameRepositoryPort,
) {}
async execute(command: DeleteUsernameCommand): Promise<boolean> {
const username: UsernameEntity = await this.usernameRepository.findOneById(
command.name,
);
username.delete();
const isDeleted: boolean = await this.usernameRepository.delete(username);
return isDeleted;
}
}

View File

@ -0,0 +1,7 @@
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
export class UsernameDeletedDomainEvent extends DomainEvent {
constructor(props: DomainEventProps<UsernameDeletedDomainEvent>) {
super(props);
}
}

View File

@ -1,6 +1,7 @@
import { AggregateID, AggregateRoot } from '@mobicoop/ddd-library';
import { CreateUsernameProps, UsernameProps } from './username.types';
import { UsernameAddedDomainEvent } from './events/username-added.domain-event';
import { UsernameDeletedDomainEvent } from './events/username-deleted.domain-event';
export class UsernameEntity extends AggregateRoot<UsernameProps> {
protected readonly _id: AggregateID;
@ -23,6 +24,14 @@ export class UsernameEntity extends AggregateRoot<UsernameProps> {
return username;
};
delete(): void {
this.addEvent(
new UsernameDeletedDomainEvent({
aggregateId: this.id,
}),
);
}
validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database
}

View File

@ -0,0 +1,45 @@
import {
DatabaseErrorException,
NotFoundException,
RpcExceptionCode,
RpcValidationPipe,
} from '@mobicoop/ddd-library';
import { Controller, UsePipes } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { DeleteUsernameRequestDto } from './dtos/delete-username.request.dto';
import { DeleteUsernameCommand } from '@modules/authentication/core/application/commands/delete-username/delete-username.command';
@UsePipes(
new RpcValidationPipe({
whitelist: true,
forbidUnknownValues: false,
}),
)
@Controller()
export class DeleteUsernameGrpcController {
constructor(private readonly commandBus: CommandBus) {}
@GrpcMethod('AuthenticationService', 'DeleteUsername')
async delete(data: DeleteUsernameRequestDto): Promise<void> {
try {
await this.commandBus.execute(new DeleteUsernameCommand(data));
} catch (error: any) {
if (error instanceof NotFoundException)
throw new RpcException({
code: RpcExceptionCode.NOT_FOUND,
message: error.message,
});
if (error instanceof DatabaseErrorException)
throw new RpcException({
code: RpcExceptionCode.INTERNAL,
message: error.message,
});
throw new RpcException({
code: RpcExceptionCode.UNKNOWN,
message: error.message,
});
}
}
}

View File

@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class DeleteUsernameRequestDto {
@IsString()
@IsNotEmpty()
name: string;
}

View File

@ -50,6 +50,8 @@ describe('Authentication entity create', () => {
expect(authenticationEntity.getProps().usernames.length).toBe(2);
expect(authenticationEntity.domainEvents.length).toBe(1);
});
});
describe('Authentication entity delete', () => {
it('should delete an authentication entity', async () => {
const authenticationEntity: AuthenticationEntity =
await AuthenticationEntity.create(createAuthenticationProps);

View File

@ -0,0 +1,54 @@
import { USERNAME_REPOSITORY } from '@modules/authentication/authentication.di-tokens';
import { DeleteUsernameCommand } from '@modules/authentication/core/application/commands/delete-username/delete-username.command';
import { DeleteUsernameService } from '@modules/authentication/core/application/commands/delete-username/delete-username.service';
import { DeleteUsernameRequestDto } from '@modules/authentication/interface/grpc-controllers/dtos/delete-username.request.dto';
import { Test, TestingModule } from '@nestjs/testing';
const deleteUsernameRequest: DeleteUsernameRequestDto = {
name: 'john.doe@email.com',
};
const mockUsernameEntity = {
delete: jest.fn(),
};
const mockUsernameRepository = {
findOneById: jest.fn().mockImplementation(() => mockUsernameEntity),
delete: jest.fn().mockImplementationOnce(() => true),
};
describe('Delete Username Service', () => {
let deleteUsernameService: DeleteUsernameService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: USERNAME_REPOSITORY,
useValue: mockUsernameRepository,
},
DeleteUsernameService,
],
}).compile();
deleteUsernameService = module.get<DeleteUsernameService>(
DeleteUsernameService,
);
});
it('should be defined', () => {
expect(deleteUsernameService).toBeDefined();
});
describe('execution', () => {
const deleteUsernameCommand = new DeleteUsernameCommand(
deleteUsernameRequest,
);
it('should delete a username', async () => {
const result: boolean = await deleteUsernameService.execute(
deleteUsernameCommand,
);
expect(result).toBeTruthy();
});
});
});

View File

@ -1,3 +1,4 @@
import { UsernameDeletedDomainEvent } from '@modules/authentication/core/domain/events/username-deleted.domain-event';
import { UsernameEntity } from '@modules/authentication/core/domain/username.entity';
import {
CreateUsernameProps,
@ -19,3 +20,15 @@ describe('Username entity create', () => {
expect(usernameEntity.domainEvents.length).toBe(1);
});
});
describe('Username entity delete', () => {
it('should delete a username entity', async () => {
const usernameEntity: UsernameEntity = await UsernameEntity.create(
createUsernameProps,
);
usernameEntity.delete();
expect(usernameEntity.domainEvents.length).toBe(2);
expect(usernameEntity.domainEvents[1]).toBeInstanceOf(
UsernameDeletedDomainEvent,
);
});
});

View File

@ -56,7 +56,7 @@ describe('Delete Authentication Grpc Controller', () => {
expect(deleteAuthenticationGrpcController).toBeDefined();
});
it('should create a new authentication', async () => {
it('should delete an authentication', async () => {
jest.spyOn(mockCommandBus, 'execute');
await deleteAuthenticationGrpcController.delete(
deleteAuthenticationRequest,

View File

@ -0,0 +1,99 @@
import {
DatabaseErrorException,
NotFoundException,
} from '@mobicoop/ddd-library';
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { DeleteUsernameGrpcController } from '@modules/authentication/interface/grpc-controllers/delete-username.grpc.controller';
import { DeleteUsernameRequestDto } from '@modules/authentication/interface/grpc-controllers/dtos/delete-username.request.dto';
import { CommandBus } from '@nestjs/cqrs';
import { RpcException } from '@nestjs/microservices';
import { Test, TestingModule } from '@nestjs/testing';
const deleteUsernameRequest: DeleteUsernameRequestDto = {
name: 'john.doe@email.com',
};
const mockCommandBus = {
execute: jest
.fn()
.mockImplementationOnce(() => ({}))
.mockImplementationOnce(() => {
throw new NotFoundException();
})
.mockImplementationOnce(() => {
throw new DatabaseErrorException();
})
.mockImplementationOnce(() => {
throw new Error();
}),
};
describe('Delete Username Grpc Controller', () => {
let deleteUsernameGrpcController: DeleteUsernameGrpcController;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: CommandBus,
useValue: mockCommandBus,
},
DeleteUsernameGrpcController,
],
}).compile();
deleteUsernameGrpcController = module.get<DeleteUsernameGrpcController>(
DeleteUsernameGrpcController,
);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(deleteUsernameGrpcController).toBeDefined();
});
it('should delete a username', async () => {
jest.spyOn(mockCommandBus, 'execute');
await deleteUsernameGrpcController.delete(deleteUsernameRequest);
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
});
it('should throw a dedicated RpcException if username does not exist', async () => {
jest.spyOn(mockCommandBus, 'execute');
expect.assertions(3);
try {
await deleteUsernameGrpcController.delete(deleteUsernameRequest);
} catch (e: any) {
expect(e).toBeInstanceOf(RpcException);
expect(e.error.code).toBe(RpcExceptionCode.NOT_FOUND);
}
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
});
it('should throw a dedicated RpcException if a database error occurs', async () => {
jest.spyOn(mockCommandBus, 'execute');
expect.assertions(3);
try {
await deleteUsernameGrpcController.delete(deleteUsernameRequest);
} catch (e: any) {
expect(e).toBeInstanceOf(RpcException);
expect(e.error.code).toBe(RpcExceptionCode.INTERNAL);
}
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
});
it('should throw a generic RpcException', async () => {
jest.spyOn(mockCommandBus, 'execute');
expect.assertions(3);
try {
await deleteUsernameGrpcController.delete(deleteUsernameRequest);
} catch (e: any) {
expect(e).toBeInstanceOf(RpcException);
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
}
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
});
});