321 lines
10 KiB
TypeScript
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);
|
|
});
|
|
});
|