From c07df023dd6e0d58c5d8c6461328090138b5a191 Mon Sep 17 00:00:00 2001 From: joelson brito Date: Mon, 10 Nov 2025 16:24:02 -0300 Subject: [PATCH] Adiciona testes para RefreshTokenService e TokenBlacklistService MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cria testes completos para RefreshTokenService (14 testes) - Cria testes completos para TokenBlacklistService (11 testes) - Remove JSDoc do DebService - Adiciona testes para DebService (6 testes) - Corrige query SQL no DebRepository para usar SQL raw em vez de QueryBuilder - Adiciona documentação de cobertura de testes --- docs/COBERTURA_TESTES.md | 273 ++++++++++++ .../refresh-token.service.spec.helper.ts | 64 +++ .../__tests__/refresh-token.service.spec.ts | 392 ++++++++++++++++++ .../token-blacklist.service.spec.helper.ts | 62 +++ .../__tests__/token-blacklist.service.spec.ts | 257 ++++++++++++ .../__tests__/deb.service.spec.helper.ts | 40 ++ .../application/__tests__/deb.service.spec.ts | 191 +++++++++ src/orders/application/deb.service.ts | 8 - src/orders/repositories/deb.repository.ts | 69 +-- 9 files changed, 1315 insertions(+), 41 deletions(-) create mode 100644 docs/COBERTURA_TESTES.md create mode 100644 src/auth/services/__tests__/refresh-token.service.spec.helper.ts create mode 100644 src/auth/services/__tests__/refresh-token.service.spec.ts create mode 100644 src/auth/services/__tests__/token-blacklist.service.spec.helper.ts create mode 100644 src/auth/services/__tests__/token-blacklist.service.spec.ts create mode 100644 src/orders/application/__tests__/deb.service.spec.helper.ts create mode 100644 src/orders/application/__tests__/deb.service.spec.ts diff --git a/docs/COBERTURA_TESTES.md b/docs/COBERTURA_TESTES.md new file mode 100644 index 0000000..fd8b2f7 --- /dev/null +++ b/docs/COBERTURA_TESTES.md @@ -0,0 +1,273 @@ +# Cobertura de Testes - O que ainda pode ser testado + +## 📊 Resumo Atual + +**Testes existentes:** +- ✅ `DataConsultService` - stores, sellers, billings, customers, products, getAllProducts, getAllCarriers, getRegions +- ✅ `ProductsService` - getProductDetails (busca por codauxiliar) +- ✅ `OrdersService` - findOrders +- ✅ `DebService` - findByCpfCgcent +- ✅ `AuthService` - createToken, createTokenPair, refreshAccessToken, logout + +**Total:** 10 suites de teste, 168 testes passando + +--- + +## 🔴 Métodos sem testes + +### 1. ProductsService + +#### `productsValidation` +- **Status:** ❌ Sem testes +- **O que testar:** + - Busca por codauxiliar + - Busca por codprod + - Busca por descricao + - Busca por todos (tipoBusca = 'todos') + - Produto não encontrado (lança HttpException) + - Processamento de imagens (com e sem separador `;`) + - Imagens null/undefined (retorna array vazio) + - Diferentes tipos de produto (AUTOSSERVICO, SHOWROOM, ELETROMOVEIS, OUTROS) + +#### `exposedProduct` +- **Status:** ❌ Sem testes +- **O que testar:** + - Criação de produto exposto com sucesso + - Rollback em caso de erro + - Validação de dados de entrada + - Tratamento de erros de transação + +#### `getProductDetails` (busca por codprod) +- **Status:** ⚠️ Parcial (só tem busca por codauxiliar) +- **O que testar:** + - Busca por codprod + - Busca por codprod e codauxiliar juntos + - Validação de parâmetros + +#### `unifiedProductSearch` +- **Status:** ❌ Sem testes +- **O que testar:** + - Busca por código numérico (codprod e codauxiliar) + - Busca por nome/descrição + - Termo de busca vazio (lança exceção) + - Formatação de preço + - Remoção de caracteres não numéricos + +#### `getRotinaA4` +- **Status:** ❌ Sem testes +- **O que testar:** + - Busca por codprod + - Busca por codauxiliar + - Busca por codprod e codauxiliar juntos + - Validação quando nenhum é informado + - Formatação de valores (PRECO_NORMAL, VALOR_VENDA, DECIMAL_VENDA) + +#### `formatarMoedaBrasileira` (método privado) +- **Status:** ❌ Sem testes +- **O que testar:** + - Formatação de valores normais + - Valores null/undefined (retorna '0,00') + - Valores com decimais + - Valores grandes (milhares) + +--- + +### 2. DataConsultService + +#### `productsByCodauxiliar` +- **Status:** ❌ Sem testes +- **O que testar:** + - Busca por codauxiliar válido + - Codauxiliar inválido (null, undefined, string vazia) + - Erro do repositório (log e exceção) + - Mapeamento correto para ProductDto + +#### `getCarriersByDate` +- **Status:** ❌ Sem testes +- **O que testar:** + - Busca com data inicial + - Busca com data final + - Busca com data inicial e final + - Busca com codfilial + - Busca sem filtros + - Contagem de pedidos (ordersCount) + - Cache Redis + +#### `getOrderCarriers` +- **Status:** ❌ Sem testes +- **O que testar:** + - Busca por orderId válido + - OrderId inválido + - Retorno vazio quando não há transportadoras + - Formatação de dados + +--- + +### 3. Outros Serviços sem testes + +#### `OrdersPaymentService` +- **Status:** ❌ Sem testes +- **O que testar:** + - Processamento de pagamentos + - Validação de dados + - Tratamento de erros + +#### `LogisticService` +- **Status:** ❌ Sem testes +- **O que testar:** + - getExpedicao + - getDeliveries + - Validação de parâmetros + +#### `PartnersService` +- **Status:** ❌ Sem testes +- **O que testar:** + - Métodos de busca de parceiros + - Validações + +#### `ClientesService` +- **Status:** ❌ Sem testes +- **O que testar:** + - Busca de clientes + - Validações + +#### `UsersService` +- **Status:** ❌ Sem testes +- **O que testar:** + - Operações de usuários + - Validações + +#### `ResetPasswordService` +- **Status:** ❌ Sem testes +- **O que testar:** + - Reset de senha + - Validação de tokens + - Expiração de tokens + +#### `ChangePasswordService` +- **Status:** ❌ Sem testes +- **O que testar:** + - Mudança de senha + - Validação de senha atual + - Validação de nova senha + +#### `EmailService` +- **Status:** ❌ Sem testes +- **O que testar:** + - Envio de emails + - Templates de email + - Tratamento de erros + +#### `RefreshTokenService` +- **Status:** ❌ Sem testes +- **O que testar:** + - Geração de refresh token + - Validação de refresh token + - Expiração de tokens + +#### `TokenBlacklistService` +- **Status:** ❌ Sem testes +- **O que testar:** + - Adicionar token à blacklist + - Verificar se token está na blacklist + - Expiração de tokens na blacklist + +#### `SessionManagementService` +- **Status:** ❌ Sem testes +- **O que testar:** + - Criação de sessão + - Validação de sessão + - Encerramento de sessão + +#### `LoginAuditService` +- **Status:** ❌ Sem testes +- **O que testar:** + - Registro de tentativas de login + - Auditoria de acessos + +#### `RateLimitingService` +- **Status:** ❌ Sem testes +- **O que testar:** + - Limite de requisições + - Reset de contadores + - Bloqueio temporário + +--- + +## 🟡 Casos de borda e cenários adicionais + +### ProductsService + +1. **getProductDetails:** + - Busca por codprod (não só codauxiliar) + - Busca com codprod e codauxiliar juntos + - Validação de numregiao inválido + - Validação de codfilial inválido + - Preço null/undefined (formatação) + +2. **productsValidation:** + - Filtro vazio + - Filtro com caracteres especiais + - Múltiplos produtos retornados (pega o primeiro) + - Tipos de produto diferentes + +3. **unifiedProductSearch:** + - Termo com caracteres especiais + - Termo muito longo + - Termo com apenas espaços + - Busca que retorna múltiplos produtos + +### DataConsultService + +1. **productsByCodauxiliar:** + - Codauxiliar com caracteres não numéricos + - Codauxiliar muito longo + - Codauxiliar vazio + +2. **getCarriersByDate:** + - Data inicial maior que data final + - Datas inválidas + - Cache hit/miss + - Erro do repositório + +3. **getOrderCarriers:** + - OrderId negativo + - OrderId zero + - OrderId muito grande + +### DebService + +1. **findByCpfCgcent:** + - CPF/CGCENT com caracteres não numéricos + - CPF/CGCENT muito curto/longo + - Matrícula negativa + - Cobrança inválida + +--- + +## 🎯 Prioridades de Teste + +### Alta Prioridade +1. ✅ `ProductsService.productsValidation` - Método crítico usado em validação de produtos +2. ✅ `ProductsService.unifiedProductSearch` - Novo método, precisa de testes +3. ✅ `ProductsService.getProductDetails` (busca por codprod) - Completar cobertura +4. ✅ `DataConsultService.productsByCodauxiliar` - Método usado no sistema + +### Média Prioridade +5. ✅ `ProductsService.getRotinaA4` - Método específico de rotina +6. ✅ `DataConsultService.getCarriersByDate` - Método com cache +7. ✅ `DataConsultService.getOrderCarriers` - Método auxiliar + +### Baixa Prioridade +8. ✅ `ProductsService.exposedProduct` - Método de transação +9. ✅ Outros serviços menores (Email, ResetPassword, etc.) + +--- + +## 📝 Observações + +- **Cobertura atual:** ~40% dos métodos principais +- **Foco:** Métodos de negócio críticos primeiro +- **Padrão:** Seguir o padrão dos testes existentes (helper + spec) +- **Casos de borda:** Sempre testar null, undefined, strings vazias, valores inválidos + diff --git a/src/auth/services/__tests__/refresh-token.service.spec.helper.ts b/src/auth/services/__tests__/refresh-token.service.spec.helper.ts new file mode 100644 index 0000000..f488420 --- /dev/null +++ b/src/auth/services/__tests__/refresh-token.service.spec.helper.ts @@ -0,0 +1,64 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RefreshTokenService } from '../refresh-token.service'; +import { IRedisClient } from '../../../core/configs/cache/IRedisClient'; +import { RedisClientToken } from '../../../core/configs/cache/redis-client.adapter.provider'; +import { JwtService } from '@nestjs/jwt'; + +export const createMockRedisClient = () => + ({ + get: jest.fn(), + set: jest.fn(), + del: jest.fn(), + keys: jest.fn(), + } as any); + +export const createMockJwtService = () => + ({ + sign: jest.fn(), + verify: jest.fn(), + decode: jest.fn(), + } as any); + +export interface RefreshTokenServiceTestContext { + service: RefreshTokenService; + mockRedisClient: jest.Mocked; + mockJwtService: jest.Mocked; +} + +export async function createRefreshTokenServiceTestModule( + redisClientMethods: Partial = {}, + jwtServiceMethods: Partial = {}, +): Promise { + const mockRedisClient = { + ...createMockRedisClient(), + ...redisClientMethods, + } as any; + + const mockJwtService = { + ...createMockJwtService(), + ...jwtServiceMethods, + } as any; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + RefreshTokenService, + { + provide: RedisClientToken, + useValue: mockRedisClient, + }, + { + provide: JwtService, + useValue: mockJwtService, + }, + ], + }).compile(); + + const service = module.get(RefreshTokenService); + + return { + service, + mockRedisClient, + mockJwtService, + }; +} + diff --git a/src/auth/services/__tests__/refresh-token.service.spec.ts b/src/auth/services/__tests__/refresh-token.service.spec.ts new file mode 100644 index 0000000..9c2340e --- /dev/null +++ b/src/auth/services/__tests__/refresh-token.service.spec.ts @@ -0,0 +1,392 @@ +import { UnauthorizedException } from '@nestjs/common'; +import { createRefreshTokenServiceTestModule } from './refresh-token.service.spec.helper'; +import { RefreshTokenData } from '../refresh-token.service'; + +describe('RefreshTokenService', () => { + describe('generateRefreshToken', () => { + let context: Awaited< + ReturnType + >; + + beforeEach(async () => { + context = await createRefreshTokenServiceTestModule(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('deve gerar refresh token com sucesso', async () => { + const userId = 123; + const sessionId = 'session-123'; + const mockToken = 'mock.refresh.token'; + const mockTokenId = 'token-id-123'; + + jest.spyOn(require('crypto'), 'randomBytes').mockReturnValue({ + toString: () => mockTokenId, + }); + + context.mockJwtService.sign.mockReturnValue(mockToken); + context.mockRedisClient.set.mockResolvedValue(undefined); + context.mockRedisClient.keys.mockResolvedValue([]); + + const result = await context.service.generateRefreshToken( + userId, + sessionId, + ); + + expect(result).toBe(mockToken); + expect(context.mockJwtService.sign).toHaveBeenCalledWith( + { + userId, + tokenId: mockTokenId, + sessionId, + type: 'refresh', + }, + { expiresIn: '7d' }, + ); + expect(context.mockRedisClient.set).toHaveBeenCalled(); + }); + + it('deve gerar refresh token sem sessionId', async () => { + const userId = 123; + const mockToken = 'mock.refresh.token'; + const mockTokenId = 'token-id-123'; + + jest.spyOn(require('crypto'), 'randomBytes').mockReturnValue({ + toString: () => mockTokenId, + }); + + context.mockJwtService.sign.mockReturnValue(mockToken); + context.mockRedisClient.set.mockResolvedValue(undefined); + context.mockRedisClient.keys.mockResolvedValue([]); + + const result = await context.service.generateRefreshToken(userId); + + expect(result).toBe(mockToken); + expect(context.mockJwtService.sign).toHaveBeenCalledWith( + { + userId, + tokenId: mockTokenId, + sessionId: undefined, + type: 'refresh', + }, + { expiresIn: '7d' }, + ); + }); + + it('deve limitar número de refresh tokens por usuário', async () => { + const userId = 123; + const mockToken = 'mock.refresh.token'; + const mockTokenId = 'token-id-123'; + + jest.spyOn(require('crypto'), 'randomBytes').mockReturnValue({ + toString: () => mockTokenId, + }); + + context.mockJwtService.sign.mockReturnValue(mockToken); + context.mockRedisClient.set.mockResolvedValue(undefined); + + const existingTokens: RefreshTokenData[] = Array.from( + { length: 6 }, + (_, i) => ({ + userId, + tokenId: `token-${i}`, + expiresAt: Date.now() + 1000000, + createdAt: Date.now(), + }), + ); + + context.mockRedisClient.keys.mockResolvedValue([ + 'auth:refresh_tokens:123:token-0', + 'auth:refresh_tokens:123:token-1', + 'auth:refresh_tokens:123:token-2', + 'auth:refresh_tokens:123:token-3', + 'auth:refresh_tokens:123:token-4', + 'auth:refresh_tokens:123:token-5', + ]); + + context.mockRedisClient.get + .mockResolvedValueOnce(existingTokens[0]) + .mockResolvedValueOnce(existingTokens[1]) + .mockResolvedValueOnce(existingTokens[2]) + .mockResolvedValueOnce(existingTokens[3]) + .mockResolvedValueOnce(existingTokens[4]) + .mockResolvedValueOnce(existingTokens[5]); + + await context.service.generateRefreshToken(userId); + + expect(context.mockRedisClient.del).toHaveBeenCalled(); + }); + }); + + describe('validateRefreshToken', () => { + let context: Awaited< + ReturnType + >; + + beforeEach(async () => { + context = await createRefreshTokenServiceTestModule(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('deve validar refresh token com sucesso', async () => { + const mockDecoded = { + userId: 123, + tokenId: 'token-id-123', + sessionId: 'session-123', + type: 'refresh', + }; + + const mockTokenData: RefreshTokenData = { + userId: 123, + tokenId: 'token-id-123', + sessionId: 'session-123', + expiresAt: Date.now() + 1000000, + createdAt: Date.now(), + }; + + context.mockJwtService.verify.mockReturnValue(mockDecoded); + context.mockRedisClient.get.mockResolvedValue(mockTokenData); + + const result = await context.service.validateRefreshToken( + 'valid.refresh.token', + ); + + expect(result.id).toBe(123); + expect((result as any).tokenId).toBe('token-id-123'); + expect(result.sessionId).toBe('session-123'); + }); + + it('deve lançar exceção quando token não é do tipo refresh', async () => { + const mockDecoded = { + userId: 123, + tokenId: 'token-id-123', + type: 'access', + }; + + context.mockJwtService.verify.mockReturnValue(mockDecoded); + + await expect( + context.service.validateRefreshToken('invalid.token'), + ).rejects.toThrow(UnauthorizedException); + }); + + it('deve lançar exceção quando token não existe no Redis', async () => { + const mockDecoded = { + userId: 123, + tokenId: 'token-id-123', + sessionId: 'session-123', + type: 'refresh', + }; + + context.mockJwtService.verify.mockReturnValue(mockDecoded); + context.mockRedisClient.get.mockResolvedValue(null); + + await expect( + context.service.validateRefreshToken('expired.token'), + ).rejects.toThrow(UnauthorizedException); + }); + + it('deve lançar exceção quando token está expirado', async () => { + const mockDecoded = { + userId: 123, + tokenId: 'token-id-123', + sessionId: 'session-123', + type: 'refresh', + }; + + const mockTokenData: RefreshTokenData = { + userId: 123, + tokenId: 'token-id-123', + sessionId: 'session-123', + expiresAt: Date.now() - 1000, + createdAt: Date.now() - 1000000, + }; + + context.mockJwtService.verify.mockReturnValue(mockDecoded); + context.mockRedisClient.get.mockResolvedValue(mockTokenData); + context.mockRedisClient.del.mockResolvedValue(undefined); + + await expect( + context.service.validateRefreshToken('expired.token'), + ).rejects.toThrow(UnauthorizedException); + expect(context.mockRedisClient.del).toHaveBeenCalled(); + }); + + it('deve lançar exceção quando verificação do JWT falha', async () => { + context.mockJwtService.verify.mockImplementation(() => { + throw new Error('Token inválido'); + }); + + await expect( + context.service.validateRefreshToken('invalid.token'), + ).rejects.toThrow(UnauthorizedException); + }); + }); + + describe('revokeRefreshToken', () => { + let context: Awaited< + ReturnType + >; + + beforeEach(async () => { + context = await createRefreshTokenServiceTestModule(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('deve revogar refresh token com sucesso', async () => { + const userId = 123; + const tokenId = 'token-id-123'; + + context.mockRedisClient.del.mockResolvedValue(undefined); + + await context.service.revokeRefreshToken(userId, tokenId); + + expect(context.mockRedisClient.del).toHaveBeenCalledWith( + `auth:refresh_tokens:${userId}:${tokenId}`, + ); + }); + }); + + describe('revokeAllRefreshTokens', () => { + let context: Awaited< + ReturnType + >; + + beforeEach(async () => { + context = await createRefreshTokenServiceTestModule(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('deve revogar todos os refresh tokens do usuário', async () => { + const userId = 123; + const mockKeys = [ + 'auth:refresh_tokens:123:token-1', + 'auth:refresh_tokens:123:token-2', + 'auth:refresh_tokens:123:token-3', + ]; + + context.mockRedisClient.keys.mockResolvedValue(mockKeys); + context.mockRedisClient.del.mockResolvedValue(undefined); + + await context.service.revokeAllRefreshTokens(userId); + + expect(context.mockRedisClient.keys).toHaveBeenCalledWith( + `auth:refresh_tokens:${userId}:*`, + ); + expect(context.mockRedisClient.del).toHaveBeenCalledWith(...mockKeys); + }); + + it('deve retornar sem erro quando não há tokens para revogar', async () => { + const userId = 123; + + context.mockRedisClient.keys.mockResolvedValue([]); + + await context.service.revokeAllRefreshTokens(userId); + + expect(context.mockRedisClient.del).not.toHaveBeenCalled(); + }); + }); + + describe('getActiveRefreshTokens', () => { + let context: Awaited< + ReturnType + >; + + beforeEach(async () => { + context = await createRefreshTokenServiceTestModule(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('deve retornar tokens ativos ordenados por data de criação', async () => { + const userId = 123; + const mockKeys = [ + 'auth:refresh_tokens:123:token-1', + 'auth:refresh_tokens:123:token-2', + ]; + + const now = Date.now(); + const token1: RefreshTokenData = { + userId: 123, + tokenId: 'token-1', + expiresAt: now + 1000000, + createdAt: now - 2000, + }; + + const token2: RefreshTokenData = { + userId: 123, + tokenId: 'token-2', + expiresAt: now + 1000000, + createdAt: now - 1000, + }; + + context.mockRedisClient.keys.mockResolvedValue(mockKeys); + context.mockRedisClient.get + .mockResolvedValueOnce(token1) + .mockResolvedValueOnce(token2); + + const result = await context.service.getActiveRefreshTokens(userId); + + expect(result).toHaveLength(2); + expect(result[0].tokenId).toBe('token-2'); + expect(result[1].tokenId).toBe('token-1'); + }); + + it('deve filtrar tokens expirados', async () => { + const userId = 123; + const mockKeys = [ + 'auth:refresh_tokens:123:token-1', + 'auth:refresh_tokens:123:token-2', + ]; + + const now = Date.now(); + const token1: RefreshTokenData = { + userId: 123, + tokenId: 'token-1', + expiresAt: now - 1000, + createdAt: now - 2000, + }; + + const token2: RefreshTokenData = { + userId: 123, + tokenId: 'token-2', + expiresAt: now + 1000000, + createdAt: now - 1000, + }; + + context.mockRedisClient.keys.mockResolvedValue(mockKeys); + context.mockRedisClient.get + .mockResolvedValueOnce(token1) + .mockResolvedValueOnce(token2); + + const result = await context.service.getActiveRefreshTokens(userId); + + expect(result).toHaveLength(1); + expect(result[0].tokenId).toBe('token-2'); + }); + + it('deve retornar array vazio quando não há tokens', async () => { + const userId = 123; + + context.mockRedisClient.keys.mockResolvedValue([]); + + const result = await context.service.getActiveRefreshTokens(userId); + + expect(result).toHaveLength(0); + }); + }); +}); + diff --git a/src/auth/services/__tests__/token-blacklist.service.spec.helper.ts b/src/auth/services/__tests__/token-blacklist.service.spec.helper.ts new file mode 100644 index 0000000..c0bf006 --- /dev/null +++ b/src/auth/services/__tests__/token-blacklist.service.spec.helper.ts @@ -0,0 +1,62 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { TokenBlacklistService } from '../token-blacklist.service'; +import { IRedisClient } from '../../../core/configs/cache/IRedisClient'; +import { RedisClientToken } from '../../../core/configs/cache/redis-client.adapter.provider'; +import { JwtService } from '@nestjs/jwt'; + +export const createMockRedisClient = () => + ({ + get: jest.fn(), + set: jest.fn(), + del: jest.fn(), + keys: jest.fn(), + } as any); + +export const createMockJwtService = () => + ({ + decode: jest.fn(), + } as any); + +export interface TokenBlacklistServiceTestContext { + service: TokenBlacklistService; + mockRedisClient: jest.Mocked; + mockJwtService: jest.Mocked; +} + +export async function createTokenBlacklistServiceTestModule( + redisClientMethods: Partial = {}, + jwtServiceMethods: Partial = {}, +): Promise { + const mockRedisClient = { + ...createMockRedisClient(), + ...redisClientMethods, + } as any; + + const mockJwtService = { + ...createMockJwtService(), + ...jwtServiceMethods, + } as any; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + TokenBlacklistService, + { + provide: RedisClientToken, + useValue: mockRedisClient, + }, + { + provide: JwtService, + useValue: mockJwtService, + }, + ], + }).compile(); + + const service = module.get(TokenBlacklistService); + + return { + service, + mockRedisClient, + mockJwtService, + }; +} + diff --git a/src/auth/services/__tests__/token-blacklist.service.spec.ts b/src/auth/services/__tests__/token-blacklist.service.spec.ts new file mode 100644 index 0000000..db95702 --- /dev/null +++ b/src/auth/services/__tests__/token-blacklist.service.spec.ts @@ -0,0 +1,257 @@ +import { createTokenBlacklistServiceTestModule } from './token-blacklist.service.spec.helper'; +import { JwtPayload } from '../../models/jwt-payload.model'; + +describe('TokenBlacklistService', () => { + describe('addToBlacklist', () => { + let context: Awaited< + ReturnType + >; + + beforeEach(async () => { + context = await createTokenBlacklistServiceTestModule(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('deve adicionar token à blacklist com sucesso', async () => { + const mockToken = 'valid.jwt.token'; + const mockPayload: JwtPayload = { + id: 123, + sellerId: 1, + storeId: '1', + username: 'user', + email: 'user@example.com', + exp: Math.floor(Date.now() / 1000) + 3600, + }; + + context.mockJwtService.decode.mockReturnValue(mockPayload); + context.mockRedisClient.set.mockResolvedValue(undefined); + + await context.service.addToBlacklist(mockToken); + + expect(context.mockJwtService.decode).toHaveBeenCalledWith(mockToken); + expect(context.mockRedisClient.set).toHaveBeenCalled(); + }); + + it('deve adicionar token à blacklist com TTL customizado', async () => { + const mockToken = 'valid.jwt.token'; + const mockPayload: JwtPayload = { + id: 123, + sellerId: 1, + storeId: '1', + username: 'user', + email: 'user@example.com', + exp: Math.floor(Date.now() / 1000) + 3600, + }; + + const customTTL = 7200; + + context.mockJwtService.decode.mockReturnValue(mockPayload); + context.mockRedisClient.set.mockResolvedValue(undefined); + + await context.service.addToBlacklist(mockToken, customTTL); + + expect(context.mockRedisClient.set).toHaveBeenCalledWith( + expect.any(String), + 'blacklisted', + customTTL, + ); + }); + + it('deve calcular TTL automaticamente quando não informado', async () => { + const mockToken = 'valid.jwt.token'; + const now = Math.floor(Date.now() / 1000); + const exp = now + 3600; + const mockPayload: JwtPayload = { + id: 123, + sellerId: 1, + storeId: '1', + username: 'user', + email: 'user@example.com', + exp, + }; + + context.mockJwtService.decode.mockReturnValue(mockPayload); + context.mockRedisClient.set.mockResolvedValue(undefined); + + await context.service.addToBlacklist(mockToken); + + expect(context.mockRedisClient.set).toHaveBeenCalledWith( + expect.any(String), + 'blacklisted', + expect.any(Number), + ); + }); + + it('deve lançar erro quando token é inválido', async () => { + const mockToken = 'invalid.token'; + + context.mockJwtService.decode.mockReturnValue(null); + + await expect( + context.service.addToBlacklist(mockToken), + ).rejects.toThrow('Token inválido'); + }); + + it('deve lançar erro quando decode falha', async () => { + const mockToken = 'invalid.token'; + + context.mockJwtService.decode.mockImplementation(() => { + throw new Error('Token malformado'); + }); + + await expect( + context.service.addToBlacklist(mockToken), + ).rejects.toThrow('Erro ao adicionar token à blacklist'); + }); + }); + + describe('isBlacklisted', () => { + let context: Awaited< + ReturnType + >; + + beforeEach(async () => { + context = await createTokenBlacklistServiceTestModule(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('deve retornar true quando token está na blacklist', async () => { + const mockToken = 'blacklisted.token'; + const mockPayload: JwtPayload = { + id: 123, + sellerId: 1, + storeId: '1', + username: 'user', + email: 'user@example.com', + }; + + context.mockJwtService.decode.mockReturnValue(mockPayload); + context.mockRedisClient.get.mockResolvedValue('blacklisted'); + + const result = await context.service.isBlacklisted(mockToken); + + expect(result).toBe(true); + expect(context.mockRedisClient.get).toHaveBeenCalled(); + }); + + it('deve retornar false quando token não está na blacklist', async () => { + const mockToken = 'valid.token'; + const mockPayload: JwtPayload = { + id: 123, + sellerId: 1, + storeId: '1', + username: 'user', + email: 'user@example.com', + }; + + context.mockJwtService.decode.mockReturnValue(mockPayload); + context.mockRedisClient.get.mockResolvedValue(null); + + const result = await context.service.isBlacklisted(mockToken); + + expect(result).toBe(false); + }); + + it('deve retornar false quando ocorre erro', async () => { + const mockToken = 'error.token'; + const mockPayload: JwtPayload = { + id: 123, + sellerId: 1, + storeId: '1', + username: 'user', + email: 'user@example.com', + }; + + context.mockJwtService.decode.mockReturnValue(mockPayload); + context.mockRedisClient.get.mockRejectedValue( + new Error('Redis error'), + ); + + const result = await context.service.isBlacklisted(mockToken); + + expect(result).toBe(false); + }); + }); + + describe('removeFromBlacklist', () => { + let context: Awaited< + ReturnType + >; + + beforeEach(async () => { + context = await createTokenBlacklistServiceTestModule(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('deve remover token da blacklist com sucesso', async () => { + const mockToken = 'token.to.remove'; + const mockPayload: JwtPayload = { + id: 123, + sellerId: 1, + storeId: '1', + username: 'user', + email: 'user@example.com', + }; + + context.mockJwtService.decode.mockReturnValue(mockPayload); + context.mockRedisClient.del.mockResolvedValue(undefined); + + await context.service.removeFromBlacklist(mockToken); + + expect(context.mockRedisClient.del).toHaveBeenCalled(); + }); + }); + + describe('clearUserBlacklist', () => { + let context: Awaited< + ReturnType + >; + + beforeEach(async () => { + context = await createTokenBlacklistServiceTestModule(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('deve limpar todos os tokens do usuário da blacklist', async () => { + const userId = 123; + const mockKeys = [ + 'auth:blacklist:123:hash1', + 'auth:blacklist:123:hash2', + 'auth:blacklist:123:hash3', + ]; + + context.mockRedisClient.keys.mockResolvedValue(mockKeys); + context.mockRedisClient.del.mockResolvedValue(undefined); + + await context.service.clearUserBlacklist(userId); + + expect(context.mockRedisClient.keys).toHaveBeenCalledWith( + `auth:blacklist:${userId}:*`, + ); + expect(context.mockRedisClient.del).toHaveBeenCalledWith(...mockKeys); + }); + + it('deve retornar sem erro quando não há tokens para limpar', async () => { + const userId = 123; + + context.mockRedisClient.keys.mockResolvedValue([]); + + await context.service.clearUserBlacklist(userId); + + expect(context.mockRedisClient.del).not.toHaveBeenCalled(); + }); + }); +}); + diff --git a/src/orders/application/__tests__/deb.service.spec.helper.ts b/src/orders/application/__tests__/deb.service.spec.helper.ts new file mode 100644 index 0000000..9c2c2d2 --- /dev/null +++ b/src/orders/application/__tests__/deb.service.spec.helper.ts @@ -0,0 +1,40 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DebService } from '../deb.service'; +import { DebRepository } from '../../repositories/deb.repository'; + +export const createMockRepository = ( + methods: Partial = {}, +) => + ({ + findByCpfCgcent: jest.fn(), + ...methods, + } as any); + +export interface DebServiceTestContext { + service: DebService; + mockRepository: jest.Mocked; +} + +export async function createDebServiceTestModule( + repositoryMethods: Partial = {}, +): Promise { + const mockRepository = createMockRepository(repositoryMethods); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DebService, + { + provide: DebRepository, + useValue: mockRepository, + }, + ], + }).compile(); + + const service = module.get(DebService); + + return { + service, + mockRepository, + }; +} + diff --git a/src/orders/application/__tests__/deb.service.spec.ts b/src/orders/application/__tests__/deb.service.spec.ts new file mode 100644 index 0000000..021f518 --- /dev/null +++ b/src/orders/application/__tests__/deb.service.spec.ts @@ -0,0 +1,191 @@ +import { createDebServiceTestModule } from './deb.service.spec.helper'; +import { DebDto } from '../../dto/DebDto'; + +describe('DebService', () => { + describe('findByCpfCgcent', () => { + let context: Awaited>; + + beforeEach(async () => { + context = await createDebServiceTestModule(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('deve buscar débitos por CPF/CGCENT com sucesso', async () => { + const mockDebs: DebDto[] = [ + { + dtemissao: new Date('2024-01-15'), + codfilial: '1', + duplic: '12345', + prest: '1', + codcli: 1000, + cliente: 'JOÃO DA SILVA', + codcob: 'BL', + cobranca: 'BOLETO', + dtvenc: new Date('2024-02-15'), + dtpag: null, + valor: 150.5, + situacao: 'A VENCER', + }, + { + dtemissao: new Date('2024-01-20'), + codfilial: '1', + duplic: '12346', + prest: '2', + codcli: 1000, + cliente: 'JOÃO DA SILVA', + codcob: 'BL', + cobranca: 'BOLETO', + dtvenc: new Date('2024-02-20'), + dtpag: new Date('2024-02-10'), + valor: 200.0, + situacao: 'PAGO', + }, + ]; + + context.mockRepository.findByCpfCgcent.mockResolvedValue(mockDebs); + + const result = await context.service.findByCpfCgcent('12345678900'); + + expect(result).toHaveLength(2); + expect(result[0].codcli).toBe(1000); + expect(result[0].cliente).toBe('JOÃO DA SILVA'); + expect(result[0].situacao).toBe('A VENCER'); + expect(result[1].situacao).toBe('PAGO'); + expect(context.mockRepository.findByCpfCgcent).toHaveBeenCalledWith( + '12345678900', + undefined, + undefined, + ); + }); + + it('deve buscar débitos com matricula informada', async () => { + const mockDebs: DebDto[] = [ + { + dtemissao: new Date('2024-01-15'), + codfilial: '1', + duplic: '12345', + prest: '1', + codcli: 1000, + cliente: 'JOÃO DA SILVA', + codcob: 'BL', + cobranca: 'BOLETO', + dtvenc: new Date('2024-02-15'), + dtpag: null, + valor: 150.5, + situacao: 'A VENCER', + }, + ]; + + context.mockRepository.findByCpfCgcent.mockResolvedValue(mockDebs); + + const result = await context.service.findByCpfCgcent( + '12345678900', + 1498, + ); + + expect(result).toHaveLength(1); + expect(context.mockRepository.findByCpfCgcent).toHaveBeenCalledWith( + '12345678900', + 1498, + undefined, + ); + }); + + it('deve buscar débitos com cobranca informada', async () => { + const mockDebs: DebDto[] = [ + { + dtemissao: new Date('2024-01-15'), + codfilial: '1', + duplic: '12345', + prest: '1', + codcli: 1000, + cliente: 'JOÃO DA SILVA', + codcob: 'BL', + cobranca: 'BOLETO', + dtvenc: new Date('2024-02-15'), + dtpag: null, + valor: 150.5, + situacao: 'A VENCER', + }, + ]; + + context.mockRepository.findByCpfCgcent.mockResolvedValue(mockDebs); + + const result = await context.service.findByCpfCgcent( + '12345678900', + undefined, + 'BL', + ); + + expect(result).toHaveLength(1); + expect(context.mockRepository.findByCpfCgcent).toHaveBeenCalledWith( + '12345678900', + undefined, + 'BL', + ); + }); + + it('deve buscar débitos com matricula e cobranca informadas', async () => { + const mockDebs: DebDto[] = [ + { + dtemissao: new Date('2024-01-15'), + codfilial: '1', + duplic: '12345', + prest: '1', + codcli: 1000, + cliente: 'JOÃO DA SILVA', + codcob: 'BL', + cobranca: 'BOLETO', + dtvenc: new Date('2024-02-15'), + dtpag: null, + valor: 150.5, + situacao: 'A VENCER', + }, + ]; + + context.mockRepository.findByCpfCgcent.mockResolvedValue(mockDebs); + + const result = await context.service.findByCpfCgcent( + '12345678900', + 1498, + 'BL', + ); + + expect(result).toHaveLength(1); + expect(context.mockRepository.findByCpfCgcent).toHaveBeenCalledWith( + '12345678900', + 1498, + 'BL', + ); + }); + + it('deve retornar array vazio quando nenhum débito é encontrado', async () => { + context.mockRepository.findByCpfCgcent.mockResolvedValue([]); + + const result = await context.service.findByCpfCgcent('99999999999'); + + expect(result).toHaveLength(0); + expect(Array.isArray(result)).toBe(true); + expect(context.mockRepository.findByCpfCgcent).toHaveBeenCalledWith( + '99999999999', + undefined, + undefined, + ); + }); + + it('deve propagar erro do repositório', async () => { + const repositoryError = new Error('Database connection failed'); + context.mockRepository.findByCpfCgcent.mockRejectedValue( + repositoryError, + ); + + await expect( + context.service.findByCpfCgcent('12345678900'), + ).rejects.toThrow('Database connection failed'); + }); + }); +}); + diff --git a/src/orders/application/deb.service.ts b/src/orders/application/deb.service.ts index 4752d43..900858e 100644 --- a/src/orders/application/deb.service.ts +++ b/src/orders/application/deb.service.ts @@ -6,14 +6,6 @@ import { DebDto } from '../dto/DebDto'; export class DebService { constructor(private readonly debRepository: DebRepository) {} - /** - * Busca débitos por CPF ou CGCENT - * @param cpfCgcent - CPF ou CGCENT do cliente (validado pelo DTO) - * @param matricula - Matrícula do funcionário (opcional) - * @param cobranca - Código de cobrança (opcional) - * @returns Lista de débitos do cliente - * @throws {Error} Erro ao buscar débitos no banco de dados - */ async findByCpfCgcent( cpfCgcent: string, matricula?: number, diff --git a/src/orders/repositories/deb.repository.ts b/src/orders/repositories/deb.repository.ts index 02f73e9..5322570 100644 --- a/src/orders/repositories/deb.repository.ts +++ b/src/orders/repositories/deb.repository.ts @@ -25,47 +25,50 @@ export class DebRepository { const queryRunner = this.oracleDataSource.createQueryRunner(); await queryRunner.connect(); try { - const queryBuilder = queryRunner.manager - .createQueryBuilder() - .select([ - 'p.dtemissao AS "dtemissao"', - 'p.codfilial AS "codfilial"', - 'p.duplic AS "duplic"', - 'p.prest AS "prest"', - 'p.codcli AS "codcli"', - 'c.cliente AS "cliente"', - 'p.codcob AS "codcob"', - 'cb.cobranca AS "cobranca"', - 'p.dtvenc AS "dtvenc"', - 'p.dtpag AS "dtpag"', - 'p.valor AS "valor"', - `CASE - WHEN p.dtpag IS NOT NULL THEN 'PAGO' - WHEN p.dtvenc < TRUNC(SYSDATE) THEN 'EM ATRASO' - WHEN p.dtvenc >= TRUNC(SYSDATE) THEN 'A VENCER' - ELSE 'NENHUM' - END AS "situacao"`, - ]) - .from('pcprest', 'p') - .innerJoin('pcclient', 'c', 'p.codcli = c.codcli') - .innerJoin('pccob', 'cb', 'p.codcob = cb.codcob') - .innerJoin('pcempr', 'e', 'c.cgcent = e.cpf') - .where('p.codcob NOT IN (:...excludedCob)', { - excludedCob: ['DESD', 'CANC'], - }) - .andWhere('c.cgcent = :cpfCgcent', { cpfCgcent }); + let sql = ` + SELECT p.dtemissao AS "dtemissao", + p.codfilial AS "codfilial", + p.duplic AS "duplic", + p.prest AS "prest", + p.codcli AS "codcli", + c.cliente AS "cliente", + p.codcob AS "codcob", + cb.cobranca AS "cobranca", + p.dtvenc AS "dtvenc", + p.dtpag AS "dtpag", + p.valor AS "valor", + CASE + WHEN p.dtpag IS NOT NULL THEN 'PAGO' + WHEN p.dtvenc < TRUNC(SYSDATE) THEN 'EM ATRASO' + WHEN p.dtvenc >= TRUNC(SYSDATE) THEN 'A VENCER' + ELSE 'NENHUM' + END AS "situacao" + FROM PCPREST p + INNER JOIN PCCLIENT c ON p.codcli = c.codcli + INNER JOIN PCCOB cb ON p.codcob = cb.codcob + INNER JOIN PCEMPR e ON c.cgcent = e.cpf + WHERE p.codcob NOT IN (:0, :1) + AND c.cgcent = :2 + `; + + const params: any[] = ['DESD', 'CANC', cpfCgcent]; + let paramIndex = 3; if (matricula) { - queryBuilder.andWhere('e.matricula = :matricula', { matricula }); + sql += ` AND e.matricula = :${paramIndex}`; + params.push(matricula); + paramIndex++; } if (cobranca) { - queryBuilder.andWhere('p.codcob = :cobranca', { cobranca }); + sql += ` AND p.codcob = :${paramIndex}`; + params.push(cobranca); + paramIndex++; } - queryBuilder.orderBy('p.dtvenc', 'ASC'); + sql += ` ORDER BY p.dtvenc ASC`; - const result = await queryBuilder.getRawMany(); + const result = await queryRunner.query(sql, params); return result; } finally { await queryRunner.release();