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

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