399 lines
12 KiB
TypeScript
399 lines
12 KiB
TypeScript
/**
|
|
* Application Use Case Tests: CastAdminVoteUseCase
|
|
*
|
|
* Tests for casting votes in admin vote sessions
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { CastAdminVoteUseCase } from './CastAdminVoteUseCase';
|
|
import { AdminVoteSessionRepository } from '../../domain/repositories/AdminVoteSessionRepository';
|
|
import { AdminVoteSession } from '../../domain/entities/AdminVoteSession';
|
|
|
|
// Mock repository
|
|
const createMockRepository = () => ({
|
|
save: vi.fn(),
|
|
findById: vi.fn(),
|
|
findActiveForAdmin: vi.fn(),
|
|
findByAdminAndLeague: vi.fn(),
|
|
findByLeague: vi.fn(),
|
|
findClosedUnprocessed: vi.fn(),
|
|
});
|
|
|
|
describe('CastAdminVoteUseCase', () => {
|
|
let useCase: CastAdminVoteUseCase;
|
|
let mockRepository: ReturnType<typeof createMockRepository>;
|
|
|
|
beforeEach(() => {
|
|
mockRepository = createMockRepository();
|
|
useCase = new CastAdminVoteUseCase(mockRepository);
|
|
});
|
|
|
|
describe('Input validation', () => {
|
|
it('should reject when voteSessionId is missing', async () => {
|
|
const result = await useCase.execute({
|
|
voteSessionId: '',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('voteSessionId is required');
|
|
});
|
|
|
|
it('should reject when voterId is missing', async () => {
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: '',
|
|
positive: true,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('voterId is required');
|
|
});
|
|
|
|
it('should reject when positive is not a boolean', async () => {
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: 'true' as any,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('positive must be a boolean value');
|
|
});
|
|
|
|
it('should reject when votedAt is not a valid date', async () => {
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
votedAt: 'invalid-date',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('votedAt must be a valid date if provided');
|
|
});
|
|
|
|
it('should accept valid input with all fields', async () => {
|
|
mockRepository.findById.mockResolvedValue({
|
|
id: 'session-123',
|
|
isVotingWindowOpen: vi.fn().mockReturnValue(true),
|
|
castVote: vi.fn(),
|
|
});
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
votedAt: '2024-01-01T00:00:00Z',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.errors).toBeUndefined();
|
|
});
|
|
|
|
it('should accept valid input without optional votedAt', async () => {
|
|
mockRepository.findById.mockResolvedValue({
|
|
id: 'session-123',
|
|
isVotingWindowOpen: vi.fn().mockReturnValue(true),
|
|
castVote: vi.fn(),
|
|
});
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.errors).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Session lookup', () => {
|
|
it('should reject when vote session is not found', async () => {
|
|
mockRepository.findById.mockResolvedValue(null);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'non-existent-session',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Vote session not found');
|
|
});
|
|
|
|
it('should find session by ID when provided', async () => {
|
|
const mockSession = {
|
|
id: 'session-123',
|
|
isVotingWindowOpen: vi.fn().mockReturnValue(true),
|
|
castVote: vi.fn(),
|
|
};
|
|
mockRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(mockRepository.findById).toHaveBeenCalledWith('session-123');
|
|
});
|
|
});
|
|
|
|
describe('Voting window validation', () => {
|
|
it('should reject when voting window is not open', async () => {
|
|
const mockSession = {
|
|
id: 'session-123',
|
|
isVotingWindowOpen: vi.fn().mockReturnValue(false),
|
|
castVote: vi.fn(),
|
|
};
|
|
mockRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Vote session is not open for voting');
|
|
expect(mockSession.isVotingWindowOpen).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should accept when voting window is open', async () => {
|
|
const mockSession = {
|
|
id: 'session-123',
|
|
isVotingWindowOpen: vi.fn().mockReturnValue(true),
|
|
castVote: vi.fn(),
|
|
};
|
|
mockRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockSession.isVotingWindowOpen).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should use current time when votedAt is not provided', async () => {
|
|
const mockSession = {
|
|
id: 'session-123',
|
|
isVotingWindowOpen: vi.fn().mockReturnValue(true),
|
|
castVote: vi.fn(),
|
|
};
|
|
mockRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(mockSession.isVotingWindowOpen).toHaveBeenCalledWith(expect.any(Date));
|
|
});
|
|
|
|
it('should use provided votedAt when available', async () => {
|
|
const mockSession = {
|
|
id: 'session-123',
|
|
isVotingWindowOpen: vi.fn().mockReturnValue(true),
|
|
castVote: vi.fn(),
|
|
};
|
|
mockRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const votedAt = new Date('2024-01-01T12:00:00Z');
|
|
await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
votedAt: votedAt.toISOString(),
|
|
});
|
|
|
|
expect(mockSession.isVotingWindowOpen).toHaveBeenCalledWith(votedAt);
|
|
});
|
|
});
|
|
|
|
describe('Vote casting', () => {
|
|
it('should cast positive vote when session is open', async () => {
|
|
const mockSession = {
|
|
id: 'session-123',
|
|
isVotingWindowOpen: vi.fn().mockReturnValue(true),
|
|
castVote: vi.fn(),
|
|
};
|
|
mockRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(mockSession.castVote).toHaveBeenCalledWith('voter-123', true, expect.any(Date));
|
|
});
|
|
|
|
it('should cast negative vote when session is open', async () => {
|
|
const mockSession = {
|
|
id: 'session-123',
|
|
isVotingWindowOpen: vi.fn().mockReturnValue(true),
|
|
castVote: vi.fn(),
|
|
};
|
|
mockRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: false,
|
|
});
|
|
|
|
expect(mockSession.castVote).toHaveBeenCalledWith('voter-123', false, expect.any(Date));
|
|
});
|
|
|
|
it('should save updated session after casting vote', async () => {
|
|
const mockSession = {
|
|
id: 'session-123',
|
|
isVotingWindowOpen: vi.fn().mockReturnValue(true),
|
|
castVote: vi.fn(),
|
|
};
|
|
mockRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(mockRepository.save).toHaveBeenCalledWith(mockSession);
|
|
});
|
|
|
|
it('should return success when vote is cast', async () => {
|
|
const mockSession = {
|
|
id: 'session-123',
|
|
isVotingWindowOpen: vi.fn().mockReturnValue(true),
|
|
castVote: vi.fn(),
|
|
};
|
|
mockRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.voteSessionId).toBe('session-123');
|
|
expect(result.voterId).toBe('voter-123');
|
|
expect(result.errors).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Error handling', () => {
|
|
it('should handle repository errors gracefully', async () => {
|
|
mockRepository.findById.mockRejectedValue(new Error('Database error'));
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Failed to cast vote: Database error');
|
|
});
|
|
|
|
it('should handle unexpected errors gracefully', async () => {
|
|
mockRepository.findById.mockRejectedValue('Unknown error');
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Failed to cast vote: Unknown error');
|
|
});
|
|
|
|
it('should handle save errors gracefully', async () => {
|
|
const mockSession = {
|
|
id: 'session-123',
|
|
isVotingWindowOpen: vi.fn().mockReturnValue(true),
|
|
castVote: vi.fn(),
|
|
};
|
|
mockRepository.findById.mockResolvedValue(mockSession);
|
|
mockRepository.save.mockRejectedValue(new Error('Save failed'));
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Failed to cast vote: Save failed');
|
|
});
|
|
});
|
|
|
|
describe('Return values', () => {
|
|
it('should return voteSessionId in success response', async () => {
|
|
const mockSession = {
|
|
id: 'session-123',
|
|
isVotingWindowOpen: vi.fn().mockReturnValue(true),
|
|
castVote: vi.fn(),
|
|
};
|
|
mockRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(result.voteSessionId).toBe('session-123');
|
|
});
|
|
|
|
it('should return voterId in success response', async () => {
|
|
const mockSession = {
|
|
id: 'session-123',
|
|
isVotingWindowOpen: vi.fn().mockReturnValue(true),
|
|
castVote: vi.fn(),
|
|
};
|
|
mockRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(result.voterId).toBe('voter-123');
|
|
});
|
|
|
|
it('should return voteSessionId in error response', async () => {
|
|
mockRepository.findById.mockResolvedValue(null);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(result.voteSessionId).toBe('session-123');
|
|
});
|
|
|
|
it('should return voterId in error response', async () => {
|
|
mockRepository.findById.mockResolvedValue(null);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
voterId: 'voter-123',
|
|
positive: true,
|
|
});
|
|
|
|
expect(result.voterId).toBe('voter-123');
|
|
});
|
|
});
|
|
}); |