chore: remediate pygienium audit findings

Dead code: 77 verified-unused exports, files (BackArrow, MenuBars,
cookies.ts, db/create.ts, schemas/comment.ts, security-headers.ts) and
12 unused dependencies removed
Comments: ~370 RESTATE comments stripped across 53 files; 2 verbose
blocks tightened; dead commented-out config removed
Complexity: bulkUpsert extracted into 11 per-entity helpers (CCN 156->~10);
login formHandler split into 3 submitters (CCN 63->~5); account page render
split into 8 section components (CCN 40); updatePost SQL builder rebuilt;
assert*Owned consolidated behind generic assertOwnedBy
Defensive guards: 4 redundant rethrow/nullish guards removed
This commit is contained in:
2026-08-11 13:36:18 -04:00
parent 33ca9213f2
commit 898c891bd5
76 changed files with 1437 additions and 2600 deletions

View File

@@ -21,7 +21,6 @@ describe("CSRF Protection", () => {
const token = generateCSRFToken();
expect(token).toBeDefined();
expect(typeof token).toBe("string");
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
expect(token).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
);
@@ -34,7 +33,6 @@ describe("CSRF Protection", () => {
});
it("should generate cryptographically secure tokens", () => {
// Generate multiple tokens and ensure no collisions
const tokens = new Set<string>();
for (let i = 0; i < 1000; i++) {
tokens.add(generateCSRFToken());
@@ -50,7 +48,6 @@ describe("CSRF Protection", () => {
expect(token).toBeDefined();
expect(typeof token).toBe("string");
// Token should be a UUID
expect(token).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
);
@@ -122,7 +119,6 @@ describe("CSRF Protection", () => {
const invalidToken1 = "b".repeat(36);
const invalidToken2 = "b".repeat(35) + "a";
// Test timing for completely different tokens
const event1 = createMockEvent({
headers: { "x-csrf-token": invalidToken1 },
cookies: { "csrf-token": validToken }
@@ -132,7 +128,6 @@ describe("CSRF Protection", () => {
validateCSRFToken(event1);
const time1 = performance.now() - start1;
// Test timing for tokens that differ only at the end
const event2 = createMockEvent({
headers: { "x-csrf-token": invalidToken2 },
cookies: { "csrf-token": validToken }
@@ -142,7 +137,6 @@ describe("CSRF Protection", () => {
validateCSRFToken(event2);
const time2 = performance.now() - start2;
// Timing difference should be minimal (less than 1ms)
// This tests for constant-time comparison
const timeDiff = Math.abs(time1 - time2);
expect(timeDiff).toBeLessThan(1);
@@ -161,7 +155,6 @@ describe("CSRF Protection", () => {
describe("CSRF Attack Scenarios", () => {
it("should prevent basic CSRF attack", () => {
// Attacker doesn't have access to the CSRF token cookie
const attackEvent = createMockEvent({
headers: { "x-csrf-token": "attacker-guessed-token" }
});
@@ -174,7 +167,6 @@ describe("CSRF Protection", () => {
const token1 = generateCSRFToken();
const token2 = generateCSRFToken();
// User has token1, attacker tries to use token2
const event = createMockEvent({
headers: { "x-csrf-token": token2 },
cookies: { "csrf-token": token1 }
@@ -198,7 +190,6 @@ describe("CSRF Protection", () => {
});
it("should prevent replay attacks with old tokens", () => {
// Simulate an old token that was captured
const oldToken = "old-captured-token-12345";
const event = createMockEvent({
@@ -206,10 +197,8 @@ describe("CSRF Protection", () => {
cookies: { "csrf-token": oldToken }
});
// Even if tokens match, they should be validated by the system
// This test validates the structure works correctly
const isValid = validateCSRFToken(event);
expect(isValid).toBe(true); // Matches are valid
expect(isValid).toBe(true);
});
});
@@ -270,9 +259,7 @@ describe("CSRF Protection", () => {
tokens.push(generateCSRFToken());
}
// Check for sequential patterns
for (let i = 1; i < tokens.length; i++) {
// Tokens should not be incrementing
expect(tokens[i]).not.toBe(
String(Number(tokens[i - 1].replace(/-/g, "")) + 1)
);
@@ -281,11 +268,9 @@ describe("CSRF Protection", () => {
it("should generate tokens with sufficient entropy", () => {
const token = generateCSRFToken();
// UUID without dashes should be 32 hex characters
const hexString = token.replace(/-/g, "");
expect(hexString).toMatch(/^[0-9a-f]{32}$/i);
// Check that not all characters are the same
const uniqueChars = new Set(hexString.split(""));
expect(uniqueChars.size).toBeGreaterThan(5);
});
@@ -299,7 +284,6 @@ describe("CSRF Protection", () => {
}
const duration = performance.now() - start;
// Should generate 1000 tokens in less than 100ms
expect(duration).toBeLessThan(100);
});
@@ -316,7 +300,6 @@ describe("CSRF Protection", () => {
}
const duration = performance.now() - start;
// Should validate 10000 tokens in less than 100ms
expect(duration).toBeLessThan(100);
});
});
@@ -402,7 +385,6 @@ describe("CSRF Protection", () => {
const sessionAToken = generateCSRFToken();
const sessionBToken = generateCSRFToken();
// Session A's cookie with Session B's header token
const event = createMockEvent({
headers: { "x-csrf-token": sessionBToken },
cookies: { "csrf-token": sessionAToken }
@@ -522,12 +504,10 @@ describe("CSRF Protection", () => {
it("should issue CSRF token on setCSRFToken then validate it", () => {
const event = createMockEvent({});
// Step 1: Login issues CSRF token
const token = setCSRFToken(event);
expect(token).toBeDefined();
expect(typeof token).toBe("string");
// Step 2: Subsequent mutation sends token back
const mutationEvent = createMockEvent({
headers: { "x-csrf-token": token },
cookies: { "csrf-token": token }
@@ -538,7 +518,6 @@ describe("CSRF Protection", () => {
});
it("should reject cross-origin POST without CSRF token", () => {
// Simulated cross-site POST: attacker can read cookies but not set headers
const attackEvent = createMockEvent({
// No x-csrf-token header (cross-origin requests can't set custom headers)
cookies: { "csrf-token": "victim-token" }

View File

@@ -59,7 +59,6 @@ describe("Input Validation and Injection Prevention", () => {
for (const email of sqlEmails) {
// Either reject as invalid, or it's properly escaped in queries
const isValid = isValidEmail(email);
// Test documents the behavior
expect(typeof isValid).toBe("boolean");
}
});
@@ -68,7 +67,6 @@ describe("Input Validation and Injection Prevention", () => {
const longEmail = "a".repeat(1000) + "@example.com";
const result = isValidEmail(longEmail);
// Should handle gracefully
expect(typeof result).toBe("boolean");
});
@@ -130,7 +128,6 @@ describe("Input Validation and Injection Prevention", () => {
it("should use parameterized queries for user authentication", async () => {
const conn = ConnectionFactory();
// Test that SQL injection attempts don't work
const maliciousEmail = "admin'--";
try {
@@ -140,7 +137,6 @@ describe("Input Validation and Injection Prevention", () => {
args: [maliciousEmail]
});
// Should return no results (no user with that exact email)
expect(result.rows.length).toBe(0);
} catch (error) {
// If error, ensure it's not a SQL error
@@ -153,7 +149,6 @@ describe("Input Validation and Injection Prevention", () => {
for (const payload of SQL_INJECTION_PAYLOADS) {
try {
// Test various injection points
await conn.execute({
sql: "SELECT * FROM User WHERE email = ?",
args: [payload]
@@ -164,7 +159,6 @@ describe("Input Validation and Injection Prevention", () => {
args: [payload]
});
// Queries should complete without SQL errors
expect(true).toBe(true);
} catch (error: any) {
// If error occurs, should not be SQL injection syntax error
@@ -184,10 +178,8 @@ describe("Input Validation and Injection Prevention", () => {
args: [unionPayload]
});
// Should not return password hashes
if (result.rows.length > 0) {
for (const row of result.rows) {
// Ensure we don't get password_hash column
expect(row).not.toHaveProperty("password_hash");
}
}
@@ -200,7 +192,6 @@ describe("Input Validation and Injection Prevention", () => {
it("should prevent blind SQL injection timing attacks", async () => {
const conn = ConnectionFactory();
// Timing-based payload
const timingPayload = "admin' AND SLEEP(5)--";
const start = performance.now();
@@ -210,18 +201,15 @@ describe("Input Validation and Injection Prevention", () => {
args: [timingPayload]
});
} catch (error) {
// Ignore errors
}
const duration = performance.now() - start;
// Should not delay for 5 seconds
expect(duration).toBeLessThan(1000);
});
it("should prevent second-order SQL injection", async () => {
const conn = ConnectionFactory();
// Store malicious data
const maliciousName = "admin'--";
try {
@@ -236,7 +224,6 @@ describe("Input Validation and Injection Prevention", () => {
]
});
// Retrieve and use (should still be safe with parameterized queries)
const result = await conn.execute({
sql: "SELECT display_name FROM User WHERE email = ?",
args: ["test-sqli@example.com"]
@@ -244,13 +231,11 @@ describe("Input Validation and Injection Prevention", () => {
expect(result.rows.length).toBeGreaterThanOrEqual(0);
// Cleanup
await conn.execute({
sql: "DELETE FROM User WHERE email = ?",
args: ["test-sqli@example.com"]
});
} catch (error) {
// Should not have SQL syntax errors
expect(error).toBeDefined();
}
});
@@ -260,7 +245,6 @@ describe("Input Validation and Injection Prevention", () => {
it("should identify potentially dangerous XSS patterns", () => {
// These payloads should be handled by frontend sanitization
for (const payload of XSS_PAYLOADS) {
// Document that these patterns exist
expect(payload).toBeDefined();
expect(typeof payload).toBe("string");
@@ -272,11 +256,9 @@ describe("Input Validation and Injection Prevention", () => {
it("should handle script tags in user input", () => {
const scriptInput = "<script>alert('XSS')</script>";
// Validation should not crash
const nameValid = isValidDisplayName(scriptInput);
expect(typeof nameValid).toBe("boolean");
// Email validation
const emailValid = isValidEmail(scriptInput);
expect(typeof emailValid).toBe("boolean");
});
@@ -450,7 +432,6 @@ describe("Input Validation and Injection Prevention", () => {
expect(typeof emailValid).toBe("boolean");
expect(typeof nameValid).toBe("boolean");
// Should complete quickly (no ReDoS)
expect(duration).toBeLessThan(100);
});
@@ -508,14 +489,12 @@ describe("Input Validation and Injection Prevention", () => {
});
it("should not be vulnerable to ReDoS attacks", () => {
// ReDoS payload with many repetitions
const redosPayload = "a".repeat(1000) + "!";
const start = performance.now();
validatePassword(redosPayload);
const duration = performance.now() - start;
// Should complete quickly
expect(duration).toBeLessThan(100);
});
});

View File

@@ -20,7 +20,6 @@ describe("Password Security", () => {
expect(hash).toBeDefined();
expect(typeof hash).toBe("string");
// Bcrypt hashes start with $2b$ or $2a$
expect(hash).toMatch(/^\$2[ab]\$/);
});
@@ -36,7 +35,6 @@ describe("Password Security", () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
// Bcrypt hashes are 60 characters long
expect(hash.length).toBe(60);
});
@@ -132,32 +130,26 @@ describe("Password Security", () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
// Measure time for correct password
const { duration: correctDuration } = await measureTime(() =>
checkPasswordSafe(password, hash)
);
// Measure time for incorrect password
const { duration: incorrectDuration } = await measureTime(() =>
checkPasswordSafe("WrongPassword123!", hash)
);
// Bcrypt comparison should take similar time regardless
const timingDifference = Math.abs(correctDuration - incorrectDuration);
// Allow reasonable variance (bcrypt is inherently slow)
expect(timingDifference).toBeLessThan(50);
});
it("should handle null hash without timing leak", async () => {
const password = "TestPassword123!";
// Measure time for null hash
const { result: result1, duration: duration1 } = await measureTime(() =>
checkPasswordSafe(password, null)
);
// Measure time for undefined hash
const { result: result2, duration: duration2 } = await measureTime(() =>
checkPasswordSafe(password, undefined)
);
@@ -165,7 +157,6 @@ describe("Password Security", () => {
expect(result1).toBe(false);
expect(result2).toBe(false);
// Should take similar time
const timingDifference = Math.abs(duration1 - duration2);
expect(timingDifference).toBeLessThan(50);
});
@@ -178,7 +169,6 @@ describe("Password Security", () => {
checkPasswordSafe(password, null)
);
// Should take at least a few milliseconds (bcrypt is slow)
expect(duration).toBeGreaterThan(1);
});
@@ -186,12 +176,10 @@ describe("Password Security", () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
// User exists
const { duration: existsDuration } = await measureTime(() =>
checkPasswordSafe("WrongPassword", hash)
);
// User doesn't exist (null hash)
const { duration: notExistsDuration } = await measureTime(() =>
checkPasswordSafe("WrongPassword", null)
);
@@ -280,9 +268,9 @@ describe("Password Security", () => {
});
it("should calculate password strength correctly", () => {
const fairPassword = "MyP@ssw0rd12"; // 12 chars
const goodPassword = "MyStr0ng!P@ssw0rd"; // 17 chars
const strongPassword = "MyV3ry!Str0ng@P@ssw0rd123"; // 25 chars
const fairPassword = "MyP@ssw0rd12";
const goodPassword = "MyStr0ng!P@ssw0rd";
const strongPassword = "MyV3ry!Str0ng@P@ssw0rd123";
expect(validatePassword(fairPassword).strength).toBe("fair");
expect(validatePassword(goodPassword).strength).toBe("good");
@@ -337,7 +325,6 @@ describe("Password Security", () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
// Measure time for multiple checks (simulating brute force)
const start = performance.now();
const attempts = 10;
@@ -348,7 +335,6 @@ describe("Password Security", () => {
const duration = performance.now() - start;
const avgPerAttempt = duration / attempts;
// Each attempt should take significant time (bcrypt is slow)
// This makes brute force impractical
expect(avgPerAttempt).toBeGreaterThan(5); // At least 5ms per attempt
});
@@ -356,18 +342,15 @@ describe("Password Security", () => {
it("should prevent rainbow table attacks with unique salts", async () => {
const password = "CommonPassword123!";
// Generate multiple hashes for same password
const hashes = await Promise.all(
Array.from({ length: 10 }, () => hashPassword(password))
);
// All hashes should be unique (different salts)
const uniqueHashes = new Set(hashes);
expect(uniqueHashes.size).toBe(10);
});
it("should prevent password spraying with validation", () => {
// Common passwords that should be rejected
const commonPasswords = [
"Password123!",
"Welcome123!",
@@ -382,7 +365,6 @@ describe("Password Security", () => {
});
it("should resist dictionary attacks", () => {
// Dictionary words that should be caught
const dictionaryBased = ["Sunshine123!", "Princess456!", "Dragon789!@"];
for (const password of dictionaryBased) {
@@ -394,7 +376,7 @@ describe("Password Security", () => {
describe("Edge Cases", () => {
it("should handle very long passwords", async () => {
const longPassword = "A1!a" + "x".repeat(1000); // Very long but valid
const longPassword = "A1!a" + "x".repeat(1000);
const hash = await hashPassword(longPassword);
const match = await checkPassword(longPassword, hash);
@@ -413,7 +395,6 @@ describe("Password Security", () => {
const hash = await hashPassword(nullBytePassword);
const match = await checkPassword(nullBytePassword, hash);
// Behavior may vary - just ensure no crash
expect(typeof match).toBe("boolean");
});
@@ -450,7 +431,6 @@ describe("Password Security", () => {
const duration = performance.now() - start;
// Bcrypt should be slow enough to deter brute force
// With 10 rounds, should take at least a few milliseconds
expect(duration).toBeGreaterThan(5);
// But not too slow for normal operation
expect(duration).toBeLessThan(500);
@@ -467,11 +447,9 @@ describe("Password Security", () => {
durations.push(performance.now() - start);
}
// Timing should be relatively consistent
const avg = durations.reduce((a, b) => a + b, 0) / durations.length;
const maxDeviation = Math.max(...durations.map((d) => Math.abs(d - avg)));
// Allow reasonable variance
expect(maxDeviation).toBeLessThan(avg * 0.5);
});
@@ -484,7 +462,6 @@ describe("Password Security", () => {
}
const duration = performance.now() - start;
// Validation is CPU-bound but should be fast
expect(duration).toBeLessThan(100);
});
});
@@ -494,12 +471,9 @@ describe("Password Security", () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
// Check that hash uses correct salt rounds
// Bcrypt format: $2b$rounds$salthash
const parts = hash.split("$");
const rounds = parseInt(parts[2]);
// Should use 10 rounds (from password.ts)
expect(rounds).toBe(10);
});
@@ -509,17 +483,14 @@ describe("Password Security", () => {
Array.from({ length: 100 }, () => hashPassword(password))
);
// Extract salts from hashes
const salts = hashes.map((hash) => {
const parts = hash.split("$");
return parts[3].substring(0, 22); // Salt is 22 characters
});
// All salts should be unique
const uniqueSalts = new Set(salts);
expect(uniqueSalts.size).toBe(100);
// Check for patterns in salts (should be random)
for (let i = 1; i < salts.length; i++) {
// Salts should not be sequential or predictable
expect(salts[i]).not.toBe(salts[i - 1]);

View File

@@ -60,12 +60,10 @@ describe("Rate Limiting", () => {
const maxAttempts = 3;
const windowMs = 60000;
// Use up all attempts
for (let i = 0; i < maxAttempts; i++) {
await checkRateLimit(identifier, maxAttempts, windowMs);
}
// Next attempt should throw
try {
await checkRateLimit(identifier, maxAttempts, windowMs);
expect.unreachable("Should have thrown");
@@ -79,7 +77,6 @@ describe("Rate Limiting", () => {
const maxAttempts = 2;
const windowMs = 60000;
// Use up all attempts
await checkRateLimit(identifier, maxAttempts, windowMs);
await checkRateLimit(identifier, maxAttempts, windowMs);
@@ -99,12 +96,10 @@ describe("Rate Limiting", () => {
const maxAttempts = 3;
const windowMs = 500; // 500ms window for testing
// Use up all attempts
for (let i = 0; i < maxAttempts; i++) {
await checkRateLimit(identifier, maxAttempts, windowMs);
}
// Should be blocked immediately after
try {
await checkRateLimit(identifier, maxAttempts, windowMs);
expect.unreachable("Should have thrown");
@@ -112,10 +107,8 @@ describe("Rate Limiting", () => {
expect(error).toBeInstanceOf(TRPCError);
}
// Wait for window to expire
await new Promise((resolve) => setTimeout(resolve, 600));
// Should be allowed again
const remaining = await checkRateLimit(identifier, maxAttempts, windowMs);
expect(remaining).toBe(maxAttempts - 1);
});
@@ -125,13 +118,11 @@ describe("Rate Limiting", () => {
const maxAttempts = 10;
const windowMs = 60000;
// Simulate concurrent requests
const results: number[] = [];
for (let i = 0; i < maxAttempts; i++) {
results.push(await checkRateLimit(identifier, maxAttempts, windowMs));
}
// All should succeed with decreasing remaining counts
expect(results).toEqual([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]);
});
@@ -142,12 +133,10 @@ describe("Rate Limiting", () => {
const id1 = uniqueId("test1");
const id2 = uniqueId("test2");
// Use up attempts for id1
for (let i = 0; i < maxAttempts; i++) {
await checkRateLimit(id1, maxAttempts, windowMs);
}
// id1 should be blocked
try {
await checkRateLimit(id1, maxAttempts, windowMs);
expect.unreachable("Should have thrown");
@@ -155,7 +144,6 @@ describe("Rate Limiting", () => {
expect(error).toBeInstanceOf(TRPCError);
}
// id2 should still work
const remaining = await checkRateLimit(id2, maxAttempts, windowMs);
expect(remaining).toBe(maxAttempts - 1);
});
@@ -225,12 +213,10 @@ describe("Rate Limiting", () => {
const email = `test-${Date.now()}@example.com`;
// IP rate limiting is skipped in test/dev, so only email limit applies
// Use up email rate limit with same email
for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) {
await rateLimitLogin(email, ip);
}
// Next attempt should fail due to email limit
try {
await rateLimitLogin(email, ip);
expect.unreachable("Should have thrown");
@@ -242,12 +228,10 @@ describe("Rate Limiting", () => {
it("should limit by email independently of IP", async () => {
const email = `test-${Date.now()}@example.com`;
// Use different IPs but same email
for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) {
await rateLimitLogin(email, randomIP());
}
// Next attempt with different IP should still fail due to email limit
try {
await rateLimitLogin(email, randomIP());
expect.unreachable("Should have thrown");
@@ -260,13 +244,11 @@ describe("Rate Limiting", () => {
const ip = randomIP();
// In test/dev, IP rate limiting is skipped
// Should allow many different emails from same IP
for (let i = 0; i < 10; i++) {
const email = `test${i}-${Date.now()}@example.com`;
await rateLimitLogin(email, ip);
}
// Should not throw since IP limits are disabled in test/dev
expect(true).toBe(true);
});
});
@@ -276,12 +258,10 @@ describe("Rate Limiting", () => {
const ip = randomIP();
// IP rate limiting is skipped in test/dev
// Should allow many attempts
for (let i = 0; i < 10; i++) {
await rateLimitPasswordReset(ip);
}
// Should not throw in test/dev
expect(true).toBe(true);
});
@@ -304,12 +284,10 @@ describe("Rate Limiting", () => {
const ip = randomIP();
// IP rate limiting is skipped in test/dev
// Should allow many attempts
for (let i = 0; i < 10; i++) {
await rateLimitRegistration(ip);
}
// Should not throw in test/dev
expect(true).toBe(true);
});
});
@@ -319,12 +297,10 @@ describe("Rate Limiting", () => {
const ip = randomIP();
// IP rate limiting is skipped in test/dev
// Should allow many attempts
for (let i = 0; i < 10; i++) {
await rateLimitEmailVerification(ip);
}
// Should not throw in test/dev
expect(true).toBe(true);
});
});
@@ -334,7 +310,6 @@ describe("Rate Limiting", () => {
const email = "victim@example.com";
const attackerIP = "1.2.3.4";
// Simulate brute force attack
let blockedAtAttempt = 0;
for (let i = 0; i < 10; i++) {
try {
@@ -347,7 +322,6 @@ describe("Rate Limiting", () => {
}
}
// Should be blocked before 10 attempts
expect(blockedAtAttempt).toBeLessThan(10);
expect(blockedAtAttempt).toBeGreaterThan(0);
});
@@ -355,7 +329,6 @@ describe("Rate Limiting", () => {
it("should prevent distributed brute force from multiple IPs", async () => {
const email = "victim@example.com";
// Simulate distributed attack from different IPs
let blockedAtAttempt = 0;
for (let i = 0; i < 10; i++) {
try {
@@ -368,7 +341,6 @@ describe("Rate Limiting", () => {
}
}
// Should be blocked at email limit (3 attempts)
expect(blockedAtAttempt).toBeLessThanOrEqual(
RATE_LIMITS.LOGIN_EMAIL.maxAttempts
);
@@ -383,7 +355,6 @@ describe("Rate Limiting", () => {
await rateLimitRegistration(attackerIP);
}
// Should not block in test/dev (IP limits disabled)
expect(true).toBe(true);
});
@@ -396,7 +367,6 @@ describe("Rate Limiting", () => {
await rateLimitPasswordReset(attackerIP);
}
// Should not block in test/dev (IP limits disabled)
expect(true).toBe(true);
});
});
@@ -408,14 +378,12 @@ describe("Rate Limiting", () => {
const unknownIP = "unknown";
const email = `test-${Date.now()}@example.com`;
// Should allow many login attempts in development with unknown IP
// (only email rate limit applies)
for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) {
const testEmail = `test-${Date.now()}-${i}@example.com`;
await rateLimitLogin(testEmail, unknownIP);
}
// Should be able to continue with different emails (no IP limit in dev)
await rateLimitLogin(`final-${Date.now()}@example.com`, unknownIP);
});
@@ -423,12 +391,10 @@ describe("Rate Limiting", () => {
const unknownIP = "unknown";
const email = `test-${Date.now()}@example.com`;
// Use up email rate limit
for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) {
await rateLimitLogin(email, unknownIP);
}
// Next attempt should fail due to email limit
try {
await rateLimitLogin(email, unknownIP);
expect.unreachable("Should have thrown");
@@ -440,55 +406,46 @@ describe("Rate Limiting", () => {
it("should handle unknown IP in password reset", async () => {
const unknownIP = "unknown";
// In development, should allow many attempts (no IP limit)
for (let i = 0; i < 10; i++) {
await rateLimitPasswordReset(unknownIP);
}
// Should not throw in development
expect(true).toBe(true);
});
it("should handle unknown IP in registration", async () => {
const unknownIP = "unknown";
// In development, should allow many attempts (no IP limit)
for (let i = 0; i < 10; i++) {
await rateLimitRegistration(unknownIP);
}
// Should not throw in development
expect(true).toBe(true);
});
it("should handle unknown IP in email verification", async () => {
const unknownIP = "unknown";
// In development, should allow many attempts (no IP limit)
for (let i = 0; i < 10; i++) {
await rateLimitEmailVerification(unknownIP);
}
// Should not throw in development
expect(true).toBe(true);
});
});
describe("Rate Limit Configuration", () => {
it("should have reasonable limits configured", () => {
// Login should be more permissive than registration
expect(RATE_LIMITS.LOGIN_IP.maxAttempts).toBeGreaterThan(
RATE_LIMITS.REGISTRATION_IP.maxAttempts
);
// All limits should be positive
expect(RATE_LIMITS.LOGIN_IP.maxAttempts).toBeGreaterThan(0);
expect(RATE_LIMITS.LOGIN_EMAIL.maxAttempts).toBeGreaterThan(0);
expect(RATE_LIMITS.PASSWORD_RESET_IP.maxAttempts).toBeGreaterThan(0);
expect(RATE_LIMITS.REGISTRATION_IP.maxAttempts).toBeGreaterThan(0);
expect(RATE_LIMITS.EMAIL_VERIFICATION_IP.maxAttempts).toBeGreaterThan(0);
// All windows should be at least 1 minute
expect(RATE_LIMITS.LOGIN_IP.windowMs).toBeGreaterThanOrEqual(60000);
expect(RATE_LIMITS.LOGIN_EMAIL.windowMs).toBeGreaterThanOrEqual(60000);
expect(RATE_LIMITS.PASSWORD_RESET_IP.windowMs).toBeGreaterThanOrEqual(
@@ -552,7 +509,6 @@ describe("Rate Limiting", () => {
const maxAttempts = 3;
const windowMs = 60000;
// Exhaust the limit: 3 allowed, 4th blocked.
for (let i = 0; i < maxAttempts; i++) {
await checkRateLimit(id, maxAttempts, windowMs);
}
@@ -574,7 +530,6 @@ describe("Rate Limiting", () => {
const maxAttempts = 5;
const windowMs = 60000;
// Instance A: 3 attempts.
clearRateLimitLocalCache();
for (let i = 0; i < 3; i++) {
await checkRateLimit(id, maxAttempts, windowMs);
@@ -582,9 +537,9 @@ describe("Rate Limiting", () => {
// Instance B (fresh local cache) makes 2 more -> combined count = 5.
clearRateLimitLocalCache();
await checkRateLimit(id, maxAttempts, windowMs); // count 4
const remaining = await checkRateLimit(id, maxAttempts, windowMs); // count 5
expect(remaining).toBe(0); // 5th allowed, no remaining
await checkRateLimit(id, maxAttempts, windowMs);
const remaining = await checkRateLimit(id, maxAttempts, windowMs);
expect(remaining).toBe(0);
// A 6th attempt from a fresh instance must be blocked — the shared store
// aggregated the count across the two "instances".

View File

@@ -4,8 +4,6 @@
*/
import type { H3Event } from "vinxi/http";
import { SignJWT } from "jose";
import { env } from "~/env/server";
/**
* Create a mock H3Event for testing
@@ -62,55 +60,6 @@ export function createMockEvent(options: {
return mockEvent;
}
/**
* Generate a valid JWT token for testing
*/
export async function createTestJWT(
userId: string,
expiresIn: string = "1h"
): Promise<string> {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
return await new SignJWT({ id: userId })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime(expiresIn)
.sign(secret);
}
/**
* Generate an expired JWT token for testing
*/
export async function createExpiredJWT(userId: string): Promise<string> {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
return await new SignJWT({ id: userId })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("-1h") // Expired 1 hour ago
.sign(secret);
}
/**
* Generate a JWT with invalid signature
*/
export async function createInvalidSignatureJWT(
userId: string
): Promise<string> {
const wrongSecret = new TextEncoder().encode("wrong-secret-key");
return await new SignJWT({ id: userId })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("1h")
.sign(wrongSecret);
}
/**
* Generate test credentials
*/
export function createTestCredentials() {
return {
email: `test-${Date.now()}@example.com`,
password: "TestPass123!@#",
passwordConfirmation: "TestPass123!@#"
};
}
/**
* Common SQL injection payloads
*/
@@ -138,26 +87,6 @@ export const XSS_PAYLOADS = [
"<input onfocus=alert('XSS') autofocus>"
];
/**
* Wait for async operations with timeout
*/
export async function waitFor(
condition: () => boolean | Promise<boolean>,
timeout: number = 5000,
interval: number = 100
): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (await condition()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error(`Timeout waiting for condition after ${timeout}ms`);
}
/**
* Measure execution time
*/
@@ -170,18 +99,6 @@ export async function measureTime<T>(
return { result, duration };
}
/**
* Generate random string for testing
*/
export function randomString(length: number = 10): string {
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return Array.from(
{ length },
() => chars[Math.floor(Math.random() * chars.length)]
).join("");
}
/**
* Generate random IP address
*/