diff --git a/libs/common/src/database/prisma/repository/prisma-user.repository.spec.ts b/libs/common/src/database/prisma/repository/prisma-user.repository.spec.ts index 71b6e43..44498f8 100644 --- a/libs/common/src/database/prisma/repository/prisma-user.repository.spec.ts +++ b/libs/common/src/database/prisma/repository/prisma-user.repository.spec.ts @@ -7,6 +7,9 @@ class PrismaServiceMock { user = { findUnique: jest.fn(), create: jest.fn(), + findMany: jest.fn(), + update: jest.fn(), + delete: jest.fn(), }; } @@ -34,38 +37,59 @@ describe('PrismaUserRepository', () => { expect(prismaUserRepository).toBeDefined(); }); - it('should call prismaService.findUnique when findOne is called', async () => { - const where: Prisma.UserWhereUniqueInput = { - id: 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf', - }; - const user = { - id: 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf', - username: 'John Doe', - password: '123456', - } as User; - prismaService.user.findUnique = jest.fn().mockReturnValueOnce(user); - const result = await prismaUserRepository.findOne(where); + it('should call prismaService.findMany when findMany is called', async () => { + const users = [ + { + id: 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf', + username: 'John Doe', + password: '123456', + }, + { + id: 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf', + username: 'Jane Doe', + password: '123456', + }, + ] as User[]; + prismaService.user.findMany = jest.fn().mockReturnValueOnce(users); + + const result = await prismaUserRepository.findMany(); - expect(result).toEqual(user); - expect(prismaService.user.findUnique).toHaveBeenCalledWith({ where }); + expect(result).toEqual(users); + expect(prismaService.user.findMany).toHaveBeenCalled(); }); - it('should call prismaService.create when create is called', async () => { - const userData: Prisma.UserCreateInput = { + it('should call prismaService.update when update is called', async () => { + const id = 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf'; + const data: Prisma.UserUpdateInput = { username: 'Alice', - password: '123456', }; - const createdUser = { + const updatedUser = { id: 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf', username: 'Alice', password: '123456', } as User; - prismaService.user.create = jest.fn().mockReturnValueOnce(createdUser); + prismaService.user.update = jest.fn().mockReturnValueOnce(updatedUser); - const result = await prismaUserRepository.create(userData); + const result = await prismaUserRepository.update(id, data); - expect(result).toEqual(createdUser); - expect(prismaService.user.create).toHaveBeenCalledWith({ data: userData }); + expect(result).toEqual(updatedUser); + expect(prismaService.user.update).toHaveBeenCalledWith({ + where: { id }, + data, + }); }); + + it('should call prismaService.delete when delete is called', async () => { + const id = 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf'; + + prismaService.user.delete = jest.fn().mockResolvedValueOnce(undefined); + + await prismaUserRepository.delete(id); + + expect(prismaService.user.delete).toHaveBeenCalledWith({ + where: { id }, + }); + }); + }); diff --git a/libs/common/src/database/prisma/repository/prisma-user.repository.ts b/libs/common/src/database/prisma/repository/prisma-user.repository.ts index 4439bd6..dacdda2 100644 --- a/libs/common/src/database/prisma/repository/prisma-user.repository.ts +++ b/libs/common/src/database/prisma/repository/prisma-user.repository.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { Prisma } from '@prisma/client'; +import { Prisma, User } from '@prisma/client'; import { UserRepository } from '../../repository/user.repositoy'; import { PrismaService } from '../prisma.service'; @@ -7,14 +7,40 @@ import { PrismaService } from '../prisma.service'; export class PrismaUserRepository implements UserRepository { constructor(private prisma: PrismaService) { } - async findOne(where: Prisma.UserWhereUniqueInput) { + async findById(id: string): Promise { const user = await this.prisma.user.findUnique({ - where, - }); + where: { + id, + }, + }) + + if (!user) { + return null + } + + return user; + } + + async findByUsername(username: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { + username, + }, + }) + + if (!user) { + return null + } return user; } + async findMany(): Promise { + const users = await this.prisma.user.findMany() + + return users; + } + async create(data: Prisma.UserCreateInput) { const user = await this.prisma.user.create({ data, @@ -22,4 +48,24 @@ export class PrismaUserRepository implements UserRepository { return user; } + + async update(id: string, data: Prisma.UserUpdateInput): Promise { + const user = this.prisma.user.update({ + where: { + id + }, + data, + }); + + return user + } + + async delete(id: string): Promise { + await this.prisma.user.delete({ + where: { + id, + }, + }) + } + } diff --git a/libs/common/src/database/repository/user.repositoy.ts b/libs/common/src/database/repository/user.repositoy.ts index 89f1412..e6592b6 100644 --- a/libs/common/src/database/repository/user.repositoy.ts +++ b/libs/common/src/database/repository/user.repositoy.ts @@ -1,6 +1,10 @@ import { Prisma, User } from "@prisma/client"; export abstract class UserRepository { - abstract findOne(where: Prisma.UserWhereUniqueInput): Promise - abstract create(user: any): Promise + abstract findById(id: string): Promise + abstract findByUsername(username: string): Promise + abstract findMany(): Promise + abstract create(data: Prisma.UserCreateInput): Promise + abstract update(id: string, data: Prisma.UserUpdateInput): Promise + abstract delete(id: string): Promise } \ No newline at end of file diff --git a/package.json b/package.json index 379df37..d2d6b56 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "test:e2e": "jest --config ./jest-e2e.json" }, "dependencies": { - "@aws-sdk/client-s3": "^3.417.0", "@nestjs/common": "^10.2.5", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", @@ -85,4 +84,4 @@ "^@app/common(|/.*)$": "/libs/common/src/$1" } } -} +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index eff9c3f..03e8ece 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,7 +12,7 @@ async function bootstrap() { const configService = app.get(EnvService) const port = configService.get('PORT') - app.useGlobalPipes(new ValidationPipe({ whitelist: true })); + app.useGlobalPipes(new ValidationPipe()); const config = new DocumentBuilder() .setTitle('API') diff --git a/src/modules/users/controllers/create-user.controller.e2e-spec.ts b/src/modules/users/controllers/create-user.controller.e2e-spec.ts index 0bc44eb..22f7b76 100644 --- a/src/modules/users/controllers/create-user.controller.e2e-spec.ts +++ b/src/modules/users/controllers/create-user.controller.e2e-spec.ts @@ -1,13 +1,15 @@ -import { INestApplication } from '@nestjs/common'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import * as request from 'supertest'; import { UsersModule } from '../users.module'; import { DatabaseModule, PrismaService } from '@app/common'; +import { randomUUID } from 'node:crypto' -describe('User (E2E)', () => { +describe('User (E2E) Create', () => { let app: INestApplication; let prisma: PrismaService; let httpServer: any; + let UUID: string; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ @@ -17,26 +19,66 @@ describe('User (E2E)', () => { prisma = moduleRef.get(PrismaService); app = moduleRef.createNestApplication(); + app.useGlobalPipes(new ValidationPipe()); httpServer = app.getHttpServer(); await app.init(); }); - + afterAll(async () => { await app.close(); }); + beforeEach(() => { + UUID = randomUUID(); + }) + test('[POST] /user', async () => { - const response = await request(httpServer).post(`/user/create`).send({}); + const response = await request(httpServer) + .post(`/user`) + .send({ + username: `user-${UUID}`, + password: '123456', + }); - expect(response.statusCode).toBe(200); + expect(response.statusCode).toBe(201); const userOnDatabase = await prisma.user.findFirst({ where: { - username: 'henrique', + username: `user-${UUID}` }, }); expect(userOnDatabase).toBeTruthy(); + expect(response.body).toEqual(userOnDatabase); + }); + + test('[POST] /user with invalid data', async () => { + const response = await request(httpServer) + .post(`/user`) + .send({ + username: '', + password: '123456', + }); + + expect(response.statusCode).toBe(400); + }); + + test('[POST] /user with already existing username', async () => { + await prisma.user.create({ + data: { + username: `user-${UUID}`, + password: '123456', + }, + }); + + const response = await request(httpServer) + .post(`/user`) + .send({ + username: `user-${UUID}`, + password: '123456', + }); + + expect(response.statusCode).toBe(409); }); }); diff --git a/src/modules/users/controllers/create-user.controller.ts b/src/modules/users/controllers/create-user.controller.ts index 5105008..28feb4b 100644 --- a/src/modules/users/controllers/create-user.controller.ts +++ b/src/modules/users/controllers/create-user.controller.ts @@ -1,24 +1,34 @@ import { Body, + ConflictException, Controller, HttpCode, + HttpException, HttpStatus, Post } from '@nestjs/common'; -import { CreateUserUseCase } from '../use-case/create-user'; -import { CreateUserDto } from '../dto/create-user.dto'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { CreateUserDto } from '../dto/create-user.dto'; import { UserDto } from '../dto/user.dto'; +import { CreateUserUseCase } from '../use-case/create-user'; -@Controller('user') +@Controller('/user') @ApiTags('user') export class CreateUserController { constructor(private createUserUseCase: CreateUserUseCase) { } - @HttpCode(HttpStatus.OK) @ApiOkResponse({ type: UserDto }) - @Post('create') - create(@Body() createUserDto: CreateUserDto) { - return this.createUserUseCase.create(); + @HttpCode(HttpStatus.CREATED) + @Post('') + handle(@Body() createUserDto: CreateUserDto) { + try { + const user = this.createUserUseCase.execute(createUserDto); + return user; + } catch (e) { + if (e instanceof ConflictException) { + throw new HttpException('This username is already in use', HttpStatus.CONFLICT); + } + throw new HttpException('Was not possible to register', HttpStatus.BAD_REQUEST); + } } } diff --git a/src/modules/users/controllers/delete-user.controller.e2e-spec.ts b/src/modules/users/controllers/delete-user.controller.e2e-spec.ts new file mode 100644 index 0000000..1c97425 --- /dev/null +++ b/src/modules/users/controllers/delete-user.controller.e2e-spec.ts @@ -0,0 +1,59 @@ +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import * as request from 'supertest'; +import { UsersModule } from '../users.module'; +import { DatabaseModule, PrismaService } from '@app/common'; +import { randomUUID } from 'node:crypto' + +describe('User (E2E) Delete', () => { + let app: INestApplication; + let prisma: PrismaService; + let httpServer: any; + let UUID: string; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [UsersModule, DatabaseModule], + providers: [], + }).compile(); + + prisma = moduleRef.get(PrismaService); + app = moduleRef.createNestApplication(); + app.useGlobalPipes(new ValidationPipe()); + httpServer = app.getHttpServer(); + + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(() => { + UUID = randomUUID(); + }) + + test('[DELETE] /user/:id', async () => { + const user = await prisma.user.create({ + data: { + username: `user-${UUID}`, + password: '123456', + }, + }); + + const response = await request(httpServer) + .delete(`/user/${user.id}`) + .send(); + + expect(response.statusCode).toBe(200); + + const userOnDatabase = await prisma.user.findUnique({ + where: { + id: user.id, + }, + }); + + expect(userOnDatabase).toBeNull(); + }); + +}); diff --git a/src/modules/users/controllers/delete-user.controller.ts b/src/modules/users/controllers/delete-user.controller.ts new file mode 100644 index 0000000..b90ddc5 --- /dev/null +++ b/src/modules/users/controllers/delete-user.controller.ts @@ -0,0 +1,18 @@ +import { + Controller, + Delete, + Param +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { DeleteUserUseCase } from '../use-case/delete-user'; + +@Controller('/user') +@ApiTags('user') +export class DeleteUserController { + constructor(private deleteUserUseCase: DeleteUserUseCase) { } + + @Delete(':id') + handle(@Param('id') id: string) { + return this.deleteUserUseCase.execute(id); + } +} diff --git a/src/modules/users/controllers/get-many-users.controller.e2e-spec.ts b/src/modules/users/controllers/get-many-users.controller.e2e-spec.ts new file mode 100644 index 0000000..08f1ba8 --- /dev/null +++ b/src/modules/users/controllers/get-many-users.controller.e2e-spec.ts @@ -0,0 +1,60 @@ +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import * as request from 'supertest'; +import { UsersModule } from '../users.module'; +import { DatabaseModule, PrismaService } from '@app/common'; +import { randomUUID } from 'node:crypto' + +describe('User (E2E) Get many', () => { + let app: INestApplication; + let prisma: PrismaService; + let httpServer: any; + let UUID: string; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [UsersModule, DatabaseModule], + providers: [], + }).compile(); + + prisma = moduleRef.get(PrismaService); + app = moduleRef.createNestApplication(); + app.useGlobalPipes(new ValidationPipe()); + httpServer = app.getHttpServer(); + + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(() => { + UUID = randomUUID(); + }) + + test('[GET] /user', async () => { + await prisma.user.createMany({ + data: [ + { + username: `user-${UUID}`, + password: '123456', + }, + { + username: `user-${randomUUID()}`, + password: '123456', + }, + ], + }); + + const response = await request(httpServer) + .get(`/user`) + .send(); + + expect(response.statusCode).toBe(200); + + const usersOnDatabase = await prisma.user.findMany(); + + expect(response.body).toEqual(usersOnDatabase); + }); +}); diff --git a/src/modules/users/controllers/get-many-users.controller.ts b/src/modules/users/controllers/get-many-users.controller.ts new file mode 100644 index 0000000..2afafce --- /dev/null +++ b/src/modules/users/controllers/get-many-users.controller.ts @@ -0,0 +1,19 @@ +import { + Controller, + Get +} from '@nestjs/common'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { UserDto } from '../dto/user.dto'; +import { GetManyUsersUseCase } from '../use-case/get-many-users'; + +@Controller('/user') +@ApiTags('user') +export class GetManyUsersController { + constructor(private getManyUsersUseCase: GetManyUsersUseCase) { } + + @ApiOkResponse({ type: UserDto, isArray: true }) + @Get('') + handle() { + return this.getManyUsersUseCase.execute(); + } +} diff --git a/src/modules/users/controllers/get-user-by-id.controller.e2e-spec.ts b/src/modules/users/controllers/get-user-by-id.controller.e2e-spec.ts new file mode 100644 index 0000000..eb7f95a --- /dev/null +++ b/src/modules/users/controllers/get-user-by-id.controller.e2e-spec.ts @@ -0,0 +1,59 @@ +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import * as request from 'supertest'; +import { UsersModule } from '../users.module'; +import { DatabaseModule, PrismaService } from '@app/common'; +import { randomUUID } from 'node:crypto' + +describe('User (E2E) Get by id', () => { + let app: INestApplication; + let prisma: PrismaService; + let httpServer: any; + let UUID: string; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [UsersModule, DatabaseModule], + providers: [], + }).compile(); + + prisma = moduleRef.get(PrismaService); + app = moduleRef.createNestApplication(); + app.useGlobalPipes(new ValidationPipe()); + httpServer = app.getHttpServer(); + + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(() => { + UUID = randomUUID(); + }) + + test('[GET] /user/:id', async () => { + const user = await prisma.user.create({ + data: { + username: `user-${UUID}`, + password: '123456', + }, + }); + + const response = await request(httpServer) + .get(`/user/${user.id}`) + .send(); + + expect(response.statusCode).toBe(200); + + const userOnDatabase = await prisma.user.findUnique({ + where: { + id: user.id, + }, + }); + + expect(response.body).toEqual(userOnDatabase); + }); + +}); diff --git a/src/modules/users/controllers/get-user-by-id.controller.ts b/src/modules/users/controllers/get-user-by-id.controller.ts new file mode 100644 index 0000000..7f7c556 --- /dev/null +++ b/src/modules/users/controllers/get-user-by-id.controller.ts @@ -0,0 +1,20 @@ +import { + Controller, + Get, + Param +} from '@nestjs/common'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { UserDto } from '../dto/user.dto'; +import { GetUserByIdUseCase } from '../use-case/get-user-by-id'; + +@Controller('/user') +@ApiTags('user') +export class GetUserByIdController { + constructor(private getUserByIdUseCase: GetUserByIdUseCase) { } + + @ApiOkResponse({ type: UserDto }) + @Get(':id') + handle(@Param('id') id: string) { + return this.getUserByIdUseCase.execute(id); + } +} diff --git a/src/modules/users/controllers/update-user.controller.e2e-spec.ts b/src/modules/users/controllers/update-user.controller.e2e-spec.ts new file mode 100644 index 0000000..e1dcd69 --- /dev/null +++ b/src/modules/users/controllers/update-user.controller.e2e-spec.ts @@ -0,0 +1,63 @@ +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import * as request from 'supertest'; +import { UsersModule } from '../users.module'; +import { DatabaseModule, PrismaService } from '@app/common'; +import { randomUUID } from 'node:crypto' + +describe('User (E2E) Update', () => { + let app: INestApplication; + let prisma: PrismaService; + let httpServer: any; + let UUID: string; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [UsersModule, DatabaseModule], + providers: [], + }).compile(); + + prisma = moduleRef.get(PrismaService); + app = moduleRef.createNestApplication(); + app.useGlobalPipes(new ValidationPipe()); + httpServer = app.getHttpServer(); + + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(() => { + UUID = randomUUID(); + }) + + test('[PATCH] /user/:id', async () => { + const user = await prisma.user.create({ + data: { + username: `user-${UUID}`, + password: '123456', + }, + }); + + const updateUserDto = { + username: `updated-user-${UUID}`, + }; + + const response = await request(httpServer) + .patch(`/user/${user.id}`) + .send(updateUserDto); + + expect(response.statusCode).toBe(200); + + const userOnDatabase = await prisma.user.findUnique({ + where: { + id: user.id, + }, + }); + + expect(response.body).toEqual({ ...userOnDatabase, ...updateUserDto }); + }); + +}); diff --git a/src/modules/users/controllers/update-user.controller.ts b/src/modules/users/controllers/update-user.controller.ts new file mode 100644 index 0000000..dbce154 --- /dev/null +++ b/src/modules/users/controllers/update-user.controller.ts @@ -0,0 +1,26 @@ +import { + Body, + Controller, + Param, + Patch +} from '@nestjs/common'; +import { ApiOkResponse, ApiParam, ApiTags } from '@nestjs/swagger'; +import { UpdateUserDto } from '../dto/update-user.dto'; +import { UserDto } from '../dto/user.dto'; +import { UpdateUserUseCase } from '../use-case/update-user'; + +@Controller('/user') +@ApiTags('user') +export class UpdateUserController { + constructor(private updateUserUseCase: UpdateUserUseCase) { } + + @ApiOkResponse({ type: UserDto }) + @Patch(':id') + @ApiParam({ name: 'id' }) + handle( + @Param('id') id, + @Body() updateUserDto: UpdateUserDto + ) { + return this.updateUserUseCase.execute(id, updateUserDto); + } +} diff --git a/src/modules/users/dto/update-user.dto.ts b/src/modules/users/dto/update-user.dto.ts new file mode 100644 index 0000000..547fe64 --- /dev/null +++ b/src/modules/users/dto/update-user.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsOptional, + IsString, + MinLength +} from 'class-validator'; + +export class UpdateUserDto { + @IsString() + @IsOptional() + @MinLength(5) + @ApiProperty() + username?: string; + + @IsString() + @IsOptional() + @MinLength(6) + @ApiProperty() + password?: string; +} \ No newline at end of file diff --git a/src/modules/users/use-case/create-user.spec.ts b/src/modules/users/use-case/create-user.spec.ts index 94ecd5d..a161bc3 100644 --- a/src/modules/users/use-case/create-user.spec.ts +++ b/src/modules/users/use-case/create-user.spec.ts @@ -2,9 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { CreateUserUseCase } from './create-user'; import { UserRepository } from '@app/common'; import { User } from '@prisma/client'; +import { CreateUserDto } from '../dto/create-user.dto'; class UserRepositoryMock { - findUnique = jest.fn(); + findByUsername = jest.fn(); create = jest.fn(); } @@ -31,19 +32,48 @@ describe('CreateUserUseCase', () => { expect(createUserUseCase).toBeDefined(); }); - it('should create and user', async () => { - const user = { - id: 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf', + it('should create a user when the username is available', async () => { + const userDto: CreateUserDto = { username: 'henrique', password: '123', - } as User; - userRepository.create = jest.fn().mockReturnValueOnce(user); - const result = await createUserUseCase.create(); + }; + const user: User = { + id: 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf', + ...userDto, + }; + userRepository.findByUsername = jest.fn().mockResolvedValue(null); + userRepository.create = jest.fn().mockResolvedValue(user); + + const result = await createUserUseCase.execute(userDto); expect(result).toEqual(user); - expect(userRepository.create).toHaveBeenCalledWith({ + expect(userRepository.findByUsername).toHaveBeenCalledWith(userDto.username); + expect(userRepository.create).toHaveBeenCalledWith(userDto); + }); + + it('should throw a conflict exception when the username is not available', async () => { + const userDto: CreateUserDto = { username: 'henrique', password: '123', - }); + }; + userRepository.findByUsername = jest.fn().mockResolvedValue(true); + + await expect(createUserUseCase.execute(userDto)).rejects.toThrow('This username is already in use'); + + expect(userRepository.findByUsername).toHaveBeenCalledWith(userDto.username); + }); + + it('should throw a bad request exception when it was not possible to register', async () => { + const userDto: CreateUserDto = { + username: 'henrique', + password: '123', + }; + userRepository.findByUsername = jest.fn().mockResolvedValue(null); + userRepository.create = jest.fn().mockRejectedValue(new Error()); + + await expect(createUserUseCase.execute(userDto)).rejects.toThrow('Was not possible to register'); + + expect(userRepository.findByUsername).toHaveBeenCalledWith(userDto.username); + expect(userRepository.create).toHaveBeenCalledWith(userDto); }); }); diff --git a/src/modules/users/use-case/create-user.ts b/src/modules/users/use-case/create-user.ts index 20c8cc2..6acdd9c 100644 --- a/src/modules/users/use-case/create-user.ts +++ b/src/modules/users/use-case/create-user.ts @@ -1,17 +1,24 @@ import { UserRepository } from '@app/common/database/repository/user.repositoy'; -import { Injectable } from '@nestjs/common'; +import { BadRequestException, ConflictException, Injectable } from '@nestjs/common'; +import { CreateUserDto } from '../dto/create-user.dto'; @Injectable() export class CreateUserUseCase { constructor(private readonly userRepository: UserRepository) { } - async create() { - const user = await this.userRepository.create({ - username: 'henrique', - password: '123', - }); + async execute(createUserDto: CreateUserDto) { + const isUsernameAvailable = await this.userRepository.findByUsername(createUserDto.username); - return user + if (isUsernameAvailable) { + throw new ConflictException('This username is already in use'); + } + + try { + const user = await this.userRepository.create(createUserDto); + return user; + } catch (e) { + throw new BadRequestException('Was not possible to register'); + } } } diff --git a/src/modules/users/use-case/delete-user.spec.ts b/src/modules/users/use-case/delete-user.spec.ts new file mode 100644 index 0000000..a175517 --- /dev/null +++ b/src/modules/users/use-case/delete-user.spec.ts @@ -0,0 +1,50 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DeleteUserUseCase } from './delete-user'; +import { UserRepository } from '@app/common'; +import { InternalServerErrorException } from '@nestjs/common'; + +class UserRepositoryMock { + delete = jest.fn(); +} + +describe('DeleteUserUseCase', () => { + let deleteUserUseCase: DeleteUserUseCase; + let userRepository: UserRepository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DeleteUserUseCase, + { + provide: UserRepository, + useClass: UserRepositoryMock, + }, + ], + }).compile(); + + deleteUserUseCase = module.get(DeleteUserUseCase); + userRepository = module.get(UserRepository); + }); + + it('should be defined', () => { + expect(deleteUserUseCase).toBeDefined(); + }); + + it('should delete a user', async () => { + const id = 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf'; + userRepository.delete = jest.fn().mockResolvedValue(undefined); + + await deleteUserUseCase.execute(id); + + expect(userRepository.delete).toHaveBeenCalledWith(id); + }); + + it('should throw an internal server error exception when it was not possible to delete', async () => { + const id = 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf'; + userRepository.delete = jest.fn().mockRejectedValue(new Error()); + + await expect(deleteUserUseCase.execute(id)).rejects.toThrow(InternalServerErrorException); + + expect(userRepository.delete).toHaveBeenCalledWith(id); + }); +}); diff --git a/src/modules/users/use-case/delete-user.ts b/src/modules/users/use-case/delete-user.ts new file mode 100644 index 0000000..b99f7b3 --- /dev/null +++ b/src/modules/users/use-case/delete-user.ts @@ -0,0 +1,16 @@ + +import { UserRepository } from '@app/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; + +@Injectable() +export class DeleteUserUseCase { + constructor(private readonly userRepository: UserRepository) { } + + async execute(id: string) { + try { + await this.userRepository.delete(id); + } catch (e) { + throw new InternalServerErrorException(); + } + } +} diff --git a/src/modules/users/use-case/get-many-users.spec.ts b/src/modules/users/use-case/get-many-users.spec.ts new file mode 100644 index 0000000..955b9f8 --- /dev/null +++ b/src/modules/users/use-case/get-many-users.spec.ts @@ -0,0 +1,53 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { GetManyUsersUseCase } from './get-many-users'; +import { UserRepository } from '@app/common'; +import { User } from '@prisma/client'; + +class UserRepositoryMock { + findMany = jest.fn(); +} + +describe('GetManyUsersUseCase', () => { + let getManyUsersUseCase: GetManyUsersUseCase; + let userRepository: UserRepository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + GetManyUsersUseCase, + { + provide: UserRepository, + useClass: UserRepositoryMock, + }, + ], + }).compile(); + + getManyUsersUseCase = module.get(GetManyUsersUseCase); + userRepository = module.get(UserRepository); + }); + + it('should be defined', () => { + expect(getManyUsersUseCase).toBeDefined(); + }); + + it('should get many users', async () => { + const users: User[] = [ + { + id: 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf', + username: 'henrique', + password: '123', + }, + { + id: 'sdfsdf-sdfsdfsd-sdfsdfsd-sdfsdf', + username: 'john', + password: '456', + }, + ]; + userRepository.findMany = jest.fn().mockResolvedValue(users); + + const result = await getManyUsersUseCase.execute(); + + expect(result).toEqual(users); + expect(userRepository.findMany).toHaveBeenCalled(); + }); +}); diff --git a/src/modules/users/use-case/get-many-users.ts b/src/modules/users/use-case/get-many-users.ts new file mode 100644 index 0000000..2555d23 --- /dev/null +++ b/src/modules/users/use-case/get-many-users.ts @@ -0,0 +1,14 @@ + +import { UserRepository } from '@app/common'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class GetManyUsersUseCase { + constructor(private readonly userRepository: UserRepository) { } + + async execute() { + const users = await this.userRepository.findMany(); + + return users; + } +} diff --git a/src/modules/users/use-case/get-user-by-id.spec.ts b/src/modules/users/use-case/get-user-by-id.spec.ts new file mode 100644 index 0000000..3007569 --- /dev/null +++ b/src/modules/users/use-case/get-user-by-id.spec.ts @@ -0,0 +1,56 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { GetUserByIdUseCase } from './get-user-by-id'; +import { UserRepository } from '@app/common'; +import { User } from '@prisma/client'; +import { NotFoundException } from '@nestjs/common'; + +class UserRepositoryMock { + findById = jest.fn(); +} + +describe('GetUserByIdUseCase', () => { + let getUserByIdUseCase: GetUserByIdUseCase; + let userRepository: UserRepository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + GetUserByIdUseCase, + { + provide: UserRepository, + useClass: UserRepositoryMock, + }, + ], + }).compile(); + + getUserByIdUseCase = module.get(GetUserByIdUseCase); + userRepository = module.get(UserRepository); + }); + + it('should be defined', () => { + expect(getUserByIdUseCase).toBeDefined(); + }); + + it('should get a user by id', async () => { + const user: User = { + id: 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf', + username: 'henrique', + password: '123', + }; + userRepository.findById = jest.fn().mockResolvedValue(user); + + const result = await getUserByIdUseCase.execute(user.id); + + expect(result).toEqual(user); + expect(userRepository.findById).toHaveBeenCalledWith(user.id); + }); + + it('should throw a not found exception when the user is not found', async () => { + const id = 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf'; + userRepository.findById = jest.fn().mockResolvedValue(null); + + await expect(getUserByIdUseCase.execute(id)).rejects.toThrow(NotFoundException); + + expect(userRepository.findById).toHaveBeenCalledWith(id); + }); +}); diff --git a/src/modules/users/use-case/get-user-by-id.ts b/src/modules/users/use-case/get-user-by-id.ts new file mode 100644 index 0000000..c0553fb --- /dev/null +++ b/src/modules/users/use-case/get-user-by-id.ts @@ -0,0 +1,18 @@ + +import { UserRepository } from '@app/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; + +@Injectable() +export class GetUserByIdUseCase { + constructor(private readonly userRepository: UserRepository) { } + + async execute(id: string) { + const user = await this.userRepository.findById(id); + + if (!user) { + throw new NotFoundException(); + } + + return user; + } +} diff --git a/src/modules/users/use-case/update-user.spec.ts b/src/modules/users/use-case/update-user.spec.ts new file mode 100644 index 0000000..4a3aa3d --- /dev/null +++ b/src/modules/users/use-case/update-user.spec.ts @@ -0,0 +1,86 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UpdateUserUseCase } from './update-user'; +import { UserRepository } from '@app/common'; +import { User } from '@prisma/client'; +import { UpdateUserDto } from '../dto/update-user.dto'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; + +class UserRepositoryMock { + findById = jest.fn(); + update = jest.fn(); +} + +describe('UpdateUserUseCase', () => { + let updateUserUseCase: UpdateUserUseCase; + let userRepository: UserRepository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + UpdateUserUseCase, + { + provide: UserRepository, + useClass: UserRepositoryMock, + }, + ], + }).compile(); + + updateUserUseCase = module.get(UpdateUserUseCase); + userRepository = module.get(UserRepository); + }); + + it('should be defined', () => { + expect(updateUserUseCase).toBeDefined(); + }); + + it('should update a user', async () => { + const id = 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf'; + const updateUserDto: UpdateUserDto = { + username: 'new-username', + }; + const user: User = { + id, + username: 'old-username', + password: '123', + }; + userRepository.findById = jest.fn().mockResolvedValue(user); + userRepository.update = jest.fn().mockResolvedValue({ ...user, ...updateUserDto }); + + const result = await updateUserUseCase.execute(id, updateUserDto); + + expect(result).toEqual({ ...user, ...updateUserDto }); + expect(userRepository.findById).toHaveBeenCalledWith(id); + expect(userRepository.update).toHaveBeenCalledWith(id, { ...user, ...updateUserDto }); + }); + + it('should throw a not found exception when the user is not found', async () => { + const id = 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf'; + const updateUserDto: UpdateUserDto = { + username: 'new-username', + }; + userRepository.findById = jest.fn().mockResolvedValue(null); + + await expect(updateUserUseCase.execute(id, updateUserDto)).rejects.toThrow(NotFoundException); + + expect(userRepository.findById).toHaveBeenCalledWith(id); + }); + + it('should throw a bad request exception when it was not possible to update', async () => { + const id = 'fsdfsd-sdfsdfsd-sdfsdfsd-sdfsdf'; + const updateUserDto: UpdateUserDto = { + username: 'new-username', + }; + const user: User = { + id, + username: 'old-username', + password: '123', + }; + userRepository.findById = jest.fn().mockResolvedValue(user); + userRepository.update = jest.fn().mockRejectedValue(new Error()); + + await expect(updateUserUseCase.execute(id, updateUserDto)).rejects.toThrow(BadRequestException); + + expect(userRepository.findById).toHaveBeenCalledWith(id); + expect(userRepository.update).toHaveBeenCalledWith(id, { ...user, ...updateUserDto }); + }); +}); diff --git a/src/modules/users/use-case/update-user.ts b/src/modules/users/use-case/update-user.ts new file mode 100644 index 0000000..10ca869 --- /dev/null +++ b/src/modules/users/use-case/update-user.ts @@ -0,0 +1,29 @@ + +import { UserRepository } from '@app/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { UpdateUserDto } from '../dto/update-user.dto'; + +@Injectable() +export class UpdateUserUseCase { + constructor(private readonly userRepository: UserRepository) { } + + async execute(id: string, updateUserDto: UpdateUserDto) { + const user = await this.userRepository.findById(id); + + if (!user) { + throw new NotFoundException(); + } + + try { + const updatedData = { + ...user, + ...updateUserDto, + }; + + const userUpdated = await this.userRepository.update(id, updatedData); + return userUpdated; + } catch (e) { + throw new BadRequestException('Was not possible to register'); + } + } +} diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts index a7653ba..bf09f23 100644 --- a/src/modules/users/users.module.ts +++ b/src/modules/users/users.module.ts @@ -2,11 +2,19 @@ import { DatabaseModule } from '@app/common'; import { Module } from '@nestjs/common'; import { CreateUserController } from './controllers/create-user.controller'; import { CreateUserUseCase } from './use-case/create-user'; +import { GetUserByIdController } from './controllers/get-user-by-id.controller'; +import { GetUserByIdUseCase } from './use-case/get-user-by-id'; +import { GetManyUsersUseCase } from './use-case/get-many-users'; +import { GetManyUsersController } from './controllers/get-many-users.controller'; +import { DeleteUserController } from './controllers/delete-user.controller'; +import { DeleteUserUseCase } from './use-case/delete-user'; +import { UpdateUserUseCase } from './use-case/update-user'; +import { UpdateUserController } from './controllers/update-user.controller'; @Module({ - controllers: [CreateUserController], + controllers: [CreateUserController, GetUserByIdController, GetManyUsersController, DeleteUserController, UpdateUserController], imports: [DatabaseModule], - providers: [CreateUserUseCase], + providers: [CreateUserUseCase, GetUserByIdUseCase, GetManyUsersUseCase, DeleteUserUseCase, UpdateUserUseCase], exports: [], }) export class UsersModule { }