Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ node_modules/
.env.local
.env_local
dist/
.next/
.next/
coverage
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,42 @@ Você pode se cadastrar como:
- 🎒 **Treinador** — cadastra e gerencia seus próprios pokémons e solicita cuidados no Centro Pokémon
- 🏥 **Enfermeira Joy** — visualiza todos os pokémons cadastrados e aprova ou recusa as solicitações de cuidado dos treinadores

### Testes unitários

O backend conta com testes unitários implementados com **Jest** e **ts-jest**, cobrindo as camadas de service e controller da aplicação.

#### Arquivos com cobertura de testes

| Arquivo | Camada |
|---|---|
| `src/auth/auth.controller.spec.ts` | Controller |
| `src/auth/auth.service.spec.ts` | Service |
| `src/pokemons/pokemons.controller.spec.ts` | Controller |
| `src/pokemons/pokemons.service.spec.ts` | Service |
| `src/pokemon-actions/pokemon-actions.controller.spec.ts` | Controller |
| `src/pokemon-actions/pokemon-actions.service.spec.ts` | Service |
| `src/users/users.service.spec.ts` | Service |

##### Coverage
![PokéCenter Backend Tests Coverage](/assets/pokecenter_backend_test_coverage.jpeg)

#### Comandos

Execute os comandos a seguir dentro do diretório `backend/`:
```bash
# Rodar todos os testes
npm run test

# Rodar os testes em modo watch — re-executa ao salvar arquivos
npm run test:watch

# Rodar os testes e exibir o relatório de cobertura
npm run test:cov

# Rodar os testes e salvar o relatório de cobertura em uma pasta personalizada
npm test -- --coverage --coverageDirectory='my-coverage-report'
```

## Deploy

> Banco de Dados: Neon
Expand Down
Binary file added assets/pokecenter_backend_test_coverage.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
124 changes: 124 additions & 0 deletions backend/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { UserRole } from '../users/enums/user-role.enum';

// ------------------------------------------------------------------ //
// Mocks
// ------------------------------------------------------------------ //

const mockAuthService = () => ({
register: jest.fn(),
login: jest.fn(),
});

// Usuário autenticado simulado — retornado pelo JwtAuthGuard via req.user
const mockAuthenticatedUser = (role: UserRole = UserRole.TRAINER) => ({
id: 'uuid-123',
email: 'ash@pokemon.com',
name: 'Ash Ketchum',
role,
});

describe('AuthController', () => {
let controller: AuthController;
let authService: jest.Mocked<AuthService>;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [
{ provide: AuthService, useFactory: mockAuthService },
],
})
// Substitui o guard real por um que sempre permite a requisição
.overrideGuard(JwtAuthGuard)
.useValue({ canActivate: () => true })
.compile();

controller = module.get<AuthController>(AuthController);
authService = module.get(AuthService);

jest.clearAllMocks();
});

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

// ------------------------------------------------------------------ //
describe('register', () => {
it('deve chamar authService.register e retornar o token', async () => {
const dto = {
name: 'Ash Ketchum',
email: 'ash@pokemon.com',
password: '123456',
role: UserRole.TRAINER,
};
const token = { access_token: 'mock_token' };

authService.register.mockResolvedValue(token);

const result = await controller.register(dto);

expect(result).toEqual(token);
expect(authService.register).toHaveBeenCalledWith(dto);
});

it('deve registrar uma enfermeira Joy corretamente', async () => {
const dto = {
name: 'Enfermeira Joy',
email: 'joy@pokemon.com',
password: '123456',
role: UserRole.NURSE,
};
const token = { access_token: 'mock_token' };

authService.register.mockResolvedValue(token);

const result = await controller.register(dto);

expect(result).toEqual(token);
expect(authService.register).toHaveBeenCalledWith(dto);
});
});

// ------------------------------------------------------------------ //
describe('login', () => {
it('deve chamar authService.login e retornar o token', async () => {
const dto = { email: 'ash@pokemon.com', password: '123456' };
const token = { access_token: 'mock_token' };

authService.login.mockResolvedValue(token);

const result = await controller.login(dto);

expect(result).toEqual(token);
expect(authService.login).toHaveBeenCalledWith(dto);
});
});

// ------------------------------------------------------------------ //
describe('getProfile', () => {
it('deve retornar os dados do treinador autenticado via req.user', () => {
const user = mockAuthenticatedUser(UserRole.TRAINER);
const req = { user };

const result = controller.getProfile(req);

expect(result).toEqual(user);
expect(result.role).toBe(UserRole.TRAINER);
});

it('deve retornar os dados da enfermeira autenticada via req.user', () => {
const user = mockAuthenticatedUser(UserRole.NURSE);
const req = { user };

const result = controller.getProfile(req);

expect(result).toEqual(user);
expect(result.role).toBe(UserRole.NURSE);
});
});
});
90 changes: 81 additions & 9 deletions backend/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersService } from '../users/users.service';
import { User } from '../users/entities/user.entity';
import { UserRole } from '../users/enums/user-role.enum';

// Mocka o módulo inteiro do bcryptjs — evita o erro "Cannot redefine property"
jest.mock('bcryptjs', () => ({
Expand All @@ -14,11 +15,13 @@ jest.mock('bcryptjs', () => ({
// Importa depois do mock para pegar a versão mockada
import * as bcrypt from 'bcryptjs';

const mockUser = (): User => ({
// Cria um usuário mock com role configurável — trainer por padrão
const mockUser = (role: UserRole = UserRole.TRAINER): User => ({
id: 'uuid-123',
name: 'Ash Ketchum',
email: 'ash@pokemon.com',
password: 'hashed_password',
role,
createdAt: new Date(),
});

Expand Down Expand Up @@ -49,40 +52,83 @@ describe('AuthService', () => {
usersService = module.get(UsersService);
jwtService = module.get(JwtService);

// Reseta os mocks do bcrypt antes de cada teste
jest.clearAllMocks();
});

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

// ------------------------------------------------------------------ //
describe('register', () => {
it('deve registrar um usuário e retornar o token', async () => {
const user = mockUser();
it('deve registrar um treinador e retornar o token', async () => {
const user = mockUser(UserRole.TRAINER);

// Usa mockResolvedValue diretamente na função mockada
(bcrypt.hash as jest.Mock).mockResolvedValue('hashed_password');
usersService.create.mockResolvedValue(user);

const result = await service.register({
name: 'Ash Ketchum',
email: 'ash@pokemon.com',
password: '123456',
role: UserRole.TRAINER,
});

expect(result).toEqual({ access_token: 'mock_token' });
expect(bcrypt.hash).toHaveBeenCalledWith('123456', 10);
expect(jwtService.sign).toHaveBeenCalledWith({
sub: user.id,
email: user.email,
role: UserRole.TRAINER,
name: user.name,
});
});

it('deve registrar uma enfermeira Joy e retornar o token', async () => {
const user = mockUser(UserRole.NURSE);

(bcrypt.hash as jest.Mock).mockResolvedValue('hashed_password');
usersService.create.mockResolvedValue(user);

const result = await service.register({
name: 'Ash Ketchum',
email: 'ash@pokemon.com',
password: '123456',
role: UserRole.NURSE,
});

expect(result).toEqual({ access_token: 'mock_token' });
expect(jwtService.sign).toHaveBeenCalledWith({
sub: user.id,
email: user.email,
role: UserRole.NURSE,
name: user.name,
});
});

it('deve registrar com role trainer por padrão quando role não for informada', async () => {
const user = mockUser(UserRole.TRAINER);

(bcrypt.hash as jest.Mock).mockResolvedValue('hashed_password');
usersService.create.mockResolvedValue(user);

await service.register({
name: 'Ash Ketchum',
email: 'ash@pokemon.com',
password: '123456',
// role não informada — deve usar trainer como padrão
});

expect(jwtService.sign).toHaveBeenCalledWith(
expect.objectContaining({ role: UserRole.TRAINER }),
);
});
});

// ------------------------------------------------------------------ //
describe('login', () => {
it('deve autenticar o usuário e retornar o token', async () => {
const user = mockUser();
it('deve autenticar um treinador e retornar o token', async () => {
const user = mockUser(UserRole.TRAINER);

usersService.findByEmail.mockResolvedValue(user);
(bcrypt.compare as jest.Mock).mockResolvedValue(true);
Expand All @@ -93,6 +139,32 @@ describe('AuthService', () => {
});

expect(result).toEqual({ access_token: 'mock_token' });
expect(jwtService.sign).toHaveBeenCalledWith({
sub: user.id,
email: user.email,
role: UserRole.TRAINER,
name: user.name,
});
});

it('deve autenticar uma enfermeira Joy e retornar o token', async () => {
const user = mockUser(UserRole.NURSE);

usersService.findByEmail.mockResolvedValue(user);
(bcrypt.compare as jest.Mock).mockResolvedValue(true);

const result = await service.login({
email: 'joy@pokemon.com',
password: '123456',
});

expect(result).toEqual({ access_token: 'mock_token' });
expect(jwtService.sign).toHaveBeenCalledWith({
sub: user.id,
email: user.email,
role: UserRole.NURSE,
name: user.name,
});
});

it('deve lançar UnauthorizedException quando usuário não existe', async () => {
Expand All @@ -107,7 +179,7 @@ describe('AuthService', () => {
});

it('deve lançar UnauthorizedException quando senha está incorreta', async () => {
const user = mockUser();
const user = mockUser(UserRole.TRAINER);

usersService.findByEmail.mockResolvedValue(user);
(bcrypt.compare as jest.Mock).mockResolvedValue(false);
Expand All @@ -117,4 +189,4 @@ describe('AuthService', () => {
).rejects.toThrow(UnauthorizedException);
});
});
});
});
2 changes: 1 addition & 1 deletion backend/src/auth/dto/register.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger/dist/decorators/api-property.decorator';
import { IsEmail, IsEnum, IsOptional, IsString, MinLength } from 'class-validator';
import { UserRole } from 'src/users/enums/user-role.enum';
import { UserRole } from '../../users/enums/user-role.enum';

export class RegisterDto {
@IsString()
Expand Down
Loading