Skip to content

Commit

Permalink
Merge pull request #85 from Quest-Finder/refactor/user-sign-up-email
Browse files Browse the repository at this point in the history
refactor(user-with-email): move to nestjs architecture
  • Loading branch information
emerson-oliveira authored Oct 29, 2024
2 parents 5267066 + 37157c9 commit 0823d13
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 42 deletions.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { ErrorDetail, ErrorDetailField } from '@/shared/dtos/error-details.dto'
import { ZodValidationPipePipe } from '@/shared/zod-validation-pipe/zod-validation-pipe.pipe'
import { Body, Controller, Post } from '@nestjs/common'
import { ApiBody, ApiResponse, ApiTags } from '@nestjs/swagger'
import { SignUpWithEmailDto, inputSignUpData } from './sign-up-with-email-dto'
import { SignUpService } from './sign-up-with-email.service'
import { SignUpService } from '../../service/sign-up-with-email/sign-up-with-email.service'
import { SignUpWithEmailDto, inputSignUpData } from './dto/sign-up-with-email-dto'

@ApiTags('SignUp-With-Email')
@Controller('/user/signup')
Expand All @@ -13,7 +13,7 @@ export class SignUpController {
@Post('/email')
@ApiBody({ type: SignUpWithEmailDto })
@ApiResponse({
status: 201,
status: 201,
description: 'Sucesso: Usuário Cadastrado',
schema: { type: 'string', example: 'asdadasdajsdhasdá8asd.asd6a54a6sd46a8asdjiqwhw.as5da4sd6sa8' }
})
Expand Down
5 changes: 5 additions & 0 deletions src/users/repository/entity/user-with-email.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type UserWithEmailModel = {
id: string
email: string
password: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { PrismaService } from '@/shared/prisma/prisma.service'
import { Test, type TestingModule } from '@nestjs/testing'
import { type UserWithEmailModel } from '../entity/user-with-email.model'
import { UserWithEmailRepository } from './user-with-email-repository'

const makeUserWithEmail = (): UserWithEmailModel => {
return {
email: 'valid@email.com',
id: 'valid-id',
password: 'encoded_password'
}
}

describe('UserWithEmailRepository', () => {
let repository: UserWithEmailRepository
let prismaService: PrismaService
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserWithEmailRepository, PrismaService]
}).compile()
repository = module.get<UserWithEmailRepository>(UserWithEmailRepository)
prismaService = module.get<PrismaService>(PrismaService)
})

beforeEach(async () => {
await prismaService.userPreferenceRpgStyle.deleteMany()
await prismaService.userPreferenceDayPeriod.deleteMany()
await prismaService.userPreferenceGamePlace.deleteMany()
await prismaService.userPreferencePlayersRange.deleteMany()
await prismaService.externalAuthMapping.deleteMany()
await prismaService.userPreference.deleteMany()
await prismaService.userSocialMedia.deleteMany()
await prismaService.userConfig.deleteMany()
await prismaService.userBadge.deleteMany()
await prismaService.user.deleteMany()
await prismaService.address.deleteMany()
await prismaService.cityState.deleteMany()
await prismaService.userWithEmail.deleteMany()
await prismaService.playerProfile.deleteMany()
await prismaService.rpgStyle.deleteMany()
await prismaService.badge.deleteMany()
await prismaService.socialMedia.deleteMany()
})

afterAll(async () => {
await prismaService.$disconnect()
})

it('should be defined', () => {
expect(repository).toBeDefined()
})

describe('Find UserWithEmail by email', () => {
it('should return undefined when email not found', async () => {
const response = await repository.findByEmail('invalid@email.com')
expect(response).not.toBeTruthy()
})

it('should return a valid user', async () => {
await prismaService.userWithEmail.create({
data: makeUserWithEmail()
})
const result = await repository.findByEmail(makeUserWithEmail().email)
expect(result).toEqual(expect.objectContaining(makeUserWithEmail()))
})
})

describe('Save a UserWithEmail ', () => {
it('should save a user with email', async () => {
const inputData = {
email: 'invalid@email.com',
password: 'hashed_password'
}
const savedUser = await repository.save(inputData)
expect(savedUser.id).toBeTruthy()
expect(savedUser).toEqual(expect.objectContaining(inputData))
})
})
})
30 changes: 30 additions & 0 deletions src/users/repository/user-with-email/user-with-email-repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { PrismaService } from '@/shared/prisma/prisma.service'
import { Injectable } from '@nestjs/common'
import { v4 } from 'uuid'
import { type UserWithEmailModel } from '../entity/user-with-email.model'

export type UserWithEmailData = Omit<UserWithEmailModel, 'id'>

@Injectable()
export class UserWithEmailRepository {
constructor (private readonly prismaService: PrismaService) { }

async findByEmail (email: string): Promise<UserWithEmailModel | undefined> {
const result = await this.prismaService.userWithEmail.findUnique({
where: {
email
}
})
return result ?? undefined
}

async save (data: UserWithEmailData): Promise<UserWithEmailModel> {
const result = await this.prismaService.userWithEmail.create({
data: {
id: v4(),
...data
}
})
return result
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
import { JwtSignAdapterV2 } from '@/infra/cryptography/jwt-sign-adapter-v2'
import { PrismaService } from '@/shared/prisma/prisma.service'
import { type UserWithEmailModel } from '@/users/repository/entity/user-with-email.model'
import { type UserWithEmailData, UserWithEmailRepository } from '@/users/repository/user-with-email/user-with-email-repository'
import { ConflictException } from '@nestjs/common'
import { Test, type TestingModule } from '@nestjs/testing'
import bcrypt from 'bcrypt'
import { SignUpService } from './sign-up-with-email.service'

const makeUserWithEmail = (): UserWithEmailModel => {
return {
email: 'valid@email.com',
id: 'valid-id',
password: 'encoded_password'
}
}
class MockUserWithEmailRepository {
async findByEmail (email: string): Promise<UserWithEmailModel | undefined> {
return makeUserWithEmail()
}

async save (data: UserWithEmailData): Promise<UserWithEmailModel> {
return makeUserWithEmail()
}
}

describe('SignUpService', () => {
let service: SignUpService

let repository: UserWithEmailRepository
const SALTED_ROUNDS = 10

const mockPrismaService = {
userWithEmail: {
findUnique: jest.fn(),
create: jest.fn()
}
}

const mockHashAdapter = {
hash: jest.fn().mockResolvedValue('hashed-password')
}
Expand All @@ -29,13 +40,13 @@ describe('SignUpService', () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SignUpService,
{ provide: PrismaService, useValue: mockPrismaService },
{ provide: 'HashAdapter', useValue: mockHashAdapter },
{ provide: JwtSignAdapterV2, useValue: mockJwtSignAdapter }
{ provide: JwtSignAdapterV2, useValue: mockJwtSignAdapter },
{ provide: UserWithEmailRepository, useClass: MockUserWithEmailRepository }
]
}).compile()

service = module.get<SignUpService>(SignUpService)
repository = module.get<UserWithEmailRepository>(UserWithEmailRepository)
})

afterEach(() => {
Expand All @@ -49,32 +60,26 @@ describe('SignUpService', () => {
it('should create a new user and return a token', async () => {
jest.spyOn(bcrypt, 'hash').mockReturnValueOnce('hashed-password')
const bcryptHashSpy = jest.spyOn(bcrypt, 'hash').mockReturnValueOnce('hashed-password')

mockPrismaService.userWithEmail.findUnique.mockResolvedValueOnce(null)
mockPrismaService.userWithEmail.create.mockResolvedValueOnce({ id: 'some-uuid' })
const repositorySaveSpy = jest.spyOn(repository, 'save')
jest.spyOn(repository, 'findByEmail').mockResolvedValueOnce(undefined)
mockJwtSignAdapter.execute.mockResolvedValueOnce({ token: 'some-token' })

const result = await service.create({ email: 'newuser@example.com', password: '123456' })

expect(bcryptHashSpy).toHaveBeenCalledWith('123456', SALTED_ROUNDS)
expect(mockPrismaService.userWithEmail.create).toHaveBeenCalledWith({
data: {
id: expect.any(String),
email: 'newuser@example.com',
password: 'hashed-password'
}
expect(repositorySaveSpy).toHaveBeenCalledWith({
email: 'newuser@example.com',
password: 'hashed-password'
})
expect(result).toEqual({ token: 'some-token' })
})

it('should throw ConflictException if user already exists', async () => {
mockPrismaService.userWithEmail.findUnique.mockResolvedValueOnce({ email: 'test@example.com' })
const repositoryFindByEmailSpy = jest.spyOn(repository, 'findByEmail')

await expect(service.create({ email: 'test@example.com', password: 'whateverpassword123' }))
.rejects.toThrow(new ConflictException('Já existe um email cadastrado com o test@example.com informado'))

expect(mockPrismaService.userWithEmail.findUnique).toHaveBeenCalledWith({
where: { email: 'test@example.com' }
})
expect(repositoryFindByEmailSpy).toHaveBeenCalledWith('test@example.com')
})
})
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
import { JwtSignAdapterV2 } from '@/infra/cryptography/jwt-sign-adapter-v2'
import { PrismaService } from '@/shared/prisma/prisma.service'
import { UserWithEmailRepository } from '@/users/repository/user-with-email/user-with-email-repository'
import { ConflictException, Injectable } from '@nestjs/common'
import bcrypt from 'bcrypt'
import { v4 } from 'uuid'
import { SignUpWithEmailDto } from './sign-up-with-email-dto'
import { SignUpWithEmailDto } from '../../controllers/sign-up-with-email/dto/sign-up-with-email-dto'

const SALT_ROUNDS = 10

@Injectable()
export class SignUpService {
constructor (
private readonly prismaService: PrismaService,
private readonly repository: UserWithEmailRepository,
private readonly jwtSignAdapterV2: JwtSignAdapterV2
) {}

async create ({ email, password }: SignUpWithEmailDto): Promise<{ token: string }> {
const signUpData = new SignUpWithEmailDto(email, password)
const user = await this.prismaService.userWithEmail.findUnique({ where: { email } })
const user = await this.repository.findByEmail(email)

if (user) {
throw new ConflictException(`Já existe um email cadastrado com o ${email} informado`)
}

const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS)
await this.prismaService.userWithEmail.create({
data: {
id: v4(),
email: signUpData.email,
password: hashedPassword
}
await this.repository.save({
email: signUpData.email,
password: hashedPassword
})

const token = this.jwtSignAdapterV2.execute(email)
Expand Down
8 changes: 5 additions & 3 deletions src/users/users.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SocialMediaRepository } from '@/social-media/repository/social-media-re
import { HttpModule } from '@nestjs/axios'
import { Module, RequestMethod, type MiddlewareConsumer, type NestModule } from '@nestjs/common'
import { FakeUserController } from './controllers/fake-user/fake-user.controller'
import { SignUpController } from './controllers/sign-up-with-email/sign-up-with-email.controller'
import { UserSocialMediaController } from './controllers/social-media/social-media.controller'
import { UserPreferenceDayPeriodController } from './controllers/user-preference-day-period/user-preference-day-period.controller'
import { UserPreferenceGamePlaceController } from './controllers/user-preference-game-place/user-preference-game-place.controller'
Expand All @@ -18,15 +19,15 @@ import { UserPreferenceDayPeriodRepository } from './repository/user-preference-
import { UserPreferenceGamePlaceRepository } from './repository/user-preference-game-place/user-preference-game-place-repository'
import { UserPreferenceRepository } from './repository/user-preference/user-preference.repository'
import { UserSocialMediaRepository } from './repository/user-social-media/user-social-media-repository'
import { UserWithEmailRepository } from './repository/user-with-email/user-with-email-repository'
import { UserRepository } from './repository/user/user-repository'
import { FakeUserService } from './service/fake-user/fake-user.service'
import { SignUpService } from './service/sign-up-with-email/sign-up-with-email.service'
import { UserPreferenceDayPeriodService } from './service/user-preference-day-period/user-preference-day-period.service'
import { UserPreferenceGamePlaceService } from './service/user-preference-game-place/user-preference-game-place.service'
import { UserPreferenceService } from './service/user-preference/user-preference.service'
import { UserSocialMediaService } from './service/user-social-media/user-social-media.service'
import { UserService } from './service/user/user.service'
import { SignUpController } from './sign-up/sign-up-with-email.controller'
import { SignUpService } from './sign-up/sign-up-with-email.service'

@Module({
providers: [
Expand All @@ -48,7 +49,8 @@ import { SignUpService } from './sign-up/sign-up-with-email.service'
RpgStylesRepository,
PlayersProfileRepository,
SignUpService,
JwtSignAdapterV2
JwtSignAdapterV2,
UserWithEmailRepository
],
controllers: [
UserSocialMediaController,
Expand Down

0 comments on commit 0823d13

Please sign in to comment.