forked from datawhale/whale-town-end
范围:src/business/chat/ - 实现 ChatService 聊天业务服务(登录/登出/消息发送/位置更新) - 实现 ChatSessionService 会话管理服务(会话创建/销毁/上下文注入) - 实现 ChatFilterService 消息过滤服务(频率限制/敏感词/权限验证) - 实现 ChatCleanupService 会话清理服务(定时清理过期会话) - 添加完整的单元测试覆盖 - 添加模块 README 文档
247 lines
7.0 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|