Some checks failed
CI / lint-typecheck (pull_request) Failing after 12s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
1218 lines
38 KiB
TypeScript
1218 lines
38 KiB
TypeScript
/**
|
|
* Application Use Case Tests: CloseAdminVoteSessionUseCase
|
|
*
|
|
* Tests for closing admin vote sessions and generating rating events
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
|
|
import { CloseAdminVoteSessionUseCase } from './CloseAdminVoteSessionUseCase';
|
|
import { RatingEventFactory } from '../../domain/services/RatingEventFactory';
|
|
import { RatingSnapshotCalculator } from '../../domain/services/RatingSnapshotCalculator';
|
|
import { AdminVoteSessionRepository } from '../../domain/repositories/AdminVoteSessionRepository';
|
|
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
|
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
|
import { AdminVoteSession, AdminVoteOutcome } from '../../domain/entities/AdminVoteSession';
|
|
|
|
// Mock repositories
|
|
const createMockRepositories = () => ({
|
|
adminVoteSessionRepository: {
|
|
save: vi.fn(),
|
|
findById: vi.fn(),
|
|
findActiveForAdmin: vi.fn(),
|
|
findByAdminAndLeague: vi.fn(),
|
|
findByLeague: vi.fn(),
|
|
findClosedUnprocessed: vi.fn(),
|
|
},
|
|
ratingEventRepository: {
|
|
save: vi.fn(),
|
|
findByUserId: vi.fn(),
|
|
findByIds: vi.fn(),
|
|
getAllByUserId: vi.fn(),
|
|
findEventsPaginated: vi.fn(),
|
|
},
|
|
userRatingRepository: {
|
|
save: vi.fn(),
|
|
},
|
|
});
|
|
|
|
// Mock services
|
|
vi.mock('../../domain/services/RatingEventFactory', () => ({
|
|
RatingEventFactory: {
|
|
createFromVote: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../domain/services/RatingSnapshotCalculator', () => ({
|
|
RatingSnapshotCalculator: {
|
|
calculate: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
describe('CloseAdminVoteSessionUseCase', () => {
|
|
let useCase: CloseAdminVoteSessionUseCase;
|
|
let mockRepositories: ReturnType<typeof createMockRepositories>;
|
|
|
|
beforeEach(() => {
|
|
mockRepositories = createMockRepositories();
|
|
useCase = new CloseAdminVoteSessionUseCase(
|
|
mockRepositories.adminVoteSessionRepository as unknown as AdminVoteSessionRepository,
|
|
mockRepositories.ratingEventRepository as unknown as RatingEventRepository,
|
|
mockRepositories.userRatingRepository as unknown as UserRatingRepository
|
|
);
|
|
vi.clearAllMocks();
|
|
// Default mock for RatingEventFactory.createFromVote to return an empty array
|
|
// to avoid "events is not iterable" error in tests that don't explicitly mock it
|
|
(RatingEventFactory.createFromVote as unknown as Mock).mockReturnValue([]);
|
|
});
|
|
|
|
describe('Input validation', () => {
|
|
it('should reject when voteSessionId is missing', async () => {
|
|
const result = await useCase.execute({
|
|
voteSessionId: '',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('voteSessionId is required');
|
|
});
|
|
|
|
it('should reject when adminId is missing', async () => {
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: '',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('adminId is required');
|
|
});
|
|
|
|
it('should accept valid input', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
console.log('Result:', JSON.stringify(result, null, 2));
|
|
console.log('Mock session closed:', mockSession.closed);
|
|
console.log('Mock session _closed:', mockSession._closed);
|
|
console.log('Mock session close called:', mockSession.close.mock.calls.length);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.errors).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Session lookup', () => {
|
|
it('should reject when vote session is not found', async () => {
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(null);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'non-existent-session',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Vote session not found');
|
|
});
|
|
|
|
it('should find session by ID when provided', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(mockRepositories.adminVoteSessionRepository.findById).toHaveBeenCalledWith('session-123');
|
|
});
|
|
});
|
|
|
|
describe('Admin ownership validation', () => {
|
|
it('should reject when admin does not own the session', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'different-admin',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Admin does not own this vote session');
|
|
});
|
|
|
|
it('should accept when admin owns the session', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Session closure validation', () => {
|
|
it('should reject when session is already closed', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: true,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Vote session is already closed');
|
|
});
|
|
|
|
it('should accept when session is not closed', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Voting window validation', () => {
|
|
it('should reject when trying to close outside voting window', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
// Mock Date to be outside the window
|
|
const originalDate = Date;
|
|
global.Date = class extends originalDate {
|
|
constructor() {
|
|
super('2026-02-02');
|
|
}
|
|
} as unknown as typeof Date;
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Cannot close session outside the voting window');
|
|
|
|
// Restore Date
|
|
global.Date = originalDate;
|
|
});
|
|
|
|
it('should accept when trying to close within voting window', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
// Mock Date to be within the window
|
|
const originalDate = Date;
|
|
global.Date = class extends originalDate {
|
|
constructor() {
|
|
super('2026-01-15T12:00:00');
|
|
}
|
|
} as unknown as typeof Date;
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
// Restore Date
|
|
global.Date = originalDate;
|
|
});
|
|
});
|
|
|
|
describe('Session closure', () => {
|
|
it('should call close method on session', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(mockSession.close).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should save closed session', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(mockRepositories.adminVoteSessionRepository.save).toHaveBeenCalledWith(mockSession);
|
|
});
|
|
|
|
it('should return outcome in success response', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.outcome).toBeDefined();
|
|
expect(result.outcome?.percentPositive).toBe(75);
|
|
expect(result.outcome?.count).toEqual({ positive: 3, negative: 1, total: 4 });
|
|
expect(result.outcome?.eligibleVoterCount).toBe(4);
|
|
expect(result.outcome?.participationRate).toBe(100);
|
|
expect(result.outcome?.outcome).toBe('positive');
|
|
});
|
|
});
|
|
|
|
describe('Rating event creation', () => {
|
|
it('should create rating events when outcome is positive', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const mockEvent = { id: 'event-123' };
|
|
(RatingEventFactory.createFromVote as unknown as Mock).mockReturnValue([mockEvent]);
|
|
|
|
await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(RatingEventFactory.createFromVote).toHaveBeenCalledWith({
|
|
userId: 'admin-123',
|
|
voteSessionId: 'session-123',
|
|
outcome: 'positive',
|
|
voteCount: 4,
|
|
eligibleVoterCount: 4,
|
|
percentPositive: 75,
|
|
});
|
|
});
|
|
|
|
it('should create rating events when outcome is negative', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 25,
|
|
count: { positive: 1, negative: 3, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'negative',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const mockEvent = { id: 'event-123' };
|
|
(RatingEventFactory.createFromVote as unknown as Mock).mockReturnValue([mockEvent]);
|
|
|
|
await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(RatingEventFactory.createFromVote).toHaveBeenCalledWith({
|
|
userId: 'admin-123',
|
|
voteSessionId: 'session-123',
|
|
outcome: 'negative',
|
|
voteCount: 4,
|
|
eligibleVoterCount: 4,
|
|
percentPositive: 25,
|
|
});
|
|
});
|
|
|
|
it('should not create rating events when outcome is tie', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 50,
|
|
count: { positive: 2, negative: 2, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'tie',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(RatingEventFactory.createFromVote).not.toHaveBeenCalled();
|
|
expect(mockRepositories.ratingEventRepository.save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should save created rating events', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const mockEvent1 = { id: 'event-123' };
|
|
const mockEvent2 = { id: 'event-124' };
|
|
(RatingEventFactory.createFromVote as unknown as Mock).mockReturnValue([mockEvent1, mockEvent2]);
|
|
|
|
await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(mockRepositories.ratingEventRepository.save).toHaveBeenCalledTimes(2);
|
|
expect(mockRepositories.ratingEventRepository.save).toHaveBeenCalledWith(mockEvent1);
|
|
expect(mockRepositories.ratingEventRepository.save).toHaveBeenCalledWith(mockEvent2);
|
|
});
|
|
|
|
it('should return eventsCreated count', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const mockEvent1 = { id: 'event-123' };
|
|
const mockEvent2 = { id: 'event-124' };
|
|
(RatingEventFactory.createFromVote as unknown as Mock).mockReturnValue([mockEvent1, mockEvent2]);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(result.eventsCreated).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('Snapshot recalculation', () => {
|
|
it('should recalculate snapshot when events are created', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const mockEvent = { id: 'event-123' };
|
|
(RatingEventFactory.createFromVote as unknown as Mock).mockReturnValue([mockEvent]);
|
|
|
|
const mockAllEvents = [{ id: 'event-1' }, { id: 'event-2' }];
|
|
mockRepositories.ratingEventRepository.getAllByUserId.mockResolvedValue(mockAllEvents);
|
|
|
|
const mockSnapshot = { userId: 'admin-123', overallReputation: 75 };
|
|
(RatingSnapshotCalculator.calculate as unknown as Mock).mockReturnValue(mockSnapshot);
|
|
|
|
await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(mockRepositories.ratingEventRepository.getAllByUserId).toHaveBeenCalledWith('admin-123');
|
|
expect(RatingSnapshotCalculator.calculate).toHaveBeenCalledWith('admin-123', mockAllEvents);
|
|
expect(mockRepositories.userRatingRepository.save).toHaveBeenCalledWith(mockSnapshot);
|
|
});
|
|
|
|
it('should not recalculate snapshot when no events are created (tie)', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 50,
|
|
count: { positive: 2, negative: 2, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'tie',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(mockRepositories.ratingEventRepository.getAllByUserId).not.toHaveBeenCalled();
|
|
expect(RatingSnapshotCalculator.calculate).not.toHaveBeenCalled();
|
|
expect(mockRepositories.userRatingRepository.save).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Error handling', () => {
|
|
it('should handle repository errors gracefully', async () => {
|
|
mockRepositories.adminVoteSessionRepository.findById.mockRejectedValue(new Error('Database error'));
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Failed to close vote session: Database error');
|
|
});
|
|
|
|
it('should handle unexpected errors gracefully', async () => {
|
|
mockRepositories.adminVoteSessionRepository.findById.mockRejectedValue('Unknown error');
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Failed to close vote session: Unknown error');
|
|
});
|
|
|
|
it('should handle save errors gracefully', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
mockRepositories.adminVoteSessionRepository.save.mockRejectedValue(new Error('Save failed'));
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors).toContain('Failed to close vote session: Save failed');
|
|
});
|
|
});
|
|
|
|
describe('Return values', () => {
|
|
it('should return voteSessionId in success response', async () => {
|
|
const futureDate = new Date('2026-02-01');
|
|
const mockSession: {
|
|
id: string;
|
|
adminId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
_closed: boolean;
|
|
_outcome?: AdminVoteOutcome;
|
|
close: Mock;
|
|
closed: boolean;
|
|
} = {
|
|
id: 'session-123',
|
|
adminId: 'admin-123',
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: futureDate,
|
|
_closed: false,
|
|
close: vi.fn().mockImplementation(function() {
|
|
if (this._closed) {
|
|
throw new Error('Session is already closed');
|
|
}
|
|
const now = new Date();
|
|
if (now < this.startDate || now > this.endDate) {
|
|
throw new Error('Cannot close session outside the voting window');
|
|
}
|
|
this._closed = true;
|
|
this._outcome = {
|
|
percentPositive: 75,
|
|
count: { positive: 3, negative: 1, total: 4 },
|
|
eligibleVoterCount: 4,
|
|
participationRate: 100,
|
|
outcome: 'positive',
|
|
};
|
|
return this._outcome;
|
|
}),
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
};
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(mockSession);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(result.voteSessionId).toBe('session-123');
|
|
});
|
|
|
|
it('should return voteSessionId in error response', async () => {
|
|
mockRepositories.adminVoteSessionRepository.findById.mockResolvedValue(null);
|
|
|
|
const result = await useCase.execute({
|
|
voteSessionId: 'session-123',
|
|
adminId: 'admin-123',
|
|
});
|
|
|
|
expect(result.voteSessionId).toBe('session-123');
|
|
});
|
|
});
|
|
});
|