- Move plans/ to dev-docs/, delete unused scaffold SVGs - Create .dockerignore, rename container to immich-folio - Extract BackLink component and toPhotoItems() helper in page.tsx - Add keyboard accessibility (role=button, tabIndex, onKeyDown) to PhotoGrid - Add aria-label to Lightbox EXIF toggle - Fix React lint: replace setState-in-effect with useState initializer - Add rate limiting to /api/auth (brute-force protection, 10 RPM) - Add HSTS and X-DNS-Prefetch-Control security headers - Add Secure flag to auth cookie in production - New unit tests: rate-limit.test.ts (5), auth.test.ts (12) - All 47 tests pass, 0 lint errors, build clean
61 lines
1.8 KiB
TypeScript
61 lines
1.8 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { checkRateLimit } from '@/lib/rate-limit';
|
|
|
|
describe('checkRateLimit', () => {
|
|
// Use unique IP prefixes per test to avoid cross-contamination
|
|
let testIp: string;
|
|
let counter = 0;
|
|
|
|
beforeEach(() => {
|
|
counter++;
|
|
testIp = `test-ip-${counter}-${Date.now()}`;
|
|
});
|
|
|
|
it('allows the first request', () => {
|
|
const result = checkRateLimit(testIp, 5);
|
|
expect(result.success).toBe(true);
|
|
expect(result.remaining).toBe(4);
|
|
});
|
|
|
|
it('tracks remaining count correctly', () => {
|
|
checkRateLimit(testIp, 5);
|
|
const result = checkRateLimit(testIp, 5);
|
|
expect(result.success).toBe(true);
|
|
expect(result.remaining).toBe(3);
|
|
});
|
|
|
|
it('blocks requests exceeding the limit', () => {
|
|
const maxRpm = 3;
|
|
checkRateLimit(testIp, maxRpm); // 1
|
|
checkRateLimit(testIp, maxRpm); // 2
|
|
checkRateLimit(testIp, maxRpm); // 3
|
|
|
|
const result = checkRateLimit(testIp, maxRpm); // 4 — blocked
|
|
expect(result.success).toBe(false);
|
|
expect(result.remaining).toBe(0);
|
|
});
|
|
|
|
it('tracks separate IPs independently', () => {
|
|
const ip1 = `${testIp}-a`;
|
|
const ip2 = `${testIp}-b`;
|
|
|
|
checkRateLimit(ip1, 2);
|
|
checkRateLimit(ip1, 2);
|
|
const blocked = checkRateLimit(ip1, 2);
|
|
expect(blocked.success).toBe(false);
|
|
|
|
// ip2 should still be allowed
|
|
const allowed = checkRateLimit(ip2, 2);
|
|
expect(allowed.success).toBe(true);
|
|
expect(allowed.remaining).toBe(1);
|
|
});
|
|
|
|
it('returns a future resetAt timestamp', () => {
|
|
const before = Date.now();
|
|
const result = checkRateLimit(testIp, 5);
|
|
expect(result.resetAt).toBeGreaterThan(before);
|
|
// Should be within ~60 seconds from now
|
|
expect(result.resetAt).toBeLessThanOrEqual(before + 61_000);
|
|
});
|
|
});
|