Skip to content

Commit

Permalink
Merge pull request #94 from zhumeisongsong/feature/get-user-by-email
Browse files Browse the repository at this point in the history
feat: ✨ add findByEmail use case
  • Loading branch information
zhumeisongsong authored Dec 4, 2024
2 parents 4194ec9 + 6635353 commit b28e98a
Show file tree
Hide file tree
Showing 13 changed files with 187 additions and 60 deletions.
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

0 comments on commit b28e98a

Please sign in to comment.