Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ✨ add findByEmail use case #94

Merged
merged 6 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions apps/users/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -18,6 +24,20 @@ import { AppService } from './app.service';
isGlobal: true,
load: [userAppConfig, databaseConfig, awsConfig, authConfig],
}),
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
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,
],
Expand Down
5 changes: 3 additions & 2 deletions libs/auth/interface-adapters/src/lib/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,7 +23,8 @@ import { AuthResolver } from './resolver/auth.resolver';
AwsCognitoService,
UsersService,
JwtService,
GetUserUseCase,
GetUserByIdUseCase,
GetUserByEmailUseCase,
{
provide: USERS_REPOSITORY,
useClass: MongooseUsersRepository,
Expand Down
3 changes: 2 additions & 1 deletion libs/users/application/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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: '[email protected]',
firstName: 'John',
lastName: 'Doe',
};
(usersRepository.findByEmail as jest.Mock).mockResolvedValue(mockUser);

const result = await getUserByEmailUseCase.execute('[email protected]');

expect(result).toEqual(mockUser);
expect(usersRepository.findByEmail).toHaveBeenCalledWith("[email protected]");
});

it('should return null when user not found', async () => {
(usersRepository.findByEmail as jest.Mock).mockResolvedValue(null);

const result = await getUserByEmailUseCase.execute('[email protected]');

expect(result).toBeNull();
expect(usersRepository.findByEmail).toHaveBeenCalledWith("[email protected]");
});
});
});
Original file line number Diff line number Diff line change
@@ -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<User | null> {
return this.usersRepository.findByEmail(email);
}
}
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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');
Expand All @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<User | null> {
const user = await this.usersRepository.findById(id);

if (!user) {
return null;
}
return user;
return await this.usersRepository.findById(id);
}
}
66 changes: 50 additions & 16 deletions libs/users/application/src/lib/users.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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(),
},
Expand All @@ -21,32 +28,59 @@ describe('UsersService', () => {
}).compile();

service = module.get<UsersService>(UsersService);
getUserUseCase = module.get<GetUserUseCase>(GetUserUseCase);
getUserByIdUseCase = module.get<GetUserByIdUseCase>(GetUserByIdUseCase);
getUserByEmailUseCase = module.get<GetUserByEmailUseCase>(GetUserByEmailUseCase);
});

it('should be defined', () => {
expect(service).toBeDefined();
describe('getUserById', () => {
it('should return user when found', async () => {
const mockUser = {
id: '1',
email: '[email protected]',
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: '[email protected]',
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('[email protected]');

expect(result).toEqual(mockUser);
expect(getUserByEmailUseCase.execute).toHaveBeenCalledWith('[email protected]');
});

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('[email protected]');

const result = await service.findById('2');
expect(result).toBeNull();
expect(getUserByEmailUseCase.execute).toHaveBeenCalledWith('[email protected]');
});
});
});
13 changes: 8 additions & 5 deletions libs/users/application/src/lib/users.service.ts
Original file line number Diff line number Diff line change
@@ -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<User | null> {
return this.getUserUseCase.execute(id);
return this.getUserByIdUseCase.execute(id);
}

async findByEmail(email: string): Promise<User | null> {
// TODO: refactor getUserUseCase
return this.getUserUseCase.execute(email);
return this.getUserByEmailUseCase.execute(email);
}
}
19 changes: 19 additions & 0 deletions libs/users/domain/src/lib/users.repository.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ class MockUsersRepository implements UsersRepository {
async findById(id: string): Promise<User | null> {
return this.users.find((user) => user.id === id) || null;
}

async findByEmail(email: string): Promise<User | null> {
return this.users.find((user) => user.email === email) || null;
}
}

describe('UsersRepository', () => {
Expand All @@ -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('[email protected]');
expect(user).toEqual({
id: '2',
email: '[email protected]',
firstName: 'Jane',
lastName: 'Smith',
});
});

test('findByEmail should return null if user not found', async () => {
const user = await usersRepository.findByEmail('[email protected]');
expect(user).toBeNull();
});
});
1 change: 1 addition & 0 deletions libs/users/domain/src/lib/users.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { User } from './user.entity';

export interface UsersRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
}

export const USERS_REPOSITORY = Symbol('USERS_REPOSITORY');
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,18 @@ export class MongooseUsersRepository implements UsersRepository {
userDocument.lastName,
);
}

async findByEmail(email: string): Promise<User | null> {
const userDocument = await this.userModel.findOne({ email }).exec();

if (!userDocument) {
return null;
}
return new User(
userDocument.id,
userDocument.email,
userDocument.firstName,
userDocument.lastName,
);
}
}
Loading
Loading