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:
joelson brito
2025-11-10 16:24:02 -03:00
parent e3acf34510
commit c07df023dd
9 changed files with 1315 additions and 41 deletions

273
docs/COBERTURA_TESTES.md Normal file
View 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

View File

@@ -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,
};
}

View 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);
});
});
});

View File

@@ -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,
};
}

View 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();
});
});
});

View 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,
};
}

View 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');
});
});
});

View File

@@ -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,

View File

@@ -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"', WHEN p.dtpag IS NOT NULL THEN 'PAGO'
`CASE WHEN p.dtvenc < TRUNC(SYSDATE) THEN 'EM ATRASO'
WHEN p.dtpag IS NOT NULL THEN 'PAGO' WHEN p.dtvenc >= TRUNC(SYSDATE) THEN 'A VENCER'
WHEN p.dtvenc < TRUNC(SYSDATE) THEN 'EM ATRASO' ELSE 'NENHUM'
WHEN p.dtvenc >= TRUNC(SYSDATE) THEN 'A VENCER' END AS "situacao"
ELSE 'NENHUM' FROM PCPREST p
END AS "situacao"`, INNER JOIN PCCLIENT c ON p.codcli = c.codcli
]) INNER JOIN PCCOB cb ON p.codcob = cb.codcob
.from('pcprest', 'p') INNER JOIN PCEMPR e ON c.cgcent = e.cpf
.innerJoin('pcclient', 'c', 'p.codcli = c.codcli') WHERE p.codcob NOT IN (:0, :1)
.innerJoin('pccob', 'cb', 'p.codcob = cb.codcob') AND c.cgcent = :2
.innerJoin('pcempr', 'e', 'c.cgcent = e.cpf') `;
.where('p.codcob NOT IN (:...excludedCob)', {
excludedCob: ['DESD', 'CANC'], const params: any[] = ['DESD', 'CANC', cpfCgcent];
}) let paramIndex = 3;
.andWhere('c.cgcent = :cpfCgcent', { cpfCgcent });
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();