diff --git a/apps/users/src/app/app.module.ts b/apps/users/src/app/app.module.ts index 31f1953..4dc918e 100644 --- a/apps/users/src/app/app.module.ts +++ b/apps/users/src/app/app.module.ts @@ -1,5 +1,11 @@ +import { ApolloServerPluginInlineTrace } from '@apollo/server/plugin/inlineTrace'; +import { + ApolloFederationDriver, + ApolloFederationDriverConfig, +} from '@nestjs/apollo'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { GraphQLModule } from '@nestjs/graphql'; import { databaseConfig, userAppConfig, @@ -18,6 +24,20 @@ import { AppService } from './app.service'; isGlobal: true, load: [userAppConfig, databaseConfig, awsConfig, authConfig], }), + GraphQLModule.forRoot({ + driver: ApolloFederationDriver, + autoSchemaFile: { + /** + * MEMO: + * Because of this problem, so mush need specify the version + * https://github.com/nestjs/graphql/issues/2646#issuecomment-1567381944 + */ + federation: 2, + }, + playground: process.env['NODE_ENV'] !== 'production', + sortSchema: true, + plugins: [ApolloServerPluginInlineTrace()], + }), UsersModule, AuthModule, ], diff --git a/libs/auth/interface-adapters/src/lib/auth.module.ts b/libs/auth/interface-adapters/src/lib/auth.module.ts index 29ee385..f481c98 100644 --- a/libs/auth/interface-adapters/src/lib/auth.module.ts +++ b/libs/auth/interface-adapters/src/lib/auth.module.ts @@ -5,7 +5,7 @@ import { MongooseModule } from '@nestjs/mongoose'; import { JwtModule, JwtService } from '@nestjs/jwt'; import { AwsCognitoService } from '@shared/infrastructure-aws-cognito'; import { DatabaseModule } from '@shared/infrastructure-mongoose'; -import { GetUserUseCase, UsersService } from '@users/application'; +import { GetUserByEmailUseCase, GetUserByIdUseCase, UsersService } from '@users/application'; import { USERS_REPOSITORY } from '@users/domain'; import { MongooseUsersRepository, @@ -23,7 +23,8 @@ import { AuthResolver } from './resolver/auth.resolver'; AwsCognitoService, UsersService, JwtService, - GetUserUseCase, + GetUserByIdUseCase, + GetUserByEmailUseCase, { provide: USERS_REPOSITORY, useClass: MongooseUsersRepository, diff --git a/libs/users/application/src/index.ts b/libs/users/application/src/index.ts index 192fddd..5c3670a 100644 --- a/libs/users/application/src/index.ts +++ b/libs/users/application/src/index.ts @@ -1,4 +1,5 @@ -export * from './lib/use-cases/get-user.use-case'; +export * from './lib/use-cases/get-user-by-id.use-case'; +export * from './lib/use-cases/get-user-by-email.use-case'; // service export * from './lib/users.service'; diff --git a/libs/users/application/src/lib/use-cases/get-user-by-email.use-case.spec.ts b/libs/users/application/src/lib/use-cases/get-user-by-email.use-case.spec.ts new file mode 100644 index 0000000..85c753e --- /dev/null +++ b/libs/users/application/src/lib/use-cases/get-user-by-email.use-case.spec.ts @@ -0,0 +1,41 @@ +import { GetUserByEmailUseCase } from './get-user-by-email.use-case'; +import { UsersRepository } from '@users/domain'; + +describe('GetUserByEmailUseCase', () => { + let getUserByEmailUseCase: GetUserByEmailUseCase; + let usersRepository: UsersRepository; + + beforeEach(() => { + usersRepository = { + findById: jest.fn(), + findByEmail: jest.fn(), + }; + getUserByEmailUseCase = new GetUserByEmailUseCase(usersRepository); + }); + + describe('execute', () => { + it('should return user when found', async () => { + const mockUser = { + id: '1', + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + }; + (usersRepository.findByEmail as jest.Mock).mockResolvedValue(mockUser); + + const result = await getUserByEmailUseCase.execute('test@example.com'); + + expect(result).toEqual(mockUser); + expect(usersRepository.findByEmail).toHaveBeenCalledWith("test@example.com"); + }); + + it('should return null when user not found', async () => { + (usersRepository.findByEmail as jest.Mock).mockResolvedValue(null); + + const result = await getUserByEmailUseCase.execute('test@example.com'); + + expect(result).toBeNull(); + expect(usersRepository.findByEmail).toHaveBeenCalledWith("test@example.com"); + }); + }); +}); diff --git a/libs/users/application/src/lib/use-cases/get-user-by-email.use-case.ts b/libs/users/application/src/lib/use-cases/get-user-by-email.use-case.ts new file mode 100644 index 0000000..5c01524 --- /dev/null +++ b/libs/users/application/src/lib/use-cases/get-user-by-email.use-case.ts @@ -0,0 +1,14 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { User, USERS_REPOSITORY, UsersRepository } from "@users/domain"; + +@Injectable() +export class GetUserByEmailUseCase { + constructor( + @Inject(USERS_REPOSITORY) + private readonly usersRepository: UsersRepository, + ) {} + + async execute(email: string): Promise { + return this.usersRepository.findByEmail(email); + } +} diff --git a/libs/users/application/src/lib/use-cases/get-user.use-case.spec.ts b/libs/users/application/src/lib/use-cases/get-user-by-id.use-case.spec.ts similarity index 69% rename from libs/users/application/src/lib/use-cases/get-user.use-case.spec.ts rename to libs/users/application/src/lib/use-cases/get-user-by-id.use-case.spec.ts index 5426856..e57326e 100644 --- a/libs/users/application/src/lib/use-cases/get-user.use-case.spec.ts +++ b/libs/users/application/src/lib/use-cases/get-user-by-id.use-case.spec.ts @@ -1,15 +1,16 @@ -import { GetUserUseCase } from './get-user.use-case'; +import { GetUserByIdUseCase } from './get-user-by-id.use-case'; import { UsersRepository } from '@users/domain'; -describe('GetUserUseCase', () => { - let getUserUseCase: GetUserUseCase; +describe('GetUserByIdUseCase', () => { + let getUserByIdUseCase: GetUserByIdUseCase; let usersRepository: UsersRepository; beforeEach(() => { usersRepository = { findById: jest.fn(), + findByEmail: jest.fn(), }; - getUserUseCase = new GetUserUseCase(usersRepository); + getUserByIdUseCase = new GetUserByIdUseCase(usersRepository); }); describe('execute', () => { @@ -22,7 +23,7 @@ describe('GetUserUseCase', () => { }; (usersRepository.findById as jest.Mock).mockResolvedValue(mockUser); - const result = await getUserUseCase.execute('1'); + const result = await getUserByIdUseCase.execute('1'); expect(result).toEqual(mockUser); expect(usersRepository.findById).toHaveBeenCalledWith('1'); @@ -31,7 +32,7 @@ describe('GetUserUseCase', () => { it('should return null when user not found', async () => { (usersRepository.findById as jest.Mock).mockResolvedValue(null); - const result = await getUserUseCase.execute('1'); + const result = await getUserByIdUseCase.execute('1'); expect(result).toBeNull(); expect(usersRepository.findById).toHaveBeenCalledWith('1'); diff --git a/libs/users/application/src/lib/use-cases/get-user.use-case.ts b/libs/users/application/src/lib/use-cases/get-user-by-id.use-case.ts similarity index 67% rename from libs/users/application/src/lib/use-cases/get-user.use-case.ts rename to libs/users/application/src/lib/use-cases/get-user-by-id.use-case.ts index be0f1e8..aa6ba98 100644 --- a/libs/users/application/src/lib/use-cases/get-user.use-case.ts +++ b/libs/users/application/src/lib/use-cases/get-user-by-id.use-case.ts @@ -2,18 +2,13 @@ import { Inject, Injectable } from '@nestjs/common'; import { User, USERS_REPOSITORY, UsersRepository } from '@users/domain'; @Injectable() -export class GetUserUseCase { +export class GetUserByIdUseCase { constructor( @Inject(USERS_REPOSITORY) private readonly usersRepository: UsersRepository, ) {} async execute(id: string): Promise { - const user = await this.usersRepository.findById(id); - - if (!user) { - return null; - } - return user; + return await this.usersRepository.findById(id); } } diff --git a/libs/users/application/src/lib/users.service.spec.ts b/libs/users/application/src/lib/users.service.spec.ts index 8e7a651..adb98d5 100644 --- a/libs/users/application/src/lib/users.service.spec.ts +++ b/libs/users/application/src/lib/users.service.spec.ts @@ -1,18 +1,25 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UsersService } from './users.service'; -import { GetUserUseCase } from './use-cases/get-user.use-case'; -import { User } from '@users/domain'; +import { GetUserByIdUseCase } from './use-cases/get-user-by-id.use-case'; +import { GetUserByEmailUseCase } from './use-cases/get-user-by-email.use-case'; describe('UsersService', () => { let service: UsersService; - let getUserUseCase: GetUserUseCase; + let getUserByIdUseCase: GetUserByIdUseCase; + let getUserByEmailUseCase: GetUserByEmailUseCase; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UsersService, { - provide: GetUserUseCase, + provide: GetUserByIdUseCase, + useValue: { + execute: jest.fn(), + }, + }, + { + provide: GetUserByEmailUseCase, useValue: { execute: jest.fn(), }, @@ -21,32 +28,59 @@ describe('UsersService', () => { }).compile(); service = module.get(UsersService); - getUserUseCase = module.get(GetUserUseCase); + getUserByIdUseCase = module.get(GetUserByIdUseCase); + getUserByEmailUseCase = module.get(GetUserByEmailUseCase); }); - it('should be defined', () => { - expect(service).toBeDefined(); + describe('getUserById', () => { + it('should return user when found', async () => { + const mockUser = { + id: '1', + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + }; + (getUserByIdUseCase.execute as jest.Mock).mockResolvedValue(mockUser); + + const result = await service.findById('1'); + + expect(result).toEqual(mockUser); + expect(getUserByIdUseCase.execute).toHaveBeenCalledWith('1'); + }); + + it('should return null when user not found', async () => { + (getUserByIdUseCase.execute as jest.Mock).mockResolvedValue(null); + + const result = await service.findById('1'); + + expect(result).toBeNull(); + expect(getUserByIdUseCase.execute).toHaveBeenCalledWith('1'); + }); }); - describe('findById', () => { - it('should return a user if found', async () => { - const user: User = { + describe('getUserByEmail', () => { + it('should return user when found', async () => { + const mockUser = { id: '1', email: 'test@example.com', firstName: 'John', lastName: 'Doe', }; - jest.spyOn(getUserUseCase, 'execute').mockResolvedValue(user); + (getUserByEmailUseCase.execute as jest.Mock).mockResolvedValue(mockUser); - const result = await service.findById('1'); - expect(result).toEqual(user); + const result = await service.findByEmail('test@example.com'); + + expect(result).toEqual(mockUser); + expect(getUserByEmailUseCase.execute).toHaveBeenCalledWith('test@example.com'); }); - it('should return null if user not found', async () => { - jest.spyOn(getUserUseCase, 'execute').mockResolvedValue(null); + it('should return null when user not found', async () => { + (getUserByEmailUseCase.execute as jest.Mock).mockResolvedValue(null); + + const result = await service.findByEmail('test@example.com'); - const result = await service.findById('2'); expect(result).toBeNull(); + expect(getUserByEmailUseCase.execute).toHaveBeenCalledWith('test@example.com'); }); }); }); diff --git a/libs/users/application/src/lib/users.service.ts b/libs/users/application/src/lib/users.service.ts index 83c908f..87d2b50 100644 --- a/libs/users/application/src/lib/users.service.ts +++ b/libs/users/application/src/lib/users.service.ts @@ -1,18 +1,21 @@ import { Injectable } from '@nestjs/common'; import { User } from '@users/domain'; -import { GetUserUseCase } from './use-cases/get-user.use-case'; +import { GetUserByIdUseCase } from './use-cases/get-user-by-id.use-case'; +import { GetUserByEmailUseCase } from './use-cases/get-user-by-email.use-case'; @Injectable() export class UsersService { - constructor(private readonly getUserUseCase: GetUserUseCase) {} + constructor( + private readonly getUserByIdUseCase: GetUserByIdUseCase, + private readonly getUserByEmailUseCase: GetUserByEmailUseCase, + ) {} async findById(id: string): Promise { - return this.getUserUseCase.execute(id); + return this.getUserByIdUseCase.execute(id); } async findByEmail(email: string): Promise { - // TODO: refactor getUserUseCase - return this.getUserUseCase.execute(email); + return this.getUserByEmailUseCase.execute(email); } } diff --git a/libs/users/domain/src/lib/users.repository.spec.ts b/libs/users/domain/src/lib/users.repository.spec.ts index ba5e3b8..d560e1e 100644 --- a/libs/users/domain/src/lib/users.repository.spec.ts +++ b/libs/users/domain/src/lib/users.repository.spec.ts @@ -15,6 +15,10 @@ class MockUsersRepository implements UsersRepository { async findById(id: string): Promise { return this.users.find((user) => user.id === id) || null; } + + async findByEmail(email: string): Promise { + return this.users.find((user) => user.email === email) || null; + } } describe('UsersRepository', () => { @@ -38,4 +42,19 @@ describe('UsersRepository', () => { const user = await usersRepository.findById('3'); expect(user).toBeNull(); }); + + test('findByEmail should return a user by email', async () => { + const user = await usersRepository.findByEmail('jane@example.com'); + expect(user).toEqual({ + id: '2', + email: 'jane@example.com', + firstName: 'Jane', + lastName: 'Smith', + }); + }); + + test('findByEmail should return null if user not found', async () => { + const user = await usersRepository.findByEmail('nonexistent@example.com'); + expect(user).toBeNull(); + }); }); diff --git a/libs/users/domain/src/lib/users.repository.ts b/libs/users/domain/src/lib/users.repository.ts index 4af6be0..593a649 100644 --- a/libs/users/domain/src/lib/users.repository.ts +++ b/libs/users/domain/src/lib/users.repository.ts @@ -2,6 +2,7 @@ import { User } from './user.entity'; export interface UsersRepository { findById(id: string): Promise; + findByEmail(email: string): Promise; } export const USERS_REPOSITORY = Symbol('USERS_REPOSITORY'); \ No newline at end of file diff --git a/libs/users/infrastructure/mongoose/src/lib/mongoose-users.repository.ts b/libs/users/infrastructure/mongoose/src/lib/mongoose-users.repository.ts index 9a560ed..c61f9a8 100644 --- a/libs/users/infrastructure/mongoose/src/lib/mongoose-users.repository.ts +++ b/libs/users/infrastructure/mongoose/src/lib/mongoose-users.repository.ts @@ -25,4 +25,18 @@ export class MongooseUsersRepository implements UsersRepository { userDocument.lastName, ); } + + async findByEmail(email: string): Promise { + const userDocument = await this.userModel.findOne({ email }).exec(); + + if (!userDocument) { + return null; + } + return new User( + userDocument.id, + userDocument.email, + userDocument.firstName, + userDocument.lastName, + ); + } } diff --git a/libs/users/interface-adapters/src/lib/users.module.ts b/libs/users/interface-adapters/src/lib/users.module.ts index b5958e7..6bb1454 100644 --- a/libs/users/interface-adapters/src/lib/users.module.ts +++ b/libs/users/interface-adapters/src/lib/users.module.ts @@ -1,12 +1,6 @@ -import { ApolloServerPluginInlineTrace } from '@apollo/server/plugin/inlineTrace'; -import { - ApolloFederationDriver, - ApolloFederationDriverConfig, -} from '@nestjs/apollo'; import { Module } from '@nestjs/common'; -import { GraphQLModule } from '@nestjs/graphql'; import { DatabaseModule } from '@shared/infrastructure-mongoose'; -import { UsersService, GetUserUseCase } from '@users/application'; +import { UsersService, GetUserByIdUseCase, GetUserByEmailUseCase } from '@users/application'; import { MongooseUsersRepository, UserDocument, @@ -21,29 +15,18 @@ import { UsersResolver } from './resolver/users.resolver'; providers: [ UsersResolver, UsersService, - GetUserUseCase, + GetUserByIdUseCase, + GetUserByEmailUseCase, { provide: USERS_REPOSITORY, useClass: MongooseUsersRepository, }, ], imports: [ - GraphQLModule.forRoot({ - driver: ApolloFederationDriver, - autoSchemaFile: { - /** - * MEMO: - * Because of this problem, so mush need specify the version - * https://github.com/nestjs/graphql/issues/2646#issuecomment-1567381944 - */ - federation: 2, - }, - playground: process.env['NODE_ENV'] !== 'production', - sortSchema: true, - plugins: [ApolloServerPluginInlineTrace()], - }), DatabaseModule, - MongooseModule.forFeature([{ name: UserDocument.name, schema: UserSchema }]) + MongooseModule.forFeature([ + { name: UserDocument.name, schema: UserSchema }, + ]), ], exports: [UsersService], })