Files
whale-town-end/src/business/chat/services/chat_cleanup.service.spec.ts
moyin 30a4a2813d feat(chat): 新增聊天业务模块
范围:src/business/chat/
- 实现 ChatService 聊天业务服务(登录/登出/消息发送/位置更新)
- 实现 ChatSessionService 会话管理服务(会话创建/销毁/上下文注入)
- 实现 ChatFilterService 消息过滤服务(频率限制/敏感词/权限验证)
- 实现 ChatCleanupService 会话清理服务(定时清理过期会话)
- 添加完整的单元测试覆盖
- 添加模块 README 文档
2026-01-14 19:17:32 +08:00

247 lines
7.0 KiB
TypeScript

/**
* 聊天会话清理服务测试
*
* 测试范围:
* - 定时清理任务启动和停止
* - 过期会话清理逻辑
* - 手动触发清理功能
* - 资源释放和错误处理
*
* @author moyin
* @version 1.0.0
* @since 2026-01-14
* @lastModified 2026-01-14
*/
import { Test, TestingModule } from '@nestjs/testing';
import { Logger } from '@nestjs/common';
import { ChatCleanupService } from './chat_cleanup.service';
import { ChatSessionService } from './chat_session.service';
describe('ChatCleanupService', () => {
let service: ChatCleanupService;
let sessionService: jest.Mocked<ChatSessionService>;
beforeEach(async () => {
const mockSessionService = {
cleanupExpiredSessions: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
ChatCleanupService,
{
provide: ChatSessionService,
useValue: mockSessionService,
},
],
}).compile();
service = module.get<ChatCleanupService>(ChatCleanupService);
sessionService = module.get(ChatSessionService);
// 禁用日志输出
jest.spyOn(Logger.prototype, 'log').mockImplementation();
jest.spyOn(Logger.prototype, 'error').mockImplementation();
jest.spyOn(Logger.prototype, 'warn').mockImplementation();
jest.spyOn(Logger.prototype, 'debug').mockImplementation();
});
afterEach(() => {
jest.clearAllMocks();
jest.clearAllTimers();
});
describe('初始化', () => {
it('应该成功创建服务实例', () => {
expect(service).toBeDefined();
});
it('应该在模块初始化时启动清理任务', async () => {
jest.useFakeTimers();
const startCleanupTaskSpy = jest.spyOn(service as any, 'startCleanupTask');
await service.onModuleInit();
expect(startCleanupTaskSpy).toHaveBeenCalled();
jest.useRealTimers();
});
it('应该在模块销毁时停止清理任务', async () => {
jest.useFakeTimers();
const stopCleanupTaskSpy = jest.spyOn(service as any, 'stopCleanupTask');
await service.onModuleDestroy();
expect(stopCleanupTaskSpy).toHaveBeenCalled();
jest.useRealTimers();
});
});
describe('定时清理任务', () => {
it('应该定时执行清理操作', async () => {
jest.useFakeTimers();
sessionService.cleanupExpiredSessions.mockResolvedValue({
cleanedCount: 5,
zulipQueueIds: ['queue_1', 'queue_2'],
});
await service.onModuleInit();
// 快进5分钟
jest.advanceTimersByTime(5 * 60 * 1000);
await Promise.resolve();
expect(sessionService.cleanupExpiredSessions).toHaveBeenCalled();
jest.useRealTimers();
});
it('应该在停止任务后不再执行清理', async () => {
jest.useFakeTimers();
sessionService.cleanupExpiredSessions.mockResolvedValue({
cleanedCount: 0,
zulipQueueIds: [],
});
await service.onModuleInit();
await service.onModuleDestroy();
sessionService.cleanupExpiredSessions.mockClear();
// 快进5分钟
jest.advanceTimersByTime(5 * 60 * 1000);
await Promise.resolve();
expect(sessionService.cleanupExpiredSessions).not.toHaveBeenCalled();
jest.useRealTimers();
});
});
describe('triggerCleanup', () => {
it('应该成功执行手动清理', async () => {
sessionService.cleanupExpiredSessions.mockResolvedValue({
cleanedCount: 3,
zulipQueueIds: ['queue_1', 'queue_2', 'queue_3'],
});
const result = await service.triggerCleanup();
expect(result.cleanedCount).toBe(3);
expect(sessionService.cleanupExpiredSessions).toHaveBeenCalledWith(30);
});
it('应该处理清理失败', async () => {
sessionService.cleanupExpiredSessions.mockRejectedValue(new Error('Redis error'));
await expect(service.triggerCleanup()).rejects.toThrow('Redis error');
});
it('应该返回清理数量为0当没有过期会话', async () => {
sessionService.cleanupExpiredSessions.mockResolvedValue({
cleanedCount: 0,
zulipQueueIds: [],
});
const result = await service.triggerCleanup();
expect(result.cleanedCount).toBe(0);
});
});
describe('清理逻辑', () => {
it('应该清理多个过期会话', async () => {
jest.useFakeTimers();
sessionService.cleanupExpiredSessions.mockResolvedValue({
cleanedCount: 10,
zulipQueueIds: Array.from({ length: 10 }, (_, i) => `queue_${i}`),
});
await service.onModuleInit();
jest.advanceTimersByTime(5 * 60 * 1000);
await Promise.resolve();
expect(sessionService.cleanupExpiredSessions).toHaveBeenCalledWith(30);
jest.useRealTimers();
});
it('应该处理清理过程中的异常', async () => {
jest.useFakeTimers();
sessionService.cleanupExpiredSessions.mockRejectedValue(new Error('Cleanup failed'));
await service.onModuleInit();
jest.advanceTimersByTime(5 * 60 * 1000);
await Promise.resolve();
// 应该记录错误但不抛出异常
expect(sessionService.cleanupExpiredSessions).toHaveBeenCalled();
jest.useRealTimers();
});
it('应该处理Zulip队列清理', async () => {
jest.useFakeTimers();
const zulipQueueIds = ['queue_1', 'queue_2', 'queue_3'];
sessionService.cleanupExpiredSessions.mockResolvedValue({
cleanedCount: 3,
zulipQueueIds,
});
await service.onModuleInit();
jest.advanceTimersByTime(5 * 60 * 1000);
await Promise.resolve();
expect(sessionService.cleanupExpiredSessions).toHaveBeenCalled();
jest.useRealTimers();
});
});
describe('边界情况', () => {
it('应该处理空的清理结果', async () => {
sessionService.cleanupExpiredSessions.mockResolvedValue({
cleanedCount: 0,
zulipQueueIds: [],
});
const result = await service.triggerCleanup();
expect(result.cleanedCount).toBe(0);
});
it('应该处理大量过期会话', async () => {
const largeCount = 1000;
sessionService.cleanupExpiredSessions.mockResolvedValue({
cleanedCount: largeCount,
zulipQueueIds: Array.from({ length: largeCount }, (_, i) => `queue_${i}`),
});
const result = await service.triggerCleanup();
expect(result.cleanedCount).toBe(largeCount);
});
it('应该处理重复启动清理任务', async () => {
jest.useFakeTimers();
await service.onModuleInit();
await service.onModuleInit();
// 应该只有一个定时器在运行
jest.advanceTimersByTime(5 * 60 * 1000);
await Promise.resolve();
jest.useRealTimers();
});
it('应该处理重复停止清理任务', async () => {
jest.useFakeTimers();
await service.onModuleInit();
await service.onModuleDestroy();
await service.onModuleDestroy();
// 不应该抛出异常
expect(service['cleanupInterval']).toBeNull();
jest.useRealTimers();
});
});
});