Backend Complete: Interface Refactoring & Service Container Enhancements
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
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user