diff --git a/jest.config.ts b/jest.config.ts index 177e68c..97cef7c 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,5 +1,7 @@ import type { Config } from 'jest'; +import * as dotenv from 'dotenv'; +dotenv.config(); const config: Config = { preset: 'ts-jest', testEnvironment: 'node', @@ -16,6 +18,7 @@ const config: Config = { moduleNameMapper: { '^src/(.*)$': '/src/$1', }, + setupFiles: ['dotenv/config'], }; export default config; diff --git a/test/auth.controller.spec.ts b/test/auth.controller.spec.ts new file mode 100644 index 0000000..8520c20 --- /dev/null +++ b/test/auth.controller.spec.ts @@ -0,0 +1,177 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { UnauthorizedException } from '@nestjs/common'; +import { LoginDto } from 'src/users/dtos/login.dto'; +import { Request, Response } from 'express'; +import { AuthService } from 'src/auth/auth.service'; +import { AuthController } from 'src/auth/auth.controller'; + +describe('AuthController', () => { + let authController: AuthController; + let authService: AuthService; + let configService: ConfigService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + providers: [ + { + provide: AuthService, + useValue: { + validateUser: jest.fn(), + login: jest.fn(), + }, + }, + { + provide: ConfigService, + useValue: { + get: jest.fn(), + }, + }, + ], + }).compile(); + + authController = module.get(AuthController); + authService = module.get(AuthService); + configService = module.get(ConfigService); + + // Defina as variáveis de ambiente para os testes + process.env.FRONTEND_URL = 'http://localhost:3000'; + }); + + describe('login', () => { + it('should return a token if credentials are valid', async () => { + const loginDto: LoginDto = { + email: 'test@example.com', + password: 'password', + }; + const user = { id: 'user-id', email: 'test@example.com' }; + const token = 'token'; + authService.validateUser = jest.fn().mockResolvedValue(user); + authService.login = jest.fn().mockResolvedValue({ access_token: token }); + + const result = await authController.login(loginDto); + + expect(authService.validateUser).toHaveBeenCalledWith( + loginDto.email, + loginDto.password, + ); + expect(authService.login).toHaveBeenCalledWith(user); + expect(result).toEqual({ access_token: token }); + }); + + it('should throw UnauthorizedException if credentials are invalid', async () => { + const loginDto: LoginDto = { + email: 'test@example.com', + password: 'password', + }; + authService.validateUser = jest.fn().mockResolvedValue(null); + + await expect(authController.login(loginDto)).rejects.toThrow( + UnauthorizedException, + ); + }); + }); + + describe('googleAuth', () => { + it('should log initiation of Google auth', async () => { + const logSpy = jest.spyOn(authController['logger'], 'log'); + const frontendUrl = process.env.FRONTEND_URL; + configService.get = jest.fn().mockReturnValue(frontendUrl); + + await authController.googleAuth(); + + expect(logSpy).toHaveBeenCalledWith(`front url: ${frontendUrl}`); + expect(logSpy).toHaveBeenCalledWith( + 'AuthController - Google Auth Initiated', + ); + }); + }); + + describe('googleAuthRedirect', () => { + it('should redirect to OAuth URL if accessToken is present', () => { + const req = { user: { accessToken: 'token' } } as unknown as Request; + const res = { redirect: jest.fn() } as unknown as Response; + const frontendUrl = process.env.FRONTEND_URL; + configService.get = jest.fn().mockReturnValue(frontendUrl); + const logSpy = jest.spyOn(authController['logger'], 'log'); + + authController.googleAuthRedirect(req, res); + + expect(logSpy).toHaveBeenCalledWith(`front url: ${frontendUrl}`); + expect(logSpy).toHaveBeenCalledWith( + 'AuthController - Google Callback Request:', + req.user, + ); + expect(res.redirect).toHaveBeenCalledWith( + `${frontendUrl}/oauth?token=token`, + ); + }); + + it('should redirect to registration URL if accessToken is not present', () => { + const req = { user: {} } as Request; + const res = { redirect: jest.fn() } as unknown as Response; + const frontendUrl = process.env.FRONTEND_URL; + configService.get = jest.fn().mockReturnValue(frontendUrl); + const logSpy = jest.spyOn(authController['logger'], 'log'); + + authController.googleAuthRedirect(req, res); + + expect(logSpy).toHaveBeenCalledWith(`front url: ${frontendUrl}`); + expect(logSpy).toHaveBeenCalledWith( + 'AuthController - Google Callback Request:', + req.user, + ); + expect(res.redirect).toHaveBeenCalledWith(`${frontendUrl}/cadastro`); + }); + }); + + describe('microsoftAuth', () => { + it('should log initiation of Microsoft auth', async () => { + const logSpy = jest.spyOn(authController['logger'], 'log'); + const frontendUrl = process.env.FRONTEND_URL; + configService.get = jest.fn().mockReturnValue(frontendUrl); + + await authController.microsoftAuth(); + + expect(logSpy).toHaveBeenCalledWith(`front url: ${frontendUrl}`); + expect(logSpy).toHaveBeenCalledWith( + 'AuthController - Microsoft Auth Initiated', + ); + }); + }); + + describe('microsoftAuthRedirect', () => { + it('should redirect to OAuth URL if accessToken is present', () => { + const req = { user: { accessToken: 'token' } } as unknown as Request; + const res = { redirect: jest.fn() } as unknown as Response; + const frontendUrl = process.env.FRONTEND_URL; + configService.get = jest.fn().mockReturnValue(frontendUrl); + const logSpy = jest.spyOn(authController['logger'], 'log'); + + authController.microsoftAuthRedirect(req, res); + + expect(logSpy).toHaveBeenCalledWith( + 'AuthController - Microsoft Callback Request:', + JSON.stringify(req.user), + ); + expect(res.redirect).toHaveBeenCalledWith(`${frontendUrl}/oauth?token=token`); + }); + + it('should redirect to registration URL if accessToken is not present', () => { + const req = { user: {} } as Request; + const res = { redirect: jest.fn() } as unknown as Response; + const frontendUrl = process.env.FRONTEND_URL; + configService.get = jest.fn().mockReturnValue(frontendUrl); + const logSpy = jest.spyOn(authController['logger'], 'log'); + + authController.microsoftAuthRedirect(req, res); + + expect(logSpy).toHaveBeenCalledWith( + 'AuthController - Microsoft Callback Request:', + JSON.stringify(req.user), + ); + expect(res.redirect).toHaveBeenCalledWith(`${frontendUrl}/cadastro`); + }); + }); +}); diff --git a/test/auth.service.spec.ts b/test/auth.service.spec.ts new file mode 100644 index 0000000..590a267 --- /dev/null +++ b/test/auth.service.spec.ts @@ -0,0 +1,77 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { JwtService } from '@nestjs/jwt'; +import { UnauthorizedException } from '@nestjs/common'; +import { AuthService } from 'src/auth/auth.service'; +import { UsersService } from 'src/users/users.service'; + +describe('AuthService', () => { + let authService: AuthService; + let usersService: UsersService; + let jwtService: JwtService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthService, + { + provide: UsersService, + useValue: { + findByEmail: jest.fn(), + }, + }, + { + provide: JwtService, + useValue: { + sign: jest.fn(), + }, + }, + ], + }).compile(); + + authService = module.get(AuthService); + usersService = module.get(UsersService); + jwtService = module.get(JwtService); + }); + + describe('validateUser', () => { + it('should throw UnauthorizedException if credentials are invalid', async () => { + const email = 'test@example.com'; + const password = 'wrongpassword'; + + jest.spyOn(usersService, 'findByEmail').mockResolvedValue(null); + + await expect(authService.validateUser(email, password)).rejects.toThrow( + UnauthorizedException, + ); + }); + }); + + describe('login', () => { + it('should return a token and user data', async () => { + const user = { + _id: 'user-id', + name: 'Test User', + email: 'test@example.com', + role: 'user', + }; + const token = 'jwt-token'; + jest.spyOn(jwtService, 'sign').mockReturnValue(token); + + const result = await authService.login(user); + + expect(jwtService.sign).toHaveBeenCalledWith({ + id: user._id, + name: user.name, + email: user.email, + sub: user._id, + role: user.role, + }); + expect(result).toEqual({ + id: user._id, + name: user.name, + email: user.email, + accessToken: token, + }); + }); + }); +}); diff --git a/test/google.strategy.spec.ts b/test/google.strategy.spec.ts new file mode 100644 index 0000000..3ed6465 --- /dev/null +++ b/test/google.strategy.spec.ts @@ -0,0 +1,170 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { UsersService } from 'src/users/users.service'; +import { Profile, VerifyCallback } from 'passport-google-oauth20'; +import { GoogleStrategy } from 'src/auth/strategies/google.strategy'; +import { AuthService } from 'src/auth/auth.service'; + +describe('GoogleStrategy', () => { + let googleStrategy: GoogleStrategy; + let usersService: UsersService; + let authService: AuthService; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let configService: ConfigService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + GoogleStrategy, + { + provide: UsersService, + useValue: { + findByEmail: jest.fn(), + createUserGoogle: jest.fn(), + }, + }, + { + provide: AuthService, + useValue: { + getJwtService: jest.fn().mockReturnValue({ + sign: jest.fn(), + }), + }, + }, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: string) => { + switch (key) { + case 'GOOGLE_CLIENT_ID': + return 'test-client-id'; + case 'GOOGLE_CLIENT_SECRET': + return 'test-client-secret'; + case 'GOOGLE_CALLBACK_URL': + return 'http://localhost:3000/auth/google/callback'; + default: + return null; + } + }), + }, + }, + ], + }).compile(); + + googleStrategy = module.get(GoogleStrategy); + usersService = module.get(UsersService); + authService = module.get(AuthService); + configService = module.get(ConfigService); + }); + + describe('validate', () => { + it('should validate user and return user with accessToken', async () => { + const email = 'test@example.com'; + const name = 'Test User'; + const profile: Profile = { + emails: [{ value: email }], + displayName: name, + } as unknown as Profile; + const accessToken = 'jwt-token'; + const user = { + _id: 'user-id', + name, + email, + role: 'user', + toObject: jest.fn().mockReturnValue({ + _id: 'user-id', + name, + email, + role: 'user', + }), + }; + const done: VerifyCallback = jest.fn(); + + jest.spyOn(usersService, 'findByEmail').mockResolvedValue(user as any); + jest + .spyOn(usersService, 'createUserGoogle') + .mockResolvedValue(user as any); + jest + .spyOn(authService.getJwtService(), 'sign') + .mockReturnValue(accessToken); + + await googleStrategy.validate( + 'accessToken', + 'refreshToken', + profile, + done, + ); + + expect(usersService.findByEmail).toHaveBeenCalledWith(email); + expect(usersService.createUserGoogle).not.toHaveBeenCalled(); + expect(authService.getJwtService().sign).toHaveBeenCalledWith({ + id: user._id, + name: user.name, + email: user.email, + sub: user._id, + role: user.role, + }); + expect(done).toHaveBeenCalledWith(null, { + ...user.toObject(), + accessToken, + }); + }); + + it('should create a new user if the user does not exist', async () => { + const email = 'test@example.com'; + const name = 'Test User'; + const profile: Profile = { + emails: [{ value: email }], + displayName: name, + } as unknown as Profile; + const accessToken = 'jwt-token'; + const user = { + _id: 'user-id', + name, + email, + role: 'user', + toObject: jest.fn().mockReturnValue({ + _id: 'user-id', + name, + email, + role: 'user', + }), + }; + const done: VerifyCallback = jest.fn(); + + jest.spyOn(usersService, 'findByEmail').mockResolvedValue(null); + jest + .spyOn(usersService, 'createUserGoogle') + .mockResolvedValue(user as any); + jest + .spyOn(authService.getJwtService(), 'sign') + .mockReturnValue(accessToken); + + await googleStrategy.validate( + 'accessToken', + 'refreshToken', + profile, + done, + ); + + expect(usersService.findByEmail).toHaveBeenCalledWith(email); + expect(usersService.createUserGoogle).toHaveBeenCalledWith({ + name, + email, + username: email, + password: '', + }); + expect(authService.getJwtService().sign).toHaveBeenCalledWith({ + id: user._id, + name: user.name, + email: user.email, + sub: user._id, + role: user.role, + }); + expect(done).toHaveBeenCalledWith(null, { + ...user.toObject(), + accessToken, + }); + }); + }); +}); diff --git a/test/microsoft.strategy.spec.ts b/test/microsoft.strategy.spec.ts new file mode 100644 index 0000000..31e96ba --- /dev/null +++ b/test/microsoft.strategy.spec.ts @@ -0,0 +1,170 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { UsersService } from 'src/users/users.service'; +import { Profile, VerifyCallback } from 'passport-microsoft'; +import { MicrosoftStrategy } from 'src/auth/strategies/microsoft.strategy'; +import { AuthService } from 'src/auth/auth.service'; + +describe('MicrosoftStrategy', () => { + let microsoftStrategy: MicrosoftStrategy; + let usersService: UsersService; + let authService: AuthService; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let configService: ConfigService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MicrosoftStrategy, + { + provide: UsersService, + useValue: { + findByEmail: jest.fn(), + createUserGoogle: jest.fn(), // Ajuste conforme o método da estratégia do Microsoft + }, + }, + { + provide: AuthService, + useValue: { + getJwtService: jest.fn().mockReturnValue({ + sign: jest.fn(), + }), + }, + }, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: string) => { + switch (key) { + case 'MICROSOFT_CLIENT_ID': + return 'test-client-id'; + case 'MICROSOFT_CLIENT_SECRET': + return 'test-client-secret'; + case 'MICROSOFT_CALLBACK_URL': + return 'http://localhost:3000/auth/microsoft/callback'; + default: + return null; + } + }), + }, + }, + ], + }).compile(); + + microsoftStrategy = module.get(MicrosoftStrategy); + usersService = module.get(UsersService); + authService = module.get(AuthService); + configService = module.get(ConfigService); + }); + + describe('validate', () => { + it('should validate user and return user with accessToken', async () => { + const email = 'test@example.com'; + const name = 'Test User'; + const profile: Profile = { + emails: [{ value: email }], + displayName: name, + } as unknown as Profile; + const accessToken = 'jwt-token'; + const user = { + _id: 'user-id', + name, + email, + role: 'user', + toObject: jest.fn().mockReturnValue({ + _id: 'user-id', + name, + email, + role: 'user', + }), + }; + const done: VerifyCallback = jest.fn(); + + jest.spyOn(usersService, 'findByEmail').mockResolvedValue(user as any); + jest + .spyOn(usersService, 'createUserGoogle') + .mockResolvedValue(user as any); + jest + .spyOn(authService.getJwtService(), 'sign') + .mockReturnValue(accessToken); + + await microsoftStrategy.validate( + 'accessToken', + 'refreshToken', + profile, + done, + ); + + expect(usersService.findByEmail).toHaveBeenCalledWith(email); + expect(usersService.createUserGoogle).not.toHaveBeenCalled(); // Ajuste conforme o método da estratégia do Microsoft + expect(authService.getJwtService().sign).toHaveBeenCalledWith({ + id: user._id, + name: user.name, + email: user.email, + sub: user._id, + role: user.role, + }); + expect(done).toHaveBeenCalledWith(null, { + ...user.toObject(), + accessToken, + }); + }); + + it('should create a new user if the user does not exist', async () => { + const email = 'test@example.com'; + const name = 'Test User'; + const profile: Profile = { + emails: [{ value: email }], + displayName: name, + } as unknown as Profile; + const accessToken = 'jwt-token'; + const user = { + _id: 'user-id', + name, + email, + role: 'user', + toObject: jest.fn().mockReturnValue({ + _id: 'user-id', + name, + email, + role: 'user', + }), + }; + const done: VerifyCallback = jest.fn(); + + jest.spyOn(usersService, 'findByEmail').mockResolvedValue(null); + jest + .spyOn(usersService, 'createUserGoogle') + .mockResolvedValue(user as any); + jest + .spyOn(authService.getJwtService(), 'sign') + .mockReturnValue(accessToken); + + await microsoftStrategy.validate( + 'accessToken', + 'refreshToken', + profile, + done, + ); + + expect(usersService.findByEmail).toHaveBeenCalledWith(email); + expect(usersService.createUserGoogle).toHaveBeenCalledWith({ + name, + email, + username: email, + password: '', + }); + expect(authService.getJwtService().sign).toHaveBeenCalledWith({ + id: user._id, + name: user.name, + email: user.email, + sub: user._id, + role: user.role, + }); + expect(done).toHaveBeenCalledWith(null, { + ...user.toObject(), + accessToken, + }); + }); + }); +}); diff --git a/test/user.pipes.spec.ts b/test/user.pipes.spec.ts new file mode 100644 index 0000000..1bf40ea --- /dev/null +++ b/test/user.pipes.spec.ts @@ -0,0 +1,45 @@ +import { BadRequestException } from '@nestjs/common'; +import { UsersValidationPipe } from 'src/users/pipes/users_validation.pipe'; + +describe('UsersValidationPipe', () => { + let pipe: UsersValidationPipe; + + beforeEach(() => { + pipe = new UsersValidationPipe(); + }); + + describe('transform', () => { + it('should throw BadRequestException if body is empty', () => { + expect(() => pipe.transform(null, { type: 'body' })).toThrow( + new BadRequestException('O corpo da requisição não pode estar vazio'), + ); + }); + + it('should not throw an exception if body is not empty', () => { + expect(() => + pipe.transform({ name: 'John Doe' }, { type: 'body' }), + ).not.toThrow(); + }); + + it('should throw BadRequestException if query parameter is missing', () => { + expect(() => + pipe.transform(null, { type: 'query', data: 'param' }), + ).toThrow( + new BadRequestException( + "O parâmetro de consulta 'param' é obrigatório", + ), + ); + }); + + it('should not throw an exception if query parameter is present', () => { + expect(() => + pipe.transform('value', { type: 'query', data: 'param' }), + ).not.toThrow(); + }); + + it('should return the value if query parameter is present', () => { + const result = pipe.transform('value', { type: 'query', data: 'param' }); + expect(result).toBe('value'); + }); + }); +}); diff --git a/test/user.service.spec.ts b/test/user.service.spec.ts index 1add766..11b75c5 100644 --- a/test/user.service.spec.ts +++ b/test/user.service.spec.ts @@ -8,6 +8,17 @@ import { NotFoundException, ConflictException } from '@nestjs/common'; import { CreateUserDtoGoogle } from 'src/users/dtos/create-user-google.dto'; interface MockUserModel { + mockReturnValue(createdUser: { + save: jest.Mock; + name: string; + email: string; + username: string; + password: string; + role?: UserRole; + _id: string; + }): unknown; + + mockImplementation(arg0: () => never): unknown; save: jest.Mock; find: jest.Mock; findById: jest.Mock; @@ -70,6 +81,36 @@ describe('UsersService', () => { }); }); + describe('verifyUser', () => { + it('should verify a user and update the verification status', async () => { + const token = 'valid-token'; + const user = { + _id: 'some-id', + verificationToken: token, + isVerified: false, + save: jest.fn(), + }; + userModel.findOne.mockReturnValue(userModel); // chainable + userModel.exec.mockResolvedValue(user); + + const result = await usersService.verifyUser(token); + + expect(result).toEqual(user); + expect(user.verificationToken).toBeUndefined(); + expect(user.isVerified).toBe(true); + expect(user.save).toHaveBeenCalled(); + }); + + it('should throw NotFoundException if token is invalid', async () => { + userModel.findOne.mockReturnValue(userModel); + userModel.exec.mockResolvedValue(null); + + await expect(usersService.verifyUser('invalid-token')).rejects.toThrow( + new NotFoundException('Invalid verification token'), + ); + }); + }); + describe('createUserGoogle', () => { it('should throw ConflictException if user already exists', async () => { const createUserGoogleDto: CreateUserDtoGoogle = { @@ -133,6 +174,33 @@ describe('UsersService', () => { }); }); + describe('updateUserRole', () => { + it("should update a user's role", async () => { + const userId = 'some-id'; + const updateRoleDto = { role: UserRole.ADMIN }; + const user = { _id: userId, role: UserRole.ALUNO, save: jest.fn() }; + userModel.findById.mockReturnValue(userModel); // chainable + userModel.exec.mockResolvedValue(user); + + const result = await usersService.updateUserRole(userId, updateRoleDto); + + expect(result).toEqual(user); + expect(user.role).toBe(updateRoleDto.role); + expect(user.save).toHaveBeenCalled(); + }); + + it('should throw NotFoundException if user not found', async () => { + userModel.findById.mockReturnValue(userModel); // chainable + userModel.exec.mockResolvedValue(null); + + await expect( + usersService.updateUserRole('invalid-id', { role: UserRole.ADMIN }), + ).rejects.toThrow( + new NotFoundException(`User with ID 'invalid-id' not found`), + ); + }); + }); + describe('deleteUserById', () => { it('should delete a user by id', async () => { const userId = 'some-id';