From 2a2bbaf8e11f35bcc16ed77be27794fd837db727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Peveri?= Date: Fri, 6 Dec 2024 14:45:09 +0100 Subject: [PATCH] Add signupUserCommandHandler --- .../_dependencyInjection/commandHandlers.ts | 2 + .../auth/command/signupUserCommandHandler.ts | 35 ++++++ src/languages/domain/user/user.ts | 7 +- src/languages/domain/user/userCreatedEvent.ts | 37 +++++++ .../command/signupUserCommandHandler.test.ts | 104 ++++++++++++++++++ .../domain/user/userCreatedEventMother.ts | 10 ++ 6 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 src/languages/application/auth/command/signupUserCommandHandler.ts create mode 100644 src/languages/domain/user/userCreatedEvent.ts create mode 100644 test/unit/languages/application/auth/command/signupUserCommandHandler.test.ts create mode 100644 test/unit/languages/domain/user/userCreatedEventMother.ts diff --git a/src/languages/_dependencyInjection/commandHandlers.ts b/src/languages/_dependencyInjection/commandHandlers.ts index 5bdc90a0..ccfe71de 100644 --- a/src/languages/_dependencyInjection/commandHandlers.ts +++ b/src/languages/_dependencyInjection/commandHandlers.ts @@ -8,9 +8,11 @@ import CreateWordCommandHandler from '@src/languages/application/term/command/wo import AddLikeTermCommandHandler from '@src/languages/application/term/command/addLikeTermCommandHandler'; import DislikeTermCommandHandler from '@src/languages/application/term/command/dislikeTermCommandHandler'; import UpdateWordCommandHandler from '@src/languages/application/term/command/word/updateWordCommandHandler'; +import SignupUserCommandHandler from '@src/languages/application/auth/command/signupUserCommandHandler'; export const commands = [ LoginUserCommandHandler, + SignupUserCommandHandler, CreateCountryCommandHandler, CreateExpressionCommandHandler, CreateUserCommandHandler, diff --git a/src/languages/application/auth/command/signupUserCommandHandler.ts b/src/languages/application/auth/command/signupUserCommandHandler.ts new file mode 100644 index 00000000..5127ebad --- /dev/null +++ b/src/languages/application/auth/command/signupUserCommandHandler.ts @@ -0,0 +1,35 @@ +import { CommandHandler, ICommandHandler } from '@src/shared/domain/bus/commandBus/commandHandler'; +import SignupUserCommand from '@src/languages/application/auth/command/signupUserCommand'; +import { Inject } from '@src/shared/domain/injector/inject.decorator'; +import UserRepository, { USER_REPOSITORY } from '@src/languages/domain/user/userRepository'; +import { EVENT_BUS, EventBus } from '@src/shared/domain/bus/eventBus/eventBus'; +import UserId from '@src/languages/domain/user/userId'; +import UserAlreadyExistsException from '@src/languages/domain/user/userAlreadyExistsException'; +import User from '@src/languages/domain/user/user'; +import Email from '@src/shared/domain/valueObjects/email'; + +@CommandHandler(SignupUserCommand) +export default class SignupUserCommandHandler implements ICommandHandler { + constructor( + @Inject(USER_REPOSITORY) private readonly userRepository: UserRepository, + @Inject(EVENT_BUS) private readonly eventBus: EventBus, + ) {} + + async execute(command: SignupUserCommand): Promise { + const userId = UserId.of(command.id); + await this.guardUserDoesNotExists(userId); + + const user = User.create(userId, command.name, command.provider, Email.of(command.email), command.photo); + + this.userRepository.save(user); + + void this.eventBus.publish(user.pullDomainEvents()); + } + + private async guardUserDoesNotExists(userId: UserId): Promise { + const user = await this.userRepository.findById(userId); + if (user) { + throw new UserAlreadyExistsException(userId.value); + } + } +} diff --git a/src/languages/domain/user/user.ts b/src/languages/domain/user/user.ts index b7153924..0e62e4b7 100644 --- a/src/languages/domain/user/user.ts +++ b/src/languages/domain/user/user.ts @@ -2,6 +2,7 @@ import { AggregateRoot } from '@src/shared/domain/aggregate/aggregateRoot'; import UserId from './userId'; import Email from '@src/shared/domain/valueObjects/email'; import UserUpdatedEvent from '@src/languages/domain/user/userUpdatedEvent'; +import UserCreatedEvent from '@src/languages/domain/user/userCreatedEvent'; export type UserPrimitives = { id: string; @@ -37,7 +38,11 @@ export default class User extends AggregateRoot { } static create(id: UserId, name: string, provider: string, email: Email, photo: string): User { - return new this(id, name, provider, email, photo, []); + const user = new this(id, name, provider, email, photo, []); + + user.record(new UserCreatedEvent(id.toString(), name, provider, email.toString(), photo)); + + return user; } update(name: string, photo: string, interests: string[]): void { diff --git a/src/languages/domain/user/userCreatedEvent.ts b/src/languages/domain/user/userCreatedEvent.ts new file mode 100644 index 00000000..c49232f1 --- /dev/null +++ b/src/languages/domain/user/userCreatedEvent.ts @@ -0,0 +1,37 @@ +import { DomainEvent } from '@src/shared/domain/bus/eventBus/domainEvent'; + +export default class UserCreatedEvent extends DomainEvent { + constructor( + public readonly id: string, + public readonly name: string, + public readonly provider: string, + public readonly email: string, + public readonly photo: string, + eventId = '', + ) { + super(id, eventId); + } + + public static fromPrimitives(payload: { [key: string]: any }): DomainEvent { + return new this( + payload['id'], + payload['name'], + payload['provider'], + payload['email'], + payload['photo'], + payload['eventId'], + ); + } + + public static eventTypeName(): string { + return 'user.created'; + } + + public classPathName(): string { + return 'languages.domain.user.userCreatedEvent'; + } + + public static aggregateTypeName(): string { + return 'user'; + } +} diff --git a/test/unit/languages/application/auth/command/signupUserCommandHandler.test.ts b/test/unit/languages/application/auth/command/signupUserCommandHandler.test.ts new file mode 100644 index 00000000..01cbc354 --- /dev/null +++ b/test/unit/languages/application/auth/command/signupUserCommandHandler.test.ts @@ -0,0 +1,104 @@ +import { beforeEach, beforeAll, describe, expect, it, jest } from '@jest/globals'; +import { EventBusMock } from '@test/unit/shared/domain/buses/eventBus/eventBusMock'; +import SignupUserCommandHandler from '@src/languages/application/auth/command/signupUserCommandHandler'; +import { SignupUserCommandMother } from '@test/unit/languages/application/auth/command/signupUserCommandMother'; +import SignupUserCommand from '@src/languages/application/auth/command/signupUserCommand'; +import { UserRepositoryMock } from '@test/unit/languages/domain/user/userRepositoryMock'; +import { UserMother } from '@test/unit/languages/domain/user/userMother'; +import UserAlreadyExistsException from '@src/languages/domain/user/userAlreadyExistsException'; +import { UserIdMother } from '@test/unit/languages/domain/user/userIdMother'; +import UserCreatedEvent from '@src/languages/domain/user/userCreatedEvent'; +import { UserCreatedEventMother } from '@test/unit/languages/domain/user/userCreatedEventMother'; + +describe('Given a SignupUserCommandHandler to handle', () => { + let userRepository: UserRepositoryMock; + let eventBus: EventBusMock; + let handler: SignupUserCommandHandler; + + const prepareDependencies = () => { + userRepository = new UserRepositoryMock(); + eventBus = new EventBusMock(); + }; + + const initHandler = () => { + handler = new SignupUserCommandHandler(userRepository, eventBus); + + jest.useFakeTimers(); + }; + + const clean = () => { + userRepository.clean(); + eventBus.clean(); + }; + + beforeAll(() => { + prepareDependencies(); + initHandler(); + }); + + beforeEach(() => { + clean(); + }); + + describe('When the user already exists', () => { + let command: SignupUserCommand; + + function startScenario() { + command = SignupUserCommandMother.random(); + userRepository.add(UserMother.random({ id: UserIdMother.random(command.id) })); + } + + beforeEach(startScenario); + + it('should raise an exception', async () => { + await expect(handler.execute(command)).rejects.toThrowError(UserAlreadyExistsException); + + expect(userRepository.storedChanged()).toBeFalsy(); + expect(userRepository.stored()).toHaveLength(0); + }); + + it('should not publish any events', async () => { + await expect(handler.execute(command)).rejects.toThrowError(UserAlreadyExistsException); + + expect(eventBus.domainEvents()).toHaveLength(0); + }); + }); + + describe('When the user does not exists', () => { + let data: { id: string; email: string; provider: string; photo: string; name: string }; + let command: SignupUserCommand; + + function startScenario() { + data = { + id: '4a4df157-8ab8-50af-bb39-88e8ce29eb16', + email: 'test@test.com', + provider: 'google', + photo: '', + name: 'test', + }; + command = SignupUserCommandMother.random(data); + } + + beforeEach(startScenario); + + it('should create the user', async () => { + await handler.execute(command); + + expect(userRepository.storedChanged()).toBeTruthy(); + expect(userRepository.stored()).toHaveLength(1); + const user = userRepository.stored()[0]; + expect(user.toPrimitives()).toEqual({ ...data, interests: [] }); + }); + + it('should publish the events', async () => { + const event = UserCreatedEventMother.createFromSignupUserCommand(command); + await handler.execute(command); + + expect(eventBus.domainEvents()).toHaveLength(1); + expect(eventBus.domainEvents()[0]).toBeInstanceOf(UserCreatedEvent); + expect(eventBus.domainEvents()[0]).toEqual({ + ...event, + }); + }); + }); +}); diff --git a/test/unit/languages/domain/user/userCreatedEventMother.ts b/test/unit/languages/domain/user/userCreatedEventMother.ts new file mode 100644 index 00000000..dc9a1052 --- /dev/null +++ b/test/unit/languages/domain/user/userCreatedEventMother.ts @@ -0,0 +1,10 @@ +import { expect } from '@jest/globals'; +import SignupUserCommand from '@src/languages/application/auth/command/signupUserCommand'; +import UserCreatedEvent from '@src/languages/domain/user/userCreatedEvent'; + +export class UserCreatedEventMother { + static createFromSignupUserCommand(command: SignupUserCommand): UserCreatedEvent { + const eventId = expect.any(String) as unknown as string; + return new UserCreatedEvent(command.id, command.name, command.provider, command.email, command.photo, eventId); + } +}