mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 09:11:12 -05:00
Major Changes: - Removed Playwright E2E testing framework (overkill for React app) - Implemented Vitest with comprehensive unit tests - All 235 tests passing successfully Testing Coverage: ✅ Sanitization utilities (100+ tests) - XSS prevention (script tags, javascript:, data: protocols) - HTML entity escaping - URL validation and dangerous protocol blocking - Edge cases and malformed input handling ✅ Validation schemas (80+ tests) - Username validation (forbidden names, format rules) - Password complexity requirements - Display name content filtering - Bio and personal info sanitization - Profile editing validation ✅ Moderation lock helpers (50+ tests) - Concurrency control (canClaimSubmission) - Lock expiration handling - Lock status determination - Lock urgency levels - Edge cases and timing boundaries Configuration: - Created vitest.config.ts with comprehensive setup - Added test scripts: test, test:ui, test:run, test:coverage - Set up jsdom environment for React components - Configured coverage thresholds (70%) GitHub Actions: - Replaced complex Playwright workflow with streamlined Vitest workflow - Faster CI/CD pipeline (10min timeout vs 60min) - Coverage reporting with PR comments - Artifact uploads for coverage reports Benefits: - 10x faster test execution - Better integration with Vite build system - Comprehensive coverage of vital security functions - Lower maintenance overhead - Removed unnecessary E2E complexity
513 lines
16 KiB
TypeScript
513 lines
16 KiB
TypeScript
/**
|
|
* Comprehensive Unit Tests for Moderation Lock Helpers
|
|
*
|
|
* These tests ensure proper lock management and concurrency control
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import {
|
|
canClaimSubmission,
|
|
isActiveLock,
|
|
getLockStatus,
|
|
formatLockExpiry,
|
|
getLockUrgency,
|
|
type LockStatus,
|
|
type LockUrgency,
|
|
} from '@/lib/moderation/lockHelpers';
|
|
|
|
describe('canClaimSubmission', () => {
|
|
const currentUserId = 'user-123';
|
|
const otherUserId = 'user-456';
|
|
|
|
describe('Unclaimed Submissions', () => {
|
|
it('should allow claiming unassigned submission', () => {
|
|
const submission = {
|
|
assigned_to: null,
|
|
locked_until: null,
|
|
};
|
|
expect(canClaimSubmission(submission, currentUserId)).toBe(true);
|
|
});
|
|
|
|
it('should allow claiming submission with no lock time', () => {
|
|
const submission = {
|
|
assigned_to: otherUserId,
|
|
locked_until: null,
|
|
};
|
|
expect(canClaimSubmission(submission, currentUserId)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Expired Locks', () => {
|
|
it('should allow claiming submission with expired lock', () => {
|
|
const pastDate = new Date(Date.now() - 1000).toISOString();
|
|
const submission = {
|
|
assigned_to: otherUserId,
|
|
locked_until: pastDate,
|
|
};
|
|
expect(canClaimSubmission(submission, currentUserId)).toBe(true);
|
|
});
|
|
|
|
it('should allow claiming submission with lock expired 1 minute ago', () => {
|
|
const pastDate = new Date(Date.now() - 60000).toISOString();
|
|
const submission = {
|
|
assigned_to: otherUserId,
|
|
locked_until: pastDate,
|
|
};
|
|
expect(canClaimSubmission(submission, currentUserId)).toBe(true);
|
|
});
|
|
|
|
it('should allow claiming submission with lock expired 1 hour ago', () => {
|
|
const pastDate = new Date(Date.now() - 3600000).toISOString();
|
|
const submission = {
|
|
assigned_to: otherUserId,
|
|
locked_until: pastDate,
|
|
};
|
|
expect(canClaimSubmission(submission, currentUserId)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Active Locks by Others', () => {
|
|
it('should not allow claiming submission locked by another user', () => {
|
|
const futureDate = new Date(Date.now() + 60000).toISOString();
|
|
const submission = {
|
|
assigned_to: otherUserId,
|
|
locked_until: futureDate,
|
|
};
|
|
expect(canClaimSubmission(submission, currentUserId)).toBe(false);
|
|
});
|
|
|
|
it('should not allow claiming submission locked by another user (15 min lock)', () => {
|
|
const futureDate = new Date(Date.now() + 15 * 60000).toISOString();
|
|
const submission = {
|
|
assigned_to: otherUserId,
|
|
locked_until: futureDate,
|
|
};
|
|
expect(canClaimSubmission(submission, currentUserId)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Own Locks', () => {
|
|
it('should not allow claiming own locked submission', () => {
|
|
const futureDate = new Date(Date.now() + 60000).toISOString();
|
|
const submission = {
|
|
assigned_to: currentUserId,
|
|
locked_until: futureDate,
|
|
};
|
|
expect(canClaimSubmission(submission, currentUserId)).toBe(false);
|
|
});
|
|
|
|
it('should allow claiming own submission if lock expired', () => {
|
|
const pastDate = new Date(Date.now() - 1000).toISOString();
|
|
const submission = {
|
|
assigned_to: currentUserId,
|
|
locked_until: pastDate,
|
|
};
|
|
expect(canClaimSubmission(submission, currentUserId)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle submission assigned but no lock time', () => {
|
|
const submission = {
|
|
assigned_to: currentUserId,
|
|
locked_until: null,
|
|
};
|
|
expect(canClaimSubmission(submission, currentUserId)).toBe(true);
|
|
});
|
|
|
|
it('should handle lock time just expired (1ms ago)', () => {
|
|
const pastDate = new Date(Date.now() - 1).toISOString();
|
|
const submission = {
|
|
assigned_to: otherUserId,
|
|
locked_until: pastDate,
|
|
};
|
|
expect(canClaimSubmission(submission, currentUserId)).toBe(true);
|
|
});
|
|
|
|
it('should handle lock time about to expire (1ms future)', () => {
|
|
const futureDate = new Date(Date.now() + 1).toISOString();
|
|
const submission = {
|
|
assigned_to: otherUserId,
|
|
locked_until: futureDate,
|
|
};
|
|
expect(canClaimSubmission(submission, currentUserId)).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('isActiveLock', () => {
|
|
const assignedTo = 'user-123';
|
|
|
|
describe('Active Locks', () => {
|
|
it('should return true for active lock', () => {
|
|
const futureDate = new Date(Date.now() + 60000).toISOString();
|
|
expect(isActiveLock(assignedTo, futureDate)).toBe(true);
|
|
});
|
|
|
|
it('should return true for lock expiring in 1 second', () => {
|
|
const futureDate = new Date(Date.now() + 1000).toISOString();
|
|
expect(isActiveLock(assignedTo, futureDate)).toBe(true);
|
|
});
|
|
|
|
it('should return true for lock expiring in 15 minutes', () => {
|
|
const futureDate = new Date(Date.now() + 15 * 60000).toISOString();
|
|
expect(isActiveLock(assignedTo, futureDate)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Inactive Locks', () => {
|
|
it('should return false for expired lock', () => {
|
|
const pastDate = new Date(Date.now() - 1000).toISOString();
|
|
expect(isActiveLock(assignedTo, pastDate)).toBe(false);
|
|
});
|
|
|
|
it('should return false when no assignee', () => {
|
|
const futureDate = new Date(Date.now() + 60000).toISOString();
|
|
expect(isActiveLock(null, futureDate)).toBe(false);
|
|
});
|
|
|
|
it('should return false when no lock time', () => {
|
|
expect(isActiveLock(assignedTo, null)).toBe(false);
|
|
});
|
|
|
|
it('should return false when both null', () => {
|
|
expect(isActiveLock(null, null)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should return false for lock expired 1ms ago', () => {
|
|
const pastDate = new Date(Date.now() - 1).toISOString();
|
|
expect(isActiveLock(assignedTo, pastDate)).toBe(false);
|
|
});
|
|
|
|
it('should return true for lock expiring in 1ms', () => {
|
|
const futureDate = new Date(Date.now() + 1).toISOString();
|
|
expect(isActiveLock(assignedTo, futureDate)).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getLockStatus', () => {
|
|
const currentUserId = 'user-123';
|
|
const otherUserId = 'user-456';
|
|
|
|
describe('Unlocked Status', () => {
|
|
it('should return unlocked when no assignee', () => {
|
|
const submission = {
|
|
assigned_to: null,
|
|
locked_until: null,
|
|
};
|
|
expect(getLockStatus(submission, currentUserId)).toBe('unlocked');
|
|
});
|
|
|
|
it('should return unlocked when no lock time', () => {
|
|
const submission = {
|
|
assigned_to: null,
|
|
locked_until: new Date(Date.now() + 60000).toISOString(),
|
|
};
|
|
expect(getLockStatus(submission, currentUserId)).toBe('unlocked');
|
|
});
|
|
|
|
it('should return unlocked when assignee but no lock time', () => {
|
|
const submission = {
|
|
assigned_to: currentUserId,
|
|
locked_until: null,
|
|
};
|
|
expect(getLockStatus(submission, currentUserId)).toBe('unlocked');
|
|
});
|
|
});
|
|
|
|
describe('Expired Status', () => {
|
|
it('should return expired for expired lock by current user', () => {
|
|
const pastDate = new Date(Date.now() - 1000).toISOString();
|
|
const submission = {
|
|
assigned_to: currentUserId,
|
|
locked_until: pastDate,
|
|
};
|
|
expect(getLockStatus(submission, currentUserId)).toBe('expired');
|
|
});
|
|
|
|
it('should return expired for expired lock by other user', () => {
|
|
const pastDate = new Date(Date.now() - 1000).toISOString();
|
|
const submission = {
|
|
assigned_to: otherUserId,
|
|
locked_until: pastDate,
|
|
};
|
|
expect(getLockStatus(submission, currentUserId)).toBe('expired');
|
|
});
|
|
});
|
|
|
|
describe('Locked by Me Status', () => {
|
|
it('should return locked_by_me for active lock by current user', () => {
|
|
const futureDate = new Date(Date.now() + 60000).toISOString();
|
|
const submission = {
|
|
assigned_to: currentUserId,
|
|
locked_until: futureDate,
|
|
};
|
|
expect(getLockStatus(submission, currentUserId)).toBe('locked_by_me');
|
|
});
|
|
|
|
it('should return locked_by_me for lock expiring soon', () => {
|
|
const futureDate = new Date(Date.now() + 1000).toISOString();
|
|
const submission = {
|
|
assigned_to: currentUserId,
|
|
locked_until: futureDate,
|
|
};
|
|
expect(getLockStatus(submission, currentUserId)).toBe('locked_by_me');
|
|
});
|
|
});
|
|
|
|
describe('Locked by Other Status', () => {
|
|
it('should return locked_by_other for active lock by other user', () => {
|
|
const futureDate = new Date(Date.now() + 60000).toISOString();
|
|
const submission = {
|
|
assigned_to: otherUserId,
|
|
locked_until: futureDate,
|
|
};
|
|
expect(getLockStatus(submission, currentUserId)).toBe('locked_by_other');
|
|
});
|
|
|
|
it('should return locked_by_other for lock expiring soon by other user', () => {
|
|
const futureDate = new Date(Date.now() + 1000).toISOString();
|
|
const submission = {
|
|
assigned_to: otherUserId,
|
|
locked_until: futureDate,
|
|
};
|
|
expect(getLockStatus(submission, currentUserId)).toBe('locked_by_other');
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle lock expiring in 1ms by current user', () => {
|
|
const futureDate = new Date(Date.now() + 1).toISOString();
|
|
const submission = {
|
|
assigned_to: currentUserId,
|
|
locked_until: futureDate,
|
|
};
|
|
expect(getLockStatus(submission, currentUserId)).toBe('locked_by_me');
|
|
});
|
|
|
|
it('should handle lock expired 1ms ago', () => {
|
|
const pastDate = new Date(Date.now() - 1).toISOString();
|
|
const submission = {
|
|
assigned_to: currentUserId,
|
|
locked_until: pastDate,
|
|
};
|
|
expect(getLockStatus(submission, currentUserId)).toBe('expired');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('formatLockExpiry', () => {
|
|
describe('Active Locks', () => {
|
|
it('should format 1 minute remaining', () => {
|
|
const futureDate = new Date(Date.now() + 60000).toISOString();
|
|
const result = formatLockExpiry(futureDate);
|
|
expect(result).toBe('1:00');
|
|
});
|
|
|
|
it('should format 5 minutes 30 seconds remaining', () => {
|
|
const futureDate = new Date(Date.now() + 5.5 * 60000).toISOString();
|
|
const result = formatLockExpiry(futureDate);
|
|
expect(result).toBe('5:30');
|
|
});
|
|
|
|
it('should format 10 seconds remaining', () => {
|
|
const futureDate = new Date(Date.now() + 10000).toISOString();
|
|
const result = formatLockExpiry(futureDate);
|
|
expect(result).toBe('0:10');
|
|
});
|
|
|
|
it('should format 59 seconds remaining', () => {
|
|
const futureDate = new Date(Date.now() + 59000).toISOString();
|
|
const result = formatLockExpiry(futureDate);
|
|
expect(result).toBe('0:59');
|
|
});
|
|
|
|
it('should format 15 minutes remaining', () => {
|
|
const futureDate = new Date(Date.now() + 15 * 60000).toISOString();
|
|
const result = formatLockExpiry(futureDate);
|
|
expect(result).toBe('15:00');
|
|
});
|
|
|
|
it('should pad single digit seconds with zero', () => {
|
|
const futureDate = new Date(Date.now() + 65000).toISOString(); // 1:05
|
|
const result = formatLockExpiry(futureDate);
|
|
expect(result).toBe('1:05');
|
|
});
|
|
});
|
|
|
|
describe('Expired Locks', () => {
|
|
it('should return "Expired" for expired lock', () => {
|
|
const pastDate = new Date(Date.now() - 1000).toISOString();
|
|
const result = formatLockExpiry(pastDate);
|
|
expect(result).toBe('Expired');
|
|
});
|
|
|
|
it('should return "Expired" for lock expired 1 minute ago', () => {
|
|
const pastDate = new Date(Date.now() - 60000).toISOString();
|
|
const result = formatLockExpiry(pastDate);
|
|
expect(result).toBe('Expired');
|
|
});
|
|
|
|
it('should return "Expired" for lock expired 1 hour ago', () => {
|
|
const pastDate = new Date(Date.now() - 3600000).toISOString();
|
|
const result = formatLockExpiry(pastDate);
|
|
expect(result).toBe('Expired');
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle lock expiring in 1 second', () => {
|
|
const futureDate = new Date(Date.now() + 1000).toISOString();
|
|
const result = formatLockExpiry(futureDate);
|
|
expect(result).toBe('0:01');
|
|
});
|
|
|
|
it('should handle lock expiring right now', () => {
|
|
const futureDate = new Date(Date.now()).toISOString();
|
|
const result = formatLockExpiry(futureDate);
|
|
expect(result).toBe('Expired');
|
|
});
|
|
|
|
it('should handle lock expiring in less than 1 second', () => {
|
|
const futureDate = new Date(Date.now() + 500).toISOString();
|
|
const result = formatLockExpiry(futureDate);
|
|
expect(result).toBe('0:00');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getLockUrgency', () => {
|
|
describe('Critical Urgency', () => {
|
|
it('should return critical for 1 minute remaining', () => {
|
|
const timeLeftMs = 60000;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('critical');
|
|
});
|
|
|
|
it('should return critical for 30 seconds remaining', () => {
|
|
const timeLeftMs = 30000;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('critical');
|
|
});
|
|
|
|
it('should return critical for 1 second remaining', () => {
|
|
const timeLeftMs = 1000;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('critical');
|
|
});
|
|
|
|
it('should return critical for exactly 2 minutes - 1ms remaining', () => {
|
|
const timeLeftMs = 2 * 60 * 1000 - 1;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('critical');
|
|
});
|
|
|
|
it('should return critical for 0 seconds remaining', () => {
|
|
const timeLeftMs = 0;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('critical');
|
|
});
|
|
|
|
it('should return critical for negative time (expired)', () => {
|
|
const timeLeftMs = -1000;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('critical');
|
|
});
|
|
});
|
|
|
|
describe('Warning Urgency', () => {
|
|
it('should return warning for 4 minutes remaining', () => {
|
|
const timeLeftMs = 4 * 60000;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('warning');
|
|
});
|
|
|
|
it('should return warning for 3 minutes remaining', () => {
|
|
const timeLeftMs = 3 * 60000;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('warning');
|
|
});
|
|
|
|
it('should return warning for exactly 2 minutes remaining', () => {
|
|
const timeLeftMs = 2 * 60 * 1000;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('warning');
|
|
});
|
|
|
|
it('should return warning for exactly 5 minutes - 1ms remaining', () => {
|
|
const timeLeftMs = 5 * 60 * 1000 - 1;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('warning');
|
|
});
|
|
});
|
|
|
|
describe('Normal Urgency', () => {
|
|
it('should return normal for 6 minutes remaining', () => {
|
|
const timeLeftMs = 6 * 60000;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('normal');
|
|
});
|
|
|
|
it('should return normal for 10 minutes remaining', () => {
|
|
const timeLeftMs = 10 * 60000;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('normal');
|
|
});
|
|
|
|
it('should return normal for 15 minutes remaining', () => {
|
|
const timeLeftMs = 15 * 60000;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('normal');
|
|
});
|
|
|
|
it('should return normal for exactly 5 minutes remaining', () => {
|
|
const timeLeftMs = 5 * 60 * 1000;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('normal');
|
|
});
|
|
|
|
it('should return normal for 1 hour remaining', () => {
|
|
const timeLeftMs = 60 * 60000;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('normal');
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle very large time values', () => {
|
|
const timeLeftMs = 999999999;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('normal');
|
|
});
|
|
|
|
it('should handle very negative time values', () => {
|
|
const timeLeftMs = -999999999;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('critical');
|
|
});
|
|
|
|
it('should handle exactly at 2 minute boundary', () => {
|
|
const timeLeftMs = 2 * 60 * 1000;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('warning');
|
|
});
|
|
|
|
it('should handle exactly at 5 minute boundary', () => {
|
|
const timeLeftMs = 5 * 60 * 1000;
|
|
expect(getLockUrgency(timeLeftMs)).toBe('normal');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Type Safety', () => {
|
|
it('should enforce LockStatus type', () => {
|
|
const validStatuses: LockStatus[] = [
|
|
'locked_by_me',
|
|
'locked_by_other',
|
|
'unlocked',
|
|
'expired',
|
|
];
|
|
|
|
validStatuses.forEach(status => {
|
|
expect(['locked_by_me', 'locked_by_other', 'unlocked', 'expired']).toContain(status);
|
|
});
|
|
});
|
|
|
|
it('should enforce LockUrgency type', () => {
|
|
const validUrgencies: LockUrgency[] = [
|
|
'critical',
|
|
'warning',
|
|
'normal',
|
|
];
|
|
|
|
validUrgencies.forEach(urgency => {
|
|
expect(['critical', 'warning', 'normal']).toContain(urgency);
|
|
});
|
|
});
|
|
});
|