Files
SerpentRace/SerpentRace_Backend/tests/Application/Deck/commands/DeckCommandHandlers.comprehensive.test.ts

487 lines
16 KiB
TypeScript

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);
});
});
});