rating
This commit is contained in:
@@ -0,0 +1,707 @@
|
||||
import { OpenAdminVoteSessionUseCase } from './OpenAdminVoteSessionUseCase';
|
||||
import { CastAdminVoteUseCase } from './CastAdminVoteUseCase';
|
||||
import { CloseAdminVoteSessionUseCase } from './CloseAdminVoteSessionUseCase';
|
||||
import { AdminVoteSession } from '../../domain/entities/AdminVoteSession';
|
||||
import { RatingEvent } from '../../domain/entities/RatingEvent';
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
|
||||
import { RatingDimensionKey } from '../../domain/value-objects/RatingDimensionKey';
|
||||
import { RatingDelta } from '../../domain/value-objects/RatingDelta';
|
||||
|
||||
// Mock Repository
|
||||
class MockAdminVoteSessionRepository {
|
||||
private sessions: Map<string, AdminVoteSession> = new Map();
|
||||
|
||||
async save(session: AdminVoteSession): Promise<AdminVoteSession> {
|
||||
this.sessions.set(session.id, session);
|
||||
return session;
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<AdminVoteSession | null> {
|
||||
return this.sessions.get(id) || null;
|
||||
}
|
||||
|
||||
async findActiveForAdmin(adminId: string, leagueId: string): Promise<AdminVoteSession[]> {
|
||||
const now = new Date();
|
||||
return Array.from(this.sessions.values()).filter(
|
||||
s => s.adminId === adminId &&
|
||||
s.leagueId === leagueId &&
|
||||
s.isVotingWindowOpen(now) &&
|
||||
!s.closed
|
||||
);
|
||||
}
|
||||
|
||||
async findByAdminAndLeague(adminId: string, leagueId: string): Promise<AdminVoteSession[]> {
|
||||
return Array.from(this.sessions.values()).filter(
|
||||
s => s.adminId === adminId && s.leagueId === leagueId
|
||||
);
|
||||
}
|
||||
|
||||
async findByLeague(leagueId: string): Promise<AdminVoteSession[]> {
|
||||
return Array.from(this.sessions.values()).filter(
|
||||
s => s.leagueId === leagueId
|
||||
);
|
||||
}
|
||||
|
||||
async findClosedUnprocessed(): Promise<AdminVoteSession[]> {
|
||||
return Array.from(this.sessions.values()).filter(
|
||||
s => s.closed && s.outcome !== undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MockRatingEventRepository {
|
||||
private events: Map<string, RatingEvent> = new Map();
|
||||
|
||||
async save(event: RatingEvent): Promise<RatingEvent> {
|
||||
this.events.set(event.id.value, event);
|
||||
return event;
|
||||
}
|
||||
|
||||
async findByUserId(userId: string): Promise<RatingEvent[]> {
|
||||
return Array.from(this.events.values()).filter(e => e.userId === userId);
|
||||
}
|
||||
|
||||
async findByIds(ids: string[]): Promise<RatingEvent[]> {
|
||||
return Array.from(this.events.values()).filter(e => ids.includes(e.id.value));
|
||||
}
|
||||
|
||||
async getAllByUserId(userId: string): Promise<RatingEvent[]> {
|
||||
return Array.from(this.events.values()).filter(e => e.userId === userId);
|
||||
}
|
||||
|
||||
async findEventsPaginated(userId: string, options?: import('@core/identity/domain/repositories/IRatingEventRepository').PaginatedQueryOptions): Promise<import('@core/identity/domain/repositories/IRatingEventRepository').PaginatedResult<RatingEvent>> {
|
||||
const allEvents = await this.findByUserId(userId);
|
||||
|
||||
// Apply filters
|
||||
let filtered = allEvents;
|
||||
if (options?.filter) {
|
||||
const filter = options.filter;
|
||||
if (filter.dimensions) {
|
||||
filtered = filtered.filter(e => filter.dimensions!.includes(e.dimension.value));
|
||||
}
|
||||
if (filter.sourceTypes) {
|
||||
filtered = filtered.filter(e => filter.sourceTypes!.includes(e.source.type));
|
||||
}
|
||||
if (filter.from) {
|
||||
filtered = filtered.filter(e => e.occurredAt >= filter.from!);
|
||||
}
|
||||
if (filter.to) {
|
||||
filtered = filtered.filter(e => e.occurredAt <= filter.to!);
|
||||
}
|
||||
if (filter.reasonCodes) {
|
||||
filtered = filtered.filter(e => filter.reasonCodes!.includes(e.reason.code));
|
||||
}
|
||||
if (filter.visibility) {
|
||||
filtered = filtered.filter(e => e.visibility.public === (filter.visibility === 'public'));
|
||||
}
|
||||
}
|
||||
|
||||
const total = filtered.length;
|
||||
const limit = options?.limit ?? 10;
|
||||
const offset = options?.offset ?? 0;
|
||||
const items = filtered.slice(offset, offset + limit);
|
||||
const hasMore = offset + limit < total;
|
||||
const nextOffset = hasMore ? offset + limit : undefined;
|
||||
|
||||
const result: import('@core/identity/domain/repositories/IRatingEventRepository').PaginatedResult<RatingEvent> = {
|
||||
items,
|
||||
total,
|
||||
limit,
|
||||
offset,
|
||||
hasMore
|
||||
};
|
||||
|
||||
if (nextOffset !== undefined) {
|
||||
result.nextOffset = nextOffset;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class MockUserRatingRepository {
|
||||
private ratings: Map<string, UserRating> = new Map();
|
||||
|
||||
async save(rating: UserRating): Promise<UserRating> {
|
||||
this.ratings.set(rating.userId, rating);
|
||||
return rating;
|
||||
}
|
||||
|
||||
async findByUserId(userId: string): Promise<UserRating | null> {
|
||||
return this.ratings.get(userId) || null;
|
||||
}
|
||||
}
|
||||
|
||||
// Mock AppendRatingEventsUseCase
|
||||
class MockAppendRatingEventsUseCase {
|
||||
constructor(
|
||||
private ratingEventRepository: any,
|
||||
private userRatingRepository: any
|
||||
) {}
|
||||
|
||||
async execute(input: any): Promise<any> {
|
||||
const events: RatingEvent[] = [];
|
||||
|
||||
// Create events from input
|
||||
for (const eventDto of input.events || []) {
|
||||
const event = RatingEvent.create({
|
||||
id: RatingEventId.generate(),
|
||||
userId: eventDto.userId,
|
||||
dimension: RatingDimensionKey.create(eventDto.dimension),
|
||||
delta: RatingDelta.create(eventDto.delta),
|
||||
weight: eventDto.weight,
|
||||
occurredAt: new Date(eventDto.occurredAt),
|
||||
createdAt: new Date(),
|
||||
source: { type: eventDto.sourceType, id: eventDto.sourceId },
|
||||
reason: {
|
||||
code: eventDto.reasonCode,
|
||||
summary: eventDto.reasonSummary,
|
||||
details: eventDto.reasonDetails || {},
|
||||
},
|
||||
visibility: { public: true, redactedFields: [] },
|
||||
version: 1,
|
||||
});
|
||||
events.push(event);
|
||||
await this.ratingEventRepository.save(event);
|
||||
}
|
||||
|
||||
// Recompute snapshot
|
||||
if (events.length > 0) {
|
||||
const allEvents = await this.ratingEventRepository.getAllByUserId(input.userId);
|
||||
// Simplified snapshot calculation
|
||||
const totalDelta = allEvents.reduce((sum, e) => sum + e.delta.value, 0);
|
||||
const snapshot = UserRating.create({
|
||||
userId: input.userId,
|
||||
driver: { value: Math.max(0, Math.min(100, 50 + totalDelta)) },
|
||||
adminTrust: { value: 50 },
|
||||
stewardTrust: { value: 50 },
|
||||
broadcasterTrust: { value: 50 },
|
||||
lastUpdated: new Date(),
|
||||
});
|
||||
await this.userRatingRepository.save(snapshot);
|
||||
}
|
||||
|
||||
return {
|
||||
events: events.map(e => e.id.value),
|
||||
snapshotUpdated: events.length > 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe('Admin Vote Session Use Cases', () => {
|
||||
let mockSessionRepo: MockAdminVoteSessionRepository;
|
||||
let mockEventRepo: MockRatingEventRepository;
|
||||
let mockRatingRepo: MockUserRatingRepository;
|
||||
let mockAppendUseCase: MockAppendRatingEventsUseCase;
|
||||
|
||||
let openUseCase: OpenAdminVoteSessionUseCase;
|
||||
let castUseCase: CastAdminVoteUseCase;
|
||||
let closeUseCase: CloseAdminVoteSessionUseCase;
|
||||
|
||||
const now = new Date('2025-01-01T00:00:00Z');
|
||||
const tomorrow = new Date('2025-01-02T00:00:00Z');
|
||||
|
||||
beforeEach(() => {
|
||||
mockSessionRepo = new MockAdminVoteSessionRepository();
|
||||
mockEventRepo = new MockRatingEventRepository();
|
||||
mockRatingRepo = new MockUserRatingRepository();
|
||||
mockAppendUseCase = new MockAppendRatingEventsUseCase(mockEventRepo, mockRatingRepo);
|
||||
|
||||
openUseCase = new OpenAdminVoteSessionUseCase(mockSessionRepo);
|
||||
castUseCase = new CastAdminVoteUseCase(mockSessionRepo);
|
||||
closeUseCase = new CloseAdminVoteSessionUseCase(
|
||||
mockSessionRepo,
|
||||
mockEventRepo,
|
||||
mockRatingRepo,
|
||||
mockAppendUseCase
|
||||
);
|
||||
|
||||
// Mock Date.now to return our test time
|
||||
jest.spyOn(Date, 'now').mockReturnValue(now.getTime());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('OpenAdminVoteSessionUseCase', () => {
|
||||
it('should successfully open a vote session', async () => {
|
||||
const input = {
|
||||
voteSessionId: 'vote-123',
|
||||
leagueId: 'league-456',
|
||||
adminId: 'admin-789',
|
||||
startDate: now.toISOString(),
|
||||
endDate: tomorrow.toISOString(),
|
||||
eligibleVoters: ['user-1', 'user-2', 'user-3'],
|
||||
};
|
||||
|
||||
const result = await openUseCase.execute(input);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.voteSessionId).toBe('vote-123');
|
||||
|
||||
// Verify session was saved
|
||||
const saved = await mockSessionRepo.findById('vote-123');
|
||||
expect(saved).toBeDefined();
|
||||
expect(saved!.adminId).toBe('admin-789');
|
||||
expect(saved!.eligibleVoters).toEqual(['user-1', 'user-2', 'user-3']);
|
||||
});
|
||||
|
||||
it('should reject duplicate session IDs', async () => {
|
||||
const input = {
|
||||
voteSessionId: 'vote-123',
|
||||
leagueId: 'league-456',
|
||||
adminId: 'admin-789',
|
||||
startDate: now.toISOString(),
|
||||
endDate: tomorrow.toISOString(),
|
||||
eligibleVoters: ['user-1'],
|
||||
};
|
||||
|
||||
await openUseCase.execute(input);
|
||||
const result = await openUseCase.execute(input);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain('Vote session with this ID already exists');
|
||||
});
|
||||
|
||||
it('should reject overlapping sessions', async () => {
|
||||
// Create first session
|
||||
await openUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
leagueId: 'league-456',
|
||||
adminId: 'admin-789',
|
||||
startDate: now.toISOString(),
|
||||
endDate: tomorrow.toISOString(),
|
||||
eligibleVoters: ['user-1'],
|
||||
});
|
||||
|
||||
// Try to create overlapping session
|
||||
const result = await openUseCase.execute({
|
||||
voteSessionId: 'vote-456',
|
||||
leagueId: 'league-456',
|
||||
adminId: 'admin-789',
|
||||
startDate: new Date('2025-01-01T12:00:00Z').toISOString(),
|
||||
endDate: new Date('2025-01-02T12:00:00Z').toISOString(),
|
||||
eligibleVoters: ['user-1'],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain('Active vote session already exists for this admin in this league with overlapping dates');
|
||||
});
|
||||
|
||||
it('should validate required fields', async () => {
|
||||
const result = await openUseCase.execute({
|
||||
voteSessionId: '',
|
||||
leagueId: '',
|
||||
adminId: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
eligibleVoters: [],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors?.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should validate date order', async () => {
|
||||
const result = await openUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
leagueId: 'league-456',
|
||||
adminId: 'admin-789',
|
||||
startDate: tomorrow.toISOString(),
|
||||
endDate: now.toISOString(),
|
||||
eligibleVoters: ['user-1'],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain('startDate must be before endDate');
|
||||
});
|
||||
|
||||
it('should validate duplicate eligible voters', async () => {
|
||||
const result = await openUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
leagueId: 'league-456',
|
||||
adminId: 'admin-789',
|
||||
startDate: now.toISOString(),
|
||||
endDate: tomorrow.toISOString(),
|
||||
eligibleVoters: ['user-1', 'user-2', 'user-1'],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain('Duplicate eligible voters are not allowed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CastAdminVoteUseCase', () => {
|
||||
beforeEach(async () => {
|
||||
// Create a session for voting tests
|
||||
await openUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
leagueId: 'league-456',
|
||||
adminId: 'admin-789',
|
||||
startDate: now.toISOString(),
|
||||
endDate: tomorrow.toISOString(),
|
||||
eligibleVoters: ['user-1', 'user-2', 'user-3'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow eligible voter to cast positive vote', async () => {
|
||||
const result = await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-1',
|
||||
positive: true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.voterId).toBe('user-1');
|
||||
|
||||
const session = await mockSessionRepo.findById('vote-123');
|
||||
expect(session!.votes.length).toBe(1);
|
||||
expect(session!.votes[0].voterId).toBe('user-1');
|
||||
expect(session!.votes[0].positive).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow eligible voter to cast negative vote', async () => {
|
||||
const result = await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-1',
|
||||
positive: false,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const session = await mockSessionRepo.findById('vote-123');
|
||||
expect(session!.votes[0].positive).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject non-eligible voter', async () => {
|
||||
const result = await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-999',
|
||||
positive: true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain('Voter user-999 is not eligible for this session');
|
||||
});
|
||||
|
||||
it('should prevent duplicate votes', async () => {
|
||||
await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-1',
|
||||
positive: true,
|
||||
});
|
||||
|
||||
const result = await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-1',
|
||||
positive: false,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain('Voter user-1 has already voted');
|
||||
});
|
||||
|
||||
it('should reject votes after session closes', async () => {
|
||||
// Close the session first
|
||||
await closeUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
adminId: 'admin-789',
|
||||
});
|
||||
|
||||
const result = await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-2',
|
||||
positive: true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain('Session is closed');
|
||||
});
|
||||
|
||||
it('should reject votes outside voting window', async () => {
|
||||
// Create session in future
|
||||
const futureStart = new Date('2025-02-01T00:00:00Z');
|
||||
const futureEnd = new Date('2025-02-02T00:00:00Z');
|
||||
|
||||
await openUseCase.execute({
|
||||
voteSessionId: 'vote-future',
|
||||
leagueId: 'league-456',
|
||||
adminId: 'admin-789',
|
||||
startDate: futureStart.toISOString(),
|
||||
endDate: futureEnd.toISOString(),
|
||||
eligibleVoters: ['user-1'],
|
||||
});
|
||||
|
||||
const result = await castUseCase.execute({
|
||||
voteSessionId: 'vote-future',
|
||||
voterId: 'user-1',
|
||||
positive: true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain('Vote session is not open for voting');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CloseAdminVoteSessionUseCase', () => {
|
||||
beforeEach(async () => {
|
||||
// Create a session for closing tests
|
||||
await openUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
leagueId: 'league-456',
|
||||
adminId: 'admin-789',
|
||||
startDate: now.toISOString(),
|
||||
endDate: tomorrow.toISOString(),
|
||||
eligibleVoters: ['user-1', 'user-2', 'user-3', 'user-4'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should close session and create positive outcome events', async () => {
|
||||
// Cast votes
|
||||
await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-1',
|
||||
positive: true,
|
||||
});
|
||||
await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-2',
|
||||
positive: true,
|
||||
});
|
||||
await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-3',
|
||||
positive: false,
|
||||
});
|
||||
|
||||
const result = await closeUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
adminId: 'admin-789',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outcome).toBeDefined();
|
||||
expect(result.outcome!.outcome).toBe('positive');
|
||||
expect(result.outcome!.percentPositive).toBe(66.67);
|
||||
expect(result.outcome!.count.total).toBe(3);
|
||||
expect(result.eventsCreated).toBe(1);
|
||||
|
||||
// Verify session is closed
|
||||
const session = await mockSessionRepo.findById('vote-123');
|
||||
expect(session!.closed).toBe(true);
|
||||
expect(session!.outcome).toBeDefined();
|
||||
|
||||
// Verify rating events were created
|
||||
const events = await mockEventRepo.findByUserId('admin-789');
|
||||
expect(events.length).toBe(1);
|
||||
expect(events[0].dimension.value).toBe('adminTrust');
|
||||
expect(events[0].reason.code).toBe('ADMIN_VOTE_OUTCOME_POSITIVE');
|
||||
});
|
||||
|
||||
it('should create negative outcome events', async () => {
|
||||
// Cast mostly negative votes
|
||||
await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-1',
|
||||
positive: false,
|
||||
});
|
||||
await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-2',
|
||||
positive: false,
|
||||
});
|
||||
await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-3',
|
||||
positive: true,
|
||||
});
|
||||
|
||||
const result = await closeUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
adminId: 'admin-789',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outcome!.outcome).toBe('negative');
|
||||
expect(result.outcome!.percentPositive).toBe(33.33);
|
||||
expect(result.eventsCreated).toBe(1);
|
||||
|
||||
const events = await mockEventRepo.findByUserId('admin-789');
|
||||
expect(events[0].reason.code).toBe('ADMIN_VOTE_OUTCOME_NEGATIVE');
|
||||
expect(events[0].delta.value).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('should handle tie outcome', async () => {
|
||||
await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-1',
|
||||
positive: true,
|
||||
});
|
||||
await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-2',
|
||||
positive: false,
|
||||
});
|
||||
|
||||
const result = await closeUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
adminId: 'admin-789',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outcome!.outcome).toBe('tie');
|
||||
expect(result.eventsCreated).toBe(0); // No events for tie
|
||||
});
|
||||
|
||||
it('should handle no votes', async () => {
|
||||
const result = await closeUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
adminId: 'admin-789',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outcome!.outcome).toBe('tie');
|
||||
expect(result.outcome!.participationRate).toBe(0);
|
||||
expect(result.eventsCreated).toBe(0);
|
||||
});
|
||||
|
||||
it('should reject closing already closed session', async () => {
|
||||
await closeUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
adminId: 'admin-789',
|
||||
});
|
||||
|
||||
const result = await closeUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
adminId: 'admin-789',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain('Vote session is already closed');
|
||||
});
|
||||
|
||||
it('should reject non-owner trying to close', async () => {
|
||||
const result = await closeUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
adminId: 'wrong-admin',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain('Admin does not own this vote session');
|
||||
});
|
||||
|
||||
it('should reject closing outside voting window', async () => {
|
||||
// Create session in future
|
||||
const futureStart = new Date('2025-02-01T00:00:00Z');
|
||||
const futureEnd = new Date('2025-02-02T00:00:00Z');
|
||||
|
||||
await openUseCase.execute({
|
||||
voteSessionId: 'vote-future',
|
||||
leagueId: 'league-456',
|
||||
adminId: 'admin-789',
|
||||
startDate: futureStart.toISOString(),
|
||||
endDate: futureEnd.toISOString(),
|
||||
eligibleVoters: ['user-1'],
|
||||
});
|
||||
|
||||
const result = await closeUseCase.execute({
|
||||
voteSessionId: 'vote-future',
|
||||
adminId: 'admin-789',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain('Cannot close session outside the voting window');
|
||||
});
|
||||
|
||||
it('should update admin rating snapshot', async () => {
|
||||
// Cast votes
|
||||
await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-1',
|
||||
positive: true,
|
||||
});
|
||||
await castUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
voterId: 'user-2',
|
||||
positive: true,
|
||||
});
|
||||
|
||||
await closeUseCase.execute({
|
||||
voteSessionId: 'vote-123',
|
||||
adminId: 'admin-789',
|
||||
});
|
||||
|
||||
// Check snapshot was updated
|
||||
const snapshot = await mockRatingRepo.findByUserId('admin-789');
|
||||
expect(snapshot).toBeDefined();
|
||||
expect(snapshot!.adminTrust.value).toBeGreaterThan(50); // Should have increased
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration: Full vote flow', () => {
|
||||
it('should complete full flow: open -> cast votes -> close -> events', async () => {
|
||||
// 1. Open session
|
||||
const openResult = await openUseCase.execute({
|
||||
voteSessionId: 'vote-full',
|
||||
leagueId: 'league-full',
|
||||
adminId: 'admin-full',
|
||||
startDate: now.toISOString(),
|
||||
endDate: tomorrow.toISOString(),
|
||||
eligibleVoters: ['user-1', 'user-2', 'user-3', 'user-4', 'user-5'],
|
||||
});
|
||||
expect(openResult.success).toBe(true);
|
||||
|
||||
// 2. Cast votes
|
||||
const votes = [
|
||||
{ voterId: 'user-1', positive: true },
|
||||
{ voterId: 'user-2', positive: true },
|
||||
{ voterId: 'user-3', positive: true },
|
||||
{ voterId: 'user-4', positive: false },
|
||||
];
|
||||
|
||||
for (const vote of votes) {
|
||||
const castResult = await castUseCase.execute({
|
||||
voteSessionId: 'vote-full',
|
||||
voterId: vote.voterId,
|
||||
positive: vote.positive,
|
||||
});
|
||||
expect(castResult.success).toBe(true);
|
||||
}
|
||||
|
||||
// 3. Close session
|
||||
const closeResult = await closeUseCase.execute({
|
||||
voteSessionId: 'vote-full',
|
||||
adminId: 'admin-full',
|
||||
});
|
||||
|
||||
expect(closeResult.success).toBe(true);
|
||||
expect(closeResult.outcome).toEqual({
|
||||
percentPositive: 75,
|
||||
count: { positive: 3, negative: 1, total: 4 },
|
||||
eligibleVoterCount: 5,
|
||||
participationRate: 80,
|
||||
outcome: 'positive',
|
||||
});
|
||||
expect(closeResult.eventsCreated).toBe(1);
|
||||
|
||||
// 4. Verify events in ledger
|
||||
const events = await mockEventRepo.findByUserId('admin-full');
|
||||
expect(events.length).toBe(1);
|
||||
|
||||
const event = events[0];
|
||||
expect(event.userId).toBe('admin-full');
|
||||
expect(event.dimension.value).toBe('adminTrust');
|
||||
expect(event.source.type).toBe('vote');
|
||||
expect(event.source.id).toBe('vote-full');
|
||||
expect(event.reason.code).toBe('ADMIN_VOTE_OUTCOME_POSITIVE');
|
||||
expect(event.reason.summary).toContain('75% positive');
|
||||
expect(event.weight).toBe(4); // vote count
|
||||
|
||||
// 5. Verify snapshot
|
||||
const snapshot = await mockRatingRepo.findByUserId('admin-full');
|
||||
expect(snapshot).toBeDefined();
|
||||
expect(snapshot!.adminTrust.value).toBeGreaterThan(50);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user