update username
This commit is contained in:
parent
28c6ca0f63
commit
805a7fe24d
|
@ -19,11 +19,14 @@ import { AddUsernameGrpcController } from './interface/grpc-controllers/add-user
|
|||
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';
|
||||
import { UpdateUsernameGrpcController } from './interface/grpc-controllers/update-username.grpc.controller';
|
||||
import { UpdateUsernameService } from './core/application/commands/update-username/update-username.service';
|
||||
|
||||
const grpcControllers = [
|
||||
CreateAuthenticationGrpcController,
|
||||
DeleteAuthenticationGrpcController,
|
||||
AddUsernameGrpcController,
|
||||
UpdateUsernameGrpcController,
|
||||
DeleteUsernameGrpcController,
|
||||
];
|
||||
|
||||
|
@ -31,6 +34,7 @@ const commandHandlers: Provider[] = [
|
|||
CreateAuthenticationService,
|
||||
DeleteAuthenticationService,
|
||||
AddUsernameService,
|
||||
UpdateUsernameService,
|
||||
DeleteUsernameService,
|
||||
];
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
import { Username } from '../../types/username';
|
||||
|
||||
export class UpdateUsernameCommand extends Command {
|
||||
readonly userId: string;
|
||||
readonly username: Username;
|
||||
|
||||
constructor(props: CommandProps<UpdateUsernameCommand>) {
|
||||
super(props);
|
||||
this.userId = props.userId;
|
||||
this.username = props.username;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import {
|
||||
AggregateID,
|
||||
ConflictException,
|
||||
UniqueConstraintException,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { USERNAME_REPOSITORY } from '@modules/authentication/authentication.di-tokens';
|
||||
import { UsernameAlreadyExistsException } from '@modules/authentication/core/domain/authentication.errors';
|
||||
import { UpdateUsernameCommand } from './update-username.command';
|
||||
import { UsernameRepositoryPort } from '../../ports/username.repository.port';
|
||||
import { UsernameEntity } from '@modules/authentication/core/domain/username.entity';
|
||||
|
||||
@CommandHandler(UpdateUsernameCommand)
|
||||
export class UpdateUsernameService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(USERNAME_REPOSITORY)
|
||||
private readonly usernameRepository: UsernameRepositoryPort,
|
||||
) {}
|
||||
|
||||
async execute(command: UpdateUsernameCommand): Promise<AggregateID> {
|
||||
try {
|
||||
const username: UsernameEntity = await this.usernameRepository.findByType(
|
||||
command.userId,
|
||||
command.username.type,
|
||||
);
|
||||
const oldName: string = username.id;
|
||||
username.update({
|
||||
name: command.username.name,
|
||||
});
|
||||
await this.usernameRepository.updateUsername(oldName, username);
|
||||
return username.getProps().name;
|
||||
} catch (error: any) {
|
||||
if (error instanceof ConflictException) {
|
||||
throw new UsernameAlreadyExistsException(error);
|
||||
}
|
||||
if (
|
||||
error instanceof UniqueConstraintException &&
|
||||
error.message.includes('username')
|
||||
) {
|
||||
throw new UsernameAlreadyExistsException(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
import { RepositoryPort } from '@mobicoop/ddd-library';
|
||||
import { UsernameEntity } from '../../domain/username.entity';
|
||||
import { Type } from '../../domain/username.types';
|
||||
|
||||
export type UsernameRepositoryPort = RepositoryPort<UsernameEntity>;
|
||||
export type UsernameRepositoryPort = RepositoryPort<UsernameEntity> & {
|
||||
findByType(userId: string, type: Type): Promise<UsernameEntity>;
|
||||
updateUsername(oldName: string, entity: UsernameEntity): Promise<void>;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class UsernameUpdatedDomainEvent extends DomainEvent {
|
||||
constructor(props: DomainEventProps<UsernameUpdatedDomainEvent>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,12 @@
|
|||
import { AggregateID, AggregateRoot } from '@mobicoop/ddd-library';
|
||||
import { CreateUsernameProps, UsernameProps } from './username.types';
|
||||
import {
|
||||
CreateUsernameProps,
|
||||
UpdateUsernameProps,
|
||||
UsernameProps,
|
||||
} from './username.types';
|
||||
import { UsernameAddedDomainEvent } from './events/username-added.domain-event';
|
||||
import { UsernameDeletedDomainEvent } from './events/username-deleted.domain-event';
|
||||
import { UsernameUpdatedDomainEvent } from './events/username-updated.domain-event';
|
||||
|
||||
export class UsernameEntity extends AggregateRoot<UsernameProps> {
|
||||
protected readonly _id: AggregateID;
|
||||
|
@ -24,6 +29,15 @@ export class UsernameEntity extends AggregateRoot<UsernameProps> {
|
|||
return username;
|
||||
};
|
||||
|
||||
update(props: UpdateUsernameProps): void {
|
||||
this.props.name = props.name;
|
||||
this.addEvent(
|
||||
new UsernameUpdatedDomainEvent({
|
||||
aggregateId: props.name,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
this.addEvent(
|
||||
new UsernameDeletedDomainEvent({
|
||||
|
|
|
@ -10,6 +10,10 @@ export interface CreateUsernameProps {
|
|||
type: Type;
|
||||
}
|
||||
|
||||
export interface UpdateUsernameProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export enum Type {
|
||||
EMAIL = 'EMAIL',
|
||||
PHONE = 'PHONE',
|
||||
|
|
|
@ -10,6 +10,7 @@ import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
|
|||
import { UsernameEntity } from '../core/domain/username.entity';
|
||||
import { UsernameRepositoryPort } from '../core/application/ports/username.repository.port';
|
||||
import { UsernameMapper } from '../username.mapper';
|
||||
import { Type } from '../core/domain/username.types';
|
||||
|
||||
export type UsernameModel = {
|
||||
username: string;
|
||||
|
@ -46,4 +47,21 @@ export class UsernameRepository
|
|||
}),
|
||||
);
|
||||
}
|
||||
|
||||
findByType = async (userId: string, type: Type): Promise<UsernameEntity> =>
|
||||
this.findOne({
|
||||
authUuid: userId,
|
||||
type,
|
||||
});
|
||||
|
||||
updateUsername = async (
|
||||
oldName: string,
|
||||
entity: UsernameEntity,
|
||||
): Promise<void> =>
|
||||
this.updateWhere(
|
||||
{
|
||||
username: oldName,
|
||||
},
|
||||
entity,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { UsernameDto } from './username.dto';
|
||||
|
||||
export class UpdateUsernameRequestDto extends UsernameDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userId: string;
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import {
|
||||
AggregateID,
|
||||
IdResponse,
|
||||
RpcExceptionCode,
|
||||
RpcValidationPipe,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { UsernameAlreadyExistsException } from '@modules/authentication/core/domain/authentication.errors';
|
||||
import { UpdateUsernameRequestDto } from './dtos/update-username.request.dto';
|
||||
import { UpdateUsernameCommand } from '@modules/authentication/core/application/commands/update-username/update-username.command';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: true,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class UpdateUsernameGrpcController {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@GrpcMethod('AuthenticationService', 'UpdateUsername')
|
||||
async updateUsername(data: UpdateUsernameRequestDto): Promise<IdResponse> {
|
||||
try {
|
||||
const aggregateID: AggregateID = await this.commandBus.execute(
|
||||
new UpdateUsernameCommand({
|
||||
userId: data.userId,
|
||||
username: {
|
||||
name: data.name,
|
||||
type: data.type,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return new IdResponse(aggregateID);
|
||||
} catch (error: any) {
|
||||
if (error instanceof UsernameAlreadyExistsException)
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.ALREADY_EXISTS,
|
||||
message: error.message,
|
||||
});
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
import {
|
||||
AggregateID,
|
||||
ConflictException,
|
||||
UniqueConstraintException,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { USERNAME_REPOSITORY } from '@modules/authentication/authentication.di-tokens';
|
||||
import { UpdateUsernameCommand } from '@modules/authentication/core/application/commands/update-username/update-username.command';
|
||||
import { UpdateUsernameService } from '@modules/authentication/core/application/commands/update-username/update-username.service';
|
||||
import { UsernameAlreadyExistsException } from '@modules/authentication/core/domain/authentication.errors';
|
||||
import { Type } from '@modules/authentication/core/domain/username.types';
|
||||
import { UpdateUsernameRequestDto } from '@modules/authentication/interface/grpc-controllers/dtos/update-username.request.dto';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const updateUsernameRequest: UpdateUsernameRequestDto = {
|
||||
userId: '165192d4-398a-4469-a16b-98c02cc6f531',
|
||||
name: 'new-john.doe@email.com',
|
||||
type: Type.EMAIL,
|
||||
};
|
||||
|
||||
const mockUsernameRepository = {
|
||||
findByType: jest.fn().mockImplementation(() => ({
|
||||
id: 'john.doe@email.com',
|
||||
update: jest.fn(),
|
||||
getProps: jest.fn().mockImplementation(() => ({
|
||||
userId: '165192d4-398a-4469-a16b-98c02cc6f531',
|
||||
name: 'new-john.doe@email.com',
|
||||
type: Type.EMAIL,
|
||||
})),
|
||||
})),
|
||||
updateUsername: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => ({}))
|
||||
.mockImplementationOnce(() => {
|
||||
throw new ConflictException('already exists');
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new UniqueConstraintException('username');
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Update Username Service', () => {
|
||||
let updateUsernameService: UpdateUsernameService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: USERNAME_REPOSITORY,
|
||||
useValue: mockUsernameRepository,
|
||||
},
|
||||
UpdateUsernameService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
updateUsernameService = module.get<UpdateUsernameService>(
|
||||
UpdateUsernameService,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(updateUsernameService).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
const updateUsernameCommand = new UpdateUsernameCommand({
|
||||
userId: updateUsernameRequest.userId,
|
||||
username: {
|
||||
name: updateUsernameRequest.name,
|
||||
type: updateUsernameRequest.type,
|
||||
},
|
||||
});
|
||||
it('should update a username', async () => {
|
||||
const result: AggregateID = await updateUsernameService.execute(
|
||||
updateUsernameCommand,
|
||||
);
|
||||
expect(result).toBe('new-john.doe@email.com');
|
||||
});
|
||||
it('should throw a dedicated exception if username already exists for this type', async () => {
|
||||
await expect(
|
||||
updateUsernameService.execute(updateUsernameCommand),
|
||||
).rejects.toBeInstanceOf(UsernameAlreadyExistsException);
|
||||
});
|
||||
it('should throw a dedicated exception if username already exists for this name', async () => {
|
||||
await expect(
|
||||
updateUsernameService.execute(updateUsernameCommand),
|
||||
).rejects.toBeInstanceOf(UsernameAlreadyExistsException);
|
||||
});
|
||||
it('should throw an error if something bad happens', async () => {
|
||||
await expect(
|
||||
updateUsernameService.execute(updateUsernameCommand),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
import { UsernameDeletedDomainEvent } from '@modules/authentication/core/domain/events/username-deleted.domain-event';
|
||||
import { UsernameUpdatedDomainEvent } from '@modules/authentication/core/domain/events/username-updated.domain-event';
|
||||
import { UsernameEntity } from '@modules/authentication/core/domain/username.entity';
|
||||
import {
|
||||
CreateUsernameProps,
|
||||
|
@ -20,6 +21,21 @@ describe('Username entity create', () => {
|
|||
expect(usernameEntity.domainEvents.length).toBe(1);
|
||||
});
|
||||
});
|
||||
describe('Username entity update', () => {
|
||||
it('should update a username entity', async () => {
|
||||
const usernameEntity: UsernameEntity = await UsernameEntity.create(
|
||||
createUsernameProps,
|
||||
);
|
||||
usernameEntity.update({
|
||||
name: 'new-john.doe@email.com',
|
||||
});
|
||||
expect(usernameEntity.getProps().name).toBe('new-john.doe@email.com');
|
||||
expect(usernameEntity.domainEvents.length).toBe(2);
|
||||
expect(usernameEntity.domainEvents[1]).toBeInstanceOf(
|
||||
UsernameUpdatedDomainEvent,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('Username entity delete', () => {
|
||||
it('should delete a username entity', async () => {
|
||||
const usernameEntity: UsernameEntity = await UsernameEntity.create(
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
import { IdResponse } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { UsernameAlreadyExistsException } from '@modules/authentication/core/domain/authentication.errors';
|
||||
import { Type } from '@modules/authentication/core/domain/username.types';
|
||||
import { UpdateUsernameRequestDto } from '@modules/authentication/interface/grpc-controllers/dtos/update-username.request.dto';
|
||||
import { UpdateUsernameGrpcController } from '@modules/authentication/interface/grpc-controllers/update-username.grpc.controller';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const updateUsernameRequest: UpdateUsernameRequestDto = {
|
||||
userId: '78153e03-4861-4f58-a705-88526efee53b',
|
||||
name: 'new-john.doe@email.com',
|
||||
type: Type.EMAIL,
|
||||
};
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => 'new-john.doe@email.com')
|
||||
.mockImplementationOnce(() => {
|
||||
throw new UsernameAlreadyExistsException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Update Username Grpc Controller', () => {
|
||||
let updateUsernameGrpcController: UpdateUsernameGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
UpdateUsernameGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
updateUsernameGrpcController = module.get<UpdateUsernameGrpcController>(
|
||||
UpdateUsernameGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(updateUsernameGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should update a username', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
const result: IdResponse =
|
||||
await updateUsernameGrpcController.updateUsername(updateUsernameRequest);
|
||||
expect(result).toBeInstanceOf(IdResponse);
|
||||
expect(result.id).toBe('new-john.doe@email.com');
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if username already exists', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await updateUsernameGrpcController.updateUsername(updateUsernameRequest);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await updateUsernameGrpcController.updateUsername(updateUsernameRequest);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -31,12 +31,13 @@ export class UsernameMapper
|
|||
|
||||
toDomain = (record: UsernameModel): UsernameEntity => {
|
||||
const entity = new UsernameEntity({
|
||||
id: record.authUuid,
|
||||
id: record.username,
|
||||
createdAt: new Date(record.createdAt),
|
||||
updatedAt: new Date(record.updatedAt),
|
||||
props: {
|
||||
name: record.username,
|
||||
type: Type[record.type],
|
||||
userId: record.authUuid,
|
||||
},
|
||||
});
|
||||
return entity;
|
||||
|
|
Loading…
Reference in New Issue