randomayzer/tests/duplicate-comments-rule.test.ts

321 lines
10 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { applyFilterRules } from '../src/core/filtering/filter-engine';
import { RawParticipant } from '../src/core/types/participant';
import { FilterRules, DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
import { computeConditionsHash } from '../src/core/randomizer/canonical';
import { verifyDrawResult, executeDeterministicDrawV1 } from '../src/core/randomizer/deterministic';
import { computeParticipantsSnapshotHash } from '../src/core/randomizer/canonical';
describe('Task 07: Duplicate Comments Rule & Backward Compatibility', () => {
// ─── 1. commentsCount merging accuracy (Fix || 1 bug) ─────────────────────────
it('duplicate participant entry with commentsCount: 0 does not artificially increment commentsCount', () => {
const rawEntries: RawParticipant[] = [
{
platformUserId: 'user_like_only',
firstName: 'Like',
lastName: 'Only',
source: 'LIKES',
liked: true,
commented: false,
commentsCount: 0,
reposted: false,
subscribed: true,
},
{
platformUserId: 'user_like_only',
firstName: 'Like',
lastName: 'Only',
source: 'LIKES',
liked: true,
commented: false,
commentsCount: 0,
reposted: false,
subscribed: true,
},
];
const result = applyFilterRules(rawEntries, DEFAULT_FILTER_RULES);
expect(result.allParticipants).toHaveLength(1);
expect(result.allParticipants[0].commentsCount).toBe(0);
});
it('merging like entry (0 comments) and comment entry (1 comment) accurately sums commentsCount to 1', () => {
const rawEntries: RawParticipant[] = [
{
platformUserId: 'user_mixed',
firstName: 'Mixed',
lastName: 'User',
source: 'LIKES',
liked: true,
commented: false,
commentsCount: 0,
reposted: false,
subscribed: true,
},
{
platformUserId: 'user_mixed',
firstName: 'Mixed',
lastName: 'User',
source: 'COMMENTS',
liked: false,
commented: true,
commentsCount: 1,
reposted: false,
subscribed: true,
},
];
const result = applyFilterRules(rawEntries, DEFAULT_FILTER_RULES);
expect(result.allParticipants).toHaveLength(1);
expect(result.allParticipants[0].liked).toBe(true);
expect(result.allParticipants[0].commented).toBe(true);
expect(result.allParticipants[0].commentsCount).toBe(1);
});
// ─── 2. Unconditional deduplication (1 user = 1 chance) ──────────────────────
it('unconditionally deduplicates participants so 1 user gets 1 chance regardless of comment volume', () => {
const rawEntries: RawParticipant[] = Array.from({ length: 10 }, (_, i) => ({
platformUserId: 'spammer_100',
firstName: 'Spammer',
lastName: 'User',
source: 'COMMENTS',
liked: true,
commented: true,
commentsCount: 1,
reposted: false,
subscribed: true,
}));
const result = applyFilterRules(rawEntries, {
...DEFAULT_FILTER_RULES,
requireComment: true,
});
expect(result.allParticipants).toHaveLength(1);
expect(result.eligibleParticipants).toHaveLength(1);
expect(result.allParticipants[0].commentsCount).toBe(10);
expect(result.allParticipants[0].eligible).toBe(true);
});
// ─── 3. Legacy snapshot verification compatibility ────────────────────────────
it('legacy snapshot with excludeDuplicateComments: true retains verified: true in verifyDrawResult', () => {
// 1. Simulate legacy filterRulesSnapshot with excludeDuplicateComments field
const legacyFilterRules = {
requireLike: true,
requireComment: false,
requireRepost: false,
requireSubscription: false,
excludeAdmins: false,
excludeBlacklistedIds: [],
excludeDuplicateComments: true,
minEligibleParticipants: 1,
};
const conditionsHash = computeConditionsHash(legacyFilterRules);
const eligibleParticipants = [
{
platformUserId: '101',
firstName: 'Alice',
lastName: 'A',
source: 'LIKES' as const,
liked: true,
commented: false,
commentsCount: 0,
reposted: false,
subscribed: true,
eligible: true,
exclusionReason: null,
},
{
platformUserId: '102',
firstName: 'Bob',
lastName: 'B',
source: 'LIKES' as const,
liked: true,
commented: false,
commentsCount: 0,
reposted: false,
subscribed: true,
eligible: true,
exclusionReason: null,
},
];
const participantsSnapshotHash = computeParticipantsSnapshotHash(eligibleParticipants);
const legacySnapshot = {
id: 'snap_legacy_001',
giveawayId: 'gw_legacy_001',
version: 1,
createdAt: '2026-08-15T12:00:00.000Z',
eligibleParticipants,
filterRulesSnapshot: legacyFilterRules,
participantCount: 2,
participantsSnapshotHash,
conditionsHash,
};
const seed = 'legacy_test_seed_1234567890abcdef1234567890abcdef';
// Execute draw with legacy snapshot
const drawResult = executeDeterministicDrawV1({
giveawayId: 'gw_legacy_001',
snapshot: legacySnapshot,
totalLoadedCount: 2,
winnersCount: 1,
reserveWinnersCount: 0,
seed,
});
// Independent verifyDrawResult replay
const verification = verifyDrawResult({
giveawayId: 'gw_legacy_001',
drawId: drawResult.drawId,
drawnAt: drawResult.drawnAt,
snapshot: legacySnapshot,
seed,
claimedWinnersCount: 1,
claimedReserveCount: 0,
claimedWinnerIds: drawResult.winnerIds,
claimedReserveWinnerIds: drawResult.reserveWinnerIds,
claimedDeterministicProofHash: drawResult.deterministicProofHash,
claimedAuditEventHash: drawResult.auditEventHash,
algorithmVersion: drawResult.algorithmVersion,
});
expect(verification.conditionsIntegrity).toBe(true);
expect(verification.participantsSnapshotIntegrity).toBe(true);
expect(verification.verified).toBe(true);
});
// ─── 4. New snapshot verification compatibility ───────────────────────────────
it('new snapshot without excludeDuplicateComments computes clean hash and verifies successfully', () => {
const newFilterRules: FilterRules = {
requireLike: true,
requireComment: true,
requireRepost: false,
requireSubscription: false,
excludeAdmins: false,
excludeBlacklistedIds: ['999'],
minEligibleParticipants: 1,
};
const conditionsHash = computeConditionsHash(newFilterRules);
const eligibleParticipants = [
{
platformUserId: '201',
firstName: 'Charlie',
lastName: 'C',
source: 'COMMENTS' as const,
liked: true,
commented: true,
commentsCount: 2,
reposted: false,
subscribed: true,
eligible: true,
exclusionReason: null,
},
];
const participantsSnapshotHash = computeParticipantsSnapshotHash(eligibleParticipants);
const newSnapshot = {
id: 'snap_new_001',
giveawayId: 'gw_new_001',
version: 1,
createdAt: '2026-08-21T12:00:00.000Z',
eligibleParticipants,
filterRulesSnapshot: newFilterRules,
participantCount: 1,
participantsSnapshotHash,
conditionsHash,
};
const seed = 'new_test_seed_1234567890abcdef1234567890abcdef';
const drawResult = executeDeterministicDrawV1({
giveawayId: 'gw_new_001',
snapshot: newSnapshot,
totalLoadedCount: 1,
winnersCount: 1,
reserveWinnersCount: 0,
seed,
});
const verification = verifyDrawResult({
giveawayId: 'gw_new_001',
drawId: drawResult.drawId,
drawnAt: drawResult.drawnAt,
snapshot: newSnapshot,
seed,
claimedWinnersCount: 1,
claimedReserveCount: 0,
claimedWinnerIds: drawResult.winnerIds,
claimedReserveWinnerIds: drawResult.reserveWinnerIds,
claimedDeterministicProofHash: drawResult.deterministicProofHash,
claimedAuditEventHash: drawResult.auditEventHash,
algorithmVersion: drawResult.algorithmVersion,
});
expect(verification.conditionsIntegrity).toBe(true);
expect(verification.verified).toBe(true);
});
// ─── 5. API Schemas Reject excludeDuplicateComments on Input ──────────────────
it('API schemas strictly reject excludeDuplicateComments preventing it from reaching snapshot', async () => {
const { filterRulesSchema, fetchParticipantsSchema, createSnapshotSchema, createGiveawaySchema } = await import(
'../src/core/validation/giveaway-schemas'
);
// 1. filterRulesSchema is strict and rejects the key
const parseResult = filterRulesSchema.safeParse({
requireLike: true,
excludeDuplicateComments: true,
});
expect(parseResult.success).toBe(false);
if (!parseResult.success) {
expect(parseResult.error.issues.some(i => i.message.includes('unrecognized_keys') || (i as any).keys?.includes('excludeDuplicateComments'))).toBe(true);
}
// 2. fetchParticipantsSchema rejects
const fetchResult = fetchParticipantsSchema.safeParse({
filterRules: {
requireLike: true,
excludeDuplicateComments: true,
},
});
expect(fetchResult.success).toBe(false);
// 3. createSnapshotSchema rejects
const snapResult = createSnapshotSchema.safeParse({
filterRules: {
requireLike: true,
excludeDuplicateComments: true,
},
});
expect(snapResult.success).toBe(false);
// 4. createGiveawaySchema rejects
const createResult = createGiveawaySchema.safeParse({
sourceUrl: 'https://vk.com/wall-1_1',
post: {
platform: 'VK',
ownerId: '-1',
postId: '1',
sourceUrl: 'https://vk.com/wall-1_1',
title: 'Test',
text: 'Test',
likesCount: 1,
commentsCount: 0,
repostsCount: 0,
},
filterRules: {
requireLike: true,
excludeDuplicateComments: true,
},
});
expect(createResult.success).toBe(false);
});
});