diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index 71674c4..6a5b9c8 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -7,8 +7,10 @@ import { JwtService } from '@nestjs/jwt'; import { SignUpDto } from '../auth/dtos/signUp.dto'; import * as bcrypt from 'bcryptjs'; import { repositoryMockFactory } from '../../test/database/utils'; -import { UnauthorizedException } from '@nestjs/common'; +import { UnauthorizedException, BadRequestException } from '@nestjs/common'; import * as nodemailer from 'nodemailer'; +import { SignInDto } from './dtos/signIn.dto'; + describe('AuthService', () => { let service: AuthService; @@ -38,20 +40,21 @@ describe('AuthService', () => { }, ], }).compile(); - + service = module.get(AuthService); userRepository = module.get>(getRepositoryToken(User)); jwtService = module.get(JwtService); - - jest.spyOn(bcrypt, 'hash').mockResolvedValueOnce('hashed-password'); - jest.spyOn(bcrypt, 'genSalt').mockResolvedValueOnce(10); - + + jest.spyOn(bcrypt, 'hash').mockResolvedValue('hashed-password'); + jest.spyOn(bcrypt, 'genSalt').mockResolvedValue(10); + jest.spyOn(bcrypt, 'compare').mockResolvedValue(true); + sendMailMock = jest.fn(); jest.spyOn(nodemailer, 'createTransport').mockReturnValue({ sendMail: sendMailMock, } as any); }); - + //signUp describe('signUp', () => { it('should create a new user and return a signed token', async () => { const signUpDto: SignUpDto = { @@ -59,9 +62,9 @@ describe('AuthService', () => { lastName: 'User', email: 'test@email.com', phone: '123456789', - password: 'password', + password: 'ValidPassword123!', // Ensure the password meets the criteria }; - + const user = new User(); user.id = '18ea976e-367b-4138-b68e-7aff3f7ae4de'; user.firstName = signUpDto.firstName; @@ -69,60 +72,56 @@ describe('AuthService', () => { user.email = signUpDto.email; user.phone = signUpDto.phone; user.role = UserRoles.User; - + jest.spyOn(userRepository, 'findOneBy').mockResolvedValueOnce(null); - jest.spyOn(userRepository, 'create').mockReturnValue(user); jest.spyOn(userRepository, 'save').mockResolvedValue(user); jest.spyOn(service, 'signIn').mockResolvedValue({ accessToken: 'access-token', refreshToken: 'refresh-token', }); - + const response = await service.signUp(signUpDto); - + expect(userRepository.findOneBy).toHaveBeenCalledWith({ email: 'test@email.com', }); - + expect(userRepository.create).toHaveBeenCalledWith({ ...signUpDto, role: UserRoles.User, password: expect.any(String), }); - expect(bcrypt.hash).toHaveBeenCalledWith('password', 10); + expect(bcrypt.hash).toHaveBeenCalledWith('ValidPassword123!', 10); expect(userRepository.save).toHaveBeenCalled(); expect(response).toEqual({ accessToken: 'access-token', refreshToken: 'refresh-token', }); }); - + it('should throw an error if user already exists', async () => { const signUpDto: SignUpDto = { firstName: 'Test', lastName: 'User', email: 'test@email.com', phone: '123456789', - password: 'password', + password: 'ValidPassword123!', // Ensure the password meets the criteria }; - + const existingUser = new User(); - existingUser.email = 'existing@email.com'; - + existingUser.email = 'test@email.com'; + jest .spyOn(userRepository, 'findOneBy') .mockResolvedValueOnce(existingUser); - - try { - await service.signUp(signUpDto); - fail('An error should be thrown'); - } catch (error) { - expect(error).toBeInstanceOf(UnauthorizedException); - expect((error as Error).message).toBe('Usuário já cadastrado.'); - expect(userRepository.create).not.toHaveBeenCalled(); - expect(userRepository.save).not.toHaveBeenCalled(); - } + + await expect(service.signUp(signUpDto)).rejects.toThrow( + UnauthorizedException, + ); + expect((await service.signUp(signUpDto).catch(e => e)).message).toBe('Usuário já cadastrado.'); + expect(userRepository.create).not.toHaveBeenCalled(); + expect(userRepository.save).not.toHaveBeenCalled(); }); }); @@ -139,7 +138,7 @@ describe('AuthService', () => { jest.spyOn(bcrypt, 'compare').mockResolvedValueOnce(false); await expect(service.signIn({ email, password, role })).rejects.toThrow( - UnauthorizedException, + BadRequestException, ); expect(userRepository.findOneBy).toHaveBeenCalledWith({ email, role }); @@ -208,41 +207,195 @@ describe('AuthService', () => { }); describe('recoverPassword', () => { - it('should throw an UnauthorizedException if the user is not found', async () => { - const email = 'notfound@example.com'; - + it('should throw UnauthorizedException if the user is not found', async () => { + const email = 'nonexistent@example.com'; + jest.spyOn(userRepository, 'findOneBy').mockResolvedValueOnce(null); - const signSpy = jest.spyOn(jwtService, 'signAsync'); - + await expect(service.recoverPassword(email)).rejects.toThrow( UnauthorizedException, ); - - expect(userRepository.findOneBy).toHaveBeenCalledWith({ email }); - expect(signSpy).not.toHaveBeenCalled(); }); - + it('should handle errors during email sending', async () => { const email = 'test@example.com'; const user = new User(); user.id = '123'; user.email = email; - + jest.spyOn(userRepository, 'findOneBy').mockResolvedValueOnce(user); jest.spyOn(jwtService, 'signAsync').mockResolvedValueOnce('mocked-token'); - sendMailMock.mockRejectedValueOnce(new Error('Email service error')); - + await expect(service.recoverPassword(email)).rejects.toThrow( 'Email service error', ); + }); + }); + + describe('signIn with keepLoggedIn', () => { + it('should return a token with 30m expiration when keepLoggedIn is false', async () => { + const signInDto: SignInDto = { + email: 'test@example.com', + password: 'password', + role: UserRoles.User, + keepLoggedIn: false, + }; + const user = new User(); + user.id = 'user-id'; + user.email = signInDto.email; + user.password = 'hashed-password'; + user.role = UserRoles.User; + + jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(user); + jest.spyOn(bcrypt, 'compare').mockResolvedValue(true); + const signAsyncSpy = jest.spyOn(jwtService, 'signAsync'); + + const result = await service.signIn(signInDto); - expect(userRepository.findOneBy).toHaveBeenCalledWith({ email }); - expect(jwtService.signAsync).toHaveBeenCalledWith( - { sub: user.id }, + expect(result.accessToken).toBeDefined(); + + expect(signAsyncSpy).toHaveBeenNthCalledWith( + 1, + { sub: user.id, email: user.email, role: user.role }, { expiresIn: '30m' }, ); - expect(sendMailMock).toHaveBeenCalled(); + }); + + it('should return a token with 7d expiration when keepLoggedIn is true', async () => { + const signInDto: SignInDto = { + email: 'test@example.com', + password: 'password', + role: UserRoles.User, + keepLoggedIn: true, + }; + const user = new User(); + user.id = 'user-id'; + user.email = signInDto.email; + user.password = 'hashed-password'; + user.role = UserRoles.User; + + jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(user); + jest.spyOn(bcrypt, 'compare').mockResolvedValue(true); + const signAsyncSpy = jest.spyOn(jwtService, 'signAsync'); + + const result = await service.signIn(signInDto); + + expect(result.accessToken).toBeDefined(); + + expect(signAsyncSpy).toHaveBeenNthCalledWith( + 1, + { sub: user.id, email: user.email, role: user.role }, + { expiresIn: '7d' }, + ); + }); + }); + + describe('generateAccessToken', () => { + it('should generate an access token with the given payload and expiration', async () => { + const payload = { sub: '123', email: 'test@example.com', role: UserRoles.User }; + const expiresIn = '30m'; + + jest.spyOn(jwtService, 'signAsync').mockResolvedValueOnce('access-token'); + + const result = await service.generateAccessToken(payload, expiresIn); + + expect(result).toBe('access-token'); + expect(jwtService.signAsync).toHaveBeenCalledWith(payload, { expiresIn }); + }); + }); + + describe('generateRefreshToken', () => { + it('should generate a refresh token with the given payload', async () => { + const payload = { sub: '123', email: 'test@example.com', role: UserRoles.User }; + + jest.spyOn(jwtService, 'signAsync').mockResolvedValueOnce('refresh-token'); + + const result = await service.generateRefreshToken(payload); + + expect(result).toBe('refresh-token'); + expect(jwtService.signAsync).toHaveBeenCalledWith(payload); + }); + }); + + describe('changePassword', () => { + it('should update the password if the user exists', async () => { + const userId = '123'; + const newPassword = 'NewPassword123!'; + const user = new User(); + user.id = userId; + user.password = 'oldPassword'; + + jest.spyOn(userRepository, 'findOneBy').mockResolvedValueOnce(user); + jest.spyOn(bcrypt, 'hash').mockResolvedValueOnce('hashedNewPassword'); + jest.spyOn(userRepository, 'save').mockResolvedValueOnce(user); + + const result = await service.changePassword(userId, newPassword); + + expect(result).toEqual({ success: true }); + expect(bcrypt.hash).toHaveBeenCalledWith(newPassword, 10); + expect(userRepository.save).toHaveBeenCalledWith(user); + }); + + it('should throw UnauthorizedException if the user does not exist', async () => { + const userId = 'nonexistent'; + const newPassword = 'NewPassword123!'; + + jest.spyOn(userRepository, 'findOneBy').mockResolvedValueOnce(null); + + await expect(service.changePassword(userId, newPassword)).rejects.toThrow( + UnauthorizedException, + ); + }); + }); + + describe('validatePassword', () => { + it('should throw BadRequestException if password is less than 8 characters', async () => { + const shortPassword = 'Pass1!'; // 6 characters, missing length + await expect(() => + (service as any).validatePassword(shortPassword), + ).toThrowError(BadRequestException); + await expect(() => + (service as any).validatePassword(shortPassword), + ).toThrowError('A senha deve ter pelo menos 8 caracteres.'); + }); + + it('should throw BadRequestException if password has no uppercase letter', async () => { + const noUppercasePassword = 'password1!'; // Missing uppercase + await expect(() => + (service as any).validatePassword(noUppercasePassword), + ).toThrowError(BadRequestException); + await expect(() => + (service as any).validatePassword(noUppercasePassword), + ).toThrowError('A senha deve conter pelo menos uma letra maiúscula.'); + }); + + it('should throw BadRequestException if password has no number', async () => { + const noNumberPassword = 'Password!'; // Missing number + await expect(() => + (service as any).validatePassword(noNumberPassword), + ).toThrowError(BadRequestException); + await expect(() => + (service as any).validatePassword(noNumberPassword), + ).toThrowError('A senha deve conter pelo menos um número.'); + }); + + it('should throw BadRequestException if password has no special character', async () => { + const noSpecialCharPassword = 'Password1'; // Missing special character + await expect(() => + (service as any).validatePassword(noSpecialCharPassword), + ).toThrowError(BadRequestException); + await expect(() => + (service as any).validatePassword(noSpecialCharPassword), + ).toThrowError('A senha deve conter pelo menos um caractere especial.'); + }); + + it('should not throw an exception if password meets all criteria', async () => { + const validPassword = 'ValidPassword123!'; // Meets all criteria + expect(() => + (service as any).validatePassword(validPassword), + ).not.toThrow(); }); }); -}); + +}); \ No newline at end of file diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index a39eab0..aed5e10 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,4 +1,8 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { + Injectable, + UnauthorizedException, + BadRequestException, +} from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { InjectRepository } from '@nestjs/typeorm'; import { User, UserRoles } from '../database/entities/user.entity'; @@ -17,28 +21,64 @@ export class AuthService { private jwtService: JwtService, ) {} + private validatePassword(password: string): void { + const minLength = 8; + const hasUpperCase = /[A-Z]/.test(password); + const hasNumber = /[0-9]/.test(password); + const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); + + if (password.length < minLength) { + throw new BadRequestException( + 'A senha deve ter pelo menos 8 caracteres.', + ); + } + if (!hasUpperCase) { + throw new BadRequestException( + 'A senha deve conter pelo menos uma letra maiúscula.', + ); + } + if (!hasNumber) { + throw new BadRequestException( + 'A senha deve conter pelo menos um número.', + ); + } + if (!hasSpecialChar) { + throw new BadRequestException( + 'A senha deve conter pelo menos um caractere especial.', + ); + } + } + async signIn({ email, password, role, - }: SignInDto): Promise { + keepLoggedIn = false, + }: SignInDto & { keepLoggedIn?: boolean }): Promise { const user = await this.usersRepository.findOneBy({ email, role }); if (!user || !(await bcrypt.compare(password, user.password))) { - throw new UnauthorizedException('E-mail ou senha inválidos.'); + throw new BadRequestException('E-mail ou senha inválidos.'); } const payload = { sub: user.id, email: user.email, role: user.role }; + const accessTokenExpiresIn = keepLoggedIn ? '7d' : '30m'; + return { - accessToken: await this.jwtService.signAsync(payload), - refreshToken: await this.jwtService.signAsync(payload), + accessToken: await this.jwtService.signAsync(payload, { + expiresIn: accessTokenExpiresIn, + }), + refreshToken: await this.jwtService.signAsync(payload), }; } async signUp(dto: SignUpDto): Promise { + this.validatePassword(dto.password); // Validate password before proceeding + const userExists = await this.usersRepository.findOneBy({ email: dto.email, }); if (userExists) throw new UnauthorizedException('Usuário já cadastrado.'); + const user = this.usersRepository.create({ ...dto, role: UserRoles.User, @@ -51,6 +91,13 @@ export class AuthService { role: user.role, }); } + async generateAccessToken(payload: any, expiresIn: string): Promise { + return this.jwtService.signAsync(payload, { expiresIn }); + } + + async generateRefreshToken(payload: any): Promise { + return this.jwtService.signAsync(payload); + } async getProfile(data: { sub: string; email: string }): Promise { const user = await this.usersRepository.findOneBy({ id: data.sub }); diff --git a/src/auth/dtos/signIn.dto.ts b/src/auth/dtos/signIn.dto.ts index bca1074..e707e1c 100644 --- a/src/auth/dtos/signIn.dto.ts +++ b/src/auth/dtos/signIn.dto.ts @@ -11,4 +11,6 @@ export class SignInDto { @IsNotEmpty() role: UserRoles; + + keepLoggedIn?: boolean; } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 28aead7..f8a44e2 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,14 +1,14 @@ -import { - Injectable, - NotFoundException, - UnauthorizedException, -} from '@nestjs/common'; -import { UpdateUserDto } from './dtos/updateUser.dto'; import { InjectRepository } from '@nestjs/typeorm'; +import { UpdateUserDto } from './dtos/updateUser.dto'; import { Repository } from 'typeorm'; import { User } from '../database/entities/user.entity'; import * as bcrypt from 'bcrypt'; import { ListUsersQueryDto } from './dtos/listUsersQuery.dto'; +import { + Injectable, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; @Injectable() export class UsersService {