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,278 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
|
||||
import { AppDataSource } from '../../../src/Infrastructure/ormconfig';
|
||||
import { ChatRepository } from '../../../src/Infrastructure/Repository/ChatRepository';
|
||||
import { ChatArchiveRepository } from '../../../src/Infrastructure/Repository/ChatArchiveRepository';
|
||||
import { UserRepository } from '../../../src/Infrastructure/Repository/UserRepository';
|
||||
import { ChatType } from '../../../src/Domain/Chat/ChatAggregate';
|
||||
import { UserState } from '../../../src/Domain/User/UserAggregate';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
describe('Chat Messaging System', () => {
|
||||
let chatRepository: ChatRepository;
|
||||
let chatArchiveRepository: ChatArchiveRepository;
|
||||
let userRepository: UserRepository;
|
||||
|
||||
let testUser1: any;
|
||||
let testUser2: any;
|
||||
let testPremiumUser: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
if (!AppDataSource.isInitialized) {
|
||||
await AppDataSource.initialize();
|
||||
}
|
||||
|
||||
chatRepository = new ChatRepository();
|
||||
chatArchiveRepository = new ChatArchiveRepository();
|
||||
userRepository = new UserRepository();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create test users
|
||||
testUser1 = await userRepository.create({
|
||||
username: `testuser1_${Date.now()}`,
|
||||
email: `test1_${Date.now()}@example.com`,
|
||||
password: 'hashedpassword',
|
||||
fname: 'Test',
|
||||
lname: 'User1',
|
||||
type: 'regular',
|
||||
state: UserState.VERIFIED_REGULAR
|
||||
});
|
||||
|
||||
testUser2 = await userRepository.create({
|
||||
username: `testuser2_${Date.now()}`,
|
||||
email: `test2_${Date.now()}@example.com`,
|
||||
password: 'hashedpassword',
|
||||
fname: 'Test',
|
||||
lname: 'User2',
|
||||
type: 'regular',
|
||||
state: UserState.VERIFIED_REGULAR
|
||||
});
|
||||
|
||||
testPremiumUser = await userRepository.create({
|
||||
username: `premiumuser_${Date.now()}`,
|
||||
email: `premium_${Date.now()}@example.com`,
|
||||
password: 'hashedpassword',
|
||||
fname: 'Premium',
|
||||
lname: 'User',
|
||||
type: 'premium',
|
||||
state: UserState.VERIFIED_PREMIUM
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (AppDataSource.isInitialized) {
|
||||
await AppDataSource.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
describe('Direct Chat Creation', () => {
|
||||
it('should create a direct chat between two users', async () => {
|
||||
const chat = await chatRepository.create({
|
||||
type: ChatType.DIRECT,
|
||||
users: [testUser1.id, testUser2.id],
|
||||
messages: [],
|
||||
lastActivity: new Date()
|
||||
});
|
||||
|
||||
expect(chat).toBeDefined();
|
||||
expect(chat.type).toBe(ChatType.DIRECT);
|
||||
expect(chat.users).toEqual([testUser1.id, testUser2.id]);
|
||||
expect(chat.messages).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Group Chat Creation', () => {
|
||||
it('should create a group chat', async () => {
|
||||
const chat = await chatRepository.create({
|
||||
type: ChatType.GROUP,
|
||||
name: 'Test Group',
|
||||
createdBy: testPremiumUser.id,
|
||||
users: [testPremiumUser.id, testUser1.id, testUser2.id],
|
||||
messages: [],
|
||||
lastActivity: new Date()
|
||||
});
|
||||
|
||||
expect(chat).toBeDefined();
|
||||
expect(chat.type).toBe(ChatType.GROUP);
|
||||
expect(chat.name).toBe('Test Group');
|
||||
expect(chat.createdBy).toBe(testPremiumUser.id);
|
||||
expect(chat.users.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Game Chat Creation', () => {
|
||||
it('should create a game chat', async () => {
|
||||
const gameId = uuidv4();
|
||||
|
||||
const chat = await chatRepository.create({
|
||||
type: ChatType.GAME,
|
||||
name: 'Test Game Chat',
|
||||
gameId: gameId,
|
||||
users: [testUser1.id, testUser2.id],
|
||||
messages: [],
|
||||
lastActivity: new Date()
|
||||
});
|
||||
|
||||
expect(chat).toBeDefined();
|
||||
expect(chat.type).toBe(ChatType.GAME);
|
||||
expect(chat.gameId).toBe(gameId);
|
||||
expect(chat.name).toBe('Test Game Chat');
|
||||
});
|
||||
|
||||
it('should find game chat by game id', async () => {
|
||||
const gameId = uuidv4();
|
||||
|
||||
await chatRepository.create({
|
||||
type: ChatType.GAME,
|
||||
name: 'Test Game Chat',
|
||||
gameId: gameId,
|
||||
users: [testUser1.id, testUser2.id],
|
||||
messages: [],
|
||||
lastActivity: new Date()
|
||||
});
|
||||
|
||||
const foundChat = await chatRepository.findByGameId(gameId);
|
||||
expect(foundChat).toBeDefined();
|
||||
expect(foundChat!.gameId).toBe(gameId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Message Management', () => {
|
||||
it('should add and retrieve messages', async () => {
|
||||
const chat = await chatRepository.create({
|
||||
type: ChatType.DIRECT,
|
||||
users: [testUser1.id, testUser2.id],
|
||||
messages: [],
|
||||
lastActivity: new Date()
|
||||
});
|
||||
|
||||
const message = {
|
||||
id: uuidv4(),
|
||||
date: new Date(),
|
||||
userid: testUser1.id,
|
||||
text: 'Hello, this is a test message!'
|
||||
};
|
||||
|
||||
await chatRepository.update(chat.id, {
|
||||
messages: [message],
|
||||
lastActivity: new Date()
|
||||
});
|
||||
|
||||
const updatedChat = await chatRepository.findById(chat.id);
|
||||
expect(updatedChat!.messages).toHaveLength(1);
|
||||
expect(updatedChat!.messages[0].text).toBe('Hello, this is a test message!');
|
||||
expect(updatedChat!.messages[0].userid).toBe(testUser1.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Chat Archiving', () => {
|
||||
it('should archive a chat with messages', async () => {
|
||||
const message = {
|
||||
id: uuidv4(),
|
||||
date: new Date(),
|
||||
userid: testUser1.id,
|
||||
text: 'Message to be archived'
|
||||
};
|
||||
|
||||
const chat = await chatRepository.create({
|
||||
type: ChatType.DIRECT,
|
||||
users: [testUser1.id, testUser2.id],
|
||||
messages: [message],
|
||||
lastActivity: new Date()
|
||||
});
|
||||
|
||||
const archive = await chatRepository.archiveChat(chat);
|
||||
|
||||
expect(archive).toBeDefined();
|
||||
expect(archive.chatId).toBe(chat.id);
|
||||
expect(archive.archivedMessages).toHaveLength(1);
|
||||
expect(archive.archivedMessages[0].text).toBe('Message to be archived');
|
||||
|
||||
// Check that chat messages were cleared
|
||||
const archivedChat = await chatRepository.findById(chat.id);
|
||||
expect(archivedChat!.messages).toEqual([]);
|
||||
expect(archivedChat!.archiveDate).toBeDefined();
|
||||
});
|
||||
|
||||
it('should retrieve archived chat', async () => {
|
||||
const message = {
|
||||
id: uuidv4(),
|
||||
date: new Date(),
|
||||
userid: testUser1.id,
|
||||
text: 'Archived message'
|
||||
};
|
||||
|
||||
const chat = await chatRepository.create({
|
||||
type: ChatType.DIRECT,
|
||||
users: [testUser1.id, testUser2.id],
|
||||
messages: [message],
|
||||
lastActivity: new Date()
|
||||
});
|
||||
|
||||
await chatRepository.archiveChat(chat);
|
||||
|
||||
const archive = await chatRepository.getArchivedChat(chat.id);
|
||||
expect(archive).toBeDefined();
|
||||
expect(archive!.archivedMessages).toHaveLength(1);
|
||||
expect(archive!.archivedMessages[0].text).toBe('Archived message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Chat Queries', () => {
|
||||
it('should find chats by user id', async () => {
|
||||
const chat1 = await chatRepository.create({
|
||||
type: ChatType.DIRECT,
|
||||
users: [testUser1.id, testUser2.id],
|
||||
messages: [],
|
||||
lastActivity: new Date()
|
||||
});
|
||||
|
||||
const chat2 = await chatRepository.create({
|
||||
type: ChatType.GROUP,
|
||||
name: 'Test Group',
|
||||
createdBy: testPremiumUser.id,
|
||||
users: [testPremiumUser.id, testUser1.id],
|
||||
messages: [],
|
||||
lastActivity: new Date()
|
||||
});
|
||||
|
||||
const userChats = await chatRepository.findByUserId(testUser1.id);
|
||||
expect(userChats.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const chatIds = userChats.map(c => c.id);
|
||||
expect(chatIds).toContain(chat1.id);
|
||||
expect(chatIds).toContain(chat2.id);
|
||||
});
|
||||
|
||||
it('should find active chats for user', async () => {
|
||||
await chatRepository.create({
|
||||
type: ChatType.DIRECT,
|
||||
users: [testUser1.id, testUser2.id],
|
||||
messages: [],
|
||||
lastActivity: new Date()
|
||||
});
|
||||
|
||||
const activeChats = await chatRepository.findActiveChatsForUser(testUser1.id);
|
||||
expect(activeChats.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// All returned chats should be active
|
||||
activeChats.forEach(chat => {
|
||||
expect(chat.users).toContain(testUser1.id);
|
||||
});
|
||||
});
|
||||
|
||||
it('should find inactive chats', async () => {
|
||||
const oldDate = new Date(Date.now() - 2 * 60 * 60 * 1000); // 2 hours ago
|
||||
|
||||
await chatRepository.create({
|
||||
type: ChatType.DIRECT,
|
||||
users: [testUser1.id, testUser2.id],
|
||||
messages: [],
|
||||
lastActivity: oldDate
|
||||
});
|
||||
|
||||
const inactiveChats = await chatRepository.findInactiveChats(60); // 60 minutes
|
||||
expect(inactiveChats.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
+402
@@ -0,0 +1,402 @@
|
||||
import { CreateContactCommandHandler } from '../../../../src/Application/Contact/commands/CreateContactCommandHandler';
|
||||
import { UpdateContactCommandHandler } from '../../../../src/Application/Contact/commands/UpdateContactCommandHandler';
|
||||
import { DeleteContactCommandHandler } from '../../../../src/Application/Contact/commands/DeleteContactCommandHandler';
|
||||
import { CreateContactCommand } from '../../../../src/Application/Contact/commands/CreateContactCommand';
|
||||
import { UpdateContactCommand } from '../../../../src/Application/Contact/commands/UpdateContactCommand';
|
||||
import { DeleteContactCommand } from '../../../../src/Application/Contact/commands/DeleteContactCommand';
|
||||
import { ContactType, ContactState } from '../../../../src/Domain/Contact/ContactAggregate';
|
||||
import { createMockContactRepository, createMockContact } from '../../../testUtils';
|
||||
|
||||
describe('Contact Command Handlers - Comprehensive', () => {
|
||||
let mockContactRepository: ReturnType<typeof createMockContactRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContactRepository = createMockContactRepository();
|
||||
});
|
||||
|
||||
describe('CreateContactCommandHandler', () => {
|
||||
let handler: CreateContactCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new CreateContactCommandHandler(mockContactRepository);
|
||||
});
|
||||
|
||||
it('should create contact successfully with all fields', async () => {
|
||||
// Arrange
|
||||
const mockContactData = createMockContact({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
userid: '123e4567-e89b-12d3-a456-426614174000',
|
||||
type: ContactType.QUESTION,
|
||||
txt: 'Test question',
|
||||
state: ContactState.ACTIVE
|
||||
});
|
||||
|
||||
mockContactRepository.create.mockResolvedValue(mockContactData);
|
||||
|
||||
const command: CreateContactCommand = {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
userid: '123e4567-e89b-12d3-a456-426614174000',
|
||||
type: ContactType.QUESTION,
|
||||
txt: 'Test question'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert - Returns ShortContactDto
|
||||
expect(result).toEqual({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
type: ContactType.QUESTION,
|
||||
state: ContactState.ACTIVE,
|
||||
createDate: expect.any(Date)
|
||||
});
|
||||
expect(mockContactRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
userid: '123e4567-e89b-12d3-a456-426614174000',
|
||||
type: ContactType.QUESTION,
|
||||
txt: 'Test question',
|
||||
state: ContactState.ACTIVE
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should create contact without userid (anonymous)', async () => {
|
||||
// Arrange
|
||||
const mockContactData = createMockContact({
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
name: 'Anonymous User',
|
||||
email: 'anon@example.com',
|
||||
userid: null,
|
||||
type: ContactType.BUG,
|
||||
txt: 'Bug report',
|
||||
state: ContactState.ACTIVE
|
||||
});
|
||||
|
||||
mockContactRepository.create.mockResolvedValue(mockContactData);
|
||||
|
||||
const command: CreateContactCommand = {
|
||||
name: 'Anonymous User',
|
||||
email: 'anon@example.com',
|
||||
type: ContactType.BUG,
|
||||
txt: 'Bug report'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
name: 'Anonymous User',
|
||||
email: 'anon@example.com',
|
||||
type: ContactType.BUG,
|
||||
state: ContactState.ACTIVE,
|
||||
createDate: expect.any(Date)
|
||||
});
|
||||
expect(mockContactRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userid: null
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should create contact with different contact types', async () => {
|
||||
const testCases = [
|
||||
{ type: ContactType.BUG, description: 'Bug report' },
|
||||
{ type: ContactType.PROBLEM, description: 'Problem report' },
|
||||
{ type: ContactType.QUESTION, description: 'Question' },
|
||||
{ type: ContactType.SALES, description: 'Sales inquiry' },
|
||||
{ type: ContactType.OTHER, description: 'Other inquiry' }
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
// Arrange
|
||||
const mockContactData = createMockContact({
|
||||
type: testCase.type,
|
||||
txt: testCase.description
|
||||
});
|
||||
|
||||
mockContactRepository.create.mockResolvedValue(mockContactData);
|
||||
|
||||
const command: CreateContactCommand = {
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
type: testCase.type,
|
||||
txt: testCase.description
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result.type).toBe(testCase.type);
|
||||
expect(mockContactRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
txt: testCase.description
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle database errors', async () => {
|
||||
// Arrange
|
||||
const command: CreateContactCommand = {
|
||||
name: 'Error User',
|
||||
email: 'error@example.com',
|
||||
type: ContactType.QUESTION,
|
||||
txt: 'This will cause an error'
|
||||
};
|
||||
|
||||
mockContactRepository.create.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to create contact');
|
||||
});
|
||||
|
||||
it('should handle non-Error exceptions', async () => {
|
||||
// Arrange
|
||||
const command: CreateContactCommand = {
|
||||
name: 'Exception User',
|
||||
email: 'exception@example.com',
|
||||
type: ContactType.QUESTION,
|
||||
txt: 'This will cause an exception'
|
||||
};
|
||||
|
||||
mockContactRepository.create.mockRejectedValue('String error');
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to create contact');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateContactCommandHandler', () => {
|
||||
let handler: UpdateContactCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new UpdateContactCommandHandler(mockContactRepository);
|
||||
});
|
||||
|
||||
it('should update contact with admin response', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
adminResponse: null,
|
||||
state: ContactState.ACTIVE
|
||||
});
|
||||
|
||||
const updatedContact = createMockContact({
|
||||
...existingContact,
|
||||
adminResponse: 'Thank you for your inquiry',
|
||||
state: ContactState.RESOLVED,
|
||||
responseDate: new Date(),
|
||||
respondedBy: 'admin123'
|
||||
});
|
||||
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.update.mockResolvedValue(updatedContact);
|
||||
|
||||
const command: UpdateContactCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
adminResponse: 'Thank you for your inquiry'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert - Returns DetailContactDto
|
||||
expect(result).toEqual({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: expect.any(String),
|
||||
email: expect.any(String),
|
||||
userid: expect.any(String),
|
||||
type: expect.any(Number),
|
||||
txt: expect.any(String),
|
||||
state: ContactState.RESOLVED,
|
||||
createDate: expect.any(Date),
|
||||
updateDate: expect.any(Date),
|
||||
adminResponse: 'Thank you for your inquiry',
|
||||
responseDate: expect.any(Date),
|
||||
respondedBy: 'admin123'
|
||||
});
|
||||
});
|
||||
|
||||
it('should update contact state', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
state: ContactState.ACTIVE
|
||||
});
|
||||
|
||||
const updatedContact = createMockContact({
|
||||
...existingContact,
|
||||
state: ContactState.RESOLVED
|
||||
});
|
||||
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.update.mockResolvedValue(updatedContact);
|
||||
|
||||
const command: UpdateContactCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
state: ContactState.RESOLVED
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result.state).toBe(ContactState.RESOLVED);
|
||||
});
|
||||
|
||||
it('should throw error when contact not found', async () => {
|
||||
// Arrange
|
||||
mockContactRepository.findById.mockResolvedValue(null);
|
||||
|
||||
const command: UpdateContactCommand = {
|
||||
id: 'non-existent-id',
|
||||
adminResponse: 'Response'
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Contact not found');
|
||||
});
|
||||
|
||||
it('should handle repository errors during update', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact();
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.update.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const command: UpdateContactCommand = {
|
||||
id: 'existing-id',
|
||||
adminResponse: 'Response'
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to update contact');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeleteContactCommandHandler', () => {
|
||||
let handler: DeleteContactCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new DeleteContactCommandHandler(mockContactRepository);
|
||||
});
|
||||
|
||||
it('should perform soft delete successfully', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000'
|
||||
});
|
||||
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.softDelete.mockResolvedValue(null);
|
||||
|
||||
const command: DeleteContactCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
hard: false
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockContactRepository.findById).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000');
|
||||
expect(mockContactRepository.softDelete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000');
|
||||
expect(mockContactRepository.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should perform hard delete successfully', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000'
|
||||
});
|
||||
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.delete.mockResolvedValue(true);
|
||||
|
||||
const command: DeleteContactCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
hard: true
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockContactRepository.delete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000');
|
||||
expect(mockContactRepository.softDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should default to soft delete when hard flag not specified', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact();
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.softDelete.mockResolvedValue(null);
|
||||
|
||||
const command: DeleteContactCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockContactRepository.softDelete).toHaveBeenCalled();
|
||||
expect(mockContactRepository.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when contact not found', async () => {
|
||||
// Arrange
|
||||
mockContactRepository.findById.mockResolvedValue(null);
|
||||
|
||||
const command: DeleteContactCommand = {
|
||||
id: 'non-existent-id',
|
||||
hard: false
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Contact not found');
|
||||
});
|
||||
|
||||
it('should handle repository errors during deletion', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact();
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.softDelete.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const command: DeleteContactCommand = {
|
||||
id: 'existing-id',
|
||||
hard: false
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to delete contact');
|
||||
});
|
||||
|
||||
it('should handle hard delete repository errors', async () => {
|
||||
// Arrange
|
||||
const existingContact = createMockContact();
|
||||
mockContactRepository.findById.mockResolvedValue(existingContact);
|
||||
mockContactRepository.delete.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const command: DeleteContactCommand = {
|
||||
id: 'existing-id',
|
||||
hard: true
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to delete contact');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import { ContactMapper } from '../../../../src/Application/DTOs/Mappers/ContactMapper';
|
||||
import { ContactType, ContactState } from '../../../../src/Domain/Contact/ContactAggregate';
|
||||
|
||||
describe('ContactMapper', () => {
|
||||
const createMockContact = (overrides: any = {}) => ({
|
||||
id: 'contact-123',
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
userid: 'user-456',
|
||||
type: ContactType.QUESTION,
|
||||
txt: 'This is a test contact message.',
|
||||
state: ContactState.ACTIVE,
|
||||
createDate: new Date('2024-01-01'),
|
||||
updateDate: new Date('2024-01-02'),
|
||||
adminResponse: null,
|
||||
responseDate: null,
|
||||
respondedBy: null,
|
||||
...overrides
|
||||
});
|
||||
|
||||
describe('toShortDto', () => {
|
||||
it('should map ContactAggregate to ShortContactDto correctly', () => {
|
||||
// Arrange
|
||||
const contact = createMockContact();
|
||||
|
||||
// Act
|
||||
const result = ContactMapper.toShortDto(contact);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
id: 'contact-123',
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
type: ContactType.QUESTION,
|
||||
createDate: new Date('2024-01-01'),
|
||||
state: ContactState.ACTIVE,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle different contact types', () => {
|
||||
// Arrange
|
||||
const bugContact = createMockContact({
|
||||
id: 'bug-contact',
|
||||
type: ContactType.BUG,
|
||||
name: 'Bug Reporter'
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = ContactMapper.toShortDto(bugContact);
|
||||
|
||||
// Assert
|
||||
expect(result.type).toBe(ContactType.BUG);
|
||||
expect(result.name).toBe('Bug Reporter');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toDetailDto', () => {
|
||||
it('should map ContactAggregate to DetailContactDto correctly', () => {
|
||||
// Arrange
|
||||
const contact = createMockContact();
|
||||
|
||||
// Act
|
||||
const result = ContactMapper.toDetailDto(contact);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
id: 'contact-123',
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
userid: 'user-456',
|
||||
type: ContactType.QUESTION,
|
||||
txt: 'This is a test contact message.',
|
||||
state: ContactState.ACTIVE,
|
||||
createDate: new Date('2024-01-01'),
|
||||
updateDate: new Date('2024-01-02'),
|
||||
adminResponse: null,
|
||||
responseDate: null,
|
||||
respondedBy: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle contact with admin response', () => {
|
||||
// Arrange
|
||||
const respondedContact = createMockContact({
|
||||
adminResponse: 'Thank you for your question. Here is the answer...',
|
||||
responseDate: new Date('2024-01-03'),
|
||||
respondedBy: 'admin-789'
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = ContactMapper.toDetailDto(respondedContact);
|
||||
|
||||
// Assert
|
||||
expect(result.adminResponse).toBe('Thank you for your question. Here is the answer...');
|
||||
expect(result.responseDate).toEqual(new Date('2024-01-03'));
|
||||
expect(result.respondedBy).toBe('admin-789');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toShortDtoList', () => {
|
||||
it('should map array of ContactAggregate to array of ShortContactDto', () => {
|
||||
// Arrange
|
||||
const contacts = [
|
||||
createMockContact({ id: 'contact-1', name: 'First Contact' }),
|
||||
createMockContact({ id: 'contact-2', name: 'Second Contact', type: ContactType.BUG }),
|
||||
createMockContact({ id: 'contact-3', name: 'Third Contact', type: ContactType.SALES })
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = ContactMapper.toShortDtoList(contacts);
|
||||
|
||||
// Assert
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0]).toEqual({
|
||||
id: 'contact-1',
|
||||
name: 'First Contact',
|
||||
email: 'john.doe@example.com',
|
||||
type: ContactType.QUESTION,
|
||||
createDate: new Date('2024-01-01'),
|
||||
state: ContactState.ACTIVE,
|
||||
});
|
||||
expect(result[1].type).toBe(ContactType.BUG);
|
||||
expect(result[2].type).toBe(ContactType.SALES);
|
||||
});
|
||||
|
||||
it('should handle empty array', () => {
|
||||
// Arrange
|
||||
const contacts: any[] = [];
|
||||
|
||||
// Act
|
||||
const result = ContactMapper.toShortDtoList(contacts);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,187 @@
|
||||
import { DeckMapper } from '../../../../src/Application/DTOs/Mappers/DeckMapper';
|
||||
import { Type, CType, State } from '../../../../src/Domain/Deck/DeckAggregate';
|
||||
|
||||
describe('DeckMapper', () => {
|
||||
const createMockDeck = (overrides: any = {}) => ({
|
||||
id: 'deck-123',
|
||||
name: 'Test Deck',
|
||||
type: Type.LUCK,
|
||||
userid: 'user-123',
|
||||
creationdate: new Date('2024-01-01'),
|
||||
cards: [
|
||||
{ text: 'Test card 1', answer: 'Answer 1' },
|
||||
{ text: 'Test card 2' }
|
||||
],
|
||||
playedNumber: 5,
|
||||
ctype: CType.PUBLIC,
|
||||
updatedate: new Date('2024-01-02'),
|
||||
state: State.ACTIVE,
|
||||
organization: null,
|
||||
...overrides
|
||||
});
|
||||
|
||||
describe('toShortDto', () => {
|
||||
it('should map DeckAggregate to ShortDeckDto correctly', () => {
|
||||
// Arrange
|
||||
const deck = createMockDeck();
|
||||
|
||||
// Act
|
||||
const result = DeckMapper.toShortDto(deck);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
id: 'deck-123',
|
||||
name: 'Test Deck',
|
||||
type: Type.LUCK,
|
||||
playedNumber: 5,
|
||||
ctype: CType.PUBLIC
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle different deck types', () => {
|
||||
// Arrange
|
||||
const jokeDeck = createMockDeck({
|
||||
id: 'joker-deck',
|
||||
name: 'Joker Deck',
|
||||
type: Type.JOKER,
|
||||
playedNumber: 10
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = DeckMapper.toShortDto(jokeDeck);
|
||||
|
||||
// Assert
|
||||
expect(result.type).toBe(Type.JOKER);
|
||||
expect(result.playedNumber).toBe(10);
|
||||
});
|
||||
|
||||
it('should handle private decks', () => {
|
||||
// Arrange
|
||||
const privateDeck = createMockDeck({
|
||||
ctype: CType.PRIVATE,
|
||||
playedNumber: 0
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = DeckMapper.toShortDto(privateDeck);
|
||||
|
||||
// Assert
|
||||
expect(result.ctype).toBe(CType.PRIVATE);
|
||||
expect(result.playedNumber).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toDetailDto', () => {
|
||||
it('should map DeckAggregate to DetailDeckDto correctly', () => {
|
||||
// Arrange
|
||||
const deck = createMockDeck();
|
||||
|
||||
// Act
|
||||
const result = DeckMapper.toDetailDto(deck);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
id: 'deck-123',
|
||||
name: 'Test Deck',
|
||||
type: Type.LUCK,
|
||||
userid: 'user-123',
|
||||
creationdate: new Date('2024-01-01'),
|
||||
cards: [
|
||||
{ text: 'Test card 1', answer: 'Answer 1' },
|
||||
{ text: 'Test card 2' }
|
||||
],
|
||||
playedNumber: 5,
|
||||
ctype: CType.PUBLIC
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty cards array', () => {
|
||||
// Arrange
|
||||
const deckWithNoCards = createMockDeck({
|
||||
cards: []
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = DeckMapper.toDetailDto(deckWithNoCards);
|
||||
|
||||
// Assert
|
||||
expect(result.cards).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle question type deck', () => {
|
||||
// Arrange
|
||||
const questionDeck = createMockDeck({
|
||||
type: Type.QUESTION,
|
||||
cards: [
|
||||
{ text: 'Question 1?', answer: 'Answer 1' },
|
||||
{ text: 'Question 2?', answer: null }
|
||||
]
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = DeckMapper.toDetailDto(questionDeck);
|
||||
|
||||
// Assert
|
||||
expect(result.type).toBe(Type.QUESTION);
|
||||
expect(result.cards).toHaveLength(2);
|
||||
expect(result.cards[1].answer).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('toShortDtoList', () => {
|
||||
it('should map array of DeckAggregate to array of ShortDeckDto', () => {
|
||||
// Arrange
|
||||
const decks = [
|
||||
createMockDeck({ id: 'deck-1', name: 'First Deck' }),
|
||||
createMockDeck({ id: 'deck-2', name: 'Second Deck', type: Type.JOKER }),
|
||||
createMockDeck({ id: 'deck-3', name: 'Third Deck', ctype: CType.PRIVATE })
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = DeckMapper.toShortDtoList(decks);
|
||||
|
||||
// Assert
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0]).toEqual({
|
||||
id: 'deck-1',
|
||||
name: 'First Deck',
|
||||
type: Type.LUCK,
|
||||
playedNumber: 5,
|
||||
ctype: CType.PUBLIC
|
||||
});
|
||||
expect(result[1].type).toBe(Type.JOKER);
|
||||
expect(result[2].ctype).toBe(CType.PRIVATE);
|
||||
});
|
||||
|
||||
it('should handle empty array', () => {
|
||||
// Arrange
|
||||
const decks: any[] = [];
|
||||
|
||||
// Act
|
||||
const result = DeckMapper.toShortDtoList(decks);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([]);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle large arrays', () => {
|
||||
// Arrange
|
||||
const decks = Array.from({ length: 50 }, (_, i) =>
|
||||
createMockDeck({
|
||||
id: `deck-${i + 1}`,
|
||||
name: `Deck ${i + 1}`,
|
||||
playedNumber: i
|
||||
})
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = DeckMapper.toShortDtoList(decks);
|
||||
|
||||
// Assert
|
||||
expect(result).toHaveLength(50);
|
||||
expect(result[0].playedNumber).toBe(0);
|
||||
expect(result[49].playedNumber).toBe(49);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
import { OrganizationMapper } from '../../../../src/Application/DTOs/Mappers/OrganizationMapper';
|
||||
import { OrganizationState, OrganizationStateType } from '../../../../src/Domain/Organization/OrganizationAggregate';
|
||||
|
||||
describe('OrganizationMapper', () => {
|
||||
const createMockOrganization = (overrides: any = {}) => ({
|
||||
id: 'org-123',
|
||||
name: 'Test Organization',
|
||||
contactfname: 'John',
|
||||
contactlname: 'Doe',
|
||||
contactphone: '+1234567890',
|
||||
contactemail: 'john@test.org',
|
||||
state: OrganizationState.ACTIVE as OrganizationStateType,
|
||||
regdate: new Date('2024-01-01'),
|
||||
updatedate: new Date('2024-01-02'),
|
||||
url: 'https://test.org',
|
||||
userinorg: 5,
|
||||
users: [
|
||||
{ id: 'user-1', name: 'User One' },
|
||||
{ id: 'user-2', name: 'User Two' }
|
||||
],
|
||||
...overrides
|
||||
});
|
||||
|
||||
describe('toShortDto', () => {
|
||||
it('should map OrganizationAggregate to ShortOrganizationDto correctly', () => {
|
||||
// Arrange
|
||||
const org = createMockOrganization();
|
||||
|
||||
// Act
|
||||
const result = OrganizationMapper.toShortDto(org);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
id: 'org-123',
|
||||
name: 'Test Organization',
|
||||
state: OrganizationState.ACTIVE,
|
||||
userinorg: 5
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle different organization states', () => {
|
||||
// Arrange
|
||||
const registeredOrg = createMockOrganization({
|
||||
state: OrganizationState.REGISTERED,
|
||||
userinorg: 0
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = OrganizationMapper.toShortDto(registeredOrg);
|
||||
|
||||
// Assert
|
||||
expect(result.state).toBe(OrganizationState.REGISTERED);
|
||||
expect(result.userinorg).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle organization with many users', () => {
|
||||
// Arrange
|
||||
const orgWithManyUsers = createMockOrganization({
|
||||
userinorg: 100
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = OrganizationMapper.toShortDto(orgWithManyUsers);
|
||||
|
||||
// Assert
|
||||
expect(result.userinorg).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toDetailDto', () => {
|
||||
it('should map OrganizationAggregate to DetailOrganizationDto correctly', () => {
|
||||
// Arrange
|
||||
const org = createMockOrganization();
|
||||
|
||||
// Act
|
||||
const result = OrganizationMapper.toDetailDto(org);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
id: 'org-123',
|
||||
name: 'Test Organization',
|
||||
contactfname: 'John',
|
||||
contactlname: 'Doe',
|
||||
contactphone: '+1234567890',
|
||||
contactemail: 'john@test.org',
|
||||
state: OrganizationState.ACTIVE,
|
||||
regdate: new Date('2024-01-01'),
|
||||
updatedate: new Date('2024-01-02'),
|
||||
url: 'https://test.org',
|
||||
userinorg: 5,
|
||||
users: ['user-1', 'user-2']
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle organization without URL', () => {
|
||||
// Arrange
|
||||
const orgWithoutUrl = createMockOrganization({
|
||||
url: null
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = OrganizationMapper.toDetailDto(orgWithoutUrl);
|
||||
|
||||
// Assert
|
||||
expect(result.url).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle organization without users', () => {
|
||||
// Arrange
|
||||
const orgWithoutUsers = createMockOrganization({
|
||||
users: null,
|
||||
userinorg: 0
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = OrganizationMapper.toDetailDto(orgWithoutUsers);
|
||||
|
||||
// Assert
|
||||
expect(result.users).toEqual([]);
|
||||
expect(result.userinorg).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle empty users array', () => {
|
||||
// Arrange
|
||||
const orgWithEmptyUsers = createMockOrganization({
|
||||
users: [],
|
||||
userinorg: 0
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = OrganizationMapper.toDetailDto(orgWithEmptyUsers);
|
||||
|
||||
// Assert
|
||||
expect(result.users).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle soft deleted organization', () => {
|
||||
// Arrange
|
||||
const softDeletedOrg = createMockOrganization({
|
||||
state: OrganizationState.SOFT_DELETE
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = OrganizationMapper.toDetailDto(softDeletedOrg);
|
||||
|
||||
// Assert
|
||||
expect(result.state).toBe(OrganizationState.SOFT_DELETE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toShortDtoList', () => {
|
||||
it('should map array of OrganizationAggregate to array of ShortOrganizationDto', () => {
|
||||
// Arrange
|
||||
const orgs = [
|
||||
createMockOrganization({ id: 'org-1', name: 'First Org', userinorg: 10 }),
|
||||
createMockOrganization({ id: 'org-2', name: 'Second Org', state: OrganizationState.REGISTERED }),
|
||||
createMockOrganization({ id: 'org-3', name: 'Third Org', userinorg: 0 })
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = OrganizationMapper.toShortDtoList(orgs);
|
||||
|
||||
// Assert
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0]).toEqual({
|
||||
id: 'org-1',
|
||||
name: 'First Org',
|
||||
state: OrganizationState.ACTIVE,
|
||||
userinorg: 10
|
||||
});
|
||||
expect(result[1].state).toBe(OrganizationState.REGISTERED);
|
||||
expect(result[2].userinorg).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle empty array', () => {
|
||||
// Arrange
|
||||
const orgs: any[] = [];
|
||||
|
||||
// Act
|
||||
const result = OrganizationMapper.toShortDtoList(orgs);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([]);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle large arrays', () => {
|
||||
// Arrange
|
||||
const orgs = Array.from({ length: 25 }, (_, i) =>
|
||||
createMockOrganization({
|
||||
id: `org-${i + 1}`,
|
||||
name: `Organization ${i + 1}`,
|
||||
userinorg: i * 2
|
||||
})
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = OrganizationMapper.toShortDtoList(orgs);
|
||||
|
||||
// Assert
|
||||
expect(result).toHaveLength(25);
|
||||
expect(result[0].userinorg).toBe(0);
|
||||
expect(result[24].userinorg).toBe(48);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
import { UserMapper } from '../../../../src/Application/DTOs/Mappers/UserMapper';
|
||||
import { UserAggregate, UserState } from '../../../../src/Domain/User/UserAggregate';
|
||||
import { createMockUser } from '../../../testUtils';
|
||||
|
||||
describe('UserMapper', () => {
|
||||
describe('toShortDto', () => {
|
||||
it('should map UserAggregate to ShortUserDto correctly', () => {
|
||||
// Arrange
|
||||
const user = createMockUser({
|
||||
id: 'user-123',
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
fname: 'John',
|
||||
lname: 'Doe',
|
||||
state: UserState.VERIFIED_REGULAR
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = UserMapper.toShortDto(user);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
id: 'user-123',
|
||||
username: 'testuser',
|
||||
state: UserState.VERIFIED_REGULAR,
|
||||
authLevel: 0
|
||||
});
|
||||
// Should not contain sensitive information
|
||||
expect(result).not.toHaveProperty('email');
|
||||
expect(result).not.toHaveProperty('password');
|
||||
expect(result).not.toHaveProperty('token');
|
||||
});
|
||||
|
||||
it('should map admin user with authLevel 1', () => {
|
||||
// Arrange
|
||||
const adminUser = createMockUser({
|
||||
id: 'admin-123',
|
||||
username: 'admin',
|
||||
email: 'admin@example.com',
|
||||
fname: 'Admin',
|
||||
lname: 'User',
|
||||
state: UserState.ADMIN
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = UserMapper.toShortDto(adminUser);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
id: 'admin-123',
|
||||
username: 'admin',
|
||||
state: UserState.ADMIN,
|
||||
authLevel: 1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toDetailDto', () => {
|
||||
it('should map UserAggregate to DetailUserDto correctly', () => {
|
||||
// Arrange
|
||||
const user = createMockUser({
|
||||
id: 'user-123',
|
||||
orgid: 'org-456',
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
fname: 'John',
|
||||
lname: 'Doe',
|
||||
token: 'verification-token',
|
||||
type: 'admin',
|
||||
phone: '+1234567890',
|
||||
state: UserState.ADMIN
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = UserMapper.toDetailDto(user);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
id: 'user-123',
|
||||
orgid: 'org-456',
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
fname: 'John',
|
||||
lname: 'Doe',
|
||||
code: 'verification-token',
|
||||
type: 'admin',
|
||||
phone: '+1234567890',
|
||||
state: UserState.ADMIN
|
||||
});
|
||||
// Should not contain password
|
||||
expect(result).not.toHaveProperty('password');
|
||||
});
|
||||
|
||||
it('should handle null values correctly', () => {
|
||||
// Arrange
|
||||
const user = createMockUser({
|
||||
id: 'user-123',
|
||||
orgid: null,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
fname: 'John',
|
||||
lname: 'Doe',
|
||||
token: null,
|
||||
type: 'regular',
|
||||
phone: null,
|
||||
state: UserState.VERIFIED_REGULAR
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = UserMapper.toDetailDto(user);
|
||||
|
||||
// Assert
|
||||
expect(result.orgid).toBeNull();
|
||||
expect(result.code).toBeNull();
|
||||
expect(result.phone).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('toShortDtoList', () => {
|
||||
it('should map array of UserAggregate to ShortUserDto array', () => {
|
||||
// Arrange
|
||||
const users = [
|
||||
createMockUser({ id: 'user-1', username: 'user1', state: UserState.VERIFIED_REGULAR }),
|
||||
createMockUser({ id: 'user-2', username: 'user2', state: UserState.REGISTERED_NOT_VERIFIED }),
|
||||
createMockUser({ id: 'user-3', username: 'user3', state: UserState.DEACTIVATED })
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = UserMapper.toShortDtoList(users);
|
||||
|
||||
// Assert
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0]).toEqual({
|
||||
id: 'user-1',
|
||||
username: 'user1',
|
||||
state: UserState.VERIFIED_REGULAR,
|
||||
authLevel: 0
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
id: 'user-2',
|
||||
username: 'user2',
|
||||
state: UserState.REGISTERED_NOT_VERIFIED,
|
||||
authLevel: 0
|
||||
});
|
||||
expect(result[2]).toEqual({
|
||||
id: 'user-3',
|
||||
username: 'user3',
|
||||
state: UserState.DEACTIVATED,
|
||||
authLevel: 0
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty array', () => {
|
||||
// Arrange
|
||||
const users: UserAggregate[] = [];
|
||||
|
||||
// Act
|
||||
const result = UserMapper.toShortDtoList(users);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
import { CreateDeckCommandHandler } from '../../../../src/Application/Deck/commands/CreateDeckCommandHandler';
|
||||
import { CreateDeckCommand } from '../../../../src/Application/Deck/commands/CreateDeckCommand';
|
||||
import { IDeckRepository } from '../../../../src/Domain/IRepository/IDeckRepository';
|
||||
import { IUserRepository } from '../../../../src/Domain/IRepository/IUserRepository';
|
||||
import { IOrganizationRepository } from '../../../../src/Domain/IRepository/IOrganizationRepository';
|
||||
import { UserState } from '../../../../src/Domain/User/UserAggregate';
|
||||
import { Type as DeckType } from '../../../../src/Domain/Deck/DeckAggregate';
|
||||
import { createMockDeck, createMockDeckRepository, createMockUserRepository, createMockOrganizationRepository, createMockUser } from '../../../testUtils';
|
||||
|
||||
describe('CreateDeckCommandHandler', () => {
|
||||
let handler: CreateDeckCommandHandler;
|
||||
let mockDeckRepository: jest.Mocked<IDeckRepository>;
|
||||
let mockUserRepository: jest.Mocked<IUserRepository>;
|
||||
let mockOrganizationRepository: jest.Mocked<IOrganizationRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockDeckRepository = createMockDeckRepository();
|
||||
mockUserRepository = createMockUserRepository();
|
||||
mockOrganizationRepository = createMockOrganizationRepository();
|
||||
|
||||
handler = new CreateDeckCommandHandler(mockDeckRepository, mockUserRepository, mockOrganizationRepository);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should successfully create a new deck with valid user', async () => {
|
||||
// Arrange
|
||||
const command: CreateDeckCommand = {
|
||||
name: 'Test Deck',
|
||||
type: DeckType.JOKER,
|
||||
userid: 'user-123',
|
||||
cards: [{ id: 'card-1', name: 'Test Card' }],
|
||||
};
|
||||
|
||||
const mockUser = createMockUser({
|
||||
id: command.userid,
|
||||
state: UserState.VERIFIED_REGULAR,
|
||||
type: 'user'
|
||||
});
|
||||
|
||||
const mockDeck = createMockDeck({
|
||||
name: command.name,
|
||||
type: command.type,
|
||||
userid: command.userid
|
||||
});
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(mockUser);
|
||||
mockDeckRepository.countActiveByUserId.mockResolvedValue(0);
|
||||
mockDeckRepository.create.mockResolvedValue(mockDeck);
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
expect(mockUserRepository.findById).toHaveBeenCalledWith(command.userid);
|
||||
expect(mockDeckRepository.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when user not found', async () => {
|
||||
// Arrange
|
||||
const command: CreateDeckCommand = {
|
||||
name: 'Test Deck',
|
||||
type: DeckType.JOKER,
|
||||
userid: 'nonexistent-user',
|
||||
cards: [],
|
||||
};
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(null);
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('User not found');
|
||||
expect(mockUserRepository.findById).toHaveBeenCalledWith(command.userid);
|
||||
expect(mockDeckRepository.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle admin user creating unlimited decks', async () => {
|
||||
// Arrange
|
||||
const command: CreateDeckCommand = {
|
||||
name: 'Admin Deck',
|
||||
type: DeckType.JOKER,
|
||||
userid: 'admin-123',
|
||||
cards: [],
|
||||
};
|
||||
|
||||
const mockAdminUser = createMockUser({
|
||||
id: command.userid,
|
||||
state: UserState.VERIFIED_REGULAR,
|
||||
type: 'admin'
|
||||
});
|
||||
|
||||
const mockDeck = createMockDeck({
|
||||
name: command.name,
|
||||
type: command.type,
|
||||
userid: command.userid
|
||||
});
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(mockAdminUser);
|
||||
mockDeckRepository.create.mockResolvedValue(mockDeck);
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
expect(mockDeckRepository.countActiveByUserId).toHaveBeenCalled(); // Admin still checks but bypasses limits
|
||||
});
|
||||
|
||||
it('should handle repository creation errors', async () => {
|
||||
// Arrange
|
||||
const command: CreateDeckCommand = {
|
||||
name: 'Test Deck',
|
||||
type: DeckType.JOKER,
|
||||
userid: 'user-123',
|
||||
cards: [],
|
||||
};
|
||||
|
||||
const mockUser = createMockUser({ id: command.userid });
|
||||
mockUserRepository.findById.mockResolvedValue(mockUser);
|
||||
mockDeckRepository.countActiveByUserId.mockResolvedValue(0);
|
||||
mockDeckRepository.create.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Database error');
|
||||
});
|
||||
|
||||
it('should create deck with different types', async () => {
|
||||
// Arrange
|
||||
const command: CreateDeckCommand = {
|
||||
name: 'Question Deck',
|
||||
type: DeckType.QUESTION,
|
||||
userid: 'user-123',
|
||||
cards: [],
|
||||
};
|
||||
|
||||
const mockUser = createMockUser({ id: command.userid });
|
||||
const mockDeck = createMockDeck({
|
||||
name: command.name,
|
||||
type: command.type,
|
||||
userid: command.userid
|
||||
});
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(mockUser);
|
||||
mockDeckRepository.countActiveByUserId.mockResolvedValue(0);
|
||||
mockDeckRepository.create.mockResolvedValue(mockDeck);
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
expect(mockDeckRepository.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: DeckType.QUESTION
|
||||
}));
|
||||
});
|
||||
|
||||
it('should handle empty cards array', async () => {
|
||||
// Arrange
|
||||
const command: CreateDeckCommand = {
|
||||
name: 'Empty Deck',
|
||||
type: DeckType.JOKER,
|
||||
userid: 'user-123',
|
||||
cards: [],
|
||||
};
|
||||
|
||||
const mockUser = createMockUser({ id: command.userid });
|
||||
const mockDeck = createMockDeck(command);
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(mockUser);
|
||||
mockDeckRepository.countActiveByUserId.mockResolvedValue(0);
|
||||
mockDeckRepository.create.mockResolvedValue(mockDeck);
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should check deck limits for regular users', async () => {
|
||||
// Arrange
|
||||
const command: CreateDeckCommand = {
|
||||
name: 'Test Deck',
|
||||
type: DeckType.JOKER,
|
||||
userid: 'user-123',
|
||||
cards: [],
|
||||
};
|
||||
|
||||
const mockUser = createMockUser({
|
||||
id: command.userid,
|
||||
type: 'user'
|
||||
});
|
||||
const mockDeck = createMockDeck({ userid: command.userid });
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(mockUser);
|
||||
mockDeckRepository.countActiveByUserId.mockResolvedValue(0);
|
||||
mockDeckRepository.create.mockResolvedValue(mockDeck);
|
||||
|
||||
// Act
|
||||
await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(mockDeckRepository.countActiveByUserId).toHaveBeenCalledWith(command.userid);
|
||||
});
|
||||
});
|
||||
});
|
||||
+486
@@ -0,0 +1,486 @@
|
||||
import { CreateDeckCommandHandler } from '../../../../src/Application/Deck/commands/CreateDeckCommandHandler';
|
||||
import { UpdateDeckCommandHandler } from '../../../../src/Application/Deck/commands/UpdateDeckCommandHandler';
|
||||
import { DeleteDeckCommandHandler } from '../../../../src/Application/Deck/commands/DeleteDeckCommandHandler';
|
||||
import { CreateDeckCommand } from '../../../../src/Application/Deck/commands/CreateDeckCommand';
|
||||
import { UpdateDeckCommand } from '../../../../src/Application/Deck/commands/UpdateDeckCommand';
|
||||
import { DeleteDeckCommand } from '../../../../src/Application/Deck/commands/DeleteDeckCommand';
|
||||
import { DeckAggregate, State as DeckState, Type as DeckType, CType } from '../../../../src/Domain/Deck/DeckAggregate';
|
||||
import { UserAggregate, UserState } from '../../../../src/Domain/User/UserAggregate';
|
||||
import { IUserRepository } from '../../../../src/Domain/IRepository/IUserRepository';
|
||||
import { IDeckRepository } from '../../../../src/Domain/IRepository/IDeckRepository';
|
||||
import { IOrganizationRepository } from '../../../../src/Domain/IRepository/IOrganizationRepository';
|
||||
import {
|
||||
createMockUser,
|
||||
createMockDeck,
|
||||
createMockUserRepository,
|
||||
createMockDeckRepository,
|
||||
createMockOrganizationRepository,
|
||||
createMockDate
|
||||
} from '../../../testUtils';
|
||||
|
||||
describe('Deck Command Handlers - Comprehensive Coverage', () => {
|
||||
let mockUserRepository: jest.Mocked<IUserRepository>;
|
||||
let mockDeckRepository: jest.Mocked<IDeckRepository>;
|
||||
let mockOrganizationRepository: jest.Mocked<IOrganizationRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserRepository = createMockUserRepository();
|
||||
mockDeckRepository = createMockDeckRepository();
|
||||
mockOrganizationRepository = createMockOrganizationRepository();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('CreateDeckCommandHandler', () => {
|
||||
let handler: CreateDeckCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new CreateDeckCommandHandler(mockDeckRepository, mockUserRepository, mockOrganizationRepository);
|
||||
});
|
||||
|
||||
it('should create a new deck successfully', async () => {
|
||||
// Arrange
|
||||
const mockUser = createMockUser({
|
||||
id: 'user-123',
|
||||
state: UserState.VERIFIED_REGULAR
|
||||
});
|
||||
const expectedDeck = createMockDeck({
|
||||
id: 'deck-123',
|
||||
name: 'Test Deck',
|
||||
type: DeckType.JOKER,
|
||||
userid: 'user-123',
|
||||
ctype: CType.PUBLIC,
|
||||
state: DeckState.ACTIVE,
|
||||
cards: []
|
||||
});
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(mockUser);
|
||||
mockDeckRepository.create.mockResolvedValue(expectedDeck);
|
||||
mockDeckRepository.countActiveByUserId.mockResolvedValue(0);
|
||||
|
||||
const command: CreateDeckCommand = {
|
||||
name: 'Test Deck',
|
||||
type: DeckType.JOKER,
|
||||
userid: 'user-123',
|
||||
cards: []
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeTruthy();
|
||||
expect(mockUserRepository.findById).toHaveBeenCalledWith('user-123');
|
||||
expect(mockDeckRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'Test Deck',
|
||||
type: DeckType.JOKER,
|
||||
userid: 'user-123'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when user not found', async () => {
|
||||
// Arrange
|
||||
mockUserRepository.findById.mockResolvedValue(null);
|
||||
|
||||
const command: CreateDeckCommand = {
|
||||
name: 'Test Deck',
|
||||
type: DeckType.JOKER,
|
||||
userid: 'nonexistent-user',
|
||||
cards: []
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('User not found');
|
||||
expect(mockUserRepository.findById).toHaveBeenCalledWith('nonexistent-user');
|
||||
expect(mockDeckRepository.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle admin users bypassing restrictions', async () => {
|
||||
// Arrange
|
||||
const adminUser = createMockUser({
|
||||
id: 'admin-123',
|
||||
type: 'admin',
|
||||
state: UserState.ADMIN
|
||||
});
|
||||
const expectedDeck = createMockDeck({
|
||||
name: 'Admin Deck',
|
||||
userid: 'admin-123'
|
||||
});
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(adminUser);
|
||||
mockDeckRepository.create.mockResolvedValue(expectedDeck);
|
||||
// Don't mock countActiveByUserId - admin should bypass this check
|
||||
|
||||
const command: CreateDeckCommand = {
|
||||
name: 'Admin Deck',
|
||||
type: DeckType.JOKER,
|
||||
userid: 'admin-123',
|
||||
cards: []
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeTruthy();
|
||||
expect(mockDeckRepository.countActiveByUserId).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle different deck types', async () => {
|
||||
// Arrange
|
||||
const mockUser = createMockUser({ id: 'user-123', state: UserState.VERIFIED_REGULAR });
|
||||
const expectedDeck = createMockDeck({
|
||||
name: 'Question Deck',
|
||||
type: DeckType.QUESTION,
|
||||
userid: 'user-123'
|
||||
});
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(mockUser);
|
||||
mockDeckRepository.create.mockResolvedValue(expectedDeck);
|
||||
mockDeckRepository.countActiveByUserId.mockResolvedValue(2);
|
||||
|
||||
const command: CreateDeckCommand = {
|
||||
name: 'Question Deck',
|
||||
type: DeckType.QUESTION,
|
||||
userid: 'user-123',
|
||||
cards: []
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeTruthy();
|
||||
expect(mockDeckRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: DeckType.QUESTION
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle repository creation errors', async () => {
|
||||
// Arrange
|
||||
const mockUser = createMockUser({ id: 'user-123', state: UserState.VERIFIED_REGULAR });
|
||||
mockUserRepository.findById.mockResolvedValue(mockUser);
|
||||
mockDeckRepository.countActiveByUserId.mockResolvedValue(0);
|
||||
mockDeckRepository.create.mockRejectedValue(new Error('Database connection failed'));
|
||||
|
||||
const command: CreateDeckCommand = {
|
||||
name: 'Test Deck',
|
||||
type: DeckType.JOKER,
|
||||
userid: 'user-123',
|
||||
cards: []
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Database connection failed');
|
||||
expect(mockDeckRepository.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle deck limit restrictions for regular users', async () => {
|
||||
// Arrange
|
||||
const mockUser = createMockUser({
|
||||
id: 'user-123',
|
||||
state: UserState.VERIFIED_REGULAR,
|
||||
type: 'regular'
|
||||
});
|
||||
mockUserRepository.findById.mockResolvedValue(mockUser);
|
||||
mockDeckRepository.countActiveByUserId.mockResolvedValue(10); // Assuming limit is 10
|
||||
|
||||
const command: CreateDeckCommand = {
|
||||
name: 'Test Deck',
|
||||
type: DeckType.JOKER,
|
||||
userid: 'user-123',
|
||||
cards: []
|
||||
};
|
||||
|
||||
// Act & Assert - This should succeed if the limit allows, or fail if over limit
|
||||
// The exact behavior depends on the business rules in CreateDeckCommandHandler
|
||||
try {
|
||||
await handler.execute(command);
|
||||
// If it succeeds, verify the deck was created
|
||||
expect(mockDeckRepository.create).toHaveBeenCalled();
|
||||
} catch (error) {
|
||||
// If it fails, verify it's a limit error
|
||||
expect((error as Error).message).toContain('limit');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateDeckCommandHandler', () => {
|
||||
let handler: UpdateDeckCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new UpdateDeckCommandHandler(mockDeckRepository);
|
||||
});
|
||||
|
||||
it('should update deck successfully', async () => {
|
||||
// Arrange
|
||||
const updatedDeck = createMockDeck({
|
||||
id: 'deck-123',
|
||||
name: 'New Name',
|
||||
ctype: CType.PUBLIC
|
||||
});
|
||||
|
||||
mockDeckRepository.update.mockResolvedValue(updatedDeck);
|
||||
|
||||
const command: UpdateDeckCommand = {
|
||||
id: 'deck-123',
|
||||
name: 'New Name'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert - Should return ShortDeckDto format
|
||||
expect(result).toEqual({
|
||||
id: 'deck-123',
|
||||
name: 'New Name',
|
||||
type: updatedDeck.type,
|
||||
playedNumber: updatedDeck.playedNumber,
|
||||
ctype: updatedDeck.ctype,
|
||||
});
|
||||
expect(mockDeckRepository.update).toHaveBeenCalledWith('deck-123', expect.objectContaining({
|
||||
id: 'deck-123',
|
||||
name: 'New Name'
|
||||
}));
|
||||
});
|
||||
|
||||
it('should return null when deck not found (repository returns null)', async () => {
|
||||
// Arrange
|
||||
mockDeckRepository.update.mockResolvedValue(null);
|
||||
|
||||
const command: UpdateDeckCommand = {
|
||||
id: 'nonexistent-deck',
|
||||
name: 'New Name'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
expect(mockDeckRepository.update).toHaveBeenCalledWith('nonexistent-deck', expect.objectContaining({
|
||||
id: 'nonexistent-deck',
|
||||
name: 'New Name'
|
||||
}));
|
||||
});
|
||||
|
||||
it('should handle partial updates', async () => {
|
||||
// Arrange
|
||||
const updatedDeck = createMockDeck({
|
||||
id: 'deck-123',
|
||||
name: 'Original Name', // Name stays the same
|
||||
ctype: CType.PRIVATE // Only ctype changes
|
||||
});
|
||||
|
||||
mockDeckRepository.update.mockResolvedValue(updatedDeck);
|
||||
|
||||
const command: UpdateDeckCommand = {
|
||||
id: 'deck-123',
|
||||
ctype: CType.PRIVATE
|
||||
// Note: name is not provided, should remain unchanged
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert - Should return ShortDeckDto format
|
||||
expect(result).toEqual({
|
||||
id: 'deck-123',
|
||||
name: 'Original Name',
|
||||
type: updatedDeck.type,
|
||||
playedNumber: updatedDeck.playedNumber,
|
||||
ctype: CType.PRIVATE,
|
||||
});
|
||||
expect(mockDeckRepository.update).toHaveBeenCalledWith('deck-123', expect.objectContaining({
|
||||
id: 'deck-123',
|
||||
ctype: CType.PRIVATE
|
||||
}));
|
||||
});
|
||||
|
||||
it('should handle repository update errors', async () => {
|
||||
// Arrange
|
||||
const existingDeck = createMockDeck({ id: 'deck-123' });
|
||||
mockDeckRepository.findById.mockResolvedValue(existingDeck);
|
||||
mockDeckRepository.update.mockRejectedValue(new Error('Update failed'));
|
||||
|
||||
const command: UpdateDeckCommand = {
|
||||
id: 'deck-123',
|
||||
name: 'New Name'
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Update failed');
|
||||
expect(mockDeckRepository.update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeleteDeckCommandHandler', () => {
|
||||
let handler: DeleteDeckCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new DeleteDeckCommandHandler(mockDeckRepository);
|
||||
});
|
||||
|
||||
it('should delete deck successfully (soft delete)', async () => {
|
||||
// Arrange
|
||||
mockDeckRepository.softDelete.mockResolvedValue(null); // Soft delete returns void
|
||||
|
||||
const command: DeleteDeckCommand = {
|
||||
id: 'deck-123',
|
||||
soft: true // Specify soft delete
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert - DeleteDeckCommandHandler always returns true
|
||||
expect(result).toBe(true);
|
||||
expect(mockDeckRepository.softDelete).toHaveBeenCalledWith('deck-123');
|
||||
});
|
||||
|
||||
it('should delete deck successfully (hard delete)', async () => {
|
||||
// Arrange
|
||||
mockDeckRepository.delete.mockResolvedValue(null); // Delete returns void
|
||||
|
||||
const command: DeleteDeckCommand = {
|
||||
id: 'deck-123',
|
||||
soft: false // Specify hard delete
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert - DeleteDeckCommandHandler always returns true
|
||||
expect(result).toBe(true);
|
||||
expect(mockDeckRepository.delete).toHaveBeenCalledWith('deck-123');
|
||||
});
|
||||
|
||||
it('should default to hard delete when soft flag not specified', async () => {
|
||||
// Arrange
|
||||
mockDeckRepository.delete.mockResolvedValue(null);
|
||||
|
||||
const command: DeleteDeckCommand = {
|
||||
id: 'deck-123'
|
||||
// Note: soft flag not specified, defaults to undefined which is falsy
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockDeckRepository.delete).toHaveBeenCalledWith('deck-123');
|
||||
expect(mockDeckRepository.softDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle repository deletion errors', async () => {
|
||||
// Arrange
|
||||
mockDeckRepository.softDelete.mockRejectedValue(new Error('Deletion failed'));
|
||||
|
||||
const command: DeleteDeckCommand = {
|
||||
id: 'deck-123',
|
||||
soft: true
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Deletion failed');
|
||||
expect(mockDeckRepository.softDelete).toHaveBeenCalledWith('deck-123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cross-Command Integration Tests', () => {
|
||||
let createHandler: CreateDeckCommandHandler;
|
||||
let updateHandler: UpdateDeckCommandHandler;
|
||||
let deleteHandler: DeleteDeckCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
createHandler = new CreateDeckCommandHandler(mockDeckRepository, mockUserRepository, mockOrganizationRepository);
|
||||
updateHandler = new UpdateDeckCommandHandler(mockDeckRepository);
|
||||
deleteHandler = new DeleteDeckCommandHandler(mockDeckRepository);
|
||||
});
|
||||
|
||||
it('should create deck and then update it', async () => {
|
||||
// Arrange - Create
|
||||
const mockUser = createMockUser({ id: 'user-123', state: UserState.VERIFIED_REGULAR });
|
||||
const createdDeck = createMockDeck({
|
||||
id: 'deck-123',
|
||||
name: 'Initial Name',
|
||||
userid: 'user-123'
|
||||
});
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(mockUser);
|
||||
mockDeckRepository.countActiveByUserId.mockResolvedValue(0);
|
||||
mockDeckRepository.create.mockResolvedValue(createdDeck);
|
||||
|
||||
// Arrange - Update
|
||||
const updatedDeck = createMockDeck({
|
||||
id: 'deck-123',
|
||||
name: 'Updated Name',
|
||||
userid: 'user-123'
|
||||
});
|
||||
mockDeckRepository.findById.mockResolvedValue(createdDeck);
|
||||
mockDeckRepository.update.mockResolvedValue(updatedDeck);
|
||||
|
||||
// Act - Create
|
||||
const createCommand: CreateDeckCommand = {
|
||||
name: 'Initial Name',
|
||||
type: DeckType.JOKER,
|
||||
userid: 'user-123',
|
||||
cards: []
|
||||
};
|
||||
const createResult = await createHandler.execute(createCommand);
|
||||
|
||||
// Act - Update
|
||||
const updateCommand: UpdateDeckCommand = {
|
||||
id: 'deck-123',
|
||||
name: 'Updated Name'
|
||||
};
|
||||
const updateResult = await updateHandler.execute(updateCommand);
|
||||
|
||||
// Assert
|
||||
expect(createResult).toBeTruthy();
|
||||
expect(updateResult?.name).toBe('Updated Name');
|
||||
expect(mockDeckRepository.create).toHaveBeenCalled();
|
||||
expect(mockDeckRepository.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle full lifecycle: create, update, delete', async () => {
|
||||
// This tests the complete lifecycle of a deck
|
||||
const mockUser = createMockUser({ id: 'user-123', state: UserState.VERIFIED_REGULAR });
|
||||
const deck = createMockDeck({ id: 'deck-123', userid: 'user-123' });
|
||||
|
||||
// Setup all mocks
|
||||
mockUserRepository.findById.mockResolvedValue(mockUser);
|
||||
mockDeckRepository.countActiveByUserId.mockResolvedValue(0);
|
||||
mockDeckRepository.create.mockResolvedValue(deck);
|
||||
mockDeckRepository.update.mockResolvedValue(deck);
|
||||
mockDeckRepository.softDelete.mockResolvedValue(null);
|
||||
|
||||
// Execute lifecycle
|
||||
const createResult = await createHandler.execute({
|
||||
name: 'Test Deck',
|
||||
type: DeckType.JOKER,
|
||||
userid: 'user-123',
|
||||
cards: []
|
||||
});
|
||||
|
||||
const updateResult = await updateHandler.execute({
|
||||
id: 'deck-123',
|
||||
name: 'Updated Deck'
|
||||
});
|
||||
|
||||
const deleteResult = await deleteHandler.execute({
|
||||
id: 'deck-123',
|
||||
soft: true
|
||||
});
|
||||
|
||||
// Assert all operations succeeded
|
||||
expect(createResult).toBeTruthy();
|
||||
expect(updateResult).toBeTruthy();
|
||||
expect(deleteResult).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
import { BoardGenerationService } from '../../../src/Application/Game/BoardGenerationService';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../../../src/Application/Services/LoggingService');
|
||||
|
||||
describe('BoardGenerationService', () => {
|
||||
let boardGenerationService: BoardGenerationService;
|
||||
|
||||
beforeEach(() => {
|
||||
boardGenerationService = new BoardGenerationService();
|
||||
});
|
||||
|
||||
describe('generateBoard', () => {
|
||||
it('should generate a board with the correct number of special fields', async () => {
|
||||
const positiveFields = 10;
|
||||
const negativeFields = 8;
|
||||
const luckFields = 5;
|
||||
|
||||
const result = await boardGenerationService.generateBoard(
|
||||
positiveFields,
|
||||
negativeFields,
|
||||
luckFields
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.fields).toHaveLength(100);
|
||||
|
||||
// Count special fields
|
||||
const actualPositive = result.fields.filter(f => f.type === 'positive').length;
|
||||
const actualNegative = result.fields.filter(f => f.type === 'negative').length;
|
||||
const actualLuck = result.fields.filter(f => f.type === 'luck').length;
|
||||
|
||||
expect(actualPositive).toBe(positiveFields);
|
||||
expect(actualNegative).toBe(negativeFields);
|
||||
expect(actualLuck).toBe(luckFields);
|
||||
});
|
||||
|
||||
it('should ensure positive fields have positive step values', async () => {
|
||||
const result = await boardGenerationService.generateBoard(5, 5, 2);
|
||||
|
||||
const positiveFields = result.fields.filter(f => f.type === 'positive');
|
||||
positiveFields.forEach(field => {
|
||||
expect(field.stepValue).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should ensure negative fields have negative step values', async () => {
|
||||
const result = await boardGenerationService.generateBoard(5, 5, 2);
|
||||
|
||||
const negativeFields = result.fields.filter(f => f.type === 'negative');
|
||||
negativeFields.forEach(field => {
|
||||
expect(field.stepValue).toBeLessThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should ensure luck fields do not have step values', async () => {
|
||||
const result = await boardGenerationService.generateBoard(5, 5, 2);
|
||||
|
||||
const luckFields = result.fields.filter(f => f.type === 'luck');
|
||||
luckFields.forEach(field => {
|
||||
expect(field.stepValue).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should produce validation results without -1 values', async () => {
|
||||
const result = await boardGenerationService.generateBoard(10, 8, 5);
|
||||
|
||||
// Check validation results for invalid moves (-1 values)
|
||||
let invalidMoves = 0;
|
||||
let totalMoves = 0;
|
||||
|
||||
Object.values(result.validationResults).forEach(diceOutcomes => {
|
||||
diceOutcomes.forEach(outcome => {
|
||||
totalMoves++;
|
||||
if (outcome === -1) {
|
||||
invalidMoves++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const errorRate = totalMoves > 0 ? (invalidMoves / totalMoves) * 100 : 0;
|
||||
|
||||
// Log the results for analysis
|
||||
console.log(`Error rate: ${errorRate}%`);
|
||||
console.log(`Invalid moves: ${invalidMoves}/${totalMoves}`);
|
||||
|
||||
// The new algorithm should produce much fewer invalid moves
|
||||
expect(errorRate).toBeLessThan(50); // Allow some errors but much better than before
|
||||
});
|
||||
|
||||
it('should respect the 20-30 movement rule in validation', async () => {
|
||||
const result = await boardGenerationService.generateBoard(10, 8, 5);
|
||||
|
||||
// Check each validation result to ensure it respects distance rules
|
||||
Object.entries(result.validationResults).forEach(([fieldPosition, diceOutcomes]) => {
|
||||
const currentPos = parseInt(fieldPosition);
|
||||
|
||||
diceOutcomes.forEach((outcome, diceIndex) => {
|
||||
if (outcome !== -1) { // Only check valid moves
|
||||
const distance = Math.abs(outcome - currentPos);
|
||||
|
||||
if (currentPos <= 85) {
|
||||
// Fields 1-85: max 20 in any direction
|
||||
expect(distance).toBeLessThanOrEqual(20);
|
||||
} else {
|
||||
// Fields 86-100: max 30 backward, max 20 forward
|
||||
if (outcome > currentPos) {
|
||||
expect(distance).toBeLessThanOrEqual(20); // forward
|
||||
} else {
|
||||
expect(distance).toBeLessThanOrEqual(30); // backward
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should position special fields safely within the safe range', async () => {
|
||||
const result = await boardGenerationService.generateBoard(10, 8, 5);
|
||||
|
||||
const specialFields = result.fields.filter(f => f.type !== 'regular');
|
||||
|
||||
// Most special fields should be in the safe range (11-90) for the new algorithm
|
||||
const safeFields = specialFields.filter(f => f.position >= 11 && f.position <= 90);
|
||||
const safePercentage = (safeFields.length / specialFields.length) * 100;
|
||||
|
||||
console.log(`Safe field percentage: ${safePercentage}%`);
|
||||
|
||||
// Expect most fields to be positioned safely
|
||||
expect(safePercentage).toBeGreaterThan(70);
|
||||
});
|
||||
});
|
||||
});
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
import { CreateOrganizationCommandHandler } from '../../../../src/Application/Organization/commands/CreateOrganizationCommandHandler';
|
||||
import { UpdateOrganizationCommandHandler } from '../../../../src/Application/Organization/commands/UpdateOrganizationCommandHandler';
|
||||
import { DeleteOrganizationCommandHandler } from '../../../../src/Application/Organization/commands/DeleteOrganizationCommandHandler';
|
||||
import { CreateOrganizationCommand } from '../../../../src/Application/Organization/commands/CreateOrganizationCommand';
|
||||
import { UpdateOrganizationCommand } from '../../../../src/Application/Organization/commands/UpdateOrganizationCommand';
|
||||
import { DeleteOrganizationCommand } from '../../../../src/Application/Organization/commands/DeleteOrganizationCommand';
|
||||
import { OrganizationState } from '../../../../src/Domain/Organization/OrganizationAggregate';
|
||||
import { createMockOrganizationRepository, createMockOrganization } from '../../../testUtils';
|
||||
|
||||
describe('Organization Command Handlers - Comprehensive', () => {
|
||||
let mockOrganizationRepository: ReturnType<typeof createMockOrganizationRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockOrganizationRepository = createMockOrganizationRepository();
|
||||
});
|
||||
|
||||
describe('CreateOrganizationCommandHandler', () => {
|
||||
let handler: CreateOrganizationCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new CreateOrganizationCommandHandler(mockOrganizationRepository);
|
||||
});
|
||||
|
||||
it('should create organization successfully', async () => {
|
||||
// Arrange
|
||||
const mockOrgData = createMockOrganization({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'Test Organization',
|
||||
contactfname: 'John',
|
||||
contactlname: 'Doe',
|
||||
contactphone: '+1234567890',
|
||||
contactemail: 'john@testorg.com',
|
||||
url: null,
|
||||
state: OrganizationState.REGISTERED
|
||||
});
|
||||
|
||||
mockOrganizationRepository.create.mockResolvedValue(mockOrgData);
|
||||
|
||||
const command: CreateOrganizationCommand = {
|
||||
name: 'Test Organization',
|
||||
contactfname: 'John',
|
||||
contactlname: 'Doe',
|
||||
contactemail: 'john@testorg.com',
|
||||
contactphone: '+1234567890'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert - Returns ShortOrganizationDto
|
||||
expect(result).toEqual({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'Test Organization',
|
||||
state: 0,
|
||||
userinorg: 0,
|
||||
maxOrganizationalDecks: 10
|
||||
});
|
||||
expect(mockOrganizationRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'Test Organization',
|
||||
contactfname: 'John',
|
||||
contactlname: 'Doe',
|
||||
contactemail: 'john@testorg.com',
|
||||
contactphone: '+1234567890',
|
||||
state: OrganizationState.REGISTERED
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should create organization with optional URL', async () => {
|
||||
// Arrange
|
||||
const mockOrgData = createMockOrganization({
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
name: 'Org with URL',
|
||||
contactfname: 'Jane',
|
||||
contactlname: 'Smith',
|
||||
contactphone: '+1987654321',
|
||||
contactemail: 'jane@orgwithurl.com',
|
||||
url: 'https://orgwithurl.com',
|
||||
state: OrganizationState.REGISTERED
|
||||
});
|
||||
|
||||
mockOrganizationRepository.create.mockResolvedValue(mockOrgData);
|
||||
|
||||
const command: CreateOrganizationCommand = {
|
||||
name: 'Org with URL',
|
||||
contactfname: 'Jane',
|
||||
contactlname: 'Smith',
|
||||
contactemail: 'jane@orgwithurl.com',
|
||||
contactphone: '+1987654321',
|
||||
url: 'https://orgwithurl.com'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
name: 'Org with URL',
|
||||
state: 0,
|
||||
userinorg: 0,
|
||||
maxOrganizationalDecks: 10
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle duplicate organization name error', async () => {
|
||||
// Arrange
|
||||
const command: CreateOrganizationCommand = {
|
||||
name: 'Duplicate Org',
|
||||
contactfname: 'John',
|
||||
contactlname: 'Doe',
|
||||
contactemail: 'john@duplicate.com',
|
||||
contactphone: '+1234567890'
|
||||
};
|
||||
|
||||
const duplicateError = new Error('duplicate key value violates unique constraint "organization_name_unique"');
|
||||
mockOrganizationRepository.create.mockRejectedValue(duplicateError);
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Organization with this name or contact email already exists');
|
||||
});
|
||||
|
||||
it('should handle generic database errors', async () => {
|
||||
// Arrange
|
||||
const command: CreateOrganizationCommand = {
|
||||
name: 'Error Org',
|
||||
contactfname: 'John',
|
||||
contactlname: 'Doe',
|
||||
contactemail: 'john@error.com',
|
||||
contactphone: '+1234567890'
|
||||
};
|
||||
|
||||
mockOrganizationRepository.create.mockRejectedValue(new Error('Database connection failed'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to create organization');
|
||||
});
|
||||
|
||||
it('should handle non-Error exceptions', async () => {
|
||||
// Arrange
|
||||
const command: CreateOrganizationCommand = {
|
||||
name: 'Non-Error Exception Org',
|
||||
contactfname: 'John',
|
||||
contactlname: 'Doe',
|
||||
contactemail: 'john@exception.com',
|
||||
contactphone: '+1234567890'
|
||||
};
|
||||
|
||||
mockOrganizationRepository.create.mockRejectedValue('String error');
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to create organization');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateOrganizationCommandHandler', () => {
|
||||
let handler: UpdateOrganizationCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new UpdateOrganizationCommandHandler(mockOrganizationRepository);
|
||||
});
|
||||
|
||||
it('should update organization successfully', async () => {
|
||||
// Arrange
|
||||
const updatedOrgData = createMockOrganization({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'Updated Organization',
|
||||
contactemail: 'john@updated.com',
|
||||
url: 'https://updated.com',
|
||||
state: OrganizationState.ACTIVE
|
||||
});
|
||||
|
||||
mockOrganizationRepository.update.mockResolvedValue(updatedOrgData);
|
||||
|
||||
const command: UpdateOrganizationCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'Updated Organization',
|
||||
contactemail: 'john@updated.com',
|
||||
url: 'https://updated.com'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert - Returns ShortOrganizationDto
|
||||
expect(result).toEqual({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'Updated Organization',
|
||||
state: 1,
|
||||
userinorg: 0,
|
||||
maxOrganizationalDecks: 10
|
||||
});
|
||||
expect(mockOrganizationRepository.update).toHaveBeenCalledWith(
|
||||
'550e8400-e29b-41d4-a716-446655440000',
|
||||
command
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null when organization not found', async () => {
|
||||
// Arrange
|
||||
mockOrganizationRepository.update.mockResolvedValue(null);
|
||||
|
||||
const command: UpdateOrganizationCommand = {
|
||||
id: 'non-existent-id',
|
||||
name: 'Non-existent Organization'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
expect(mockOrganizationRepository.update).toHaveBeenCalledWith('non-existent-id', command);
|
||||
});
|
||||
|
||||
it('should update organization with partial data', async () => {
|
||||
// Arrange
|
||||
const partialUpdatedOrgData = createMockOrganization({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'Original Name',
|
||||
contactemail: 'john@newmail.com',
|
||||
state: OrganizationState.ACTIVE
|
||||
});
|
||||
|
||||
mockOrganizationRepository.update.mockResolvedValue(partialUpdatedOrgData);
|
||||
|
||||
const command: UpdateOrganizationCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
contactemail: 'john@newmail.com'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
name: 'Original Name',
|
||||
state: 1,
|
||||
userinorg: 0,
|
||||
maxOrganizationalDecks: 10
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeleteOrganizationCommandHandler', () => {
|
||||
let handler: DeleteOrganizationCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new DeleteOrganizationCommandHandler(mockOrganizationRepository);
|
||||
});
|
||||
|
||||
it('should perform soft delete successfully', async () => {
|
||||
// Arrange
|
||||
mockOrganizationRepository.softDelete.mockResolvedValue(null);
|
||||
|
||||
const command: DeleteOrganizationCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
soft: true
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockOrganizationRepository.softDelete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000');
|
||||
expect(mockOrganizationRepository.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should perform hard delete successfully', async () => {
|
||||
// Arrange
|
||||
mockOrganizationRepository.delete.mockResolvedValue(true);
|
||||
|
||||
const command: DeleteOrganizationCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
soft: false
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockOrganizationRepository.delete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000');
|
||||
expect(mockOrganizationRepository.softDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should default to hard delete when soft flag not specified', async () => {
|
||||
// Arrange
|
||||
mockOrganizationRepository.delete.mockResolvedValue(true);
|
||||
|
||||
const command: DeleteOrganizationCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockOrganizationRepository.delete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000');
|
||||
expect(mockOrganizationRepository.softDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle soft delete with repository error gracefully', async () => {
|
||||
// Arrange
|
||||
mockOrganizationRepository.softDelete.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const command: DeleteOrganizationCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
soft: true
|
||||
};
|
||||
|
||||
// Act & Assert - Handler doesn't catch errors, they bubble up
|
||||
await expect(handler.execute(command)).rejects.toThrow('Database error');
|
||||
});
|
||||
|
||||
it('should handle hard delete with repository error gracefully', async () => {
|
||||
// Arrange
|
||||
mockOrganizationRepository.delete.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const command: DeleteOrganizationCommand = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
soft: false
|
||||
};
|
||||
|
||||
// Act & Assert - Handler doesn't catch errors, they bubble up
|
||||
await expect(handler.execute(command)).rejects.toThrow('Database error');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,188 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
// Mock JWTService before importing anything else
|
||||
const mockJWTService = {
|
||||
verify: jest.fn(),
|
||||
refreshIfNeeded: jest.fn(),
|
||||
create: jest.fn(),
|
||||
shouldRefreshToken: jest.fn(),
|
||||
test: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('../../../src/Application/Services/JWTService', () => {
|
||||
return {
|
||||
JWTService: jest.fn().mockImplementation(() => mockJWTService)
|
||||
};
|
||||
});
|
||||
|
||||
// Now import the middleware which will use the mocked JWTService
|
||||
import { authRequired, adminRequired } from '../../../src/Application/Services/AuthMiddleware';
|
||||
|
||||
describe('AuthMiddleware', () => {
|
||||
let mockRequest: Partial<Request>;
|
||||
let mockResponse: Partial<Response>;
|
||||
let mockNext: NextFunction;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockRequest = {
|
||||
cookies: {}
|
||||
};
|
||||
|
||||
mockResponse = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn().mockReturnThis(),
|
||||
cookie: jest.fn()
|
||||
};
|
||||
|
||||
mockNext = jest.fn();
|
||||
});
|
||||
|
||||
describe('authRequired', () => {
|
||||
it('should call next() when token is valid', () => {
|
||||
// Arrange
|
||||
const validPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 0 as 0 | 1,
|
||||
orgId: 'org-123'
|
||||
};
|
||||
|
||||
mockJWTService.verify.mockReturnValue(validPayload);
|
||||
mockJWTService.refreshIfNeeded.mockReturnValue(false); // Token doesn't need refresh
|
||||
|
||||
// Act
|
||||
authRequired(mockRequest as Request, mockResponse as Response, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest);
|
||||
expect(mockJWTService.refreshIfNeeded).toHaveBeenCalledWith(validPayload, mockResponse);
|
||||
expect((mockRequest as any).user).toBe(validPayload);
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect(mockResponse.status).not.toHaveBeenCalled();
|
||||
expect(mockResponse.json).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 401 when token is invalid', () => {
|
||||
// Arrange
|
||||
mockJWTService.verify.mockReturnValue(null);
|
||||
|
||||
// Act
|
||||
authRequired(mockRequest as Request, mockResponse as Response, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest);
|
||||
expect(mockJWTService.refreshIfNeeded).not.toHaveBeenCalled();
|
||||
expect(mockNext).not.toHaveBeenCalled();
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Unauthorized' });
|
||||
});
|
||||
|
||||
it('should refresh token when needed', () => {
|
||||
// Arrange
|
||||
const validPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 0 as 0 | 1,
|
||||
orgId: 'org-123'
|
||||
};
|
||||
|
||||
mockJWTService.verify.mockReturnValue(validPayload);
|
||||
mockJWTService.refreshIfNeeded.mockReturnValue(true); // Token needs refresh
|
||||
|
||||
// Act
|
||||
authRequired(mockRequest as Request, mockResponse as Response, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest);
|
||||
expect(mockJWTService.refreshIfNeeded).toHaveBeenCalledWith(validPayload, mockResponse);
|
||||
expect((mockRequest as any).user).toBe(validPayload);
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect(mockResponse.status).not.toHaveBeenCalled();
|
||||
expect(mockResponse.json).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('adminRequired', () => {
|
||||
it('should call next() when token is valid and user is admin', () => {
|
||||
// Arrange
|
||||
const adminPayload = {
|
||||
userId: 'admin-123',
|
||||
authLevel: 1 as 0 | 1,
|
||||
orgId: 'org-123'
|
||||
};
|
||||
|
||||
mockJWTService.verify.mockReturnValue(adminPayload);
|
||||
mockJWTService.refreshIfNeeded.mockReturnValue(false);
|
||||
|
||||
// Act
|
||||
adminRequired(mockRequest as Request, mockResponse as Response, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest);
|
||||
expect(mockJWTService.refreshIfNeeded).toHaveBeenCalledWith(adminPayload, mockResponse);
|
||||
expect((mockRequest as any).user).toBe(adminPayload);
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect(mockResponse.status).not.toHaveBeenCalled();
|
||||
expect(mockResponse.json).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 403 when token is invalid', () => {
|
||||
// Arrange
|
||||
mockJWTService.verify.mockReturnValue(null);
|
||||
|
||||
// Act
|
||||
adminRequired(mockRequest as Request, mockResponse as Response, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest);
|
||||
expect(mockJWTService.refreshIfNeeded).not.toHaveBeenCalled();
|
||||
expect(mockNext).not.toHaveBeenCalled();
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(403);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Forbidden' });
|
||||
});
|
||||
|
||||
it('should return 403 when user is not admin', () => {
|
||||
// Arrange
|
||||
const regularUserPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 0 as 0 | 1,
|
||||
orgId: 'org-123'
|
||||
};
|
||||
|
||||
mockJWTService.verify.mockReturnValue(regularUserPayload);
|
||||
|
||||
// Act
|
||||
adminRequired(mockRequest as Request, mockResponse as Response, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest);
|
||||
expect(mockJWTService.refreshIfNeeded).not.toHaveBeenCalled();
|
||||
expect(mockNext).not.toHaveBeenCalled();
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(403);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Forbidden' });
|
||||
});
|
||||
|
||||
it('should refresh token for valid admin user', () => {
|
||||
// Arrange
|
||||
const adminPayload = {
|
||||
userId: 'admin-123',
|
||||
authLevel: 1 as 0 | 1,
|
||||
orgId: 'org-123'
|
||||
};
|
||||
|
||||
mockJWTService.verify.mockReturnValue(adminPayload);
|
||||
mockJWTService.refreshIfNeeded.mockReturnValue(true);
|
||||
|
||||
// Act
|
||||
adminRequired(mockRequest as Request, mockResponse as Response, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest);
|
||||
expect(mockJWTService.refreshIfNeeded).toHaveBeenCalledWith(adminPayload, mockResponse);
|
||||
expect((mockRequest as any).user).toBe(adminPayload);
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect(mockResponse.status).not.toHaveBeenCalled();
|
||||
expect(mockResponse.json).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
import { WebSocketService } from '../../../src/Application/Services/WebSocketService';
|
||||
import { Server as HttpServer } from 'http';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
describe('Chat Configuration', () => {
|
||||
let mockHttpServer: HttpServer;
|
||||
|
||||
beforeAll(() => {
|
||||
// Create a more complete HTTP server mock that extends EventEmitter
|
||||
const httpServerMock = new EventEmitter();
|
||||
|
||||
// Add necessary methods that Socket.IO expects
|
||||
Object.assign(httpServerMock, {
|
||||
on: jest.fn(),
|
||||
listen: jest.fn(),
|
||||
close: jest.fn(),
|
||||
listeners: jest.fn().mockReturnValue([]),
|
||||
removeListener: jest.fn(),
|
||||
removeAllListeners: jest.fn(),
|
||||
setMaxListeners: jest.fn(),
|
||||
getMaxListeners: jest.fn().mockReturnValue(0),
|
||||
listenerCount: jest.fn().mockReturnValue(0),
|
||||
prependListener: jest.fn(),
|
||||
prependOnceListener: jest.fn(),
|
||||
off: jest.fn(),
|
||||
once: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
// HTTP server specific
|
||||
timeout: 0,
|
||||
keepAliveTimeout: 5000,
|
||||
maxHeadersCount: null,
|
||||
headersTimeout: 60000,
|
||||
requestTimeout: 0
|
||||
});
|
||||
|
||||
mockHttpServer = httpServerMock as unknown as HttpServer;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up environment variables
|
||||
delete process.env.CHAT_MAX_MESSAGES_PER_USER;
|
||||
delete process.env.CHAT_MESSAGE_CLEANUP_WEEKS;
|
||||
delete process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES;
|
||||
});
|
||||
|
||||
describe('Environment Variable Configuration', () => {
|
||||
it('should use default chat configuration values', () => {
|
||||
const service = new WebSocketService(mockHttpServer);
|
||||
|
||||
expect(service['maxMessagesPerUser']).toBe(100);
|
||||
expect(service['messageCleanupWeeks']).toBe(4);
|
||||
expect(service['chatTimeout']).toBe(30);
|
||||
});
|
||||
|
||||
it('should use environment variable for CHAT_MAX_MESSAGES_PER_USER', () => {
|
||||
process.env.CHAT_MAX_MESSAGES_PER_USER = '50';
|
||||
|
||||
const service = new WebSocketService(mockHttpServer);
|
||||
|
||||
expect(service['maxMessagesPerUser']).toBe(50);
|
||||
});
|
||||
|
||||
it('should use environment variable for CHAT_MESSAGE_CLEANUP_WEEKS', () => {
|
||||
// Arrange
|
||||
process.env.CHAT_MESSAGE_CLEANUP_WEEKS = '8';
|
||||
|
||||
// Act
|
||||
const service = new WebSocketService(mockHttpServer);
|
||||
|
||||
// Assert
|
||||
expect(service['messageCleanupWeeks']).toBe(8);
|
||||
});
|
||||
|
||||
it('should use environment variable for CHAT_INACTIVITY_TIMEOUT_MINUTES', () => {
|
||||
// Arrange
|
||||
process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES = '60';
|
||||
|
||||
// Act
|
||||
const service = new WebSocketService(mockHttpServer);
|
||||
|
||||
// Assert
|
||||
expect(service['chatTimeout']).toBe(60);
|
||||
});
|
||||
|
||||
it('should handle invalid numeric environment variables gracefully', () => {
|
||||
// Arrange
|
||||
process.env.CHAT_MAX_MESSAGES_PER_USER = 'invalid';
|
||||
process.env.CHAT_MESSAGE_CLEANUP_WEEKS = 'also-invalid';
|
||||
process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES = 'not-a-number';
|
||||
|
||||
// Act
|
||||
const service = new WebSocketService(mockHttpServer);
|
||||
|
||||
// Assert - parseInt of invalid strings returns NaN
|
||||
expect(service['maxMessagesPerUser']).toBe(NaN);
|
||||
expect(service['messageCleanupWeeks']).toBe(NaN);
|
||||
expect(service['chatTimeout']).toBe(NaN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rate Limiting Logic', () => {
|
||||
it('should initialize with empty user message counts', () => {
|
||||
// Act
|
||||
const service = new WebSocketService(mockHttpServer);
|
||||
|
||||
// Assert
|
||||
expect(service['userMessageCounts']).toBeDefined();
|
||||
expect(service['userMessageCounts'].size).toBe(0);
|
||||
});
|
||||
|
||||
it('should allow messages within rate limit', () => {
|
||||
// Arrange
|
||||
process.env.CHAT_MAX_MESSAGES_PER_USER = '5';
|
||||
const service = new WebSocketService(mockHttpServer);
|
||||
const userId = 'test-user';
|
||||
|
||||
// Act & Assert - should allow first 5 messages
|
||||
for (let i = 0; i < 5; i++) {
|
||||
expect(service['checkMessageRateLimit'](userId)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should block messages when rate limit exceeded', () => {
|
||||
// Arrange
|
||||
process.env.CHAT_MAX_MESSAGES_PER_USER = '3';
|
||||
const service = new WebSocketService(mockHttpServer);
|
||||
const userId = 'test-user';
|
||||
|
||||
// Act - send 3 messages (should be allowed)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
expect(service['checkMessageRateLimit'](userId)).toBe(true);
|
||||
}
|
||||
|
||||
// Assert - 4th message should be blocked
|
||||
expect(service['checkMessageRateLimit'](userId)).toBe(false);
|
||||
});
|
||||
|
||||
it('should reset rate limit after time window', (done) => {
|
||||
// Arrange
|
||||
process.env.CHAT_MAX_MESSAGES_PER_USER = '2';
|
||||
const service = new WebSocketService(mockHttpServer);
|
||||
const userId = 'test-user';
|
||||
|
||||
// Act - exhaust rate limit
|
||||
expect(service['checkMessageRateLimit'](userId)).toBe(true);
|
||||
expect(service['checkMessageRateLimit'](userId)).toBe(true);
|
||||
expect(service['checkMessageRateLimit'](userId)).toBe(false); // Should be blocked
|
||||
|
||||
// Mock time passage by manipulating the internal state
|
||||
const userStats = service['userMessageCounts'].get(userId)!;
|
||||
userStats.lastReset = Date.now() - (60 * 1000 + 1); // More than 1 minute ago
|
||||
service['userMessageCounts'].set(userId, userStats);
|
||||
|
||||
// Assert - should be allowed again after reset
|
||||
expect(service['checkMessageRateLimit'](userId)).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { container } from '../../../src/Application/Services/DIContainer';
|
||||
import { IUserRepository } from '../../../src/Domain/IRepository/IUserRepository';
|
||||
import { IChatRepository } from '../../../src/Domain/IRepository/IChatRepository';
|
||||
import { LoggingService } from '../../../src/Application/Services/LoggingService';
|
||||
|
||||
describe('DIContainer', () => {
|
||||
// Cleanup after all tests to prevent Jest hanging
|
||||
afterAll(async () => {
|
||||
await LoggingService.getInstance().shutdown();
|
||||
});
|
||||
|
||||
describe('Repositories', () => {
|
||||
it('should return singleton IUserRepository instance', () => {
|
||||
const repo1 = container.userRepository;
|
||||
const repo2 = container.userRepository;
|
||||
|
||||
expect(repo1).toBeTruthy();
|
||||
expect(repo1).toBe(repo2); // Same instance (singleton)
|
||||
expect(typeof repo1.findById).toBe('function'); // Has interface methods
|
||||
});
|
||||
|
||||
it('should return singleton IChatRepository instance', () => {
|
||||
const repo1 = container.chatRepository;
|
||||
const repo2 = container.chatRepository;
|
||||
|
||||
expect(repo1).toBeTruthy();
|
||||
expect(repo1).toBe(repo2); // Same instance (singleton)
|
||||
expect(typeof repo1.findById).toBe('function'); // Has interface methods
|
||||
});
|
||||
});
|
||||
|
||||
describe('Command Handlers', () => {
|
||||
it('should return singleton CreateUserCommandHandler instance', () => {
|
||||
const handler1 = container.createUserCommandHandler;
|
||||
const handler2 = container.createUserCommandHandler;
|
||||
|
||||
expect(handler1).toBeTruthy();
|
||||
expect(handler1).toBe(handler2); // Same instance (singleton)
|
||||
});
|
||||
|
||||
it('should return singleton LoginCommandHandler instance', () => {
|
||||
const handler1 = container.loginCommandHandler;
|
||||
const handler2 = container.loginCommandHandler;
|
||||
|
||||
expect(handler1).toBeTruthy();
|
||||
expect(handler1).toBe(handler2); // Same instance (singleton)
|
||||
});
|
||||
|
||||
it('should return singleton DeactivateUserCommandHandler instance', () => {
|
||||
const handler1 = container.deactivateUserCommandHandler;
|
||||
const handler2 = container.deactivateUserCommandHandler;
|
||||
|
||||
expect(handler1).toBeTruthy();
|
||||
expect(handler1).toBe(handler2); // Same instance (singleton)
|
||||
});
|
||||
|
||||
it('should return singleton DeleteUserCommandHandler instance', () => {
|
||||
const handler1 = container.deleteUserCommandHandler;
|
||||
const handler2 = container.deleteUserCommandHandler;
|
||||
|
||||
expect(handler1).toBeTruthy();
|
||||
expect(handler1).toBe(handler2); // Same instance (singleton)
|
||||
});
|
||||
|
||||
it('should return singleton DeleteDeckCommandHandler instance', () => {
|
||||
const handler1 = container.deleteDeckCommandHandler;
|
||||
const handler2 = container.deleteDeckCommandHandler;
|
||||
|
||||
expect(handler1).toBeTruthy();
|
||||
expect(handler1).toBe(handler2); // Same instance (singleton)
|
||||
});
|
||||
|
||||
it('should return singleton DeleteOrganizationCommandHandler instance', () => {
|
||||
const handler1 = container.deleteOrganizationCommandHandler;
|
||||
const handler2 = container.deleteOrganizationCommandHandler;
|
||||
|
||||
expect(handler1).toBeTruthy();
|
||||
expect(handler1).toBe(handler2); // Same instance (singleton)
|
||||
});
|
||||
});
|
||||
|
||||
describe('Query Handlers', () => {
|
||||
it('should return singleton GetUserByIdQueryHandler instance', () => {
|
||||
const handler1 = container.getUserByIdQueryHandler;
|
||||
const handler2 = container.getUserByIdQueryHandler;
|
||||
|
||||
expect(handler1).toBeTruthy();
|
||||
expect(handler1).toBe(handler2); // Same instance (singleton)
|
||||
});
|
||||
|
||||
it('should return singleton GetUsersByPageQueryHandler instance', () => {
|
||||
const handler1 = container.getUsersByPageQueryHandler;
|
||||
const handler2 = container.getUsersByPageQueryHandler;
|
||||
|
||||
expect(handler1).toBeTruthy();
|
||||
expect(handler1).toBe(handler2); // Same instance (singleton)
|
||||
});
|
||||
});
|
||||
|
||||
describe('Services', () => {
|
||||
it('should return singleton JWTService instance', () => {
|
||||
const service1 = container.jwtService;
|
||||
const service2 = container.jwtService;
|
||||
|
||||
expect(service1).toBeTruthy();
|
||||
expect(service1).toBe(service2); // Same instance (singleton)
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,224 @@
|
||||
import { EmailService, EmailOptions } from '../../../src/Application/Services/EmailService';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Mock nodemailer
|
||||
jest.mock('nodemailer');
|
||||
jest.mock('fs');
|
||||
|
||||
// Mock logger
|
||||
jest.mock('../../../src/Application/Services/Logger', () => ({
|
||||
logError: jest.fn(),
|
||||
logAuth: jest.fn(),
|
||||
logStartup: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('EmailService', () => {
|
||||
let emailService: EmailService;
|
||||
let mockTransporter: jest.Mocked<nodemailer.Transporter>;
|
||||
let mockCreateTransporter: jest.MockedFunction<typeof nodemailer.createTransport>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Mock nodemailer.createTransporter
|
||||
mockTransporter = {
|
||||
sendMail: jest.fn(),
|
||||
} as any;
|
||||
|
||||
mockCreateTransporter = nodemailer.createTransport as jest.MockedFunction<typeof nodemailer.createTransport>;
|
||||
mockCreateTransporter.mockReturnValue(mockTransporter);
|
||||
|
||||
// Mock fs
|
||||
(fs.readFileSync as jest.Mock).mockImplementation((filePath: string) => {
|
||||
if (filePath.includes('html')) {
|
||||
return 'HTML template: {{name}}';
|
||||
}
|
||||
return 'Text template: {{name}}';
|
||||
});
|
||||
|
||||
(fs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
|
||||
emailService = new EmailService();
|
||||
});
|
||||
|
||||
describe('sendEmail', () => {
|
||||
it('should send email successfully', async () => {
|
||||
// Arrange
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: 'Test Subject',
|
||||
html: '<p>Test HTML</p>',
|
||||
text: 'Test Text',
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' });
|
||||
|
||||
// Act
|
||||
const result = await emailService.sendEmail(emailOptions);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockTransporter.sendMail).toHaveBeenCalledWith({
|
||||
from: process.env.EMAIL_FROM || 'noreply@serpentrace.com',
|
||||
to: emailOptions.to,
|
||||
subject: emailOptions.subject,
|
||||
html: emailOptions.html,
|
||||
text: emailOptions.text,
|
||||
});
|
||||
});
|
||||
|
||||
it('should send email with template', async () => {
|
||||
// Arrange
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: 'Test Subject',
|
||||
template: 'verification',
|
||||
templateData: { name: 'John', token: 'abc123' },
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' });
|
||||
|
||||
// Act
|
||||
const result = await emailService.sendEmail(emailOptions);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockTransporter.sendMail).toHaveBeenCalledWith({
|
||||
from: process.env.EMAIL_FROM || 'noreply@serpentrace.com',
|
||||
to: emailOptions.to,
|
||||
subject: emailOptions.subject,
|
||||
html: expect.stringContaining('John'),
|
||||
text: expect.stringContaining('John'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle email send failure', async () => {
|
||||
// Arrange
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: 'Test Subject',
|
||||
text: 'Test Text',
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockRejectedValue(new Error('SMTP Error'));
|
||||
|
||||
// Act
|
||||
const result = await emailService.sendEmail(emailOptions);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle missing template files', async () => {
|
||||
// Arrange
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: 'Test Subject',
|
||||
template: 'nonexistent',
|
||||
templateData: { name: 'John' },
|
||||
};
|
||||
|
||||
(fs.existsSync as jest.Mock).mockReturnValue(false);
|
||||
|
||||
// Act
|
||||
const result = await emailService.sendEmail(emailOptions);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle template processing errors', async () => {
|
||||
// Arrange
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: 'Test Subject',
|
||||
template: 'verification',
|
||||
templateData: { name: 'John' },
|
||||
};
|
||||
|
||||
(fs.readFileSync as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('File read error');
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await emailService.sendEmail(emailOptions);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should use fallback content when template data is missing', async () => {
|
||||
// Arrange
|
||||
const emailOptions: EmailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: 'Test Subject',
|
||||
template: 'verification',
|
||||
};
|
||||
|
||||
mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' });
|
||||
|
||||
// Act
|
||||
const result = await emailService.sendEmail(emailOptions);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with environment variables', () => {
|
||||
// Arrange
|
||||
const originalEnv = process.env;
|
||||
process.env = {
|
||||
...originalEnv,
|
||||
EMAIL_HOST: 'test-smtp.com',
|
||||
EMAIL_PORT: '465',
|
||||
EMAIL_SECURE: 'true',
|
||||
EMAIL_USER: 'test@example.com',
|
||||
EMAIL_PASS: 'testpass',
|
||||
EMAIL_FROM: 'sender@example.com',
|
||||
};
|
||||
|
||||
// Act
|
||||
const service = new EmailService();
|
||||
|
||||
// Assert
|
||||
expect(mockCreateTransporter).toHaveBeenCalledWith({
|
||||
host: 'test-smtp.com',
|
||||
port: 465,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: 'test@example.com',
|
||||
pass: 'testpass',
|
||||
},
|
||||
});
|
||||
|
||||
// Restore environment
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it('should use default values when environment variables are missing', () => {
|
||||
// Arrange
|
||||
const originalEnv = process.env;
|
||||
process.env = {};
|
||||
|
||||
// Act
|
||||
const service = new EmailService();
|
||||
|
||||
// Assert
|
||||
expect(mockCreateTransporter).toHaveBeenCalledWith({
|
||||
host: 'smtp.gmail.com',
|
||||
port: 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: '',
|
||||
pass: '',
|
||||
},
|
||||
});
|
||||
|
||||
// Restore environment
|
||||
process.env = originalEnv;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { JWTService, TokenPayload } from '../../../src/Application/Services/JWTService';
|
||||
import { Request, Response } from 'express';
|
||||
import { UserState } from '../../../src/Domain/User/UserAggregate';
|
||||
|
||||
describe('JWTService - Token Refresh Logic', () => {
|
||||
let jwtService: JWTService;
|
||||
let mockRequest: Partial<Request>;
|
||||
let mockResponse: Partial<Response>;
|
||||
let dateNowSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
jwtService = new JWTService();
|
||||
|
||||
mockRequest = {
|
||||
cookies: {}
|
||||
};
|
||||
|
||||
mockResponse = {
|
||||
cookie: jest.fn()
|
||||
};
|
||||
|
||||
// Create a fresh spy for Date.now in each test
|
||||
dateNowSpy = jest.spyOn(Date, 'now');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Always restore Date.now after each test
|
||||
dateNowSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('shouldRefreshToken', () => {
|
||||
it('should return true when token is 75% through its lifetime', () => {
|
||||
// Token issued at time 100, expires at 900 (lifetime: 800)
|
||||
// 75% of 800 = 600, so at time 700 (100 + 600), it should refresh
|
||||
const payload: TokenPayload = {
|
||||
userId: 'test-user',
|
||||
authLevel: 0 as 0 | 1,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'test-org',
|
||||
iat: 100,
|
||||
exp: 900
|
||||
};
|
||||
|
||||
// Mock current time as 700 (which is 75% through the token lifetime)
|
||||
dateNowSpy.mockReturnValue(700 * 1000);
|
||||
|
||||
const result = jwtService.shouldRefreshToken(payload);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when token is more than 75% through its lifetime', () => {
|
||||
const payload: TokenPayload = {
|
||||
userId: 'test-user',
|
||||
authLevel: 0 as 0 | 1,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'test-org',
|
||||
iat: 100,
|
||||
exp: 900
|
||||
};
|
||||
|
||||
// Mock current time as 750 (which is 81.25% through the token lifetime)
|
||||
dateNowSpy.mockReturnValue(750 * 1000);
|
||||
|
||||
const result = jwtService.shouldRefreshToken(payload);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when token is less than 75% through its lifetime', () => {
|
||||
const payload: TokenPayload = {
|
||||
userId: 'test-user',
|
||||
authLevel: 0 as 0 | 1,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'test-org',
|
||||
iat: 100,
|
||||
exp: 900
|
||||
};
|
||||
|
||||
// Mock current time as 600 (which is 62.5% through the token lifetime)
|
||||
dateNowSpy.mockReturnValue(600 * 1000);
|
||||
|
||||
const result = jwtService.shouldRefreshToken(payload);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when payload does not have required timestamp fields', () => {
|
||||
const payload: TokenPayload = {
|
||||
userId: 'test-user',
|
||||
authLevel: 0 as 0 | 1,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'test-org'
|
||||
};
|
||||
|
||||
const result = jwtService.shouldRefreshToken(payload);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshIfNeeded', () => {
|
||||
it('should return new token when refresh is needed', () => {
|
||||
// Setup a payload that needs refresh (75% through lifetime)
|
||||
const payload: TokenPayload = {
|
||||
userId: 'test-user',
|
||||
authLevel: 0 as 0 | 1,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'test-org',
|
||||
iat: 100,
|
||||
exp: 900
|
||||
};
|
||||
|
||||
// Mock current time as 700 (75% through the token lifetime)
|
||||
dateNowSpy.mockReturnValue(700 * 1000);
|
||||
|
||||
const result = jwtService.refreshIfNeeded(payload, mockResponse as Response);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockResponse.cookie).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false when refresh is not needed', () => {
|
||||
// Setup a payload that doesn't need refresh (less than 75% through lifetime)
|
||||
const payload: TokenPayload = {
|
||||
userId: 'test-user',
|
||||
authLevel: 0 as 0 | 1,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'test-org',
|
||||
iat: 100,
|
||||
exp: 900
|
||||
};
|
||||
|
||||
// Mock current time as 600 (62.5% through the token lifetime)
|
||||
dateNowSpy.mockReturnValue(600 * 1000);
|
||||
|
||||
const result = jwtService.refreshIfNeeded(payload, mockResponse as Response);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockResponse.cookie).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,403 @@
|
||||
import { JWTService, TokenPayload } from '../../../src/Application/Services/JWTService';
|
||||
import { Request, Response } from 'express';
|
||||
import { UserState } from '../../../src/Domain/User/UserAggregate';
|
||||
|
||||
|
||||
describe('JWTService', () => {
|
||||
let jwtService: JWTService;
|
||||
let mockRequest: Partial<Request>;
|
||||
let mockResponse: Partial<Response>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jwtService = new JWTService();
|
||||
|
||||
// Set a test secret for consistent testing
|
||||
process.env.JWT_SECRET = 'test-secret-key-for-testing';
|
||||
process.env.JWT_EXPIRY = '3600'; // 1 hour
|
||||
|
||||
// Mock express Request and Response
|
||||
mockRequest = {
|
||||
cookies: {}
|
||||
};
|
||||
|
||||
mockResponse = {
|
||||
cookie: jest.fn()
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up environment
|
||||
delete process.env.JWT_SECRET;
|
||||
delete process.env.JWT_EXPIRY;
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a valid JWT token and set cookie', () => {
|
||||
// Arrange
|
||||
const payload: TokenPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-456'
|
||||
};
|
||||
|
||||
// Act
|
||||
const token = jwtService.create(payload, mockResponse as Response);
|
||||
|
||||
// Assert
|
||||
expect(token).toBeDefined();
|
||||
expect(typeof token).toBe('string');
|
||||
expect(token.split('.')).toHaveLength(3); // JWT has 3 parts
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
'auth_token',
|
||||
token,
|
||||
expect.objectContaining({
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
maxAge: 86400000 // 24 hours in milliseconds
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should create different tokens for different payloads', () => {
|
||||
// Arrange
|
||||
const payload1: TokenPayload = {
|
||||
userId: 'user-1',
|
||||
authLevel: 0 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-1'
|
||||
};
|
||||
|
||||
const payload2: TokenPayload = {
|
||||
userId: 'user-2',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_PREMIUM,
|
||||
orgId: 'org-2'
|
||||
};
|
||||
|
||||
// Act
|
||||
const token1 = jwtService.create(payload1, mockResponse as Response);
|
||||
const token2 = jwtService.create(payload2, mockResponse as Response);
|
||||
|
||||
// Assert
|
||||
expect(token1).toBeDefined();
|
||||
expect(token2).toBeDefined();
|
||||
expect(token1).not.toBe(token2);
|
||||
});
|
||||
|
||||
it('should set secure cookie in production environment', () => {
|
||||
// Arrange
|
||||
process.env.NODE_ENV = 'production';
|
||||
const payload: TokenPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-456'
|
||||
};
|
||||
|
||||
// Act
|
||||
const token = jwtService.create(payload, mockResponse as Response);
|
||||
|
||||
// Assert
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
'auth_token',
|
||||
token,
|
||||
expect.objectContaining({
|
||||
secure: true
|
||||
})
|
||||
);
|
||||
|
||||
// Clean up
|
||||
delete process.env.NODE_ENV;
|
||||
});
|
||||
});
|
||||
|
||||
describe('verify', () => {
|
||||
it('should verify a valid token from cookies', () => {
|
||||
// Arrange
|
||||
const payload: TokenPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-456'
|
||||
};
|
||||
|
||||
const token = jwtService.create(payload, mockResponse as Response);
|
||||
mockRequest.cookies = { auth_token: token };
|
||||
|
||||
// Act
|
||||
const result = jwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.userId).toBe('user-123');
|
||||
expect(result!.authLevel).toBe(1);
|
||||
expect(result!.orgId).toBe('org-456');
|
||||
});
|
||||
|
||||
it('should return null when no token is present in cookies', () => {
|
||||
// Arrange
|
||||
mockRequest.cookies = {};
|
||||
|
||||
// Act
|
||||
const result = jwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for invalid token', () => {
|
||||
// Arrange
|
||||
mockRequest.cookies = { auth_token: 'invalid.jwt.token' };
|
||||
|
||||
// Act
|
||||
const result = jwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for malformed token', () => {
|
||||
// Arrange
|
||||
mockRequest.cookies = { auth_token: 'not-a-jwt-token' };
|
||||
|
||||
// Act
|
||||
const result = jwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('token creation with different payloads', () => {
|
||||
it('should create tokens with dynamic user data', () => {
|
||||
// Arrange
|
||||
const timestamp = Date.now();
|
||||
const testPayload: TokenPayload = {
|
||||
userId: `test-user-${timestamp}`,
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: `test-org-${timestamp}`
|
||||
};
|
||||
|
||||
// Act
|
||||
const token = jwtService.create(testPayload, mockResponse as Response);
|
||||
|
||||
// Assert
|
||||
expect(token).toBeDefined();
|
||||
expect(typeof token).toBe('string');
|
||||
expect(mockResponse.cookie).toHaveBeenCalled();
|
||||
|
||||
// Verify we can decode it back
|
||||
mockRequest.cookies = { auth_token: token };
|
||||
const verifiedPayload = jwtService.verify(mockRequest as Request);
|
||||
|
||||
expect(verifiedPayload).toBeDefined();
|
||||
expect(verifiedPayload!.userId).toBe(testPayload.userId);
|
||||
expect(verifiedPayload!.orgId).toBe(testPayload.orgId);
|
||||
expect(verifiedPayload!.authLevel).toBe(testPayload.authLevel);
|
||||
});
|
||||
|
||||
it('should create different tokens for different timestamps', async () => {
|
||||
// Arrange
|
||||
const timestamp1 = Date.now();
|
||||
const payload1: TokenPayload = {
|
||||
userId: `test-user-${timestamp1}`,
|
||||
authLevel: 0 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: `test-org-${timestamp1}`
|
||||
};
|
||||
|
||||
// Add a small delay to ensure different timestamps
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
|
||||
const timestamp2 = Date.now();
|
||||
const payload2: TokenPayload = {
|
||||
userId: `test-user-${timestamp2}`,
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_PREMIUM,
|
||||
orgId: `test-org-${timestamp2}`
|
||||
};
|
||||
|
||||
// Act
|
||||
const token1 = jwtService.create(payload1, mockResponse as Response);
|
||||
const token2 = jwtService.create(payload2, mockResponse as Response);
|
||||
|
||||
// Assert
|
||||
expect(token1).not.toBe(token2);
|
||||
expect(payload1.userId).not.toBe(payload2.userId);
|
||||
expect(payload1.orgId).not.toBe(payload2.orgId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration scenarios', () => {
|
||||
it('should create and verify token in complete flow', () => {
|
||||
// Arrange
|
||||
const originalPayload: TokenPayload = {
|
||||
userId: 'integration-user',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'integration-org'
|
||||
};
|
||||
|
||||
// Act - Complete flow
|
||||
const token = jwtService.create(originalPayload, mockResponse as Response);
|
||||
mockRequest.cookies = { auth_token: token };
|
||||
const verifiedPayload = jwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(token).toBeDefined();
|
||||
expect(verifiedPayload).toBeDefined();
|
||||
expect(verifiedPayload!.userId).toBe('integration-user');
|
||||
expect(verifiedPayload!.authLevel).toBe(1);
|
||||
expect(verifiedPayload!.orgId).toBe('integration-org');
|
||||
});
|
||||
});
|
||||
|
||||
describe('JWT_EXPIRATION duration parsing', () => {
|
||||
it('should parse JWT_EXPIRATION in hours format', () => {
|
||||
// Arrange
|
||||
delete process.env.JWT_EXPIRY;
|
||||
process.env.JWT_EXPIRATION = '2h';
|
||||
|
||||
// Act
|
||||
const newJwtService = new JWTService();
|
||||
const payload: TokenPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-456'
|
||||
};
|
||||
|
||||
const token = newJwtService.create(payload, mockResponse as Response);
|
||||
|
||||
// Update mock request with the created token
|
||||
mockRequest.cookies = { auth_token: token };
|
||||
const verifiedPayload = newJwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(token).toBeDefined();
|
||||
expect(verifiedPayload).toBeDefined();
|
||||
expect(verifiedPayload!.exp).toBeDefined();
|
||||
|
||||
// Token should expire in approximately 2 hours (7200 seconds)
|
||||
const expectedExp = Math.floor(Date.now() / 1000) + 7200;
|
||||
expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds
|
||||
|
||||
// Cleanup
|
||||
delete process.env.JWT_EXPIRATION;
|
||||
});
|
||||
|
||||
it('should parse JWT_EXPIRATION in days format', () => {
|
||||
// Arrange
|
||||
delete process.env.JWT_EXPIRY;
|
||||
process.env.JWT_EXPIRATION = '7d';
|
||||
|
||||
// Act
|
||||
const newJwtService = new JWTService();
|
||||
const payload: TokenPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-456'
|
||||
};
|
||||
|
||||
const token = newJwtService.create(payload, mockResponse as Response);
|
||||
|
||||
// Update mock request with the created token
|
||||
mockRequest.cookies = { auth_token: token };
|
||||
const verifiedPayload = newJwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(token).toBeDefined();
|
||||
expect(verifiedPayload).toBeDefined();
|
||||
|
||||
// Token should expire in approximately 7 days (604800 seconds)
|
||||
const expectedExp = Math.floor(Date.now() / 1000) + 604800;
|
||||
expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds
|
||||
|
||||
// Cleanup
|
||||
delete process.env.JWT_EXPIRATION;
|
||||
});
|
||||
|
||||
it('should parse JWT_EXPIRATION in minutes format', () => {
|
||||
// Arrange
|
||||
delete process.env.JWT_EXPIRY;
|
||||
process.env.JWT_EXPIRATION = '30m';
|
||||
|
||||
// Act
|
||||
const newJwtService = new JWTService();
|
||||
const payload: TokenPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-456'
|
||||
};
|
||||
|
||||
const token = newJwtService.create(payload, mockResponse as Response);
|
||||
|
||||
// Update mock request with the created token
|
||||
mockRequest.cookies = { auth_token: token };
|
||||
const verifiedPayload = newJwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(token).toBeDefined();
|
||||
expect(verifiedPayload).toBeDefined();
|
||||
|
||||
// Token should expire in approximately 30 minutes (1800 seconds)
|
||||
const expectedExp = Math.floor(Date.now() / 1000) + 1800;
|
||||
expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds
|
||||
|
||||
// Cleanup
|
||||
delete process.env.JWT_EXPIRATION;
|
||||
});
|
||||
|
||||
it('should prioritize JWT_EXPIRY over JWT_EXPIRATION when both are set', () => {
|
||||
// Arrange
|
||||
process.env.JWT_EXPIRY = '1800'; // 30 minutes in seconds
|
||||
process.env.JWT_EXPIRATION = '1h'; // 1 hour
|
||||
|
||||
// Act
|
||||
const newJwtService = new JWTService();
|
||||
const payload: TokenPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-456'
|
||||
};
|
||||
|
||||
const token = newJwtService.create(payload, mockResponse as Response);
|
||||
|
||||
// Update mock request with the created token
|
||||
mockRequest.cookies = { auth_token: token };
|
||||
const verifiedPayload = newJwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(token).toBeDefined();
|
||||
expect(verifiedPayload).toBeDefined();
|
||||
|
||||
// Should use JWT_EXPIRY (1800 seconds), not JWT_EXPIRATION (3600 seconds)
|
||||
const expectedExp = Math.floor(Date.now() / 1000) + 1800;
|
||||
expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds
|
||||
|
||||
// Cleanup
|
||||
delete process.env.JWT_EXPIRY;
|
||||
delete process.env.JWT_EXPIRATION;
|
||||
});
|
||||
|
||||
it('should throw error for invalid JWT_EXPIRATION format', () => {
|
||||
// Arrange
|
||||
delete process.env.JWT_EXPIRY;
|
||||
process.env.JWT_EXPIRATION = 'invalid-format';
|
||||
|
||||
// Act & Assert
|
||||
expect(() => {
|
||||
new JWTService();
|
||||
}).toThrow('Invalid duration format: invalid-format. Use format like \'24h\', \'7d\', \'30m\'');
|
||||
|
||||
// Cleanup
|
||||
delete process.env.JWT_EXPIRATION;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,217 @@
|
||||
import { LoggingService, LogLevel } from '../../../src/Application/Services/LoggingService';
|
||||
import { logAuth, logError, logDatabase, logStartup } from '../../../src/Application/Services/Logger';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('LoggingService', () => {
|
||||
let loggingService: LoggingService;
|
||||
const testLogsDir = path.join(process.cwd(), 'test-logs');
|
||||
|
||||
beforeEach(() => {
|
||||
// Clean up any existing test logs
|
||||
if (fs.existsSync(testLogsDir)) {
|
||||
fs.rmSync(testLogsDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Mock environment variables for testing
|
||||
process.env.MAX_LOGS_PER_FILE = '10';
|
||||
process.env.MINIO_ENDPOINT = '';
|
||||
|
||||
loggingService = LoggingService.getInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test logs
|
||||
if (fs.existsSync(testLogsDir)) {
|
||||
fs.rmSync(testLogsDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Clean up environment variables
|
||||
delete process.env.MAX_LOGS_PER_FILE;
|
||||
delete process.env.MINIO_ENDPOINT;
|
||||
});
|
||||
|
||||
describe('Log Level Functions', () => {
|
||||
it('should log authentication events', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'info').mockImplementation();
|
||||
|
||||
logAuth('Test auth message', 'user123', { action: 'login' });
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
const logCall = consoleSpy.mock.calls[0][0];
|
||||
expect(logCall).toContain('[AUTH]');
|
||||
expect(logCall).toContain('Test auth message');
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should log error events with stack trace', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
const testError = new Error('Test error message');
|
||||
|
||||
logError('Test error occurred', testError);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
const logCall = consoleSpy.mock.calls[0][0];
|
||||
expect(logCall).toContain('[ERROR]');
|
||||
expect(logCall).toContain('Test error occurred');
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should log database operations with timing', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'info').mockImplementation();
|
||||
|
||||
logDatabase('Query executed', 'SELECT * FROM users', 45);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
const logCall = consoleSpy.mock.calls[0][0];
|
||||
expect(logCall).toContain('[DATABASE]');
|
||||
expect(logCall).toContain('Query executed');
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should log startup events', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||
|
||||
logStartup('Application started', { version: '1.0.0' });
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
const logCall = consoleSpy.mock.calls[0][0];
|
||||
expect(logCall).toContain('[STARTUP]');
|
||||
expect(logCall).toContain('Application started');
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Log Formatting', () => {
|
||||
it('should include timestamp in log entries', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||
|
||||
logStartup('Test message');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
const logCall = consoleSpy.mock.calls[0][0];
|
||||
|
||||
// Check if timestamp is in ISO format
|
||||
const timestampRegex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/;
|
||||
expect(logCall).toMatch(timestampRegex);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should include metadata in log entries', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'info').mockImplementation();
|
||||
const metadata = { userId: '123', action: 'test' };
|
||||
|
||||
logAuth('Test with metadata', 'user123', metadata);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
const logCall = consoleSpy.mock.calls[0][0];
|
||||
expect(logCall).toContain('Meta:');
|
||||
expect(logCall).toContain('"userId":"123"');
|
||||
expect(logCall).toContain('"action":"test"');
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Request Logging Middleware', () => {
|
||||
it('should create request logging middleware', () => {
|
||||
const middleware = loggingService.requestLoggingMiddleware();
|
||||
|
||||
expect(typeof middleware).toBe('function');
|
||||
expect(middleware.length).toBe(3); // req, res, next
|
||||
});
|
||||
|
||||
it('should create error logging middleware', () => {
|
||||
const middleware = loggingService.errorLoggingMiddleware();
|
||||
|
||||
expect(typeof middleware).toBe('function');
|
||||
expect(middleware.length).toBe(4); // error, req, res, next
|
||||
});
|
||||
});
|
||||
|
||||
describe('Log Levels', () => {
|
||||
it('should have all required log levels defined', () => {
|
||||
expect(LogLevel.REQUEST).toBe('REQUEST');
|
||||
expect(LogLevel.ERROR).toBe('ERROR');
|
||||
expect(LogLevel.WARNING).toBe('WARNING');
|
||||
expect(LogLevel.AUTH).toBe('AUTH');
|
||||
expect(LogLevel.DATABASE).toBe('DATABASE');
|
||||
expect(LogLevel.STARTUP).toBe('STARTUP');
|
||||
expect(LogLevel.CONNECTION).toBe('CONNECTION');
|
||||
expect(LogLevel.OTHER).toBe('OTHER');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Singleton Pattern', () => {
|
||||
it('should return the same instance', () => {
|
||||
const instance1 = LoggingService.getInstance();
|
||||
const instance2 = LoggingService.getInstance();
|
||||
|
||||
expect(instance1).toBe(instance2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('File Operations', () => {
|
||||
it('should handle missing Minio configuration gracefully', () => {
|
||||
// Test that the service starts without Minio config
|
||||
expect(() => LoggingService.getInstance()).not.toThrow();
|
||||
});
|
||||
|
||||
it('should generate monthly directory structure', () => {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const expectedPath = path.join('logs', `${year}-${month}`);
|
||||
|
||||
// This tests the internal logic through the public interface
|
||||
logStartup('Test for directory creation');
|
||||
|
||||
// Since we can't directly test the private method, we verify the service doesn't crash
|
||||
expect(loggingService).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle logging errors gracefully', () => {
|
||||
// Mock fs.appendFileSync to throw an error
|
||||
const originalAppendFileSync = fs.appendFileSync;
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
fs.appendFileSync = jest.fn(() => {
|
||||
throw new Error('Disk full');
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
logStartup('This should not crash');
|
||||
}).not.toThrow();
|
||||
|
||||
// Restore original function
|
||||
fs.appendFileSync = originalAppendFileSync;
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should continue logging to console even if file logging fails', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||
|
||||
// Mock file system to fail
|
||||
const originalAppendFileSync = fs.appendFileSync;
|
||||
fs.appendFileSync = jest.fn(() => {
|
||||
throw new Error('File system error');
|
||||
});
|
||||
|
||||
logStartup('Test message');
|
||||
|
||||
// Should still log to console
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
|
||||
// Restore
|
||||
fs.appendFileSync = originalAppendFileSync;
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,270 @@
|
||||
import { PasswordService } from '../../../src/Application/Services/PasswordService';
|
||||
|
||||
// Mock bcrypt completely
|
||||
jest.mock('bcrypt');
|
||||
|
||||
describe('PasswordService', () => {
|
||||
// Mock functions for bcrypt
|
||||
const mockBcryptHash = jest.fn();
|
||||
const mockBcryptCompare = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Reset console.error mock to avoid noise in tests
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
// Setup bcrypt mocks
|
||||
const bcrypt = require('bcrypt');
|
||||
bcrypt.hash = mockBcryptHash;
|
||||
bcrypt.compare = mockBcryptCompare;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('hashPassword', () => {
|
||||
it('should hash a valid password successfully', async () => {
|
||||
// Arrange
|
||||
const password = 'validPassword123!';
|
||||
const hashedPassword = '$2b$12$hashed.password.here';
|
||||
|
||||
mockBcryptHash.mockResolvedValue(hashedPassword);
|
||||
|
||||
// Act
|
||||
const result = await PasswordService.hashPassword(password);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(hashedPassword);
|
||||
expect(mockBcryptHash).toHaveBeenCalledWith(password, 12);
|
||||
});
|
||||
|
||||
it('should throw error for empty password', async () => {
|
||||
// Arrange
|
||||
const password = '';
|
||||
|
||||
// Act & Assert
|
||||
await expect(PasswordService.hashPassword(password)).rejects.toThrow('Password must be a non-empty string');
|
||||
expect(mockBcryptHash).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error for non-string password', async () => {
|
||||
// Arrange
|
||||
const password = null as any;
|
||||
|
||||
// Act & Assert
|
||||
await expect(PasswordService.hashPassword(password)).rejects.toThrow('Password must be a non-empty string');
|
||||
expect(mockBcryptHash).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle bcrypt errors and throw generic error', async () => {
|
||||
// Arrange
|
||||
const password = 'validPassword123!';
|
||||
mockBcryptHash.mockRejectedValue(new Error('Bcrypt error'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(PasswordService.hashPassword(password)).rejects.toThrow('Failed to hash password');
|
||||
expect(mockBcryptHash).toHaveBeenCalledWith(password, 12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyPassword', () => {
|
||||
it('should return true for matching password and hash', async () => {
|
||||
// Arrange
|
||||
const password = 'validPassword123!';
|
||||
const hashedPassword = '$2b$12$hashed.password.here';
|
||||
|
||||
mockBcryptCompare.mockResolvedValue(true);
|
||||
|
||||
// Act
|
||||
const result = await PasswordService.verifyPassword(password, hashedPassword);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockBcryptCompare).toHaveBeenCalledWith(password, hashedPassword);
|
||||
});
|
||||
|
||||
it('should return false for non-matching password and hash', async () => {
|
||||
// Arrange
|
||||
const password = 'wrongPassword';
|
||||
const hashedPassword = '$2b$12$hashed.password.here';
|
||||
|
||||
mockBcryptCompare.mockResolvedValue(false);
|
||||
|
||||
// Act
|
||||
const result = await PasswordService.verifyPassword(password, hashedPassword);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
expect(mockBcryptCompare).toHaveBeenCalledWith(password, hashedPassword);
|
||||
});
|
||||
|
||||
it('should return false for empty password', async () => {
|
||||
// Arrange
|
||||
const password = '';
|
||||
const hashedPassword = '$2b$12$hashed.password.here';
|
||||
|
||||
// Act
|
||||
const result = await PasswordService.verifyPassword(password, hashedPassword);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
expect(mockBcryptCompare).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false for empty hashed password', async () => {
|
||||
// Arrange
|
||||
const password = 'validPassword123!';
|
||||
const hashedPassword = '';
|
||||
|
||||
// Act
|
||||
const result = await PasswordService.verifyPassword(password, hashedPassword);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
expect(mockBcryptCompare).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false for non-string inputs', async () => {
|
||||
// Arrange
|
||||
const password = null as any;
|
||||
const hashedPassword = undefined as any;
|
||||
|
||||
// Act
|
||||
const result = await PasswordService.verifyPassword(password, hashedPassword);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
expect(mockBcryptCompare).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false when bcrypt throws error', async () => {
|
||||
// Arrange
|
||||
const password = 'validPassword123!';
|
||||
const hashedPassword = '$2b$12$hashed.password.here';
|
||||
|
||||
mockBcryptCompare.mockRejectedValue(new Error('Bcrypt compare error'));
|
||||
|
||||
// Act
|
||||
const result = await PasswordService.verifyPassword(password, hashedPassword);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
expect(mockBcryptCompare).toHaveBeenCalledWith(password, hashedPassword);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatePasswordStrength', () => {
|
||||
it('should return valid for strong password', () => {
|
||||
// Arrange
|
||||
const password = 'StrongPass123!';
|
||||
|
||||
// Act
|
||||
const result = PasswordService.validatePasswordStrength(password);
|
||||
|
||||
// Assert
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return invalid for short password', () => {
|
||||
// Arrange
|
||||
const password = 'Short1!';
|
||||
|
||||
// Act
|
||||
const result = PasswordService.validatePasswordStrength(password);
|
||||
|
||||
// Assert
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Password must be at least 8 characters long');
|
||||
});
|
||||
|
||||
it('should return invalid for password without uppercase', () => {
|
||||
// Arrange
|
||||
const password = 'lowercase123!';
|
||||
|
||||
// Act
|
||||
const result = PasswordService.validatePasswordStrength(password);
|
||||
|
||||
// Assert
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Password must contain at least one uppercase letter');
|
||||
});
|
||||
|
||||
it('should return invalid for password without lowercase', () => {
|
||||
// Arrange
|
||||
const password = 'UPPERCASE123!';
|
||||
|
||||
// Act
|
||||
const result = PasswordService.validatePasswordStrength(password);
|
||||
|
||||
// Assert
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Password must contain at least one lowercase letter');
|
||||
});
|
||||
|
||||
it('should return invalid for password without numbers', () => {
|
||||
// Arrange
|
||||
const password = 'NoNumbers!';
|
||||
|
||||
// Act
|
||||
const result = PasswordService.validatePasswordStrength(password);
|
||||
|
||||
// Assert
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Password must contain at least one number');
|
||||
});
|
||||
|
||||
it('should return invalid for password without special characters', () => {
|
||||
// Arrange
|
||||
const password = 'NoSpecial123';
|
||||
|
||||
// Act
|
||||
const result = PasswordService.validatePasswordStrength(password);
|
||||
|
||||
// Assert
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Password must contain at least one special character');
|
||||
});
|
||||
|
||||
it('should return multiple errors for weak password', () => {
|
||||
// Arrange
|
||||
const password = 'weak';
|
||||
|
||||
// Act
|
||||
const result = PasswordService.validatePasswordStrength(password);
|
||||
|
||||
// Assert
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toHaveLength(4);
|
||||
expect(result.errors).toContain('Password must be at least 8 characters long');
|
||||
expect(result.errors).toContain('Password must contain at least one uppercase letter');
|
||||
expect(result.errors).toContain('Password must contain at least one number');
|
||||
expect(result.errors).toContain('Password must contain at least one special character');
|
||||
});
|
||||
|
||||
it('should handle empty password', () => {
|
||||
// Arrange
|
||||
const password = '';
|
||||
|
||||
// Act
|
||||
const result = PasswordService.validatePasswordStrength(password);
|
||||
|
||||
// Assert
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Password must be provided as a string');
|
||||
});
|
||||
|
||||
it('should handle null password', () => {
|
||||
// Arrange
|
||||
const password = null as any;
|
||||
|
||||
// Act
|
||||
const result = PasswordService.validatePasswordStrength(password);
|
||||
|
||||
// Assert
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Password must be provided as a string');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,405 @@
|
||||
import { TokenService } from '../../../src/Application/Services/TokenService';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
// Mock crypto module
|
||||
jest.mock('crypto');
|
||||
|
||||
describe('TokenService', () => {
|
||||
let mockRandomBytes: jest.Mock;
|
||||
let mockCreateHash: jest.Mock;
|
||||
let mockHashUpdate: jest.Mock;
|
||||
let mockHashDigest: jest.Mock;
|
||||
let dateSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Restore Date mock if it exists
|
||||
if (dateSpy) {
|
||||
dateSpy.mockRestore();
|
||||
}
|
||||
|
||||
mockRandomBytes = jest.mocked(crypto.randomBytes);
|
||||
mockHashUpdate = jest.fn().mockReturnThis();
|
||||
mockHashDigest = jest.fn();
|
||||
mockCreateHash = jest.fn().mockReturnValue({
|
||||
update: mockHashUpdate,
|
||||
digest: mockHashDigest
|
||||
});
|
||||
|
||||
// Mock crypto.createHash properly
|
||||
jest.mocked(crypto.createHash).mockImplementation(mockCreateHash);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up Date mock
|
||||
if (dateSpy) {
|
||||
dateSpy.mockRestore();
|
||||
dateSpy = undefined as any;
|
||||
}
|
||||
});
|
||||
|
||||
describe('generateSecureToken', () => {
|
||||
it('should generate a secure token with default length', () => {
|
||||
// Arrange
|
||||
const mockBuffer = {
|
||||
toString: jest.fn().mockReturnValue('abcdef1234567890')
|
||||
};
|
||||
mockRandomBytes.mockReturnValue(mockBuffer as any);
|
||||
|
||||
// Act
|
||||
const token = TokenService.generateSecureToken();
|
||||
|
||||
// Assert
|
||||
expect(token).toBe('abcdef1234567890');
|
||||
expect(mockRandomBytes).toHaveBeenCalledWith(32);
|
||||
expect(mockBuffer.toString).toHaveBeenCalledWith('hex');
|
||||
});
|
||||
|
||||
it('should generate a secure token with custom length', () => {
|
||||
// Arrange
|
||||
const mockBuffer = {
|
||||
toString: jest.fn().mockReturnValue('abcdef')
|
||||
};
|
||||
mockRandomBytes.mockReturnValue(mockBuffer as any);
|
||||
|
||||
// Act
|
||||
const token = TokenService.generateSecureToken(16);
|
||||
|
||||
// Assert
|
||||
expect(token).toBe('abcdef');
|
||||
expect(mockRandomBytes).toHaveBeenCalledWith(16);
|
||||
expect(mockBuffer.toString).toHaveBeenCalledWith('hex');
|
||||
});
|
||||
|
||||
it('should handle crypto errors', () => {
|
||||
// Arrange
|
||||
mockRandomBytes.mockImplementation(() => {
|
||||
throw new Error('Crypto error');
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
expect(() => TokenService.generateSecureToken()).toThrow('Failed to generate secure token');
|
||||
expect(mockRandomBytes).toHaveBeenCalledWith(32);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateVerificationToken', () => {
|
||||
it('should generate verification token with correct expiration', () => {
|
||||
// Arrange
|
||||
const mockBuffer = {
|
||||
toString: jest.fn().mockReturnValue('verification123')
|
||||
};
|
||||
mockRandomBytes.mockReturnValue(mockBuffer as any);
|
||||
const mockDate = new Date('2023-01-01T12:00:00Z');
|
||||
dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any);
|
||||
|
||||
// Act
|
||||
const result = TokenService.generateVerificationToken();
|
||||
|
||||
// Assert
|
||||
expect(result.token).toBe('verification123');
|
||||
expect(result.createdAt).toEqual(mockDate);
|
||||
expect(result.expiresAt).toEqual(new Date('2023-01-02T12:00:00Z')); // 24 hours later
|
||||
expect(mockRandomBytes).toHaveBeenCalledWith(32);
|
||||
expect(mockBuffer.toString).toHaveBeenCalledWith('hex');
|
||||
});
|
||||
|
||||
it('should handle token generation errors', () => {
|
||||
// Arrange
|
||||
mockRandomBytes.mockImplementation(() => {
|
||||
throw new Error('Random bytes failed');
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
expect(() => TokenService.generateVerificationToken()).toThrow('Failed to generate verification token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generatePasswordResetToken', () => {
|
||||
it('should generate password reset token with correct expiration', () => {
|
||||
// Arrange
|
||||
const mockBuffer = {
|
||||
toString: jest.fn().mockReturnValue('reset456')
|
||||
};
|
||||
mockRandomBytes.mockReturnValue(mockBuffer as any);
|
||||
const mockDate = new Date('2023-01-01T12:00:00Z');
|
||||
dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any);
|
||||
|
||||
// Act
|
||||
const result = TokenService.generatePasswordResetToken();
|
||||
|
||||
// Assert
|
||||
expect(result.token).toBe('reset456');
|
||||
expect(result.createdAt).toEqual(mockDate);
|
||||
expect(result.expiresAt).toEqual(new Date('2023-01-01T13:00:00Z')); // 1 hour later
|
||||
expect(mockRandomBytes).toHaveBeenCalledWith(32);
|
||||
expect(mockBuffer.toString).toHaveBeenCalledWith('hex');
|
||||
});
|
||||
|
||||
it('should handle token generation errors', () => {
|
||||
// Arrange
|
||||
mockRandomBytes.mockImplementation(() => {
|
||||
throw new Error('Random bytes failed');
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
expect(() => TokenService.generatePasswordResetToken()).toThrow('Failed to generate password reset token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hashToken', () => {
|
||||
it('should hash token correctly', async () => {
|
||||
// Arrange
|
||||
const token = 'test-token-123';
|
||||
const hashedToken = 'hashed-token-result';
|
||||
mockHashDigest.mockReturnValue(hashedToken);
|
||||
|
||||
// Act
|
||||
const result = await TokenService.hashToken(token);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(hashedToken);
|
||||
expect(mockCreateHash).toHaveBeenCalledWith('sha256');
|
||||
expect(mockHashUpdate).toHaveBeenCalledWith(token);
|
||||
expect(mockHashDigest).toHaveBeenCalledWith('hex');
|
||||
});
|
||||
|
||||
it('should handle hashing errors', async () => {
|
||||
// Arrange
|
||||
const token = 'test-token-123';
|
||||
mockCreateHash.mockImplementation(() => {
|
||||
throw new Error('Hashing failed');
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
await expect(TokenService.hashToken(token)).rejects.toThrow('Failed to hash token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyToken', () => {
|
||||
it('should return true when tokens match', async () => {
|
||||
// Arrange
|
||||
const plainToken = 'plain-token';
|
||||
const hashedToken = 'expected-hash';
|
||||
mockHashDigest.mockReturnValue(hashedToken);
|
||||
|
||||
// Act
|
||||
const result = await TokenService.verifyToken(plainToken, hashedToken);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockCreateHash).toHaveBeenCalledWith('sha256');
|
||||
expect(mockHashUpdate).toHaveBeenCalledWith(plainToken);
|
||||
expect(mockHashDigest).toHaveBeenCalledWith('hex');
|
||||
});
|
||||
|
||||
it('should return false when tokens do not match', async () => {
|
||||
// Arrange
|
||||
const plainToken = 'plain-token';
|
||||
const hashedToken = 'expected-hash';
|
||||
const actualHash = 'different-hash';
|
||||
mockHashDigest.mockReturnValue(actualHash);
|
||||
|
||||
// Act
|
||||
const result = await TokenService.verifyToken(plainToken, hashedToken);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
expect(mockCreateHash).toHaveBeenCalledWith('sha256');
|
||||
expect(mockHashUpdate).toHaveBeenCalledWith(plainToken);
|
||||
expect(mockHashDigest).toHaveBeenCalledWith('hex');
|
||||
});
|
||||
|
||||
it('should handle verification errors', async () => {
|
||||
// Arrange
|
||||
const plainToken = 'plain-token';
|
||||
const hashedToken = 'expected-hash';
|
||||
mockCreateHash.mockImplementation(() => {
|
||||
throw new Error('Hash creation failed');
|
||||
});
|
||||
|
||||
// Act & Assert - TokenService.verifyToken catches errors and returns false, doesn't throw
|
||||
const result = await TokenService.verifyToken(plainToken, hashedToken);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTokenExpired', () => {
|
||||
it('should return false for non-expired token', () => {
|
||||
// Arrange
|
||||
const currentTime = new Date('2023-01-01T12:00:00Z');
|
||||
const futureDate = new Date('2023-01-01T13:00:00Z'); // 1 hour from now
|
||||
|
||||
dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any);
|
||||
|
||||
// Act
|
||||
const result = TokenService.isTokenExpired(futureDate);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
|
||||
// Cleanup
|
||||
dateSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should return true for expired token', () => {
|
||||
// Arrange
|
||||
const currentTime = new Date('2023-01-01T12:00:00Z');
|
||||
const pastDate = new Date('2023-01-01T11:00:00Z'); // 1 hour ago
|
||||
|
||||
dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any);
|
||||
|
||||
// Act
|
||||
const result = TokenService.isTokenExpired(pastDate);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Cleanup
|
||||
dateSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should return true for exactly expired token', () => {
|
||||
// Arrange
|
||||
const currentTime = new Date('2023-01-01T12:00:00Z');
|
||||
const exactlyNow = new Date('2023-01-01T12:00:00Z');
|
||||
|
||||
dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any);
|
||||
|
||||
// Act
|
||||
const result = TokenService.isTokenExpired(exactlyNow);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false); // new Date() > expiresAt is false when they're equal
|
||||
|
||||
// Cleanup
|
||||
dateSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateTokenWithExpiry', () => {
|
||||
it('should validate token format correctly', () => {
|
||||
// Arrange - valid hex token with expected length (64 chars for 32 bytes)
|
||||
const validToken = 'a'.repeat(64); // 64 hex characters
|
||||
|
||||
// Act
|
||||
const result = TokenService.isValidTokenFormat(validToken);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid token format', () => {
|
||||
// Arrange
|
||||
const invalidTokens = [
|
||||
'', // empty
|
||||
'invalid-token-with-dashes', // non-hex characters
|
||||
'abc123', // too short
|
||||
null as any, // null
|
||||
undefined as any, // undefined
|
||||
123 as any // not string
|
||||
];
|
||||
|
||||
invalidTokens.forEach(invalidToken => {
|
||||
// Act
|
||||
const result = TokenService.isValidTokenFormat(invalidToken);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateVerificationUrl', () => {
|
||||
it('should generate correct verification URL', () => {
|
||||
// Arrange
|
||||
const baseUrl = 'https://example.com';
|
||||
const token = 'verification-token-123';
|
||||
|
||||
// Act
|
||||
const url = TokenService.generateVerificationUrl(baseUrl, token);
|
||||
|
||||
// Assert
|
||||
expect(url).toBe('https://example.com/api/auth/verify-email?token=verification-token-123');
|
||||
});
|
||||
|
||||
it('should handle base URL with trailing slash', () => {
|
||||
// Arrange
|
||||
const baseUrl = 'https://example.com/';
|
||||
const token = 'verification-token-123';
|
||||
|
||||
// Act
|
||||
const url = TokenService.generateVerificationUrl(baseUrl, token);
|
||||
|
||||
// Assert
|
||||
expect(url).toBe('https://example.com/api/auth/verify-email?token=verification-token-123');
|
||||
});
|
||||
|
||||
it('should encode special characters in token', () => {
|
||||
// Arrange
|
||||
const baseUrl = 'https://example.com';
|
||||
const token = 'token+with/special=chars';
|
||||
|
||||
// Act
|
||||
const url = TokenService.generateVerificationUrl(baseUrl, token);
|
||||
|
||||
// Assert
|
||||
expect(url).toContain(encodeURIComponent(token));
|
||||
});
|
||||
});
|
||||
|
||||
describe('generatePasswordResetUrl', () => {
|
||||
it('should generate correct password reset URL', () => {
|
||||
// Arrange
|
||||
const baseUrl = 'https://example.com';
|
||||
const token = 'reset-token-456';
|
||||
|
||||
// Act
|
||||
const url = TokenService.generatePasswordResetUrl(baseUrl, token);
|
||||
|
||||
// Assert
|
||||
expect(url).toBe('https://example.com/api/auth/reset-password?token=reset-token-456');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getExpirationInfo', () => {
|
||||
it('should return correct info for non-expired token', () => {
|
||||
// Arrange
|
||||
const currentTime = new Date('2023-01-01T12:00:00Z');
|
||||
const futureDate = new Date('2023-01-01T14:00:00Z'); // 2 hours from now
|
||||
|
||||
dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any);
|
||||
|
||||
// Act
|
||||
const result = TokenService.getExpirationInfo(futureDate);
|
||||
|
||||
// Assert
|
||||
expect(result.expired).toBe(false);
|
||||
expect(result.timeLeft).toContain('Expires in');
|
||||
expect(result.timeLeft).toContain('hour(s)');
|
||||
|
||||
// Cleanup
|
||||
dateSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should return correct info for expired token', () => {
|
||||
// Arrange
|
||||
const currentTime = new Date('2023-01-01T12:00:00Z');
|
||||
const pastDate = new Date('2023-01-01T11:30:00Z'); // 30 minutes ago
|
||||
|
||||
dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any);
|
||||
|
||||
// Act
|
||||
const result = TokenService.getExpirationInfo(pastDate);
|
||||
|
||||
// Assert
|
||||
expect(result.expired).toBe(true);
|
||||
expect(result.timeLeft).toContain('Expired');
|
||||
expect(result.timeLeft).toContain('minute(s) ago');
|
||||
|
||||
// Cleanup
|
||||
dateSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
import { ValidationMiddleware } from '../../../src/Application/Services/ValidationMiddleware';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ErrorResponseService } from '../../../src/Application/Services/ErrorResponseService';
|
||||
|
||||
jest.mock('../../../src/Application/Services/ErrorResponseService');
|
||||
jest.mock('../../../src/Application/Services/Logger');
|
||||
|
||||
describe('ValidationMiddleware', () => {
|
||||
let req: Partial<Request>;
|
||||
let res: Partial<Response>;
|
||||
let next: NextFunction;
|
||||
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
body: {},
|
||||
params: {},
|
||||
query: {},
|
||||
path: '/test'
|
||||
};
|
||||
res = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn().mockReturnThis()
|
||||
};
|
||||
next = jest.fn();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('validateRequiredFields', () => {
|
||||
it('should pass validation when all required fields are present', () => {
|
||||
req.body = { username: 'testuser', email: 'test@example.com' };
|
||||
|
||||
const middleware = ValidationMiddleware.validateRequiredFields(['username', 'email']);
|
||||
middleware(req as Request, res as Response, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith();
|
||||
expect(ErrorResponseService.sendBadRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail validation when required fields are missing', () => {
|
||||
req.body = { username: 'testuser' }; // missing email
|
||||
|
||||
const middleware = ValidationMiddleware.validateRequiredFields(['username', 'email']);
|
||||
middleware(req as Request, res as Response, next);
|
||||
|
||||
expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith(
|
||||
res,
|
||||
'Missing required fields',
|
||||
{ missingFields: ['email'] }
|
||||
);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail validation when fields are empty strings', () => {
|
||||
req.body = { username: '', email: 'test@example.com' };
|
||||
|
||||
const middleware = ValidationMiddleware.validateRequiredFields(['username', 'email']);
|
||||
middleware(req as Request, res as Response, next);
|
||||
|
||||
expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith(
|
||||
res,
|
||||
'Missing required fields',
|
||||
{ missingFields: ['username'] }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEmailFormat', () => {
|
||||
it('should pass validation for valid email', () => {
|
||||
req.body = { email: 'test@example.com' };
|
||||
|
||||
const middleware = ValidationMiddleware.validateEmailFormat(['email']);
|
||||
middleware(req as Request, res as Response, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith();
|
||||
expect(ErrorResponseService.sendBadRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail validation for invalid email', () => {
|
||||
req.body = { email: 'invalid-email' };
|
||||
|
||||
const middleware = ValidationMiddleware.validateEmailFormat(['email']);
|
||||
middleware(req as Request, res as Response, next);
|
||||
|
||||
expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith(
|
||||
res,
|
||||
'Email format validation failed',
|
||||
{ errors: ["Field 'email' must contain a valid email address"] }
|
||||
);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateUUIDFormat', () => {
|
||||
it('should pass validation for valid UUID', () => {
|
||||
req.params = { userId: '123e4567-e89b-12d3-a456-426614174000' };
|
||||
|
||||
const middleware = ValidationMiddleware.validateUUIDFormat(['userId']);
|
||||
middleware(req as Request, res as Response, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith();
|
||||
expect(ErrorResponseService.sendBadRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail validation for invalid UUID', () => {
|
||||
req.params = { userId: 'invalid-uuid' };
|
||||
|
||||
const middleware = ValidationMiddleware.validateUUIDFormat(['userId']);
|
||||
middleware(req as Request, res as Response, next);
|
||||
|
||||
expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith(
|
||||
res,
|
||||
'UUID format validation failed',
|
||||
{ errors: ["Field 'userId' must contain a valid UUID"] }
|
||||
);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateStringLength', () => {
|
||||
it('should pass validation for strings within length constraints', () => {
|
||||
req.body = { username: 'testuser', password: 'password123' };
|
||||
|
||||
const middleware = ValidationMiddleware.validateStringLength({
|
||||
username: { min: 3, max: 20 },
|
||||
password: { min: 8, max: 50 }
|
||||
});
|
||||
middleware(req as Request, res as Response, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith();
|
||||
expect(ErrorResponseService.sendBadRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail validation for strings that are too short', () => {
|
||||
req.body = { username: 'ab' }; // too short (min 3)
|
||||
|
||||
const middleware = ValidationMiddleware.validateStringLength({
|
||||
username: { min: 3, max: 20 }
|
||||
});
|
||||
middleware(req as Request, res as Response, next);
|
||||
|
||||
expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith(
|
||||
res,
|
||||
'String length validation failed',
|
||||
{ errors: ["Field 'username' must be at least 3 characters"] }
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail validation for strings that are too long', () => {
|
||||
req.body = { username: 'a'.repeat(25) }; // too long (max 20)
|
||||
|
||||
const middleware = ValidationMiddleware.validateStringLength({
|
||||
username: { min: 3, max: 20 }
|
||||
});
|
||||
middleware(req as Request, res as Response, next);
|
||||
|
||||
expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith(
|
||||
res,
|
||||
'String length validation failed',
|
||||
{ errors: ["Field 'username' must not exceed 20 characters"] }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('combine', () => {
|
||||
it('should run all validations in sequence and pass if all succeed', (done) => {
|
||||
req.body = { username: 'testuser', email: 'test@example.com' };
|
||||
|
||||
const nextSpy = jest.fn(() => {
|
||||
try {
|
||||
expect(nextSpy).toHaveBeenCalledWith();
|
||||
expect(ErrorResponseService.sendBadRequest).not.toHaveBeenCalled();
|
||||
done();
|
||||
} catch (error) {
|
||||
done(error);
|
||||
}
|
||||
});
|
||||
|
||||
const combinedMiddleware = ValidationMiddleware.combine([
|
||||
ValidationMiddleware.validateRequiredFields(['username', 'email']),
|
||||
ValidationMiddleware.validateEmailFormat(['email']),
|
||||
ValidationMiddleware.validateStringLength({ username: { min: 3, max: 20 } })
|
||||
]);
|
||||
|
||||
combinedMiddleware(req as Request, res as Response, nextSpy);
|
||||
});
|
||||
|
||||
it('should stop at first validation failure', () => {
|
||||
req.body = { username: 'testuser' }; // missing email
|
||||
|
||||
const combinedMiddleware = ValidationMiddleware.combine([
|
||||
ValidationMiddleware.validateRequiredFields(['username', 'email']),
|
||||
ValidationMiddleware.validateEmailFormat(['email']), // this won't run
|
||||
ValidationMiddleware.validateStringLength({ username: { min: 3, max: 20 } }) // this won't run
|
||||
]);
|
||||
|
||||
combinedMiddleware(req as Request, res as Response, next);
|
||||
|
||||
expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith(
|
||||
res,
|
||||
'Missing required fields',
|
||||
{ missingFields: ['email'] }
|
||||
);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
// Comprehensive test coverage for User Command Handlers
|
||||
import { CreateUserCommand } from '../../../../src/Application/User/commands/CreateUserCommand';
|
||||
import { CreateUserCommandHandler } from '../../../../src/Application/User/commands/CreateUserCommandHandler';
|
||||
import { LoginCommand } from '../../../../src/Application/User/commands/LoginCommand';
|
||||
import { LoginCommandHandler } from '../../../../src/Application/User/commands/LoginCommandHandler';
|
||||
import { UpdateUserCommand } from '../../../../src/Application/User/commands/UpdateUserCommand';
|
||||
import { UpdateUserCommandHandler } from '../../../../src/Application/User/commands/UpdateUserCommandHandler';
|
||||
import { DeactivateUserCommand } from '../../../../src/Application/User/commands/DeactivateUserCommand';
|
||||
import { DeactivateUserCommandHandler } from '../../../../src/Application/User/commands/DeactivateUserCommandHandler';
|
||||
import { IUserRepository } from '../../../../src/Domain/IRepository/IUserRepository';
|
||||
import { IOrganizationRepository } from '../../../../src/Domain/IRepository/IOrganizationRepository';
|
||||
import { JWTService } from '../../../../src/Application/Services/JWTService';
|
||||
import { PasswordService } from '../../../../src/Application/Services/PasswordService';
|
||||
import { UserState } from '../../../../src/Domain/User/UserAggregate';
|
||||
import {
|
||||
createMockUser,
|
||||
createMockUserRepository,
|
||||
createMockOrganizationRepository,
|
||||
createMockJWTService
|
||||
} from '../../../testUtils';
|
||||
|
||||
// Mock PasswordService static methods
|
||||
jest.mock('../../../../src/Application/Services/PasswordService', () => ({
|
||||
PasswordService: {
|
||||
validatePasswordStrength: jest.fn().mockReturnValue({ isValid: true, errors: [] }),
|
||||
hashPassword: jest.fn().mockResolvedValue('hashed-password'),
|
||||
verifyPassword: jest.fn().mockResolvedValue(true)
|
||||
}
|
||||
}));
|
||||
|
||||
describe('User Command Handlers - Comprehensive Coverage', () => {
|
||||
describe('CreateUserCommandHandler', () => {
|
||||
let mockUserRepository: jest.Mocked<IUserRepository>;
|
||||
let handler: CreateUserCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserRepository = createMockUserRepository();
|
||||
handler = new CreateUserCommandHandler(mockUserRepository);
|
||||
});
|
||||
|
||||
it('should create a new user successfully', async () => {
|
||||
// Arrange
|
||||
const command: CreateUserCommand = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'Password123!', // Strong password
|
||||
fname: 'Test',
|
||||
lname: 'User',
|
||||
type: 'regular'
|
||||
};
|
||||
|
||||
const mockUser = createMockUser({
|
||||
username: command.username,
|
||||
email: command.email,
|
||||
state: UserState.REGISTERED_NOT_VERIFIED
|
||||
});
|
||||
|
||||
// CreateUserCommandHandler doesn't check existing users - goes directly to create
|
||||
mockUserRepository.create.mockResolvedValue(mockUser);
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
// CreateUserCommandHandler doesn't call findByUsername/findByEmail
|
||||
expect(mockUserRepository.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when username already exists', async () => {
|
||||
// Arrange
|
||||
const command: CreateUserCommand = {
|
||||
username: 'existinguser',
|
||||
email: 'test@example.com',
|
||||
password: 'Password123!', // Strong password
|
||||
fname: 'Test',
|
||||
lname: 'User',
|
||||
type: 'regular'
|
||||
};
|
||||
|
||||
// Simulate database constraint error for duplicate username
|
||||
mockUserRepository.create.mockRejectedValue(new Error('duplicate key value violates unique constraint'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('User with this username or email already exists');
|
||||
});
|
||||
|
||||
it('should throw error when email already exists', async () => {
|
||||
// Arrange
|
||||
const command: CreateUserCommand = {
|
||||
username: 'testuser',
|
||||
email: 'existing@example.com',
|
||||
password: 'Password123!', // Strong password
|
||||
fname: 'Test',
|
||||
lname: 'User',
|
||||
type: 'regular'
|
||||
};
|
||||
|
||||
// Simulate database constraint error for duplicate email
|
||||
mockUserRepository.create.mockRejectedValue(new Error('unique constraint violation'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('User with this username or email already exists');
|
||||
});
|
||||
|
||||
it('should handle repository errors', async () => {
|
||||
// Arrange
|
||||
const command: CreateUserCommand = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'Password123!', // Strong password
|
||||
fname: 'Test',
|
||||
lname: 'User',
|
||||
type: 'regular'
|
||||
};
|
||||
|
||||
mockUserRepository.findByUsername.mockResolvedValue(null);
|
||||
mockUserRepository.findByEmail.mockResolvedValue(null);
|
||||
mockUserRepository.create.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Failed to create user');
|
||||
});
|
||||
});
|
||||
|
||||
describe('LoginCommandHandler', () => {
|
||||
let mockUserRepository: jest.Mocked<IUserRepository>;
|
||||
let mockOrgRepository: jest.Mocked<IOrganizationRepository>;
|
||||
let mockJwtService: jest.Mocked<JWTService>;
|
||||
let handler: LoginCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserRepository = createMockUserRepository();
|
||||
mockOrgRepository = createMockOrganizationRepository();
|
||||
mockJwtService = createMockJWTService();
|
||||
handler = new LoginCommandHandler(mockUserRepository, mockJwtService, mockOrgRepository);
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Set default PasswordService behavior
|
||||
const mockPasswordService = PasswordService as jest.Mocked<typeof PasswordService>;
|
||||
mockPasswordService.verifyPassword.mockResolvedValue(true); // Default to valid password
|
||||
});
|
||||
|
||||
it('should login user with valid credentials', async () => {
|
||||
// Arrange
|
||||
const command: LoginCommand = {
|
||||
username: 'testuser',
|
||||
password: 'Password123!'
|
||||
};
|
||||
|
||||
const mockUser = createMockUser({
|
||||
username: command.username,
|
||||
state: UserState.VERIFIED_REGULAR
|
||||
});
|
||||
|
||||
mockUserRepository.findByUsername.mockResolvedValue(mockUser);
|
||||
mockJwtService.create.mockReturnValue('jwt-token');
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.token).toBe('jwt-token');
|
||||
expect(mockJwtService.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle user not found', async () => {
|
||||
// Arrange
|
||||
const command: LoginCommand = {
|
||||
username: 'nonexistent',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
mockUserRepository.findByUsername.mockResolvedValue(null);
|
||||
|
||||
// Act & Assert
|
||||
const result = await handler.execute(command);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle invalid password', async () => {
|
||||
// Arrange
|
||||
const command: LoginCommand = {
|
||||
username: 'testuser',
|
||||
password: 'wrongpassword'
|
||||
};
|
||||
|
||||
const mockUser = createMockUser({
|
||||
username: command.username,
|
||||
password: 'hashedpassword'
|
||||
});
|
||||
|
||||
mockUserRepository.findByUsername.mockResolvedValue(mockUser);
|
||||
|
||||
// Mock password verification to return false for wrong password
|
||||
const mockPasswordService = PasswordService as jest.Mocked<typeof PasswordService>;
|
||||
mockPasswordService.verifyPassword.mockResolvedValue(false);
|
||||
|
||||
// Act & Assert
|
||||
const result = await handler.execute(command);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle unverified user', async () => {
|
||||
// Arrange - LoginCommandHandler doesn't reject unverified users, it processes them normally
|
||||
const command: LoginCommand = {
|
||||
username: 'testuser',
|
||||
password: 'Password123!'
|
||||
};
|
||||
|
||||
const mockUser = createMockUser({
|
||||
username: command.username,
|
||||
password: 'hashedpassword',
|
||||
state: UserState.REGISTERED_NOT_VERIFIED
|
||||
});
|
||||
|
||||
mockUserRepository.findByUsername.mockResolvedValue(mockUser);
|
||||
mockJwtService.create.mockReturnValue('jwt-token');
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert - LoginCommandHandler processes unverified users normally
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.user).toBeDefined();
|
||||
expect(result!.token).toBe('jwt-token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdateUserCommandHandler', () => {
|
||||
let mockUserRepository: jest.Mocked<IUserRepository>;
|
||||
let handler: UpdateUserCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserRepository = createMockUserRepository();
|
||||
handler = new UpdateUserCommandHandler(mockUserRepository);
|
||||
});
|
||||
|
||||
it('should update user successfully', async () => {
|
||||
// Arrange
|
||||
const command: UpdateUserCommand = {
|
||||
id: 'user-123',
|
||||
email: 'newemail@example.com'
|
||||
};
|
||||
|
||||
const existingUser = createMockUser({ id: command.id });
|
||||
const updatedUser = createMockUser({
|
||||
id: command.id,
|
||||
email: command.email
|
||||
});
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(existingUser);
|
||||
mockUserRepository.update.mockResolvedValue(updatedUser);
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
expect(mockUserRepository.update).toHaveBeenCalledWith(command.id, expect.any(Object));
|
||||
});
|
||||
|
||||
it('should return null when user not found', async () => {
|
||||
// Arrange
|
||||
const command: UpdateUserCommand = {
|
||||
id: 'nonexistent-user',
|
||||
email: 'newemail@example.com'
|
||||
};
|
||||
|
||||
mockUserRepository.update.mockResolvedValue(null); // UpdateUserCommandHandler calls update directly, not findById first
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
expect(mockUserRepository.update).toHaveBeenCalledWith(command.id, expect.any(Object));
|
||||
});
|
||||
|
||||
it('should handle partial updates', async () => {
|
||||
// Arrange
|
||||
const command: UpdateUserCommand = {
|
||||
id: 'user-123',
|
||||
username: 'newusername'
|
||||
};
|
||||
|
||||
const existingUser = createMockUser({ id: command.id });
|
||||
const updatedUser = createMockUser({
|
||||
id: command.id,
|
||||
username: command.username
|
||||
});
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(existingUser);
|
||||
mockUserRepository.update.mockResolvedValue(updatedUser);
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeactivateUserCommandHandler', () => {
|
||||
let mockUserRepository: jest.Mocked<IUserRepository>;
|
||||
let handler: DeactivateUserCommandHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserRepository = createMockUserRepository();
|
||||
handler = new DeactivateUserCommandHandler(mockUserRepository);
|
||||
});
|
||||
|
||||
it('should deactivate user successfully', async () => {
|
||||
// Arrange
|
||||
const command: DeactivateUserCommand = {
|
||||
id: 'user-123'
|
||||
};
|
||||
|
||||
const deactivatedUser = createMockUser({
|
||||
id: command.id,
|
||||
state: UserState.DEACTIVATED
|
||||
});
|
||||
|
||||
mockUserRepository.deactivate.mockResolvedValue(deactivatedUser);
|
||||
|
||||
// Act
|
||||
const result = await handler.execute(command);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(mockUserRepository.deactivate).toHaveBeenCalledWith(command.id);
|
||||
});
|
||||
|
||||
it('should handle repository errors', async () => {
|
||||
// Arrange
|
||||
const command: DeactivateUserCommand = {
|
||||
id: 'user-123'
|
||||
};
|
||||
|
||||
mockUserRepository.deactivate.mockRejectedValue(new Error('Deactivation failed'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.execute(command)).rejects.toThrow('Deactivation failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cross-Command Integration Tests', () => {
|
||||
let mockUserRepository: jest.Mocked<IUserRepository>;
|
||||
let mockOrgRepository: jest.Mocked<IOrganizationRepository>;
|
||||
let mockJwtService: jest.Mocked<JWTService>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserRepository = createMockUserRepository();
|
||||
mockOrgRepository = createMockOrganizationRepository();
|
||||
mockJwtService = createMockJWTService();
|
||||
});
|
||||
|
||||
it('should create user and then login', async () => {
|
||||
// Arrange
|
||||
const createHandler = new CreateUserCommandHandler(mockUserRepository);
|
||||
const loginHandler = new LoginCommandHandler(mockUserRepository, mockJwtService, mockOrgRepository);
|
||||
|
||||
const createCommand: CreateUserCommand = {
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'Password123!', // Strong password
|
||||
fname: 'Test',
|
||||
lname: 'User',
|
||||
type: 'regular'
|
||||
};
|
||||
|
||||
const loginCommand: LoginCommand = {
|
||||
username: 'testuser',
|
||||
password: 'Password123!' // Strong password
|
||||
};
|
||||
|
||||
const mockUser = createMockUser({
|
||||
username: createCommand.username,
|
||||
email: createCommand.email,
|
||||
state: UserState.VERIFIED_REGULAR
|
||||
});
|
||||
|
||||
// Mock create user flow
|
||||
mockUserRepository.findByUsername.mockResolvedValueOnce(null);
|
||||
mockUserRepository.findByEmail.mockResolvedValue(null);
|
||||
mockUserRepository.create.mockResolvedValue(mockUser);
|
||||
|
||||
// Mock login flow
|
||||
mockUserRepository.findByUsername.mockResolvedValueOnce(mockUser);
|
||||
mockJwtService.create.mockReturnValue('jwt-token');
|
||||
|
||||
// Act
|
||||
const createResult = await createHandler.execute(createCommand);
|
||||
const loginResult = await loginHandler.execute(loginCommand);
|
||||
|
||||
// Assert
|
||||
expect(createResult).toBeDefined();
|
||||
expect(loginResult).toBeDefined();
|
||||
});
|
||||
|
||||
it('should update user after creation', async () => {
|
||||
// Arrange
|
||||
const updateHandler = new UpdateUserCommandHandler(mockUserRepository);
|
||||
|
||||
const updateCommand: UpdateUserCommand = {
|
||||
id: 'user-123',
|
||||
email: 'updated@example.com'
|
||||
};
|
||||
|
||||
const existingUser = createMockUser({ id: updateCommand.id });
|
||||
const updatedUser = createMockUser({
|
||||
id: updateCommand.id,
|
||||
email: updateCommand.email
|
||||
});
|
||||
|
||||
mockUserRepository.findById.mockResolvedValue(existingUser);
|
||||
mockUserRepository.update.mockResolvedValue(updatedUser);
|
||||
|
||||
// Act
|
||||
const result = await updateHandler.execute(updateCommand);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,286 @@
|
||||
// Comprehensive test coverage for Repository layer
|
||||
import { IUserRepository } from '../src/Domain/IRepository/IUserRepository';
|
||||
import { IDeckRepository } from '../src/Domain/IRepository/IDeckRepository';
|
||||
import { IOrganizationRepository } from '../src/Domain/IRepository/IOrganizationRepository';
|
||||
import { IContactRepository } from '../src/Domain/IRepository/IContactRepository';
|
||||
import { UserAggregate, UserState } from '../src/Domain/User/UserAggregate';
|
||||
import { DeckAggregate, Type as DeckType } from '../src/Domain/Deck/DeckAggregate';
|
||||
import { OrganizationAggregate } from '../src/Domain/Organization/OrganizationAggregate';
|
||||
import { ContactAggregate } from '../src/Domain/Contact/ContactAggregate';
|
||||
import {
|
||||
createMockUser,
|
||||
createMockDeck,
|
||||
createMockOrganization,
|
||||
createMockContact,
|
||||
createMockUserRepository,
|
||||
createMockDeckRepository,
|
||||
createMockOrganizationRepository,
|
||||
createMockContactRepository
|
||||
} from './testUtils';
|
||||
|
||||
describe('Repository Layer - Comprehensive Coverage', () => {
|
||||
describe('IUserRepository Interface Coverage', () => {
|
||||
let mockUserRepository: jest.Mocked<IUserRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserRepository = createMockUserRepository();
|
||||
});
|
||||
|
||||
it('should implement all required methods', () => {
|
||||
expect(mockUserRepository.create).toBeDefined();
|
||||
expect(mockUserRepository.findByPage).toBeDefined();
|
||||
expect(mockUserRepository.findByPageIncludingDeleted).toBeDefined();
|
||||
expect(mockUserRepository.findById).toBeDefined();
|
||||
expect(mockUserRepository.findByIdIncludingDeleted).toBeDefined();
|
||||
expect(mockUserRepository.findByUsername).toBeDefined();
|
||||
expect(mockUserRepository.findByEmail).toBeDefined();
|
||||
expect(mockUserRepository.findByToken).toBeDefined();
|
||||
expect(mockUserRepository.search).toBeDefined();
|
||||
expect(mockUserRepository.searchIncludingDeleted).toBeDefined();
|
||||
expect(mockUserRepository.update).toBeDefined();
|
||||
expect(mockUserRepository.delete).toBeDefined();
|
||||
expect(mockUserRepository.softDelete).toBeDefined();
|
||||
expect(mockUserRepository.deactivate).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle user creation', async () => {
|
||||
const userData = { username: 'testuser', email: 'test@example.com' };
|
||||
const mockUser = createMockUser(userData);
|
||||
mockUserRepository.create.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await mockUserRepository.create(userData);
|
||||
expect(result).toEqual(mockUser);
|
||||
expect(mockUserRepository.create).toHaveBeenCalledWith(userData);
|
||||
});
|
||||
|
||||
it('should handle paginated user retrieval', async () => {
|
||||
const mockUsers = [createMockUser(), createMockUser({ id: 'user2' })];
|
||||
mockUserRepository.findByPage.mockResolvedValue({ users: mockUsers, totalCount: 2 });
|
||||
|
||||
const result = await mockUserRepository.findByPage(0, 10);
|
||||
expect(result.users).toHaveLength(2);
|
||||
expect(result.totalCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle user search operations', async () => {
|
||||
const mockUsers = [createMockUser({ username: 'searchtest' })];
|
||||
mockUserRepository.search.mockResolvedValue({ users: mockUsers, totalCount: 1 });
|
||||
|
||||
const result = await mockUserRepository.search('searchtest');
|
||||
expect(result.users).toHaveLength(1);
|
||||
expect(result.users[0].username).toBe('searchtest');
|
||||
});
|
||||
|
||||
it('should handle user state transitions', async () => {
|
||||
const mockUser = createMockUser({ state: UserState.VERIFIED_REGULAR });
|
||||
mockUserRepository.deactivate.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await mockUserRepository.deactivate('user-id');
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
});
|
||||
|
||||
describe('IDeckRepository Interface Coverage', () => {
|
||||
let mockDeckRepository: jest.Mocked<IDeckRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockDeckRepository = createMockDeckRepository();
|
||||
});
|
||||
|
||||
it('should implement all required methods including new ones', () => {
|
||||
expect(mockDeckRepository.create).toBeDefined();
|
||||
expect(mockDeckRepository.findByPage).toBeDefined();
|
||||
expect(mockDeckRepository.findByPageIncludingDeleted).toBeDefined();
|
||||
expect(mockDeckRepository.findById).toBeDefined();
|
||||
expect(mockDeckRepository.findByIdIncludingDeleted).toBeDefined();
|
||||
expect(mockDeckRepository.search).toBeDefined();
|
||||
expect(mockDeckRepository.searchIncludingDeleted).toBeDefined();
|
||||
expect(mockDeckRepository.update).toBeDefined();
|
||||
expect(mockDeckRepository.delete).toBeDefined();
|
||||
expect(mockDeckRepository.softDelete).toBeDefined();
|
||||
expect(mockDeckRepository.countActiveByUserId).toBeDefined();
|
||||
expect(mockDeckRepository.countOrganizationalByUserId).toBeDefined();
|
||||
expect(mockDeckRepository.findFilteredDecks).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle deck counting operations', async () => {
|
||||
mockDeckRepository.countActiveByUserId.mockResolvedValue(5);
|
||||
mockDeckRepository.countOrganizationalByUserId.mockResolvedValue(3);
|
||||
|
||||
const activeCount = await mockDeckRepository.countActiveByUserId('user-id');
|
||||
const orgCount = await mockDeckRepository.countOrganizationalByUserId('user-id');
|
||||
|
||||
expect(activeCount).toBe(5);
|
||||
expect(orgCount).toBe(3);
|
||||
});
|
||||
|
||||
it('should handle filtered deck retrieval', async () => {
|
||||
const mockDecks = [createMockDeck(), createMockDeck({ id: 'deck2' })];
|
||||
mockDeckRepository.findFilteredDecks.mockResolvedValue({ decks: mockDecks, totalCount: 2 });
|
||||
|
||||
const result = await mockDeckRepository.findFilteredDecks('user-id', 'org-id', false, 0, 10);
|
||||
expect(result.decks).toHaveLength(2);
|
||||
expect(result.totalCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle different deck types', async () => {
|
||||
const jokerDeck = createMockDeck({ type: DeckType.JOKER });
|
||||
const luckDeck = createMockDeck({ type: DeckType.LUCK });
|
||||
const questionDeck = createMockDeck({ type: DeckType.QUESTION });
|
||||
|
||||
mockDeckRepository.create.mockResolvedValueOnce(jokerDeck);
|
||||
mockDeckRepository.create.mockResolvedValueOnce(luckDeck);
|
||||
mockDeckRepository.create.mockResolvedValueOnce(questionDeck);
|
||||
|
||||
const result1 = await mockDeckRepository.create({ type: DeckType.JOKER });
|
||||
const result2 = await mockDeckRepository.create({ type: DeckType.LUCK });
|
||||
const result3 = await mockDeckRepository.create({ type: DeckType.QUESTION });
|
||||
|
||||
expect(result1.type).toBe(DeckType.JOKER);
|
||||
expect(result2.type).toBe(DeckType.LUCK);
|
||||
expect(result3.type).toBe(DeckType.QUESTION);
|
||||
});
|
||||
});
|
||||
|
||||
describe('IOrganizationRepository Interface Coverage', () => {
|
||||
let mockOrgRepository: jest.Mocked<IOrganizationRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockOrgRepository = createMockOrganizationRepository();
|
||||
});
|
||||
|
||||
it('should implement all required methods', () => {
|
||||
expect(mockOrgRepository.create).toBeDefined();
|
||||
expect(mockOrgRepository.findByPage).toBeDefined();
|
||||
expect(mockOrgRepository.findByPageIncludingDeleted).toBeDefined();
|
||||
expect(mockOrgRepository.findById).toBeDefined();
|
||||
expect(mockOrgRepository.findByIdIncludingDeleted).toBeDefined();
|
||||
expect(mockOrgRepository.search).toBeDefined();
|
||||
expect(mockOrgRepository.searchIncludingDeleted).toBeDefined();
|
||||
expect(mockOrgRepository.update).toBeDefined();
|
||||
expect(mockOrgRepository.delete).toBeDefined();
|
||||
expect(mockOrgRepository.softDelete).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle organization CRUD operations', async () => {
|
||||
const orgData = { name: 'Test Org', contactemail: 'test@org.com' };
|
||||
const mockOrg = createMockOrganization(orgData);
|
||||
|
||||
mockOrgRepository.create.mockResolvedValue(mockOrg);
|
||||
mockOrgRepository.findById.mockResolvedValue(mockOrg);
|
||||
mockOrgRepository.update.mockResolvedValue(mockOrg);
|
||||
mockOrgRepository.softDelete.mockResolvedValue(mockOrg);
|
||||
|
||||
const created = await mockOrgRepository.create(orgData);
|
||||
const found = await mockOrgRepository.findById('org-id');
|
||||
const updated = await mockOrgRepository.update('org-id', { name: 'Updated Org' });
|
||||
const deleted = await mockOrgRepository.softDelete('org-id');
|
||||
|
||||
expect(created.name).toBe('Test Org');
|
||||
expect(found).toEqual(mockOrg);
|
||||
expect(updated).toEqual(mockOrg);
|
||||
expect(deleted).toEqual(mockOrg);
|
||||
});
|
||||
});
|
||||
|
||||
describe('IContactRepository Interface Coverage', () => {
|
||||
let mockContactRepository: jest.Mocked<IContactRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContactRepository = createMockContactRepository();
|
||||
});
|
||||
|
||||
it('should implement all required methods', () => {
|
||||
expect(mockContactRepository.create).toBeDefined();
|
||||
expect(mockContactRepository.findById).toBeDefined();
|
||||
expect(mockContactRepository.findByPage).toBeDefined();
|
||||
expect(mockContactRepository.findByPageIncludingDeleted).toBeDefined();
|
||||
expect(mockContactRepository.findByIdIncludingDeleted).toBeDefined();
|
||||
expect(mockContactRepository.search).toBeDefined();
|
||||
expect(mockContactRepository.searchIncludingDeleted).toBeDefined();
|
||||
expect(mockContactRepository.update).toBeDefined();
|
||||
expect(mockContactRepository.delete).toBeDefined();
|
||||
expect(mockContactRepository.softDelete).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle contact search operations', async () => {
|
||||
const mockContacts = [createMockContact({ email: 'test@example.com' })];
|
||||
mockContactRepository.search.mockResolvedValue(mockContacts);
|
||||
mockContactRepository.searchIncludingDeleted.mockResolvedValue(mockContacts);
|
||||
|
||||
const activeResults = await mockContactRepository.search('test');
|
||||
const allResults = await mockContactRepository.searchIncludingDeleted('test');
|
||||
|
||||
expect(activeResults).toHaveLength(1);
|
||||
expect(allResults).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should handle contact lifecycle', async () => {
|
||||
const contactData = { email: 'user@example.com', message: 'Help Request' };
|
||||
const mockContact = createMockContact(contactData);
|
||||
|
||||
mockContactRepository.create.mockResolvedValue(mockContact);
|
||||
mockContactRepository.findById.mockResolvedValue(mockContact);
|
||||
mockContactRepository.findByIdIncludingDeleted.mockResolvedValue(mockContact);
|
||||
|
||||
const created = await mockContactRepository.create(contactData);
|
||||
const found = await mockContactRepository.findById('contact-id');
|
||||
const foundWithDeleted = await mockContactRepository.findByIdIncludingDeleted('contact-id');
|
||||
|
||||
expect(created.email).toBe('user@example.com');
|
||||
expect(found).toEqual(mockContact);
|
||||
expect(foundWithDeleted).toEqual(mockContact);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cross-Repository Integration Tests', () => {
|
||||
let userRepo: jest.Mocked<IUserRepository>;
|
||||
let deckRepo: jest.Mocked<IDeckRepository>;
|
||||
let orgRepo: jest.Mocked<IOrganizationRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
userRepo = createMockUserRepository();
|
||||
deckRepo = createMockDeckRepository();
|
||||
orgRepo = createMockOrganizationRepository();
|
||||
});
|
||||
|
||||
it('should simulate user-deck relationship operations', async () => {
|
||||
const mockUser = createMockUser({ id: 'user-123' });
|
||||
const mockDecks = [
|
||||
createMockDeck({ userid: 'user-123', name: 'Deck 1' }),
|
||||
createMockDeck({ userid: 'user-123', name: 'Deck 2' })
|
||||
];
|
||||
|
||||
userRepo.findById.mockResolvedValue(mockUser);
|
||||
deckRepo.findFilteredDecks.mockResolvedValue({ decks: mockDecks, totalCount: 2 });
|
||||
deckRepo.countActiveByUserId.mockResolvedValue(2);
|
||||
|
||||
const user = await userRepo.findById('user-123');
|
||||
const userDecks = await deckRepo.findFilteredDecks('user-123');
|
||||
const deckCount = await deckRepo.countActiveByUserId('user-123');
|
||||
|
||||
expect(user).toBeDefined();
|
||||
expect(userDecks.decks).toHaveLength(2);
|
||||
expect(deckCount).toBe(2);
|
||||
expect(userDecks.decks.every(deck => deck.userid === 'user-123')).toBe(true);
|
||||
});
|
||||
|
||||
it('should simulate organization-user relationship operations', async () => {
|
||||
const mockOrg = createMockOrganization({ id: 'org-123', name: 'Test Organization' });
|
||||
const mockUsers = [
|
||||
createMockUser({ orgid: 'org-123' }),
|
||||
createMockUser({ orgid: 'org-123', id: 'user-2' })
|
||||
];
|
||||
|
||||
orgRepo.findById.mockResolvedValue(mockOrg);
|
||||
userRepo.findByPage.mockResolvedValue({ users: mockUsers, totalCount: 2 });
|
||||
|
||||
const org = await orgRepo.findById('org-123');
|
||||
const orgUsers = await userRepo.findByPage(0, 10);
|
||||
|
||||
expect(org).toBeDefined();
|
||||
expect(orgUsers.users).toHaveLength(2);
|
||||
expect(orgUsers.users.every(user => user.orgid === 'org-123')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
// Set the NODE_ENV to test for all Jest tests
|
||||
process.env.NODE_ENV = 'test';
|
||||
@@ -0,0 +1,26 @@
|
||||
// Jest test setup file
|
||||
import { jest } from '@jest/globals';
|
||||
import { LoggingService } from '../src/Application/Services/LoggingService';
|
||||
|
||||
// Mock environment variables
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.JWT_SECRET = 'test-jwt-secret';
|
||||
process.env.EMAIL_HOST = 'test.smtp.com';
|
||||
process.env.EMAIL_PORT = '587';
|
||||
process.env.EMAIL_USER = 'test@example.com';
|
||||
process.env.EMAIL_PASS = 'testpass';
|
||||
process.env.EMAIL_FROM = 'test@example.com';
|
||||
process.env.APP_BASE_URL = 'http://localhost:3000';
|
||||
|
||||
// Global test timeout
|
||||
jest.setTimeout(10000);
|
||||
|
||||
// Global cleanup to prevent Jest from hanging
|
||||
afterAll(async () => {
|
||||
try {
|
||||
await LoggingService.getInstance().shutdown();
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors in tests
|
||||
console.log('Test cleanup completed');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
import { UserAggregate, UserState } from '../src/Domain/User/UserAggregate';
|
||||
import { OrganizationAggregate, OrganizationState } from '../src/Domain/Organization/OrganizationAggregate';
|
||||
import { DeckAggregate, State as DeckState, Type as DeckType, CType } from '../src/Domain/Deck/DeckAggregate';
|
||||
import { ContactAggregate, ContactState, ContactType } from '../src/Domain/Contact/ContactAggregate';
|
||||
import { IUserRepository } from '../src/Domain/IRepository/IUserRepository';
|
||||
import { IOrganizationRepository } from '../src/Domain/IRepository/IOrganizationRepository';
|
||||
import { IDeckRepository } from '../src/Domain/IRepository/IDeckRepository';
|
||||
import { IContactRepository } from '../src/Domain/IRepository/IContactRepository';
|
||||
|
||||
export const createMockUser = (overrides: Partial<UserAggregate> = {}): UserAggregate => ({
|
||||
id: '123e4567-e89b-12d3-a456-426614174000',
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
password: 'hashedPassword',
|
||||
fname: 'Test',
|
||||
lname: 'User',
|
||||
orgid: null,
|
||||
token: null,
|
||||
TokenExpires: null,
|
||||
type: 'regular',
|
||||
phone: null,
|
||||
state: UserState.REGISTERED_NOT_VERIFIED,
|
||||
regdate: new Date('2025-01-01'),
|
||||
updatedate: new Date('2025-01-01'),
|
||||
Orglogindate: null,
|
||||
...overrides
|
||||
});
|
||||
|
||||
export const createMockOrganization = (overrides: Partial<OrganizationAggregate> = {}): OrganizationAggregate => ({
|
||||
id: '123e4567-e89b-12d3-a456-426614174001',
|
||||
name: 'Test Organization',
|
||||
contactfname: 'John',
|
||||
contactlname: 'Doe',
|
||||
contactphone: '+1234567890',
|
||||
contactemail: 'contact@testorg.com',
|
||||
state: OrganizationState.ACTIVE,
|
||||
regdate: new Date('2025-01-01'),
|
||||
updatedate: new Date('2025-01-01'),
|
||||
url: null,
|
||||
userinorg: 0,
|
||||
maxOrganizationalDecks: 10,
|
||||
users: [],
|
||||
...overrides
|
||||
});
|
||||
|
||||
export const createMockDeck = (overrides: Partial<DeckAggregate> = {}): DeckAggregate => ({
|
||||
id: '123e4567-e89b-12d3-a456-426614174002',
|
||||
name: 'Test Deck',
|
||||
type: DeckType.JOKER,
|
||||
userid: '123e4567-e89b-12d3-a456-426614174000',
|
||||
creationdate: new Date('2025-01-01'),
|
||||
cards: [],
|
||||
playedNumber: 0,
|
||||
ctype: CType.PUBLIC,
|
||||
updatedate: new Date('2025-01-01'),
|
||||
state: DeckState.ACTIVE,
|
||||
organization: null,
|
||||
...overrides
|
||||
});
|
||||
|
||||
export const createMockContact = (overrides: Partial<ContactAggregate> = {}): ContactAggregate => ({
|
||||
id: '123e4567-e89b-12d3-a456-426614174003',
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
userid: '123e4567-e89b-12d3-a456-426614174000',
|
||||
type: ContactType.QUESTION,
|
||||
txt: 'This is a test contact message.',
|
||||
state: ContactState.ACTIVE,
|
||||
createDate: new Date('2025-01-01'),
|
||||
updateDate: new Date('2025-01-01'),
|
||||
adminResponse: null,
|
||||
responseDate: null,
|
||||
respondedBy: null,
|
||||
...overrides
|
||||
});
|
||||
|
||||
export const createMockDate = () => new Date('2025-01-01T00:00:00Z');
|
||||
|
||||
// Mock Repository Factory Functions
|
||||
export const createMockUserRepository = (): jest.Mocked<IUserRepository> => ({
|
||||
create: jest.fn(),
|
||||
findByPage: jest.fn(),
|
||||
findByPageIncludingDeleted: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
findByIdIncludingDeleted: jest.fn(),
|
||||
findByUsername: jest.fn(),
|
||||
findByEmail: jest.fn(),
|
||||
findByToken: jest.fn(),
|
||||
search: jest.fn(),
|
||||
searchIncludingDeleted: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
deactivate: jest.fn(),
|
||||
} as jest.Mocked<IUserRepository>);
|
||||
|
||||
export const createMockOrganizationRepository = (): jest.Mocked<IOrganizationRepository> => ({
|
||||
create: jest.fn(),
|
||||
findByPage: jest.fn(),
|
||||
findByPageIncludingDeleted: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
findByIdIncludingDeleted: jest.fn(),
|
||||
search: jest.fn(),
|
||||
searchIncludingDeleted: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
} as jest.Mocked<IOrganizationRepository>);
|
||||
|
||||
export const createMockDeckRepository = (): jest.Mocked<IDeckRepository> => ({
|
||||
create: jest.fn(),
|
||||
findByPage: jest.fn(),
|
||||
findByPageIncludingDeleted: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
findByIdIncludingDeleted: jest.fn(),
|
||||
search: jest.fn(),
|
||||
searchIncludingDeleted: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
countActiveByUserId: jest.fn(),
|
||||
countOrganizationalByUserId: jest.fn(),
|
||||
findFilteredDecks: jest.fn(),
|
||||
} as jest.Mocked<IDeckRepository>);
|
||||
|
||||
export const createMockContactRepository = (): jest.Mocked<IContactRepository> => ({
|
||||
create: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
findByPage: jest.fn(),
|
||||
findByPageIncludingDeleted: jest.fn(),
|
||||
findByIdIncludingDeleted: jest.fn(),
|
||||
search: jest.fn(),
|
||||
searchIncludingDeleted: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
} as jest.Mocked<IContactRepository>);
|
||||
|
||||
export const createMockJWTService = () => ({
|
||||
create: jest.fn(),
|
||||
verify: jest.fn(),
|
||||
shouldRefreshToken: jest.fn(),
|
||||
parseDuration: jest.fn(),
|
||||
} as any);
|
||||
|
||||
export const createMockTokenService = () => ({
|
||||
generateSecureToken: jest.fn(),
|
||||
generateVerificationToken: jest.fn(),
|
||||
generatePasswordResetToken: jest.fn(),
|
||||
isTokenExpired: jest.fn(),
|
||||
validateToken: jest.fn(),
|
||||
} as any);
|
||||
|
||||
export const createMockEmailService = () => ({
|
||||
sendEmail: jest.fn(),
|
||||
sendVerificationEmail: jest.fn(),
|
||||
sendPasswordResetEmail: jest.fn(),
|
||||
sendContactResponseEmail: jest.fn(),
|
||||
loadTemplate: jest.fn(),
|
||||
} as any);
|
||||
|
||||
export const createMockPasswordService = () => ({
|
||||
hashPassword: jest.fn(),
|
||||
verifyPassword: jest.fn(),
|
||||
validatePasswordStrength: jest.fn(),
|
||||
generateRandomPassword: jest.fn(),
|
||||
} as any);
|
||||
Reference in New Issue
Block a user