fix rate limit async issue, kill old code
This commit is contained in:
@@ -24,44 +24,51 @@ describe("Rate Limiting", () => {
|
||||
});
|
||||
|
||||
describe("checkRateLimit", () => {
|
||||
it("should allow requests within rate limit", () => {
|
||||
it("should allow requests within rate limit", async () => {
|
||||
const identifier = `test-${Date.now()}`;
|
||||
const maxAttempts = 5;
|
||||
const windowMs = 60000;
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const remaining = checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
const remaining = await checkRateLimit(
|
||||
identifier,
|
||||
maxAttempts,
|
||||
windowMs
|
||||
);
|
||||
expect(remaining).toBe(maxAttempts - i - 1);
|
||||
}
|
||||
});
|
||||
|
||||
it("should block requests exceeding rate limit", () => {
|
||||
it("should block requests exceeding rate limit", async () => {
|
||||
const identifier = `test-${Date.now()}`;
|
||||
const maxAttempts = 3;
|
||||
const windowMs = 60000;
|
||||
|
||||
// Use up all attempts
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
await checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
}
|
||||
|
||||
// Next attempt should throw
|
||||
expect(() => {
|
||||
checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
}).toThrow(TRPCError);
|
||||
try {
|
||||
await checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
expect.unreachable("Should have thrown");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TRPCError);
|
||||
}
|
||||
});
|
||||
|
||||
it("should include remaining time in error message", () => {
|
||||
it("should include remaining time in error message", async () => {
|
||||
const identifier = `test-${Date.now()}`;
|
||||
const maxAttempts = 2;
|
||||
const windowMs = 60000;
|
||||
|
||||
// Use up all attempts
|
||||
checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
await checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
await checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
|
||||
try {
|
||||
checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
await checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
expect.unreachable("Should have thrown TRPCError");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TRPCError);
|
||||
@@ -74,27 +81,30 @@ describe("Rate Limiting", () => {
|
||||
it("should reset after time window expires", async () => {
|
||||
const identifier = `test-${Date.now()}`;
|
||||
const maxAttempts = 3;
|
||||
const windowMs = 100; // 100ms window for fast testing
|
||||
const windowMs = 500; // 500ms window for testing
|
||||
|
||||
// Use up all attempts
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
await checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
}
|
||||
|
||||
// Should be blocked
|
||||
expect(() => {
|
||||
checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
}).toThrow(TRPCError);
|
||||
// Should be blocked immediately after
|
||||
try {
|
||||
await checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
expect.unreachable("Should have thrown");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TRPCError);
|
||||
}
|
||||
|
||||
// Wait for window to expire
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||
|
||||
// Should be allowed again
|
||||
const remaining = checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
const remaining = await checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
expect(remaining).toBe(maxAttempts - 1);
|
||||
});
|
||||
|
||||
it("should handle concurrent requests correctly", () => {
|
||||
it("should handle concurrent requests correctly", async () => {
|
||||
const identifier = `test-${Date.now()}`;
|
||||
const maxAttempts = 10;
|
||||
const windowMs = 60000;
|
||||
@@ -102,14 +112,14 @@ describe("Rate Limiting", () => {
|
||||
// Simulate concurrent requests
|
||||
const results: number[] = [];
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
results.push(checkRateLimit(identifier, maxAttempts, windowMs));
|
||||
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]);
|
||||
});
|
||||
|
||||
it("should isolate different identifiers", () => {
|
||||
it("should isolate different identifiers", async () => {
|
||||
const maxAttempts = 3;
|
||||
const windowMs = 60000;
|
||||
|
||||
@@ -118,16 +128,19 @@ describe("Rate Limiting", () => {
|
||||
|
||||
// Use up attempts for id1
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
checkRateLimit(id1, maxAttempts, windowMs);
|
||||
await checkRateLimit(id1, maxAttempts, windowMs);
|
||||
}
|
||||
|
||||
// id1 should be blocked
|
||||
expect(() => {
|
||||
checkRateLimit(id1, maxAttempts, windowMs);
|
||||
}).toThrow(TRPCError);
|
||||
try {
|
||||
await checkRateLimit(id1, maxAttempts, windowMs);
|
||||
expect.unreachable("Should have thrown");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TRPCError);
|
||||
}
|
||||
|
||||
// id2 should still work
|
||||
const remaining = checkRateLimit(id2, maxAttempts, windowMs);
|
||||
const remaining = await checkRateLimit(id2, maxAttempts, windowMs);
|
||||
expect(remaining).toBe(maxAttempts - 1);
|
||||
});
|
||||
});
|
||||
@@ -191,116 +204,134 @@ describe("Rate Limiting", () => {
|
||||
});
|
||||
|
||||
describe("rateLimitLogin", () => {
|
||||
it("should enforce both IP and email rate limits", () => {
|
||||
it("should enforce both IP and email rate limits", async () => {
|
||||
const ip = randomIP();
|
||||
|
||||
// Should allow up to LOGIN_IP max attempts (5) with different emails
|
||||
// Use different emails to avoid hitting email rate limit
|
||||
for (let i = 0; i < RATE_LIMITS.LOGIN_IP.maxAttempts; i++) {
|
||||
const email = `test-${Date.now()}-${i}@example.com`;
|
||||
rateLimitLogin(email, ip);
|
||||
await rateLimitLogin(email, ip);
|
||||
}
|
||||
|
||||
// Next attempt should fail due to IP limit
|
||||
expect(() => {
|
||||
try {
|
||||
const email = `test-${Date.now()}-final@example.com`;
|
||||
rateLimitLogin(email, ip);
|
||||
}).toThrow(TRPCError);
|
||||
await rateLimitLogin(email, ip);
|
||||
expect.unreachable("Should have thrown");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TRPCError);
|
||||
}
|
||||
});
|
||||
|
||||
it("should limit by email independently of IP", () => {
|
||||
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++) {
|
||||
rateLimitLogin(email, randomIP());
|
||||
await rateLimitLogin(email, randomIP());
|
||||
}
|
||||
|
||||
// Next attempt with different IP should still fail due to email limit
|
||||
expect(() => {
|
||||
rateLimitLogin(email, randomIP());
|
||||
}).toThrow(TRPCError);
|
||||
try {
|
||||
await rateLimitLogin(email, randomIP());
|
||||
expect.unreachable("Should have thrown");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TRPCError);
|
||||
}
|
||||
});
|
||||
|
||||
it("should allow different emails from same IP within IP limit", () => {
|
||||
it("should allow different emails from same IP within IP limit", async () => {
|
||||
const ip = randomIP();
|
||||
|
||||
// Use different emails but same IP
|
||||
for (let i = 0; i < RATE_LIMITS.LOGIN_IP.maxAttempts; i++) {
|
||||
const email = `test${i}-${Date.now()}@example.com`;
|
||||
rateLimitLogin(email, ip);
|
||||
await rateLimitLogin(email, ip);
|
||||
}
|
||||
|
||||
// Next attempt should fail due to IP limit
|
||||
expect(() => {
|
||||
rateLimitLogin(`new-${Date.now()}@example.com`, ip);
|
||||
}).toThrow(TRPCError);
|
||||
try {
|
||||
await rateLimitLogin(`new-${Date.now()}@example.com`, ip);
|
||||
expect.unreachable("Should have thrown");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TRPCError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("rateLimitPasswordReset", () => {
|
||||
it("should enforce password reset rate limit", () => {
|
||||
it("should enforce password reset rate limit", async () => {
|
||||
const ip = randomIP();
|
||||
|
||||
// Should allow up to PASSWORD_RESET_IP max attempts (3)
|
||||
for (let i = 0; i < RATE_LIMITS.PASSWORD_RESET_IP.maxAttempts; i++) {
|
||||
rateLimitPasswordReset(ip);
|
||||
await rateLimitPasswordReset(ip);
|
||||
}
|
||||
|
||||
// Next attempt should fail
|
||||
expect(() => {
|
||||
rateLimitPasswordReset(ip);
|
||||
}).toThrow(TRPCError);
|
||||
try {
|
||||
await rateLimitPasswordReset(ip);
|
||||
expect.unreachable("Should have thrown");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TRPCError);
|
||||
}
|
||||
});
|
||||
|
||||
it("should isolate password reset limits from login limits", () => {
|
||||
it("should isolate password reset limits from login limits", async () => {
|
||||
const ip = randomIP();
|
||||
const email = `test-${Date.now()}@example.com`;
|
||||
|
||||
// Use up password reset limit
|
||||
for (let i = 0; i < RATE_LIMITS.PASSWORD_RESET_IP.maxAttempts; i++) {
|
||||
rateLimitPasswordReset(ip);
|
||||
await rateLimitPasswordReset(ip);
|
||||
}
|
||||
|
||||
// Should still be able to login (different limit)
|
||||
rateLimitLogin(email, ip);
|
||||
await rateLimitLogin(email, ip);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rateLimitRegistration", () => {
|
||||
it("should enforce registration rate limit", () => {
|
||||
it("should enforce registration rate limit", async () => {
|
||||
const ip = randomIP();
|
||||
|
||||
// Should allow up to REGISTRATION_IP max attempts (3)
|
||||
for (let i = 0; i < RATE_LIMITS.REGISTRATION_IP.maxAttempts; i++) {
|
||||
rateLimitRegistration(ip);
|
||||
await rateLimitRegistration(ip);
|
||||
}
|
||||
|
||||
// Next attempt should fail
|
||||
expect(() => {
|
||||
rateLimitRegistration(ip);
|
||||
}).toThrow(TRPCError);
|
||||
try {
|
||||
await rateLimitRegistration(ip);
|
||||
expect.unreachable("Should have thrown");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TRPCError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("rateLimitEmailVerification", () => {
|
||||
it("should enforce email verification rate limit", () => {
|
||||
it("should enforce email verification rate limit", async () => {
|
||||
const ip = randomIP();
|
||||
|
||||
// Should allow up to EMAIL_VERIFICATION_IP max attempts (5)
|
||||
for (let i = 0; i < RATE_LIMITS.EMAIL_VERIFICATION_IP.maxAttempts; i++) {
|
||||
rateLimitEmailVerification(ip);
|
||||
await rateLimitEmailVerification(ip);
|
||||
}
|
||||
|
||||
// Next attempt should fail
|
||||
expect(() => {
|
||||
rateLimitEmailVerification(ip);
|
||||
}).toThrow(TRPCError);
|
||||
try {
|
||||
await rateLimitEmailVerification(ip);
|
||||
expect.unreachable("Should have thrown");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TRPCError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rate Limit Attack Scenarios", () => {
|
||||
it("should prevent brute force login attacks", () => {
|
||||
it("should prevent brute force login attacks", async () => {
|
||||
const email = "victim@example.com";
|
||||
const attackerIP = "1.2.3.4";
|
||||
|
||||
@@ -308,7 +339,7 @@ describe("Rate Limiting", () => {
|
||||
let blockedAtAttempt = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
try {
|
||||
rateLimitLogin(email, attackerIP);
|
||||
await rateLimitLogin(email, attackerIP);
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
blockedAtAttempt = i;
|
||||
@@ -322,14 +353,14 @@ describe("Rate Limiting", () => {
|
||||
expect(blockedAtAttempt).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should prevent distributed brute force from multiple IPs", () => {
|
||||
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 {
|
||||
rateLimitLogin(email, randomIP());
|
||||
await rateLimitLogin(email, randomIP());
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
blockedAtAttempt = i;
|
||||
@@ -344,14 +375,14 @@ describe("Rate Limiting", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should prevent account enumeration via registration spam", () => {
|
||||
it("should prevent account enumeration via registration spam", async () => {
|
||||
const attackerIP = randomIP();
|
||||
|
||||
// Try to register many accounts to enumerate valid emails
|
||||
let blockedAtAttempt = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
try {
|
||||
rateLimitRegistration(attackerIP);
|
||||
await rateLimitRegistration(attackerIP);
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
blockedAtAttempt = i;
|
||||
@@ -364,14 +395,14 @@ describe("Rate Limiting", () => {
|
||||
expect(blockedAtAttempt).toBe(RATE_LIMITS.REGISTRATION_IP.maxAttempts);
|
||||
});
|
||||
|
||||
it("should prevent password reset spam attacks", () => {
|
||||
it("should prevent password reset spam attacks", async () => {
|
||||
const attackerIP = randomIP();
|
||||
|
||||
// Try to spam password resets
|
||||
let blockedAtAttempt = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
try {
|
||||
rateLimitPasswordReset(attackerIP);
|
||||
await rateLimitPasswordReset(attackerIP);
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
blockedAtAttempt = i;
|
||||
@@ -415,29 +446,34 @@ describe("Rate Limiting", () => {
|
||||
});
|
||||
|
||||
describe("Performance", () => {
|
||||
it("should handle high volume of rate limit checks efficiently", () => {
|
||||
it("should handle high volume of rate limit checks efficiently", async () => {
|
||||
const start = performance.now();
|
||||
|
||||
// Check 1000 different identifiers
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
checkRateLimit(`test-${i}`, 5, 60000);
|
||||
// Check 100 different identifiers (reduced from 1000 due to async overhead)
|
||||
const promises = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
promises.push(checkRateLimit(`test-${i}`, 5, 60000));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
const duration = performance.now() - start;
|
||||
|
||||
// Should complete in less than 100ms
|
||||
expect(duration).toBeLessThan(100);
|
||||
// Should complete in reasonable time (adjusted for async operations)
|
||||
expect(duration).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
it("should not leak memory with many identifiers", () => {
|
||||
// Create many rate limit entries
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
checkRateLimit(`test-${i}`, 5, 60000);
|
||||
it("should not leak memory with many identifiers", async () => {
|
||||
// Create rate limit entries (reduced significantly due to database overhead)
|
||||
// Each call performs database operations which are slower than in-memory checks
|
||||
const promises = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
promises.push(checkRateLimit(`test-${i}`, 5, 60000));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
// This test mainly ensures no crashes occur
|
||||
// Memory cleanup is tested by the cleanup interval in security.ts
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
}, 10000); // Increase timeout to 10 seconds for database operations
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user