mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-05 17:33:31 -04:00
- Add test-specific Drizzle config and database connection - Create test version of auth module using test database - Add comprehensive integration tests for signin migration logic - Add unit tests for deduplication algorithm - Tests cover edge cases like multiple duplicates, timing, and error handling 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
245 lines
6.9 KiB
TypeScript
245 lines
6.9 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
|
|
describe('Signin Migration Logic (Unit Tests)', () => {
|
|
|
|
// Test the deduplication algorithm independently
|
|
it('should correctly identify and remove duplicates keeping earliest', () => {
|
|
// Mock completion data structure
|
|
type MockCompletion = {
|
|
id: string;
|
|
anonymousId: string;
|
|
date: string;
|
|
guessCount: number;
|
|
completedAt: Date;
|
|
};
|
|
|
|
// Test data: multiple completions on same date
|
|
const allUserCompletions: MockCompletion[] = [
|
|
{
|
|
id: 'comp1',
|
|
anonymousId: 'user123',
|
|
date: '2024-01-01',
|
|
guessCount: 4,
|
|
completedAt: new Date('2024-01-01T08:00:00Z') // Earliest
|
|
},
|
|
{
|
|
id: 'comp2',
|
|
anonymousId: 'user123',
|
|
date: '2024-01-01',
|
|
guessCount: 2,
|
|
completedAt: new Date('2024-01-01T14:00:00Z') // Later
|
|
},
|
|
{
|
|
id: 'comp3',
|
|
anonymousId: 'user123',
|
|
date: '2024-01-01',
|
|
guessCount: 6,
|
|
completedAt: new Date('2024-01-01T20:00:00Z') // Latest
|
|
},
|
|
{
|
|
id: 'comp4',
|
|
anonymousId: 'user123',
|
|
date: '2024-01-02',
|
|
guessCount: 3,
|
|
completedAt: new Date('2024-01-02T09:00:00Z') // Unique date
|
|
}
|
|
];
|
|
|
|
// Implement the deduplication logic from signin server action
|
|
const dateGroups = new Map<string, MockCompletion[]>();
|
|
for (const completion of allUserCompletions) {
|
|
const date = completion.date;
|
|
if (!dateGroups.has(date)) {
|
|
dateGroups.set(date, []);
|
|
}
|
|
dateGroups.get(date)!.push(completion);
|
|
}
|
|
|
|
// Process dates with duplicates
|
|
const duplicateIds: string[] = [];
|
|
const keptEntries: MockCompletion[] = [];
|
|
|
|
for (const [date, completions] of dateGroups) {
|
|
if (completions.length > 1) {
|
|
// Sort by completedAt timestamp (earliest first)
|
|
completions.sort((a, b) => a.completedAt.getTime() - b.completedAt.getTime());
|
|
|
|
// Keep the first (earliest), mark the rest for deletion
|
|
const toKeep = completions[0];
|
|
const toDelete = completions.slice(1);
|
|
|
|
keptEntries.push(toKeep);
|
|
duplicateIds.push(...toDelete.map(c => c.id));
|
|
} else {
|
|
// Single entry for this date, keep it
|
|
keptEntries.push(completions[0]);
|
|
}
|
|
}
|
|
|
|
// Verify the logic worked correctly
|
|
expect(duplicateIds).toHaveLength(2); // comp2 and comp3 should be deleted
|
|
expect(duplicateIds).toContain('comp2');
|
|
expect(duplicateIds).toContain('comp3');
|
|
expect(duplicateIds).not.toContain('comp1'); // comp1 should be kept (earliest)
|
|
expect(duplicateIds).not.toContain('comp4'); // comp4 should be kept (unique date)
|
|
|
|
// Verify kept entries
|
|
expect(keptEntries).toHaveLength(2);
|
|
|
|
// Check that the earliest entry for 2024-01-01 was kept
|
|
const jan1Entry = keptEntries.find(e => e.date === '2024-01-01');
|
|
expect(jan1Entry).toBeTruthy();
|
|
expect(jan1Entry!.id).toBe('comp1'); // Earliest timestamp
|
|
expect(jan1Entry!.guessCount).toBe(4);
|
|
expect(jan1Entry!.completedAt.getTime()).toBe(new Date('2024-01-01T08:00:00Z').getTime());
|
|
|
|
// Check that unique date entry was preserved
|
|
const jan2Entry = keptEntries.find(e => e.date === '2024-01-02');
|
|
expect(jan2Entry).toBeTruthy();
|
|
expect(jan2Entry!.id).toBe('comp4');
|
|
});
|
|
|
|
it('should handle no duplicates correctly', () => {
|
|
type MockCompletion = {
|
|
id: string;
|
|
anonymousId: string;
|
|
date: string;
|
|
guessCount: number;
|
|
completedAt: Date;
|
|
};
|
|
|
|
// Test data: all unique dates
|
|
const allUserCompletions: MockCompletion[] = [
|
|
{
|
|
id: 'comp1',
|
|
anonymousId: 'user123',
|
|
date: '2024-01-01',
|
|
guessCount: 4,
|
|
completedAt: new Date('2024-01-01T08:00:00Z')
|
|
},
|
|
{
|
|
id: 'comp2',
|
|
anonymousId: 'user123',
|
|
date: '2024-01-02',
|
|
guessCount: 2,
|
|
completedAt: new Date('2024-01-02T14:00:00Z')
|
|
}
|
|
];
|
|
|
|
// Run deduplication logic
|
|
const dateGroups = new Map<string, MockCompletion[]>();
|
|
for (const completion of allUserCompletions) {
|
|
if (!dateGroups.has(completion.date)) {
|
|
dateGroups.set(completion.date, []);
|
|
}
|
|
dateGroups.get(completion.date)!.push(completion);
|
|
}
|
|
|
|
const duplicateIds: string[] = [];
|
|
for (const [date, completions] of dateGroups) {
|
|
if (completions.length > 1) {
|
|
completions.sort((a, b) => a.completedAt.getTime() - b.completedAt.getTime());
|
|
const toDelete = completions.slice(1);
|
|
duplicateIds.push(...toDelete.map(c => c.id));
|
|
}
|
|
}
|
|
|
|
// Should find no duplicates
|
|
expect(duplicateIds).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle edge case with same timestamp', () => {
|
|
type MockCompletion = {
|
|
id: string;
|
|
anonymousId: string;
|
|
date: string;
|
|
guessCount: number;
|
|
completedAt: Date;
|
|
};
|
|
|
|
// Edge case: same completion time (very unlikely but possible)
|
|
const sameTime = new Date('2024-01-01T08:00:00Z');
|
|
const allUserCompletions: MockCompletion[] = [
|
|
{
|
|
id: 'comp1',
|
|
anonymousId: 'user123',
|
|
date: '2024-01-01',
|
|
guessCount: 3,
|
|
completedAt: sameTime
|
|
},
|
|
{
|
|
id: 'comp2',
|
|
anonymousId: 'user123',
|
|
date: '2024-01-01',
|
|
guessCount: 5,
|
|
completedAt: sameTime
|
|
}
|
|
];
|
|
|
|
// Run deduplication logic
|
|
const dateGroups = new Map<string, MockCompletion[]>();
|
|
for (const completion of allUserCompletions) {
|
|
if (!dateGroups.has(completion.date)) {
|
|
dateGroups.set(completion.date, []);
|
|
}
|
|
dateGroups.get(completion.date)!.push(completion);
|
|
}
|
|
|
|
const duplicateIds: string[] = [];
|
|
for (const [date, completions] of dateGroups) {
|
|
if (completions.length > 1) {
|
|
completions.sort((a, b) => a.completedAt.getTime() - b.completedAt.getTime());
|
|
const toDelete = completions.slice(1);
|
|
duplicateIds.push(...toDelete.map(c => c.id));
|
|
}
|
|
}
|
|
|
|
// Should still remove one duplicate (deterministically based on array order)
|
|
expect(duplicateIds).toHaveLength(1);
|
|
// Since they have the same timestamp, it keeps the first one in the sorted array
|
|
expect(duplicateIds[0]).toBe('comp2'); // Second entry gets removed
|
|
});
|
|
|
|
it('should validate migration condition logic', () => {
|
|
// Test the condition check that determines when migration should occur
|
|
const testCases = [
|
|
{
|
|
anonymousId: 'device2-id',
|
|
userId: 'device1-id',
|
|
shouldMigrate: true,
|
|
description: 'Different IDs should trigger migration'
|
|
},
|
|
{
|
|
anonymousId: 'same-id',
|
|
userId: 'same-id',
|
|
shouldMigrate: false,
|
|
description: 'Same IDs should not trigger migration'
|
|
},
|
|
{
|
|
anonymousId: null as any,
|
|
userId: 'user-id',
|
|
shouldMigrate: false,
|
|
description: 'Null anonymous ID should not trigger migration'
|
|
},
|
|
{
|
|
anonymousId: undefined as any,
|
|
userId: 'user-id',
|
|
shouldMigrate: false,
|
|
description: 'Undefined anonymous ID should not trigger migration'
|
|
},
|
|
{
|
|
anonymousId: '',
|
|
userId: 'user-id',
|
|
shouldMigrate: false,
|
|
description: 'Empty anonymous ID should not trigger migration'
|
|
}
|
|
];
|
|
|
|
for (const testCase of testCases) {
|
|
// This is the exact condition from signin/+page.server.ts
|
|
const shouldMigrate = !!(testCase.anonymousId && testCase.anonymousId !== testCase.userId);
|
|
|
|
expect(shouldMigrate).toBe(testCase.shouldMigrate);
|
|
}
|
|
});
|
|
}); |