Adiciona testes para RefreshTokenService e TokenBlacklistService
- 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
This commit is contained in:
273
docs/COBERTURA_TESTES.md
Normal file
273
docs/COBERTURA_TESTES.md
Normal file
@@ -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
|
||||||
|
|
||||||
@@ -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<IRedisClient>;
|
||||||
|
mockJwtService: jest.Mocked<JwtService>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRefreshTokenServiceTestModule(
|
||||||
|
redisClientMethods: Partial<IRedisClient> = {},
|
||||||
|
jwtServiceMethods: Partial<JwtService> = {},
|
||||||
|
): Promise<RefreshTokenServiceTestContext> {
|
||||||
|
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>(RefreshTokenService);
|
||||||
|
|
||||||
|
return {
|
||||||
|
service,
|
||||||
|
mockRedisClient,
|
||||||
|
mockJwtService,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
392
src/auth/services/__tests__/refresh-token.service.spec.ts
Normal file
392
src/auth/services/__tests__/refresh-token.service.spec.ts
Normal file
@@ -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<typeof createRefreshTokenServiceTestModule>
|
||||||
|
>;
|
||||||
|
|
||||||
|
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<typeof createRefreshTokenServiceTestModule>
|
||||||
|
>;
|
||||||
|
|
||||||
|
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<typeof createRefreshTokenServiceTestModule>
|
||||||
|
>;
|
||||||
|
|
||||||
|
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<typeof createRefreshTokenServiceTestModule>
|
||||||
|
>;
|
||||||
|
|
||||||
|
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<typeof createRefreshTokenServiceTestModule>
|
||||||
|
>;
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -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<IRedisClient>;
|
||||||
|
mockJwtService: jest.Mocked<JwtService>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTokenBlacklistServiceTestModule(
|
||||||
|
redisClientMethods: Partial<IRedisClient> = {},
|
||||||
|
jwtServiceMethods: Partial<JwtService> = {},
|
||||||
|
): Promise<TokenBlacklistServiceTestContext> {
|
||||||
|
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>(TokenBlacklistService);
|
||||||
|
|
||||||
|
return {
|
||||||
|
service,
|
||||||
|
mockRedisClient,
|
||||||
|
mockJwtService,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
257
src/auth/services/__tests__/token-blacklist.service.spec.ts
Normal file
257
src/auth/services/__tests__/token-blacklist.service.spec.ts
Normal file
@@ -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<typeof createTokenBlacklistServiceTestModule>
|
||||||
|
>;
|
||||||
|
|
||||||
|
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<typeof createTokenBlacklistServiceTestModule>
|
||||||
|
>;
|
||||||
|
|
||||||
|
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<typeof createTokenBlacklistServiceTestModule>
|
||||||
|
>;
|
||||||
|
|
||||||
|
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<typeof createTokenBlacklistServiceTestModule>
|
||||||
|
>;
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
40
src/orders/application/__tests__/deb.service.spec.helper.ts
Normal file
40
src/orders/application/__tests__/deb.service.spec.helper.ts
Normal file
@@ -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<DebRepository> = {},
|
||||||
|
) =>
|
||||||
|
({
|
||||||
|
findByCpfCgcent: jest.fn(),
|
||||||
|
...methods,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
export interface DebServiceTestContext {
|
||||||
|
service: DebService;
|
||||||
|
mockRepository: jest.Mocked<DebRepository>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createDebServiceTestModule(
|
||||||
|
repositoryMethods: Partial<DebRepository> = {},
|
||||||
|
): Promise<DebServiceTestContext> {
|
||||||
|
const mockRepository = createMockRepository(repositoryMethods);
|
||||||
|
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
DebService,
|
||||||
|
{
|
||||||
|
provide: DebRepository,
|
||||||
|
useValue: mockRepository,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
const service = module.get<DebService>(DebService);
|
||||||
|
|
||||||
|
return {
|
||||||
|
service,
|
||||||
|
mockRepository,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
191
src/orders/application/__tests__/deb.service.spec.ts
Normal file
191
src/orders/application/__tests__/deb.service.spec.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { createDebServiceTestModule } from './deb.service.spec.helper';
|
||||||
|
import { DebDto } from '../../dto/DebDto';
|
||||||
|
|
||||||
|
describe('DebService', () => {
|
||||||
|
describe('findByCpfCgcent', () => {
|
||||||
|
let context: Awaited<ReturnType<typeof createDebServiceTestModule>>;
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -6,14 +6,6 @@ import { DebDto } from '../dto/DebDto';
|
|||||||
export class DebService {
|
export class DebService {
|
||||||
constructor(private readonly debRepository: DebRepository) {}
|
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(
|
async findByCpfCgcent(
|
||||||
cpfCgcent: string,
|
cpfCgcent: string,
|
||||||
matricula?: number,
|
matricula?: number,
|
||||||
|
|||||||
@@ -25,47 +25,50 @@ export class DebRepository {
|
|||||||
const queryRunner = this.oracleDataSource.createQueryRunner();
|
const queryRunner = this.oracleDataSource.createQueryRunner();
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
try {
|
try {
|
||||||
const queryBuilder = queryRunner.manager
|
let sql = `
|
||||||
.createQueryBuilder()
|
SELECT p.dtemissao AS "dtemissao",
|
||||||
.select([
|
p.codfilial AS "codfilial",
|
||||||
'p.dtemissao AS "dtemissao"',
|
p.duplic AS "duplic",
|
||||||
'p.codfilial AS "codfilial"',
|
p.prest AS "prest",
|
||||||
'p.duplic AS "duplic"',
|
p.codcli AS "codcli",
|
||||||
'p.prest AS "prest"',
|
c.cliente AS "cliente",
|
||||||
'p.codcli AS "codcli"',
|
p.codcob AS "codcob",
|
||||||
'c.cliente AS "cliente"',
|
cb.cobranca AS "cobranca",
|
||||||
'p.codcob AS "codcob"',
|
p.dtvenc AS "dtvenc",
|
||||||
'cb.cobranca AS "cobranca"',
|
p.dtpag AS "dtpag",
|
||||||
'p.dtvenc AS "dtvenc"',
|
p.valor AS "valor",
|
||||||
'p.dtpag AS "dtpag"',
|
CASE
|
||||||
'p.valor AS "valor"',
|
|
||||||
`CASE
|
|
||||||
WHEN p.dtpag IS NOT NULL THEN 'PAGO'
|
WHEN p.dtpag IS NOT NULL THEN 'PAGO'
|
||||||
WHEN p.dtvenc < TRUNC(SYSDATE) THEN 'EM ATRASO'
|
WHEN p.dtvenc < TRUNC(SYSDATE) THEN 'EM ATRASO'
|
||||||
WHEN p.dtvenc >= TRUNC(SYSDATE) THEN 'A VENCER'
|
WHEN p.dtvenc >= TRUNC(SYSDATE) THEN 'A VENCER'
|
||||||
ELSE 'NENHUM'
|
ELSE 'NENHUM'
|
||||||
END AS "situacao"`,
|
END AS "situacao"
|
||||||
])
|
FROM PCPREST p
|
||||||
.from('pcprest', 'p')
|
INNER JOIN PCCLIENT c ON p.codcli = c.codcli
|
||||||
.innerJoin('pcclient', 'c', 'p.codcli = c.codcli')
|
INNER JOIN PCCOB cb ON p.codcob = cb.codcob
|
||||||
.innerJoin('pccob', 'cb', 'p.codcob = cb.codcob')
|
INNER JOIN PCEMPR e ON c.cgcent = e.cpf
|
||||||
.innerJoin('pcempr', 'e', 'c.cgcent = e.cpf')
|
WHERE p.codcob NOT IN (:0, :1)
|
||||||
.where('p.codcob NOT IN (:...excludedCob)', {
|
AND c.cgcent = :2
|
||||||
excludedCob: ['DESD', 'CANC'],
|
`;
|
||||||
})
|
|
||||||
.andWhere('c.cgcent = :cpfCgcent', { cpfCgcent });
|
const params: any[] = ['DESD', 'CANC', cpfCgcent];
|
||||||
|
let paramIndex = 3;
|
||||||
|
|
||||||
if (matricula) {
|
if (matricula) {
|
||||||
queryBuilder.andWhere('e.matricula = :matricula', { matricula });
|
sql += ` AND e.matricula = :${paramIndex}`;
|
||||||
|
params.push(matricula);
|
||||||
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cobranca) {
|
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;
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
|
|||||||
Reference in New Issue
Block a user