86211923db
Repository Interface Optimization: - Created IBaseRepository.ts and IPaginatedRepository.ts - Refactored all 7 repository interfaces to extend base interfaces - Eliminated ~200 lines of redundant code (70% reduction) - Improved type safety and maintainability Dependency Injection Improvements: - Added EmailService and GameTokenService to DIContainer - Updated CreateUserCommandHandler constructor for DI - Updated RequestPasswordResetCommandHandler constructor for DI - Enhanced testability and service consistency Environment Configuration: - Created comprehensive .env.example with 40+ variables - Organized into 12 logical sections (Database, Security, Email, etc.) - Added security guidelines and best practices - Documented all backend environment requirements Documentation: - Added comprehensive codebase review - Created refactoring summary report - Added frontend implementation guide Impact: Improved code quality, reduced maintenance overhead, enhanced developer experience
246 lines
8.7 KiB
TypeScript
246 lines
8.7 KiB
TypeScript
import { RedisService } from '../../../src/Application/Services/RedisService';
|
|
import { logStartup, logError } from '../../../src/Application/Services/Logger';
|
|
|
|
describe('RedisService', () => {
|
|
let redisService: RedisService;
|
|
|
|
beforeAll(async () => {
|
|
redisService = RedisService.getInstance();
|
|
|
|
try {
|
|
await redisService.connect();
|
|
} catch (error) {
|
|
console.log('Redis not available for testing, skipping Redis tests');
|
|
return;
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (redisService.isRedisConnected()) {
|
|
await redisService.disconnect();
|
|
}
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// Skip tests if Redis is not connected
|
|
if (!redisService.isRedisConnected()) {
|
|
return;
|
|
}
|
|
|
|
// Clean up test data
|
|
const activeChats = await redisService.getAllActiveChats();
|
|
for (const chat of activeChats) {
|
|
if (chat.chatId.startsWith('test-')) {
|
|
await redisService.removeActiveChat(chat.chatId);
|
|
}
|
|
}
|
|
|
|
await redisService.removeActiveUser('test-user-1');
|
|
await redisService.removeActiveUser('test-user-2');
|
|
});
|
|
|
|
describe('Active Chat Management', () => {
|
|
it('should store and retrieve active chats', async () => {
|
|
if (!redisService.isRedisConnected()) {
|
|
return;
|
|
}
|
|
|
|
const testChatData = {
|
|
chatId: 'test-chat-1',
|
|
participants: ['user-1', 'user-2'],
|
|
lastActivity: new Date(),
|
|
messageCount: 5,
|
|
chatType: 'direct' as const,
|
|
name: 'Test Chat'
|
|
};
|
|
|
|
await redisService.setActiveChat('test-chat-1', testChatData);
|
|
const retrieved = await redisService.getActiveChat('test-chat-1');
|
|
|
|
expect(retrieved).toBeDefined();
|
|
expect(retrieved!.chatId).toBe('test-chat-1');
|
|
expect(retrieved!.participants).toEqual(['user-1', 'user-2']);
|
|
expect(retrieved!.messageCount).toBe(5);
|
|
expect(retrieved!.chatType).toBe('direct');
|
|
expect(retrieved!.name).toBe('Test Chat');
|
|
});
|
|
|
|
it('should return null for non-existent chat', async () => {
|
|
if (!redisService.isRedisConnected()) {
|
|
return;
|
|
}
|
|
|
|
const retrieved = await redisService.getActiveChat('non-existent-chat');
|
|
expect(retrieved).toBeNull();
|
|
});
|
|
|
|
it('should remove active chats', async () => {
|
|
if (!redisService.isRedisConnected()) {
|
|
return;
|
|
}
|
|
|
|
const testChatData = {
|
|
chatId: 'test-chat-2',
|
|
participants: ['user-1', 'user-2'],
|
|
lastActivity: new Date(),
|
|
messageCount: 0,
|
|
chatType: 'group' as const
|
|
};
|
|
|
|
await redisService.setActiveChat('test-chat-2', testChatData);
|
|
let retrieved = await redisService.getActiveChat('test-chat-2');
|
|
expect(retrieved).toBeDefined();
|
|
|
|
await redisService.removeActiveChat('test-chat-2');
|
|
retrieved = await redisService.getActiveChat('test-chat-2');
|
|
expect(retrieved).toBeNull();
|
|
});
|
|
|
|
it('should update chat activity', async () => {
|
|
if (!redisService.isRedisConnected()) {
|
|
return;
|
|
}
|
|
|
|
const originalTime = new Date(Date.now() - 60000); // 1 minute ago
|
|
const testChatData = {
|
|
chatId: 'test-chat-3',
|
|
participants: ['user-1', 'user-2'],
|
|
lastActivity: originalTime,
|
|
messageCount: 5,
|
|
chatType: 'direct' as const
|
|
};
|
|
|
|
await redisService.setActiveChat('test-chat-3', testChatData);
|
|
|
|
// Wait a bit to ensure timestamp difference
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
|
|
await redisService.updateChatActivity('test-chat-3', 6);
|
|
|
|
const retrieved = await redisService.getActiveChat('test-chat-3');
|
|
expect(retrieved).toBeDefined();
|
|
expect(retrieved!.messageCount).toBe(6);
|
|
expect(retrieved!.lastActivity.getTime()).toBeGreaterThan(originalTime.getTime());
|
|
});
|
|
});
|
|
|
|
describe('Active User Management', () => {
|
|
it('should store and retrieve active users', async () => {
|
|
if (!redisService.isRedisConnected()) {
|
|
return;
|
|
}
|
|
|
|
const testUserData = {
|
|
userId: 'test-user-1',
|
|
activeChatIds: ['chat-1', 'chat-2'],
|
|
lastActivity: new Date(),
|
|
isOnline: true
|
|
};
|
|
|
|
await redisService.setActiveUser('test-user-1', testUserData);
|
|
const retrieved = await redisService.getActiveUser('test-user-1');
|
|
|
|
expect(retrieved).toBeDefined();
|
|
expect(retrieved!.userId).toBe('test-user-1');
|
|
expect(retrieved!.activeChatIds).toEqual(['chat-1', 'chat-2']);
|
|
expect(retrieved!.isOnline).toBe(true);
|
|
});
|
|
|
|
it('should manage user-chat associations', async () => {
|
|
if (!redisService.isRedisConnected()) {
|
|
return;
|
|
}
|
|
|
|
// Add user to chats
|
|
await redisService.addUserToChat('test-user-2', 'chat-1');
|
|
await redisService.addUserToChat('test-user-2', 'chat-2');
|
|
|
|
let activeChatIds = await redisService.getUserActiveChats('test-user-2');
|
|
expect(activeChatIds).toContain('chat-1');
|
|
expect(activeChatIds).toContain('chat-2');
|
|
|
|
// Remove user from one chat
|
|
await redisService.removeUserFromChat('test-user-2', 'chat-1');
|
|
activeChatIds = await redisService.getUserActiveChats('test-user-2');
|
|
expect(activeChatIds).not.toContain('chat-1');
|
|
expect(activeChatIds).toContain('chat-2');
|
|
});
|
|
});
|
|
|
|
describe('Inactive Chat Cleanup', () => {
|
|
it('should identify inactive chats', async () => {
|
|
if (!redisService.isRedisConnected()) {
|
|
return;
|
|
}
|
|
|
|
const oldTime = new Date(Date.now() - 2 * 60 * 60 * 1000); // 2 hours ago
|
|
const recentTime = new Date();
|
|
|
|
// Create an inactive chat
|
|
await redisService.setActiveChat('test-inactive-chat', {
|
|
chatId: 'test-inactive-chat',
|
|
participants: ['user-1', 'user-2'],
|
|
lastActivity: oldTime,
|
|
messageCount: 3,
|
|
chatType: 'direct'
|
|
});
|
|
|
|
// Create an active chat
|
|
await redisService.setActiveChat('test-active-chat', {
|
|
chatId: 'test-active-chat',
|
|
participants: ['user-1', 'user-3'],
|
|
lastActivity: recentTime,
|
|
messageCount: 1,
|
|
chatType: 'direct'
|
|
});
|
|
|
|
const inactiveChats = await redisService.getInactiveChats(60); // 60 minutes
|
|
expect(inactiveChats).toContain('test-inactive-chat');
|
|
expect(inactiveChats).not.toContain('test-active-chat');
|
|
|
|
// Cleanup
|
|
await redisService.removeActiveChat('test-inactive-chat');
|
|
await redisService.removeActiveChat('test-active-chat');
|
|
});
|
|
|
|
it('should cleanup inactive chats', async () => {
|
|
if (!redisService.isRedisConnected()) {
|
|
return;
|
|
}
|
|
|
|
const oldTime = new Date(Date.now() - 2 * 60 * 60 * 1000); // 2 hours ago
|
|
|
|
await redisService.setActiveChat('test-cleanup-chat', {
|
|
chatId: 'test-cleanup-chat',
|
|
participants: ['user-1', 'user-2'],
|
|
lastActivity: oldTime,
|
|
messageCount: 0,
|
|
chatType: 'direct'
|
|
});
|
|
|
|
const cleanedUp = await redisService.cleanupInactiveChats(60);
|
|
expect(cleanedUp).toContain('test-cleanup-chat');
|
|
|
|
// Verify chat was removed
|
|
const retrieved = await redisService.getActiveChat('test-cleanup-chat');
|
|
expect(retrieved).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('Health Check', () => {
|
|
it('should ping Redis successfully', async () => {
|
|
if (!redisService.isRedisConnected()) {
|
|
return;
|
|
}
|
|
|
|
const pingResult = await redisService.ping();
|
|
expect(pingResult).toBe(true);
|
|
});
|
|
|
|
it('should report connection status', () => {
|
|
const isConnected = redisService.isRedisConnected();
|
|
expect(typeof isConnected).toBe('boolean');
|
|
});
|
|
});
|
|
});
|