forked from mirror/crumbs-original
- notes-service.test.ts (24 tests): listNotes, getNote, createNote, updateNote, deleteNote, searchNotes, reorderNotes with owner/collaborator patterns - api-utils.test.ts (10 tests): canAccessNote, requireNoteAccess, requireNoteOwnership, getUserId - rate-limit.test.ts (8 tests): checkRateLimit, recordLoginAttempt, clearOldAttempts - tags.test.ts (6 tests): syncNoteTags, fetchTagsForNotes - collaborators.test.ts (4 tests): fetchCollaboratorsForNotes, addCollaborator, removeCollaborator Also fixes test-helpers.ts missing thumbnail_path/featured columns and rate-limit.ts raw SQL Date binding issue. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
101 lines
3.1 KiB
TypeScript
101 lines
3.1 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { createTestDb } from './db/test-helpers.js';
|
|
import { loginAttempts } from './db/schema.js';
|
|
import type { Db } from './db/index.js';
|
|
import { checkRateLimit, recordLoginAttempt, clearOldAttempts } from './rate-limit.js';
|
|
|
|
let db: Db;
|
|
|
|
beforeEach(() => {
|
|
const testDb = createTestDb();
|
|
db = testDb.db;
|
|
});
|
|
|
|
const IP = '192.168.1.1';
|
|
const EMAIL = 'test@test.com';
|
|
|
|
function addFailedAttempts(count: number, now: Date) {
|
|
for (let i = 0; i < count; i++) {
|
|
recordLoginAttempt(db, IP, EMAIL, false, now);
|
|
}
|
|
}
|
|
|
|
describe('checkRateLimit', () => {
|
|
it('should allow first attempt', () => {
|
|
const result = checkRateLimit(db, IP, EMAIL);
|
|
expect(result.allowed).toBe(true);
|
|
});
|
|
|
|
it('should allow up to 4 failed attempts', () => {
|
|
const now = new Date();
|
|
addFailedAttempts(4, now);
|
|
const result = checkRateLimit(db, IP, EMAIL, now);
|
|
expect(result.allowed).toBe(true);
|
|
});
|
|
|
|
it('should block after 5 failed attempts within window', () => {
|
|
const now = new Date();
|
|
addFailedAttempts(5, now);
|
|
const result = checkRateLimit(db, IP, EMAIL, now);
|
|
expect(result.allowed).toBe(false);
|
|
expect(result.retryAfter).toBeDefined();
|
|
expect(result.retryAfter!).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should return correct retryAfter value', () => {
|
|
const now = new Date();
|
|
addFailedAttempts(5, now);
|
|
const result = checkRateLimit(db, IP, EMAIL, now);
|
|
// Lockout is 15 minutes (900 seconds)
|
|
expect(result.retryAfter).toBe(900);
|
|
});
|
|
|
|
it('should allow again after lockout expires', () => {
|
|
const attemptTime = new Date('2024-01-01T00:00:00Z');
|
|
addFailedAttempts(5, attemptTime);
|
|
|
|
// Check at attempt time — should be blocked
|
|
const blocked = checkRateLimit(db, IP, EMAIL, attemptTime);
|
|
expect(blocked.allowed).toBe(false);
|
|
|
|
// Check 16 minutes later — should be allowed
|
|
const later = new Date(attemptTime.getTime() + 16 * 60 * 1000);
|
|
const allowed = checkRateLimit(db, IP, EMAIL, later);
|
|
expect(allowed.allowed).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('recordLoginAttempt', () => {
|
|
it('should insert attempt record', () => {
|
|
recordLoginAttempt(db, IP, EMAIL, false);
|
|
const rows = db.select().from(loginAttempts).all();
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].ip).toBe(IP);
|
|
expect(rows[0].email).toBe(EMAIL);
|
|
});
|
|
|
|
it('should record success/failure flag correctly', () => {
|
|
recordLoginAttempt(db, IP, EMAIL, true);
|
|
recordLoginAttempt(db, IP, EMAIL, false);
|
|
const rows = db.select().from(loginAttempts).all();
|
|
expect(rows[0].success).toBe(true);
|
|
expect(rows[1].success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('clearOldAttempts', () => {
|
|
it('should remove attempts older than 2x lockout', () => {
|
|
const old = new Date('2024-01-01T00:00:00Z');
|
|
const recent = new Date('2024-01-01T01:00:00Z');
|
|
recordLoginAttempt(db, IP, EMAIL, false, old);
|
|
recordLoginAttempt(db, IP, EMAIL, false, recent);
|
|
|
|
// Clear at a time where old is > 2x lockout (30 min) ago but recent is not
|
|
const clearTime = new Date(old.getTime() + 31 * 60 * 1000);
|
|
clearOldAttempts(db, clearTime);
|
|
|
|
const rows = db.select().from(loginAttempts).all();
|
|
expect(rows).toHaveLength(1);
|
|
});
|
|
});
|