From 52fd0b952bea05181e2c29db2c642d67e7f76d3c Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 21 Jul 2023 14:22:13 +0200 Subject: [PATCH] user update --- .../adapters/primaries/user.controller.ts | 0 .../adapters/primaries/user.presenter.ts | 0 .../adapters/primaries/user.proto | 0 .../adapters/secondaries/message-publisher.ts | 0 .../adapters/secondaries/users.repository.ts | 0 .../commands/create-user.command.ts | 0 .../commands/delete-user.command.ts | 0 .../commands/update-user.command.ts | 0 .../domain/dtos/create-user.request.ts | 0 .../domain/dtos/find-all-users.request.ts | 0 .../domain/dtos/find-user-by-uuid.request.ts | 0 .../domain/dtos/update-user.request.ts | 0 .../user/old => .old}/domain/entities/user.ts | 0 .../domain/usecases/create-user.usecase.ts | 0 .../domain/usecases/delete-user.usecase.ts | 0 .../domain/usecases/find-all-users.usecase.ts | 0 .../usecases/find-user-by-uuid.usecase.ts | 0 .../domain/usecases/update-user.usecase.ts | 0 .../user/old => .old}/mappers/user.profile.ts | 0 .../queries/find-all-users.query.ts | 0 .../queries/find-user-by-uuid.query.ts | 0 .../tests/unit/find-all-users.usecase.spec.ts | 0 .../unit/find-user-by-uuid.usecase.spec.ts | 0 package-lock.json | 24 +++- package.json | 1 + src/app.module.ts | 2 + .../create-user/create-user.service.ts | 24 +++- .../update-user/update-user.service.ts | 10 +- src/modules/user/core/domain/user.entity.ts | 8 +- src/modules/user/core/domain/user.types.ts | 24 ++-- .../create-user.grpc.controller.ts | 12 +- .../dtos/create-user.request.dto.ts | 0 .../dtos/update-user.request.dto.ts | 0 .../update-user.grpc.controller.ts | 0 .../{dtos => }/grpc-controllers/user.proto | 14 +- .../unit/core/create-user.service.spec.ts | 34 ++++- .../unit/core/update-user.service.spec.ts | 126 ++++++++++++++++++ .../create-user.grpc.controller.spec.ts | 40 +++++- .../update-user.grpc.controller.spec.ts | 4 +- src/modules/user/user.module.ts | 103 +++++++------- 40 files changed, 328 insertions(+), 98 deletions(-) rename {src/modules/user/old => .old}/adapters/primaries/user.controller.ts (100%) rename {src/modules/user/old => .old}/adapters/primaries/user.presenter.ts (100%) rename {src/modules/user/old => .old}/adapters/primaries/user.proto (100%) rename {src/modules/user/old => .old}/adapters/secondaries/message-publisher.ts (100%) rename {src/modules/user/old => .old}/adapters/secondaries/users.repository.ts (100%) rename {src/modules/user/old => .old}/commands/create-user.command.ts (100%) rename {src/modules/user/old => .old}/commands/delete-user.command.ts (100%) rename {src/modules/user/old => .old}/commands/update-user.command.ts (100%) rename {src/modules/user/old => .old}/domain/dtos/create-user.request.ts (100%) rename {src/modules/user/old => .old}/domain/dtos/find-all-users.request.ts (100%) rename {src/modules/user/old => .old}/domain/dtos/find-user-by-uuid.request.ts (100%) rename {src/modules/user/old => .old}/domain/dtos/update-user.request.ts (100%) rename {src/modules/user/old => .old}/domain/entities/user.ts (100%) rename {src/modules/user/old => .old}/domain/usecases/create-user.usecase.ts (100%) rename {src/modules/user/old => .old}/domain/usecases/delete-user.usecase.ts (100%) rename {src/modules/user/old => .old}/domain/usecases/find-all-users.usecase.ts (100%) rename {src/modules/user/old => .old}/domain/usecases/find-user-by-uuid.usecase.ts (100%) rename {src/modules/user/old => .old}/domain/usecases/update-user.usecase.ts (100%) rename {src/modules/user/old => .old}/mappers/user.profile.ts (100%) rename {src/modules/user/old => .old}/queries/find-all-users.query.ts (100%) rename {src/modules/user/old => .old}/queries/find-user-by-uuid.query.ts (100%) rename {src/modules/user/old => .old}/tests/unit/find-all-users.usecase.spec.ts (100%) rename {src/modules/user/old => .old}/tests/unit/find-user-by-uuid.usecase.spec.ts (100%) rename src/modules/user/interface/{dtos => }/grpc-controllers/create-user.grpc.controller.ts (80%) rename src/modules/user/interface/{dtos => }/grpc-controllers/dtos/create-user.request.dto.ts (100%) rename src/modules/user/interface/{dtos => }/grpc-controllers/dtos/update-user.request.dto.ts (100%) rename src/modules/user/interface/{dtos => }/grpc-controllers/update-user.grpc.controller.ts (100%) rename src/modules/user/interface/{dtos => }/grpc-controllers/user.proto (63%) create mode 100644 src/modules/user/tests/unit/core/update-user.service.spec.ts diff --git a/src/modules/user/old/adapters/primaries/user.controller.ts b/.old/adapters/primaries/user.controller.ts similarity index 100% rename from src/modules/user/old/adapters/primaries/user.controller.ts rename to .old/adapters/primaries/user.controller.ts diff --git a/src/modules/user/old/adapters/primaries/user.presenter.ts b/.old/adapters/primaries/user.presenter.ts similarity index 100% rename from src/modules/user/old/adapters/primaries/user.presenter.ts rename to .old/adapters/primaries/user.presenter.ts diff --git a/src/modules/user/old/adapters/primaries/user.proto b/.old/adapters/primaries/user.proto similarity index 100% rename from src/modules/user/old/adapters/primaries/user.proto rename to .old/adapters/primaries/user.proto diff --git a/src/modules/user/old/adapters/secondaries/message-publisher.ts b/.old/adapters/secondaries/message-publisher.ts similarity index 100% rename from src/modules/user/old/adapters/secondaries/message-publisher.ts rename to .old/adapters/secondaries/message-publisher.ts diff --git a/src/modules/user/old/adapters/secondaries/users.repository.ts b/.old/adapters/secondaries/users.repository.ts similarity index 100% rename from src/modules/user/old/adapters/secondaries/users.repository.ts rename to .old/adapters/secondaries/users.repository.ts diff --git a/src/modules/user/old/commands/create-user.command.ts b/.old/commands/create-user.command.ts similarity index 100% rename from src/modules/user/old/commands/create-user.command.ts rename to .old/commands/create-user.command.ts diff --git a/src/modules/user/old/commands/delete-user.command.ts b/.old/commands/delete-user.command.ts similarity index 100% rename from src/modules/user/old/commands/delete-user.command.ts rename to .old/commands/delete-user.command.ts diff --git a/src/modules/user/old/commands/update-user.command.ts b/.old/commands/update-user.command.ts similarity index 100% rename from src/modules/user/old/commands/update-user.command.ts rename to .old/commands/update-user.command.ts diff --git a/src/modules/user/old/domain/dtos/create-user.request.ts b/.old/domain/dtos/create-user.request.ts similarity index 100% rename from src/modules/user/old/domain/dtos/create-user.request.ts rename to .old/domain/dtos/create-user.request.ts diff --git a/src/modules/user/old/domain/dtos/find-all-users.request.ts b/.old/domain/dtos/find-all-users.request.ts similarity index 100% rename from src/modules/user/old/domain/dtos/find-all-users.request.ts rename to .old/domain/dtos/find-all-users.request.ts diff --git a/src/modules/user/old/domain/dtos/find-user-by-uuid.request.ts b/.old/domain/dtos/find-user-by-uuid.request.ts similarity index 100% rename from src/modules/user/old/domain/dtos/find-user-by-uuid.request.ts rename to .old/domain/dtos/find-user-by-uuid.request.ts diff --git a/src/modules/user/old/domain/dtos/update-user.request.ts b/.old/domain/dtos/update-user.request.ts similarity index 100% rename from src/modules/user/old/domain/dtos/update-user.request.ts rename to .old/domain/dtos/update-user.request.ts diff --git a/src/modules/user/old/domain/entities/user.ts b/.old/domain/entities/user.ts similarity index 100% rename from src/modules/user/old/domain/entities/user.ts rename to .old/domain/entities/user.ts diff --git a/src/modules/user/old/domain/usecases/create-user.usecase.ts b/.old/domain/usecases/create-user.usecase.ts similarity index 100% rename from src/modules/user/old/domain/usecases/create-user.usecase.ts rename to .old/domain/usecases/create-user.usecase.ts diff --git a/src/modules/user/old/domain/usecases/delete-user.usecase.ts b/.old/domain/usecases/delete-user.usecase.ts similarity index 100% rename from src/modules/user/old/domain/usecases/delete-user.usecase.ts rename to .old/domain/usecases/delete-user.usecase.ts diff --git a/src/modules/user/old/domain/usecases/find-all-users.usecase.ts b/.old/domain/usecases/find-all-users.usecase.ts similarity index 100% rename from src/modules/user/old/domain/usecases/find-all-users.usecase.ts rename to .old/domain/usecases/find-all-users.usecase.ts diff --git a/src/modules/user/old/domain/usecases/find-user-by-uuid.usecase.ts b/.old/domain/usecases/find-user-by-uuid.usecase.ts similarity index 100% rename from src/modules/user/old/domain/usecases/find-user-by-uuid.usecase.ts rename to .old/domain/usecases/find-user-by-uuid.usecase.ts diff --git a/src/modules/user/old/domain/usecases/update-user.usecase.ts b/.old/domain/usecases/update-user.usecase.ts similarity index 100% rename from src/modules/user/old/domain/usecases/update-user.usecase.ts rename to .old/domain/usecases/update-user.usecase.ts diff --git a/src/modules/user/old/mappers/user.profile.ts b/.old/mappers/user.profile.ts similarity index 100% rename from src/modules/user/old/mappers/user.profile.ts rename to .old/mappers/user.profile.ts diff --git a/src/modules/user/old/queries/find-all-users.query.ts b/.old/queries/find-all-users.query.ts similarity index 100% rename from src/modules/user/old/queries/find-all-users.query.ts rename to .old/queries/find-all-users.query.ts diff --git a/src/modules/user/old/queries/find-user-by-uuid.query.ts b/.old/queries/find-user-by-uuid.query.ts similarity index 100% rename from src/modules/user/old/queries/find-user-by-uuid.query.ts rename to .old/queries/find-user-by-uuid.query.ts diff --git a/src/modules/user/old/tests/unit/find-all-users.usecase.spec.ts b/.old/tests/unit/find-all-users.usecase.spec.ts similarity index 100% rename from src/modules/user/old/tests/unit/find-all-users.usecase.spec.ts rename to .old/tests/unit/find-all-users.usecase.spec.ts diff --git a/src/modules/user/old/tests/unit/find-user-by-uuid.usecase.spec.ts b/.old/tests/unit/find-user-by-uuid.usecase.spec.ts similarity index 100% rename from src/modules/user/old/tests/unit/find-user-by-uuid.usecase.spec.ts rename to .old/tests/unit/find-user-by-uuid.usecase.spec.ts diff --git a/package-lock.json b/package-lock.json index 604bacb..d9cf986 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", "@nestjs/cqrs": "^9.0.1", + "@nestjs/event-emitter": "^2.0.0", "@nestjs/microservices": "^9.2.1", "@nestjs/platform-express": "^9.0.0", "@nestjs/terminus": "^9.2.2", @@ -1937,6 +1938,19 @@ "@nestjs/common": "^9.4.2" } }, + "node_modules/@mobicoop/ddd-library/node_modules/@nestjs/event-emitter": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-1.4.2.tgz", + "integrity": "sha512-5mskPMS4KVH6LghC+NynfdmGiMCOOv9CdgVpuWGipLrJECv5KWc7vaW5o/9BYrcqPkN7Ted6CJ+O4AfsTiRlgw==", + "dependencies": { + "eventemitter2": "6.4.9" + }, + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0", + "reflect-metadata": "^0.1.12" + } + }, "node_modules/@mobicoop/health-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@mobicoop/health-module/-/health-module-2.0.0.tgz", @@ -2206,15 +2220,15 @@ } }, "node_modules/@nestjs/event-emitter": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-1.4.2.tgz", - "integrity": "sha512-5mskPMS4KVH6LghC+NynfdmGiMCOOv9CdgVpuWGipLrJECv5KWc7vaW5o/9BYrcqPkN7Ted6CJ+O4AfsTiRlgw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.0.0.tgz", + "integrity": "sha512-fZRv3+PmqXcbqCDRXRWhKDa+v3gmPUq4x5sQE5reVlDtEaCoAXwtGrtNswPtqd0msjyo8OWZF9k1sFjeRL6Xag==", "dependencies": { "eventemitter2": "6.4.9" }, "peerDependencies": { - "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0", - "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0", + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", "reflect-metadata": "^0.1.12" } }, diff --git a/package.json b/package.json index 5099135..b6893b0 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", "@nestjs/cqrs": "^9.0.1", + "@nestjs/event-emitter": "^2.0.0", "@nestjs/microservices": "^9.2.1", "@nestjs/platform-express": "^9.0.0", "@nestjs/terminus": "^9.2.2", diff --git a/src/app.module.ts b/src/app.module.ts index 9e7c1bc..5ecd4ee 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,10 +14,12 @@ import { MessagerModule } from './modules/messager/messager.module'; import { USER_REPOSITORY } from './modules/user/user.di-tokens'; import { MESSAGE_PUBLISHER } from './modules/messager/messager.di-tokens'; import { MessagePublisherPort } from '@mobicoop/ddd-library'; +import { EventEmitterModule } from '@nestjs/event-emitter'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), + EventEmitterModule.forRoot(), ConfigurationModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], diff --git a/src/modules/user/core/application/commands/create-user/create-user.service.ts b/src/modules/user/core/application/commands/create-user/create-user.service.ts index 077ca82..e564340 100644 --- a/src/modules/user/core/application/commands/create-user/create-user.service.ts +++ b/src/modules/user/core/application/commands/create-user/create-user.service.ts @@ -1,11 +1,19 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { Inject } from '@nestjs/common'; -import { AggregateID, ConflictException } from '@mobicoop/ddd-library'; +import { + AggregateID, + ConflictException, + UniqueConstraintException, +} from '@mobicoop/ddd-library'; import { CreateUserCommand } from './create-user.command'; import { USER_REPOSITORY } from '@modules/user/user.di-tokens'; import { UserRepositoryPort } from '../../ports/user.repository.port'; import { UserEntity } from '../../../domain/user.entity'; -import { UserAlreadyExistsException } from '../../../domain/user.errors'; +import { + EmailAlreadyExistsException, + PhoneAlreadyExistsException, + UserAlreadyExistsException, +} from '../../../domain/user.errors'; @CommandHandler(CreateUserCommand) export class CreateUserService implements ICommandHandler { @@ -29,6 +37,18 @@ export class CreateUserService implements ICommandHandler { if (error instanceof ConflictException) { throw new UserAlreadyExistsException(error); } + if ( + error instanceof UniqueConstraintException && + error.message.includes('email') + ) { + throw new EmailAlreadyExistsException(error); + } + if ( + error instanceof UniqueConstraintException && + error.message.includes('phone') + ) { + throw new PhoneAlreadyExistsException(error); + } throw error; } } diff --git a/src/modules/user/core/application/commands/update-user/update-user.service.ts b/src/modules/user/core/application/commands/update-user/update-user.service.ts index 78d6241..10096cf 100644 --- a/src/modules/user/core/application/commands/update-user/update-user.service.ts +++ b/src/modules/user/core/application/commands/update-user/update-user.service.ts @@ -1,10 +1,6 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { Inject } from '@nestjs/common'; -import { - AggregateID, - ConflictException, - UniqueConstraintException, -} from '@mobicoop/ddd-library'; +import { AggregateID, UniqueConstraintException } from '@mobicoop/ddd-library'; import { USER_REPOSITORY } from '@modules/user/user.di-tokens'; import { UserRepositoryPort } from '../../ports/user.repository.port'; import { UserEntity } from '../../../domain/user.entity'; @@ -12,7 +8,6 @@ import { UpdateUserCommand } from './update-user.command'; import { EmailAlreadyExistsException, PhoneAlreadyExistsException, - UserAlreadyExistsException, } from '@modules/user/core/domain/user.errors'; @CommandHandler(UpdateUserCommand) @@ -36,9 +31,6 @@ export class UpdateUserService implements ICommandHandler { await this.userRepository.update(user.id, user); return user.id; } catch (error: any) { - if (error instanceof ConflictException) { - throw new UserAlreadyExistsException(error); - } if ( error instanceof UniqueConstraintException && error.message.includes('email') diff --git a/src/modules/user/core/domain/user.entity.ts b/src/modules/user/core/domain/user.entity.ts index 8ec3605..0bb810f 100644 --- a/src/modules/user/core/domain/user.entity.ts +++ b/src/modules/user/core/domain/user.entity.ts @@ -25,10 +25,10 @@ export class UserEntity extends AggregateRoot { }; update(props: UpdateUserProps): void { - this.props.firstName = props.firstName; - this.props.lastName = props.lastName; - this.props.email = props.email; - this.props.phone = props.phone; + this.props.firstName = props.firstName ?? this.props.firstName; + this.props.lastName = props.lastName ?? this.props.lastName; + this.props.email = props.email ?? this.props.email; + this.props.phone = props.phone ?? this.props.phone; this.addEvent( new UserUpdatedDomainEvent({ aggregateId: this._id, diff --git a/src/modules/user/core/domain/user.types.ts b/src/modules/user/core/domain/user.types.ts index d2c8782..1cf4f95 100644 --- a/src/modules/user/core/domain/user.types.ts +++ b/src/modules/user/core/domain/user.types.ts @@ -1,22 +1,22 @@ // All properties that a User has export interface UserProps { - firstName: string; - lastName: string; - email: string; - phone: string; + firstName?: string; + lastName?: string; + email?: string; + phone?: string; } // Properties that are needed for a User creation export interface CreateUserProps { - firstName: string; - lastName: string; - email: string; - phone: string; + firstName?: string; + lastName?: string; + email?: string; + phone?: string; } export interface UpdateUserProps { - firstName: string; - lastName: string; - email: string; - phone: string; + firstName?: string; + lastName?: string; + email?: string; + phone?: string; } diff --git a/src/modules/user/interface/dtos/grpc-controllers/create-user.grpc.controller.ts b/src/modules/user/interface/grpc-controllers/create-user.grpc.controller.ts similarity index 80% rename from src/modules/user/interface/dtos/grpc-controllers/create-user.grpc.controller.ts rename to src/modules/user/interface/grpc-controllers/create-user.grpc.controller.ts index e234c5b..15ce502 100644 --- a/src/modules/user/interface/dtos/grpc-controllers/create-user.grpc.controller.ts +++ b/src/modules/user/interface/grpc-controllers/create-user.grpc.controller.ts @@ -7,7 +7,11 @@ import { RpcExceptionCode } from '@mobicoop/ddd-library'; import { RpcValidationPipe } from '@mobicoop/ddd-library'; import { CreateUserRequestDto } from './dtos/create-user.request.dto'; import { CreateUserCommand } from '@modules/user/core/application/commands/create-user/create-user.command'; -import { UserAlreadyExistsException } from '@modules/user/core/domain/user.errors'; +import { + EmailAlreadyExistsException, + PhoneAlreadyExistsException, + UserAlreadyExistsException, +} from '@modules/user/core/domain/user.errors'; @UsePipes( new RpcValidationPipe({ @@ -27,7 +31,11 @@ export class CreateUserGrpcController { ); return new IdResponse(aggregateID); } catch (error: any) { - if (error instanceof UserAlreadyExistsException) + if ( + error instanceof UserAlreadyExistsException || + error instanceof EmailAlreadyExistsException || + error instanceof PhoneAlreadyExistsException + ) throw new RpcException({ code: RpcExceptionCode.ALREADY_EXISTS, message: error.message, diff --git a/src/modules/user/interface/dtos/grpc-controllers/dtos/create-user.request.dto.ts b/src/modules/user/interface/grpc-controllers/dtos/create-user.request.dto.ts similarity index 100% rename from src/modules/user/interface/dtos/grpc-controllers/dtos/create-user.request.dto.ts rename to src/modules/user/interface/grpc-controllers/dtos/create-user.request.dto.ts diff --git a/src/modules/user/interface/dtos/grpc-controllers/dtos/update-user.request.dto.ts b/src/modules/user/interface/grpc-controllers/dtos/update-user.request.dto.ts similarity index 100% rename from src/modules/user/interface/dtos/grpc-controllers/dtos/update-user.request.dto.ts rename to src/modules/user/interface/grpc-controllers/dtos/update-user.request.dto.ts diff --git a/src/modules/user/interface/dtos/grpc-controllers/update-user.grpc.controller.ts b/src/modules/user/interface/grpc-controllers/update-user.grpc.controller.ts similarity index 100% rename from src/modules/user/interface/dtos/grpc-controllers/update-user.grpc.controller.ts rename to src/modules/user/interface/grpc-controllers/update-user.grpc.controller.ts diff --git a/src/modules/user/interface/dtos/grpc-controllers/user.proto b/src/modules/user/interface/grpc-controllers/user.proto similarity index 63% rename from src/modules/user/interface/dtos/grpc-controllers/user.proto rename to src/modules/user/interface/grpc-controllers/user.proto index cde99ea..075c6e7 100644 --- a/src/modules/user/interface/dtos/grpc-controllers/user.proto +++ b/src/modules/user/interface/grpc-controllers/user.proto @@ -3,19 +3,19 @@ syntax = "proto3"; package user; service UserService { - rpc FindOneByUuid(UserByUuid) returns (User); + rpc FindOneById(UserById) returns (User); rpc FindAll(UserFilter) returns (Users); - rpc Create(User) returns (User); - rpc Update(User) returns (User); - rpc Delete(UserByUuid) returns (Empty); + rpc Create(User) returns (UserById); + rpc Update(User) returns (UserById); + rpc Delete(UserById) returns (Empty); } -message UserByUuid { - string uuid = 1; +message UserById { + string id = 1; } message User { - string uuid = 1; + string id = 1; string firstName = 2; string lastName = 3; string email = 4; diff --git a/src/modules/user/tests/unit/core/create-user.service.spec.ts b/src/modules/user/tests/unit/core/create-user.service.spec.ts index 32bb093..fbd1c60 100644 --- a/src/modules/user/tests/unit/core/create-user.service.spec.ts +++ b/src/modules/user/tests/unit/core/create-user.service.spec.ts @@ -1,12 +1,16 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { AggregateID } from '@mobicoop/ddd-library'; +import { AggregateID, UniqueConstraintException } from '@mobicoop/ddd-library'; import { ConflictException } from '@mobicoop/ddd-library'; -import { CreateUserRequestDto } from '@modules/user/interface/dtos/grpc-controllers/dtos/create-user.request.dto'; +import { CreateUserRequestDto } from '@modules/user/interface/grpc-controllers/dtos/create-user.request.dto'; import { CreateUserService } from '@modules/user/core/application/commands/create-user/create-user.service'; import { USER_REPOSITORY } from '@modules/user/user.di-tokens'; import { UserEntity } from '@modules/user/core/domain/user.entity'; import { CreateUserCommand } from '@modules/user/core/application/commands/create-user/create-user.command'; -import { UserAlreadyExistsException } from '@modules/user/core/domain/user.errors'; +import { + EmailAlreadyExistsException, + PhoneAlreadyExistsException, + UserAlreadyExistsException, +} from '@modules/user/core/domain/user.errors'; const createUserRequest: CreateUserRequestDto = { firstName: 'John', @@ -23,7 +27,13 @@ const mockUserRepository = { throw new Error(); }) .mockImplementationOnce(() => { - throw new ConflictException('already exists'); + throw new ConflictException('User already exists'); + }) + .mockImplementationOnce(() => { + throw new UniqueConstraintException('email already exists'); + }) + .mockImplementationOnce(() => { + throw new UniqueConstraintException('phone already exists'); }), }; @@ -75,5 +85,21 @@ describe('create-user.service', () => { createUserService.execute(createUserCommand), ).rejects.toBeInstanceOf(UserAlreadyExistsException); }); + it('should throw an exception if Email already exists', async () => { + UserEntity.create = jest.fn().mockReturnValue({ + id: '047a6ecf-23d4-4d3e-877c-3225d560a8da', + }); + await expect( + createUserService.execute(createUserCommand), + ).rejects.toBeInstanceOf(EmailAlreadyExistsException); + }); + it('should throw an exception if Phone already exists', async () => { + UserEntity.create = jest.fn().mockReturnValue({ + id: '047a6ecf-23d4-4d3e-877c-3225d560a8da', + }); + await expect( + createUserService.execute(createUserCommand), + ).rejects.toBeInstanceOf(PhoneAlreadyExistsException); + }); }); }); diff --git a/src/modules/user/tests/unit/core/update-user.service.spec.ts b/src/modules/user/tests/unit/core/update-user.service.spec.ts new file mode 100644 index 0000000..797f850 --- /dev/null +++ b/src/modules/user/tests/unit/core/update-user.service.spec.ts @@ -0,0 +1,126 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { + AggregateID, + NotFoundException, + UniqueConstraintException, +} from '@mobicoop/ddd-library'; +import { USER_REPOSITORY } from '@modules/user/user.di-tokens'; +import { UserEntity } from '@modules/user/core/domain/user.entity'; +import { + EmailAlreadyExistsException, + PhoneAlreadyExistsException, +} from '@modules/user/core/domain/user.errors'; +import { UpdateUserRequestDto } from '@modules/user/interface/grpc-controllers/dtos/update-user.request.dto'; +import { UpdateUserService } from '@modules/user/core/application/commands/update-user/update-user.service'; +import { UpdateUserCommand } from '@modules/user/core/application/commands/update-user/update-user.command'; + +const updateFirstNameUserRequest: UpdateUserRequestDto = { + id: 'c97b1783-76cf-4840-b298-b90b13c58894', + firstName: 'Johnny', +}; + +const updateEmailUserRequest: UpdateUserRequestDto = { + id: 'c97b1783-76cf-4840-b298-b90b13c58894', + email: 'john.doe@already.exists.email.com', +}; + +const updatePhoneUserRequest: UpdateUserRequestDto = { + id: 'c97b1783-76cf-4840-b298-b90b13c58894', + phone: '+33611223344', +}; + +const now = new Date(); +const userToUpdate: UserEntity = new UserEntity({ + id: 'c97b1783-76cf-4840-b298-b90b13c58894', + createdAt: now, + updatedAt: now, + props: { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@email.com', + phone: '+33611223344', + }, +}); + +const mockUserRepository = { + findOneById: jest + .fn() + .mockImplementationOnce(() => { + throw new NotFoundException('Record not found'); + }) + .mockImplementation(() => userToUpdate), + update: jest + .fn() + .mockImplementationOnce(() => 'c97b1783-76cf-4840-b298-b90b13c58894') + .mockImplementationOnce(() => { + throw new UniqueConstraintException('email already exists'); + }) + .mockImplementationOnce(() => { + throw new UniqueConstraintException('phone already exists'); + }) + .mockImplementationOnce(() => { + throw new Error(); + }), +}; + +describe('update-user.service', () => { + let updateUserService: UpdateUserService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: USER_REPOSITORY, + useValue: mockUserRepository, + }, + UpdateUserService, + ], + }).compile(); + + updateUserService = module.get(UpdateUserService); + }); + + it('should be defined', () => { + expect(updateUserService).toBeDefined(); + }); + + describe('execution', () => { + it('should throw an exception if use is not found', async () => { + const updateUserCommand = new UpdateUserCommand( + updateFirstNameUserRequest, + ); + await expect( + updateUserService.execute(updateUserCommand), + ).rejects.toBeInstanceOf(NotFoundException); + }); + it('should update a user firstName', async () => { + jest.spyOn(userToUpdate, 'update'); + const updateUserCommand = new UpdateUserCommand( + updateFirstNameUserRequest, + ); + const result: AggregateID = await updateUserService.execute( + updateUserCommand, + ); + expect(result).toBe('c97b1783-76cf-4840-b298-b90b13c58894'); + expect(userToUpdate.update).toHaveBeenCalledTimes(1); + }); + it('should throw an exception if Email already exists', async () => { + const updateUserCommand = new UpdateUserCommand(updateEmailUserRequest); + await expect( + updateUserService.execute(updateUserCommand), + ).rejects.toBeInstanceOf(EmailAlreadyExistsException); + }); + it('should throw an exception if Phone already exists', async () => { + const updateUserCommand = new UpdateUserCommand(updatePhoneUserRequest); + await expect( + updateUserService.execute(updateUserCommand), + ).rejects.toBeInstanceOf(PhoneAlreadyExistsException); + }); + it('should throw an error if something bad happens', async () => { + const updateUserCommand = new UpdateUserCommand(updatePhoneUserRequest); + await expect( + updateUserService.execute(updateUserCommand), + ).rejects.toBeInstanceOf(Error); + }); + }); +}); diff --git a/src/modules/user/tests/unit/interface/create-user.grpc.controller.spec.ts b/src/modules/user/tests/unit/interface/create-user.grpc.controller.spec.ts index 3d7acc0..b2df2f5 100644 --- a/src/modules/user/tests/unit/interface/create-user.grpc.controller.spec.ts +++ b/src/modules/user/tests/unit/interface/create-user.grpc.controller.spec.ts @@ -3,9 +3,13 @@ import { RpcExceptionCode } from '@mobicoop/ddd-library'; import { CommandBus } from '@nestjs/cqrs'; import { RpcException } from '@nestjs/microservices'; import { Test, TestingModule } from '@nestjs/testing'; -import { UserAlreadyExistsException } from '@modules/user/core/domain/user.errors'; -import { CreateUserGrpcController } from '@modules/user/interface/dtos/grpc-controllers/create-user.grpc.controller'; -import { CreateUserRequestDto } from '@modules/user/interface/dtos/grpc-controllers/dtos/create-user.request.dto'; +import { + EmailAlreadyExistsException, + PhoneAlreadyExistsException, + UserAlreadyExistsException, +} from '@modules/user/core/domain/user.errors'; +import { CreateUserGrpcController } from '@modules/user/interface/grpc-controllers/create-user.grpc.controller'; +import { CreateUserRequestDto } from '@modules/user/interface/grpc-controllers/dtos/create-user.request.dto'; const createUserRequest: CreateUserRequestDto = { firstName: 'John', @@ -21,6 +25,12 @@ const mockCommandBus = { .mockImplementationOnce(() => { throw new UserAlreadyExistsException(); }) + .mockImplementationOnce(() => { + throw new EmailAlreadyExistsException(); + }) + .mockImplementationOnce(() => { + throw new PhoneAlreadyExistsException(); + }) .mockImplementationOnce(() => { throw new Error(); }), @@ -75,6 +85,30 @@ describe('Create User Grpc Controller', () => { expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); }); + it('should throw a dedicated RpcException if email already exists', async () => { + jest.spyOn(mockCommandBus, 'execute'); + expect.assertions(3); + try { + await createUserGrpcController.create(createUserRequest); + } catch (e: any) { + expect(e).toBeInstanceOf(RpcException); + expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS); + } + expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); + }); + + it('should throw a dedicated RpcException if phone already exists', async () => { + jest.spyOn(mockCommandBus, 'execute'); + expect.assertions(3); + try { + await createUserGrpcController.create(createUserRequest); + } 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); diff --git a/src/modules/user/tests/unit/interface/update-user.grpc.controller.spec.ts b/src/modules/user/tests/unit/interface/update-user.grpc.controller.spec.ts index 4986616..637a926 100644 --- a/src/modules/user/tests/unit/interface/update-user.grpc.controller.spec.ts +++ b/src/modules/user/tests/unit/interface/update-user.grpc.controller.spec.ts @@ -4,8 +4,8 @@ import { EmailAlreadyExistsException, PhoneAlreadyExistsException, } from '@modules/user/core/domain/user.errors'; -import { UpdateUserRequestDto } from '@modules/user/interface/dtos/grpc-controllers/dtos/update-user.request.dto'; -import { UpdateUserGrpcController } from '@modules/user/interface/dtos/grpc-controllers/update-user.grpc.controller'; +import { UpdateUserRequestDto } from '@modules/user/interface/grpc-controllers/dtos/update-user.request.dto'; +import { UpdateUserGrpcController } from '@modules/user/interface/grpc-controllers/update-user.grpc.controller'; import { CommandBus } from '@nestjs/cqrs'; import { RpcException } from '@nestjs/microservices'; diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index 23315ba..15646bf 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -1,60 +1,67 @@ import { RedisClientOptions } from '@liaoliaots/nestjs-redis'; -import { Module } from '@nestjs/common'; +import { Module, Provider } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { CqrsModule } from '@nestjs/cqrs'; import { redisStore } from 'cache-manager-ioredis-yet'; -import { DatabaseModule } from '../database/database.module'; -import { UserController } from './adapters/primaries/user.controller'; -import { UsersRepository } from './adapters/secondaries/users.repository'; -import { CreateUserUseCase } from './domain/usecases/create-user.usecase'; -import { DeleteUserUseCase } from './domain/usecases/delete-user.usecase'; -import { FindAllUsersUseCase } from './domain/usecases/find-all-users.usecase'; -import { FindUserByUuidUseCase } from './domain/usecases/find-user-by-uuid.usecase'; -import { UpdateUserUseCase } from './domain/usecases/update-user.usecase'; -import { UserProfile } from './mappers/user.profile'; import { CacheModule } from '@nestjs/cache-manager'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; -import { - MESSAGE_BROKER_PUBLISHER, - MESSAGE_PUBLISHER, -} from '../../app.constants'; -import { MessagePublisher } from './adapters/secondaries/message-publisher'; +import { CreateUserGrpcController } from './interface/grpc-controllers/create-user.grpc.controller'; +import { UpdateUserGrpcController } from './interface/grpc-controllers/update-user.grpc.controller'; +import { CreateUserService } from './core/application/commands/create-user/create-user.service'; +import { UpdateUserService } from './core/application/commands/update-user/update-user.service'; +import { USER_MESSAGE_PUBLISHER, USER_REPOSITORY } from './user.di-tokens'; +import { UserRepository } from './infrastructure/user.repository'; +import { UserMapper } from './user.mapper'; +import { PrismaService } from './infrastructure/prisma.service'; + +const imports = [ + CqrsModule, + CacheModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + store: await redisStore({ + host: configService.get('REDIS_HOST'), + port: configService.get('REDIS_PORT'), + password: configService.get('REDIS_PASSWORD'), + ttl: configService.get('CACHE_TTL'), + }), + }), + inject: [ConfigService], + }), +]; + +const grpcControllers = [CreateUserGrpcController, UpdateUserGrpcController]; + +const commandHandlers: Provider[] = [CreateUserService, UpdateUserService]; + +const mappers: Provider[] = [UserMapper]; + +const repositories: Provider[] = [ + { + provide: USER_REPOSITORY, + useClass: UserRepository, + }, +]; + +const messagePublishers: Provider[] = [ + { + provide: USER_MESSAGE_PUBLISHER, + useExisting: MessageBrokerPublisher, + }, +]; + +const orms: Provider[] = [PrismaService]; @Module({ - imports: [ - DatabaseModule, - CqrsModule, - CacheModule.registerAsync({ - imports: [ConfigModule], - useFactory: async (configService: ConfigService) => ({ - store: await redisStore({ - host: configService.get('REDIS_HOST'), - port: configService.get('REDIS_PORT'), - password: configService.get('REDIS_PASSWORD'), - ttl: configService.get('CACHE_TTL'), - }), - }), - inject: [ConfigService], - }), - ], - controllers: [UserController], + imports, + controllers: [...grpcControllers], providers: [ - UserProfile, - UsersRepository, - FindAllUsersUseCase, - FindUserByUuidUseCase, - CreateUserUseCase, - UpdateUserUseCase, - DeleteUserUseCase, - { - provide: MESSAGE_BROKER_PUBLISHER, - useClass: MessageBrokerPublisher, - }, - { - provide: MESSAGE_PUBLISHER, - useClass: MessagePublisher, - }, + ...commandHandlers, + ...mappers, + ...repositories, + ...messagePublishers, + ...orms, ], - exports: [], + exports: [PrismaService, UserMapper, USER_REPOSITORY], }) export class UserModule {}