diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61d9d3e..77484f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: Nest.JS clean architecture CI on: push: - branches: ['*'] + branches: [main, develop] pull_request: - branches: [main] + branches: [main, develop] jobs: lint-commit: runs-on: ubuntu-latest diff --git a/.husky/pre-commit b/.husky/pre-commit index 34ed966..59d0261 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,2 @@ -pnpm lint-staged +pnpm lint-staged && pnpm test diff --git a/eslint.config.mjs b/eslint.config.mjs index f4dfe9c..9be0fd7 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -32,7 +32,7 @@ export default tseslint.config( '@typescript-eslint/no-explicit-any': 'off', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'off', - 'no-console': 'warn', + 'no-console': 'error', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-namespace': 'off', }, diff --git a/package.json b/package.json index 9c29446..1956170 100644 --- a/package.json +++ b/package.json @@ -82,9 +82,7 @@ "lint-staged": { "*.ts": [ "pnpm lint", - "pnpm format", - "pnpm run test", - "pnpm run test:e2e" + "pnpm format" ] } } diff --git a/src/application/di/auth.module.ts b/src/application/di/auth.module.ts new file mode 100644 index 0000000..c8ba8cc --- /dev/null +++ b/src/application/di/auth.module.ts @@ -0,0 +1,46 @@ +import { AuthFacadeUsecase } from '@/domain/auth'; +import { AuthorizeUsecase } from '@/domain/auth/usecases'; +import { UserRepository } from '@/domain/user'; +import { AuthController } from '@/presentation/controllers/auth'; + +import { UserModel } from '@/infrastructure/typeorm/models'; +import { UserEntity } from '@/domain/user'; +import { UserRepositoryImpl } from '@/infrastructure/typeorm/repositories'; +import { Module } from '@nestjs/common'; +import { Mapper } from '@/core'; +import { UserMapper } from '@/infrastructure/typeorm/mappers'; +import { CacheService } from '@/infrastructure/redis/cache'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +@Module({}) +export class AuthFacadeModule { + static register() { + return { + module: AuthFacadeModule, + imports: [TypeOrmModule.forFeature([UserModel])], + providers: [ + AuthFacadeUsecase, + AuthorizeUsecase, + + { + provide: UserRepository, + useClass: UserRepositoryImpl, + }, + + { + provide: Mapper, + useClass: UserMapper, + }, + ], + exports: [AuthFacadeUsecase, AuthorizeUsecase], + }; + } +} + +@Module({ + imports: [AuthFacadeModule.register()], + controllers: [AuthController], + providers: [CacheService], + exports: [], +}) +export class AuthModule {} diff --git a/src/application/di/index.ts b/src/application/di/index.ts index 46e1af5..e4008bc 100644 --- a/src/application/di/index.ts +++ b/src/application/di/index.ts @@ -1,3 +1,5 @@ export { PresentationModule } from './presentation.module'; export { TypeOrmModule } from './typeorm.module'; export { AppModule } from './app.module'; +export { AuthModule } from './auth.module'; +export { UserModule } from './user.module'; diff --git a/src/application/di/presentation.module.ts b/src/application/di/presentation.module.ts index 31a4b30..1e1d8bc 100644 --- a/src/application/di/presentation.module.ts +++ b/src/application/di/presentation.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; -import { UserFacadeUsecase } from '@/domain/user'; import { UserModule } from './user.module'; +import { AuthModule } from './auth.module'; @Module({ - imports: [UserModule], + imports: [UserModule, AuthModule], controllers: [], providers: [], }) diff --git a/src/application/di/user.module.ts b/src/application/di/user.module.ts index 55717cc..04d41d5 100644 --- a/src/application/di/user.module.ts +++ b/src/application/di/user.module.ts @@ -5,12 +5,12 @@ import { UserController } from '@/presentation/controllers/user'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { UserModel as TypeormUserModel } from '@/infrastructure/typeorm/models'; +import { UserModel } from '@/infrastructure/typeorm/models'; import { UserEntity } from '@/domain/user'; -import { UserRepositoryImpl as RepoTypeorm } from '@/infrastructure/typeorm/repositories'; +import { UserRepositoryImpl } from '@/infrastructure/typeorm/repositories'; import { Mapper } from '@/core'; -import { UserMapper as TypeormUserMapper } from '@/infrastructure/typeorm/mappers'; +import { UserMapper } from '@/infrastructure/typeorm/mappers'; @Module({}) export class UserFacadeModule { @@ -18,7 +18,7 @@ export class UserFacadeModule { return { module: UserFacadeModule, - imports: [TypeOrmModule.forFeature([TypeormUserModel])], + imports: [TypeOrmModule.forFeature([UserModel])], providers: [ UserFacadeUsecase, CreateUserUsecase, @@ -26,12 +26,12 @@ export class UserFacadeModule { { provide: UserRepository, - useClass: RepoTypeorm, + useClass: UserRepositoryImpl, }, { - provide: Mapper, - useClass: TypeormUserMapper, + provide: Mapper, + useClass: UserMapper, }, ], exports: [UserFacadeUsecase, CreateUserUsecase, GetUserUsecase], @@ -42,7 +42,13 @@ export class UserFacadeModule { @Module({ imports: [UserFacadeModule.register()], controllers: [UserController], - providers: [CacheService], + providers: [ + CacheService, + { + provide: Mapper, + useClass: UserMapper, + }, + ], exports: [], }) export class UserModule {} diff --git a/src/application/dtos/auth/authorize.dto.ts b/src/application/dtos/auth/authorize.dto.ts new file mode 100644 index 0000000..2182cc3 --- /dev/null +++ b/src/application/dtos/auth/authorize.dto.ts @@ -0,0 +1,9 @@ +import { Length } from 'class-validator'; + +export class AuthorizeDto { + @Length(4, 20) + username: string; + + @Length(6) + password: string; +} diff --git a/src/application/dtos/user/create-user.dto.ts b/src/application/dtos/user/create-user.dto.ts index 35e8ac6..9edab28 100644 --- a/src/application/dtos/user/create-user.dto.ts +++ b/src/application/dtos/user/create-user.dto.ts @@ -1,4 +1,4 @@ -import { IsNotEmpty, IsString, Length } from 'class-validator'; +import { IsString, Length } from 'class-validator'; export class CreateUserDto { @IsString() diff --git a/src/core/mapper.ts b/src/core/mapper.ts index ff00ca2..3be915e 100644 --- a/src/core/mapper.ts +++ b/src/core/mapper.ts @@ -1,4 +1,4 @@ -export abstract class Mapper { - abstract toEntity(entity: Partial): Partial; - abstract toModel(domain: Partial): Partial; +export abstract class Mapper { + abstract toEntity(entity: Partial): Partial; + abstract toModel(domain: Partial): Partial; } diff --git a/src/core/repository.ts b/src/core/repository.ts index b9ebd3d..3b04a8a 100644 --- a/src/core/repository.ts +++ b/src/core/repository.ts @@ -10,4 +10,6 @@ export abstract class Repository { ): Promise>; abstract delete(id: string): Promise>; abstract create(data: Partial): Promise>; + + abstract findOne(filter: Partial): Promise>; } diff --git a/src/core/usecase.ts b/src/core/usecase.ts index b66d2ca..9782a3c 100644 --- a/src/core/usecase.ts +++ b/src/core/usecase.ts @@ -1,3 +1,3 @@ -export abstract class Usecase { +export abstract class Usecase { abstract execute(...args: any[]): Promise>; } diff --git a/src/domain/auth/auth.facade.usecase.ts b/src/domain/auth/auth.facade.usecase.ts new file mode 100644 index 0000000..0f60dc3 --- /dev/null +++ b/src/domain/auth/auth.facade.usecase.ts @@ -0,0 +1,15 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { AuthorizeUsecase } from './usecases'; +import { AuthorizeDto } from '@/application/dtos/auth/authorize.dto'; + +@Injectable() +export class AuthFacadeUsecase { + constructor( + @Inject(AuthorizeUsecase) + private readonly authorizeUsecase: AuthorizeUsecase, + ) {} + + authorize(payload: AuthorizeDto) { + return this.authorizeUsecase.execute(payload); + } +} diff --git a/src/domain/auth/auth.usecase.ts b/src/domain/auth/auth.usecase.ts new file mode 100644 index 0000000..97d6088 --- /dev/null +++ b/src/domain/auth/auth.usecase.ts @@ -0,0 +1,3 @@ +import { Usecase } from '@/core'; + +export abstract class AuthUsecase extends Usecase {} diff --git a/src/domain/auth/index.ts b/src/domain/auth/index.ts new file mode 100644 index 0000000..ae97eed --- /dev/null +++ b/src/domain/auth/index.ts @@ -0,0 +1,2 @@ +export { AuthUsecase } from './auth.usecase'; +export { AuthFacadeUsecase } from './auth.facade.usecase'; diff --git a/src/domain/auth/usecases/authorize.usecase.spec.ts b/src/domain/auth/usecases/authorize.usecase.spec.ts new file mode 100644 index 0000000..174fddc --- /dev/null +++ b/src/domain/auth/usecases/authorize.usecase.spec.ts @@ -0,0 +1,65 @@ +import { Test } from '@nestjs/testing'; +import { AuthUsecase } from '../auth.usecase'; +import { AuthorizeUsecase } from './authorize.usecase'; +import { UserEntity, UserRepository } from '@/domain/user'; +import { faker } from '@faker-js/faker'; +import bcrypt from 'bcryptjs'; + +const mockPayload = { + username: faker.internet.userName(), + password: faker.internet.password(), +}; +const mockUser = { + id: faker.string.uuid(), + username: mockPayload.username, + password: bcrypt.hashSync(mockPayload.password, 10), + createdAt: new Date(), + deletedAt: null, + updatedAt: new Date(), +}; + +describe('AuthorizeUsecase', () => { + let usecase: AuthUsecase; + let repository: UserRepository; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + AuthorizeUsecase, + { + provide: UserRepository, + useValue: { + findOne: jest.fn(), + }, + }, + ], + }).compile(); + usecase = module.get(AuthorizeUsecase); + repository = module.get(UserRepository); + }); + + it('should be defined', () => { + expect(usecase).toBeDefined(); + expect(repository).toBeDefined(); + }); + it('should return null if user not found', async () => { + jest.spyOn(repository, 'findOne').mockResolvedValue(null); + expect( + await usecase.execute({ username: 'username', password: 'password' }), + ).toBeNull(); + }); + + it('should return user if user found', async () => { + jest.spyOn(repository, 'findOne').mockResolvedValue(mockUser); + const result = (await usecase.execute(mockPayload)) as UserEntity; + expect(result.id).toEqual(mockUser.id); + }); + it('should return null if password is invalid', async () => { + jest.spyOn(repository, 'findOne').mockResolvedValue(mockUser); + const result = (await usecase.execute({ + username: 'test', + password: 'wrong password', + })) as UserEntity; + expect(result).toEqual(null); + }); +}); diff --git a/src/domain/auth/usecases/authorize.usecase.ts b/src/domain/auth/usecases/authorize.usecase.ts new file mode 100644 index 0000000..61a0ff0 --- /dev/null +++ b/src/domain/auth/usecases/authorize.usecase.ts @@ -0,0 +1,22 @@ +import { AuthorizeDto } from '@/application/dtos/auth/authorize.dto'; +import { AuthUsecase } from '@/domain/auth'; +import { UserRepository } from '@/domain/user'; +import { Injectable } from '@nestjs/common'; +import bcrypt from 'bcryptjs'; + +@Injectable() +export class AuthorizeUsecase extends AuthUsecase { + constructor(private readonly userRepository: UserRepository) { + super(); + } + + async execute(payload: AuthorizeDto): Promise> { + const { username, password } = payload; + const user = await this.userRepository.findOne({ username }); + + if (!user) return null; + const isValidPassword = bcrypt.compareSync(password, user.password); + if (!isValidPassword) return null; + return user; + } +} diff --git a/src/domain/auth/usecases/index.ts b/src/domain/auth/usecases/index.ts new file mode 100644 index 0000000..f647e7f --- /dev/null +++ b/src/domain/auth/usecases/index.ts @@ -0,0 +1 @@ +export { AuthorizeUsecase } from './authorize.usecase'; diff --git a/src/domain/user/user.entity.ts b/src/domain/user/user.entity.ts index 53c384b..966a55e 100644 --- a/src/domain/user/user.entity.ts +++ b/src/domain/user/user.entity.ts @@ -1,16 +1,20 @@ import { Entity } from '@/core/entity'; +import { Exclude } from 'class-transformer'; export class UserEntity extends Entity { + @Exclude() + public password: string; constructor( public id: string, public username: string, + password: string, public createdAt: Date, public updatedAt: Date, public deletedAt: Date, - - public password?: string, ) { super(); + + this.password = password; } } diff --git a/src/domain/user/user.facade.usecase.ts b/src/domain/user/user.facade.usecase.ts index a185c4d..fc9822b 100644 --- a/src/domain/user/user.facade.usecase.ts +++ b/src/domain/user/user.facade.usecase.ts @@ -10,11 +10,11 @@ export class UserFacadeUsecase { @Inject(CreateUserUsecase) private readonly createUserUsecase: UserUsecase, ) {} - async createUser(data: Partial) { + createUser(data: Partial) { return this.createUserUsecase.execute(data); } - async getUser(id: string) { + getUser(id: string) { return this.getUserUsecase.execute(id); } } diff --git a/src/infrastructure/typeorm/mappers/user.mapper.ts b/src/infrastructure/typeorm/mappers/user.mapper.ts index 74a9010..67dbbd5 100644 --- a/src/infrastructure/typeorm/mappers/user.mapper.ts +++ b/src/infrastructure/typeorm/mappers/user.mapper.ts @@ -8,6 +8,7 @@ export class UserMapper extends Mapper { return new UserEntity( data.id, data.username, + data.password, data.createdAt, data.updatedAt, data.deletedAt, diff --git a/src/infrastructure/typeorm/models/user.model.ts b/src/infrastructure/typeorm/models/user.model.ts index efca3e4..6165e10 100644 --- a/src/infrastructure/typeorm/models/user.model.ts +++ b/src/infrastructure/typeorm/models/user.model.ts @@ -23,7 +23,7 @@ export class UserModel { username: string; @Column({ - select: false, + select: true, }) password?: string; diff --git a/src/infrastructure/typeorm/repositories/user.repository.impl.ts b/src/infrastructure/typeorm/repositories/user.repository.impl.ts index 9092f33..780c161 100644 --- a/src/infrastructure/typeorm/repositories/user.repository.impl.ts +++ b/src/infrastructure/typeorm/repositories/user.repository.impl.ts @@ -17,6 +17,14 @@ export class UserRepositoryImpl extends UserRepository { ) { super(); } + + async findOne(filter: Partial): Promise> { + const userTypeorm = await this.userRepository.findOne({ + where: filter, + }); + return this.userMapper.toEntity(userTypeorm); + } + async findUnique(id: string) { this.logger.log('[Typeorm] findUnique', id); if (!uuid.validate(id)) return null; diff --git a/src/infrastructure/typeorm/repositories/user.repository.spec.ts b/src/infrastructure/typeorm/repositories/user.repository.spec.ts index 1522bfd..a93e121 100644 --- a/src/infrastructure/typeorm/repositories/user.repository.spec.ts +++ b/src/infrastructure/typeorm/repositories/user.repository.spec.ts @@ -159,4 +159,12 @@ describe('UserRepository', () => { const result = await repository.findMany({}); expect(result).toEqual(users); }); + it('should return only one user', async () => { + const user = mockUser; + jest + .spyOn(userRepository, 'findOne') + .mockImplementation(() => Promise.resolve(user)); + const result = await repository.findOne({ username: user.username }); + expect(result).toEqual(user); + }); }); diff --git a/src/main.ts b/src/main.ts index 7f732ec..c857e29 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,7 @@ import { HttpExceptionFilter, validationExceptionFactory, } from '@/presentation/exceptions'; +import { TransformInterceptor } from './presentation/commons/interceptors/transform.interceptor'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -16,8 +17,12 @@ async function bootstrap() { }), ); - app.useGlobalInterceptors(new ResponseInterceptor(app.get(Reflector))); app.useGlobalFilters(new HttpExceptionFilter()); + + app.useGlobalInterceptors( + new TransformInterceptor(), + new ResponseInterceptor(app.get(Reflector)), + ); await app.listen(process.env.PORT); } bootstrap(); diff --git a/src/presentation/commons/decorators/index.ts b/src/presentation/commons/decorators/index.ts new file mode 100644 index 0000000..de784c0 --- /dev/null +++ b/src/presentation/commons/decorators/index.ts @@ -0,0 +1 @@ +export { ResponseMessage } from './ResponseMessage'; diff --git a/src/presentation/commons/interceptors/index.ts b/src/presentation/commons/interceptors/index.ts index c4f4deb..c6c7d48 100644 --- a/src/presentation/commons/interceptors/index.ts +++ b/src/presentation/commons/interceptors/index.ts @@ -1 +1,2 @@ export { ResponseInterceptor } from './response.interceptor'; +export { TransformInterceptor } from './transform.interceptor'; diff --git a/src/presentation/commons/interceptors/transform.interceptor.ts b/src/presentation/commons/interceptors/transform.interceptor.ts new file mode 100644 index 0000000..b6d013b --- /dev/null +++ b/src/presentation/commons/interceptors/transform.interceptor.ts @@ -0,0 +1,9 @@ +import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'; +import { classToPlain, instanceToPlain } from 'class-transformer'; +import { map, Observable } from 'rxjs'; + +export class TransformInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe(map((data) => instanceToPlain(data))); + } +} diff --git a/src/presentation/controllers/auth/auth.controller.spec.ts b/src/presentation/controllers/auth/auth.controller.spec.ts new file mode 100644 index 0000000..0691db3 --- /dev/null +++ b/src/presentation/controllers/auth/auth.controller.spec.ts @@ -0,0 +1,66 @@ +import { Test } from '@nestjs/testing'; +import { AuthController } from './auth.controller'; +import { AuthFacadeUsecase } from '@/domain/auth'; +import { CacheService } from '@/infrastructure/redis/cache'; +import { faker } from '@faker-js/faker'; + +const mockPayload = { + username: faker.internet.userName(), + password: faker.internet.password(), +}; + +describe('AuthController', () => { + let controller: AuthController; + + let authFacadeUsecase: Partial = { + authorize: jest.fn(), + }; + + let cacheService: Partial = { + get: jest.fn(), + set: jest.fn(), + }; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + controllers: [AuthController], + providers: [ + { + provide: AuthFacadeUsecase, + useValue: authFacadeUsecase, + }, + { + provide: CacheService, + useValue: cacheService, + }, + ], + }).compile(); + controller = module.get(AuthController); + authFacadeUsecase = module.get(AuthFacadeUsecase); + cacheService = module.get(CacheService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should return access token and refresh token', () => { + const accessToken = faker.string.alphanumeric(10); + const refreshToken = faker.string.alphanumeric(10); + authFacadeUsecase.authorize = jest.fn().mockResolvedValue({ + accessToken, + refreshToken, + }); + + expect(controller.authorize(mockPayload)).resolves.toEqual({ + accessToken, + refreshToken, + }); + }); + + it('should not return access token and refresh token', () => { + authFacadeUsecase.authorize = jest.fn().mockResolvedValue(null); + + expect(controller.authorize(mockPayload)).rejects.toThrow('Unauthorized'); + }); +}); diff --git a/src/presentation/controllers/auth/auth.controller.ts b/src/presentation/controllers/auth/auth.controller.ts new file mode 100644 index 0000000..aeb9ae9 --- /dev/null +++ b/src/presentation/controllers/auth/auth.controller.ts @@ -0,0 +1,26 @@ +import { AuthorizeDto } from '@/application/dtos/auth/authorize.dto'; +import { AuthFacadeUsecase } from '@/domain/auth'; +import { ResponseMessage } from '@/presentation/commons/decorators'; +import { + Body, + Controller, + HttpException, + HttpStatus, + Post, + UnauthorizedException, +} from '@nestjs/common'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authFacadeUsecase: AuthFacadeUsecase) {} + + @Post('/authorize') + @ResponseMessage('Authorize successfully') + async authorize(@Body() payload: AuthorizeDto) { + const result = await this.authFacadeUsecase.authorize(payload); + if (result == null) { + throw new UnauthorizedException('Unauthorized'); + } + return result; + } +} diff --git a/src/presentation/controllers/auth/index.ts b/src/presentation/controllers/auth/index.ts new file mode 100644 index 0000000..74c6815 --- /dev/null +++ b/src/presentation/controllers/auth/index.ts @@ -0,0 +1 @@ +export { AuthController } from './auth.controller'; diff --git a/src/presentation/controllers/user/user.controller.spec.ts b/src/presentation/controllers/user/user.controller.spec.ts index 248de13..47ce0ec 100644 --- a/src/presentation/controllers/user/user.controller.spec.ts +++ b/src/presentation/controllers/user/user.controller.spec.ts @@ -1,10 +1,13 @@ import { Test } from '@nestjs/testing'; import { UserController } from '@/presentation/controllers/user'; -import { UserFacadeUsecase } from '@/domain/user'; +import { UserEntity, UserFacadeUsecase } from '@/domain/user'; import { CacheService } from '@/infrastructure/redis/cache'; import _ from 'lodash'; import { faker } from '@faker-js/faker'; +import { Mapper } from '@/core'; +import { UserModel } from '@/infrastructure/typeorm/models'; +import { UserMapper } from '@/infrastructure/typeorm/mappers'; const mockUser = { id: faker.string.uuid(), username: faker.internet.userName(), @@ -25,7 +28,7 @@ const mockExistUser = { describe('UserController', () => { let controller: UserController; - let userFaceUsecase: Partial = { + let userFacadeUsecase: Partial = { getUser: jest.fn(), createUser: jest.fn(), }; @@ -41,7 +44,12 @@ describe('UserController', () => { providers: [ { provide: UserFacadeUsecase, - useValue: userFaceUsecase, + useValue: userFacadeUsecase, + }, + + { + provide: Mapper, + useClass: UserMapper, }, { provide: CacheService, @@ -50,7 +58,7 @@ describe('UserController', () => { ], }).compile(); controller = module.get(UserController); - userFaceUsecase = module.get(UserFacadeUsecase); + userFacadeUsecase = module.get(UserFacadeUsecase); cacheService = module.get(CacheService); }); it('should be defined', () => { @@ -61,7 +69,7 @@ describe('UserController', () => { jest.clearAllMocks(); }); it('[createUser] should create user', async () => { - jest.spyOn(userFaceUsecase, 'createUser').mockResolvedValueOnce(mockUser); + jest.spyOn(userFacadeUsecase, 'createUser').mockResolvedValueOnce(mockUser); const expectedOutput = await controller.createUser(mockUser); expect(expectedOutput.username).toEqual(mockUser.username); @@ -86,7 +94,7 @@ describe('UserController', () => { it('[getUser] expect correct output when have not cache yet', async () => { jest.spyOn(cacheService, 'get').mockResolvedValueOnce(null); - jest.spyOn(userFaceUsecase, 'getUser').mockResolvedValueOnce(mockUser); + jest.spyOn(userFacadeUsecase, 'getUser').mockResolvedValueOnce(mockUser); const expectedOutput = await controller.getUser(mockUser.id); expect(expectedOutput).toEqual(mockUser); diff --git a/src/presentation/controllers/user/user.controller.ts b/src/presentation/controllers/user/user.controller.ts index 2655e11..d60f301 100644 --- a/src/presentation/controllers/user/user.controller.ts +++ b/src/presentation/controllers/user/user.controller.ts @@ -9,13 +9,15 @@ import { ParseUUIDPipe, Post, } from '@nestjs/common'; -import { UserFacadeUsecase } from '@/domain/user'; +import { UserEntity, UserFacadeUsecase } from '@/domain/user'; import { CacheService } from '@/infrastructure/redis/cache'; import { CreateUserDto } from '@/application/dtos/user/create-user.dto'; import * as uuid from 'uuid'; import * as _ from 'lodash'; import { ResponseMessage } from '@/presentation/commons/decorators/ResponseMessage'; import { IsUUID } from 'class-validator'; +import { Mapper } from '@/core'; +import { UserModel } from '@/infrastructure/typeorm/models'; @Controller('/user') export class UserController { @@ -23,6 +25,8 @@ export class UserController { constructor( private readonly userFacadeUsecase: UserFacadeUsecase, + private readonly userMapper: Mapper, + private readonly cacheService: CacheService, ) {} @@ -33,11 +37,10 @@ export class UserController { if (!uuid.validate(id)) return null; const cached = await this.cacheService.get(id); - if (cached) return cached; + if (cached) return this.userMapper.toEntity(cached); const user = await this.userFacadeUsecase.getUser(id); if (user) this.cacheService.set(id, user); - console.log(user); return user; } diff --git a/src/presentation/exceptions/http.exception.ts b/src/presentation/exceptions/http.exception.ts index 0c1818e..fbd98df 100644 --- a/src/presentation/exceptions/http.exception.ts +++ b/src/presentation/exceptions/http.exception.ts @@ -5,6 +5,7 @@ import { ExceptionFilter, HttpException, Logger, + UnauthorizedException, } from '@nestjs/common'; import { ValidationException } from './validation.exception'; import { QueryFailedError } from 'typeorm'; @@ -16,8 +17,8 @@ export class HttpExceptionFilter implements ExceptionFilter { this.logger.error(exception.message, exception); const ctx = host.switchToHttp(); const response = ctx.getResponse() as any; - let code = 500, - error = 'Internal Server Error'; + let code = exception.getStatus ? exception.getStatus() : 500, + error = exception.message || 'Internal Server Error'; switch (exception.constructor) { case QueryFailedError: diff --git a/src/presentation/exceptions/validation.exception.ts b/src/presentation/exceptions/validation.exception.ts index 406d28e..9a4abd5 100644 --- a/src/presentation/exceptions/validation.exception.ts +++ b/src/presentation/exceptions/validation.exception.ts @@ -13,7 +13,7 @@ export const validationExceptionFactory = (errors: ValidationError[]) => { }; return new ValidationException({ message: 'Bad Input', - error: { + validation: { ...formatError(errors), }, timestamp: new Date().toISOString(), diff --git a/test/auth.e2e-spec.ts b/test/auth.e2e-spec.ts new file mode 100644 index 0000000..07f9770 --- /dev/null +++ b/test/auth.e2e-spec.ts @@ -0,0 +1,63 @@ +import { AppModule, TypeOrmModule } from '@/application/di'; +import { typeormModule } from '@/application/di/app.module'; +import { UserModel } from '@/infrastructure/typeorm/models'; +import { faker } from '@faker-js/faker'; +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; + +import request from 'supertest'; + +describe('[E2E] AuthController', () => { + let app: INestApplication; + let httpServer: any; + beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }) + .overrideModule(typeormModule) + .useModule( + TypeOrmModule.forTest({ + entities: [UserModel], + url: process.env.DATABASE_TEST_URL, + }), + ) + .compile(); + + app = module.createNestApplication(); + await app.init(); + httpServer = app.getHttpServer(); + }); + it('should be defined', () => { + expect(app).toBeDefined(); + expect(httpServer).toBeDefined(); + }); + + it('POST /auth/authorize (should created and authorized)', async () => { + const payload = { + username: faker.internet.userName(), + password: faker.internet.password(), + }; + const responseCreate = await request(httpServer) + .post(`/user/create`) + .send(payload) + .expect(201); + expect(responseCreate.body.id).toBeDefined(); + + const responseAuthorize = await request(httpServer) + .post(`/auth/authorize`) + .send(payload) + .expect(201); + expect(responseAuthorize.body.id).toBeDefined(); + }); + + it('POST /auth/authorize (should not authorized)', async () => { + const responseAuthorize = await request(httpServer) + .post(`/auth/authorize`) + .send({ + username: faker.internet.userName(), + password: faker.internet.password(), + }) + .expect(401); + expect(responseAuthorize.body.id).toBeUndefined(); + }); +});