it("blocks the IP once maxAttempts is reached", () => {
createLimiter({ lockoutMs: 10_000 });
limiter.recordFailure("10.0.0.2");
limiter.recordFailure("10.0.0.2"); const result = limiter.check("10.0.0.2");
expect(result.allowed).toBe(false);
expect(result.remaining).toBe(0);
expect(result.retryAfterMs).toBeGreaterThan(0);
expect(result.retryAfterMs).toBeLessThanOrEqual(10_000);
});
it("unblocks after the lockout period expires", () => {
vi.useFakeTimers(); try {
createLimiter({ lockoutMs: 5_000 });
limiter.recordFailure("10.0.0.3");
limiter.recordFailure("10.0.0.3");
expect(limiter.check("10.0.0.3").allowed).toBe(false);
// Advance just past the lockout.
vi.advanceTimersByTime(5_001); const result = limiter.check("10.0.0.3");
expect(result.allowed).toBe(true);
expect(result.remaining).toBe(2);
} finally {
vi.useRealTimers();
}
});
it("does not extend lockout when failures are recorded while already locked", () => {
vi.useFakeTimers(); try {
createLimiter({ lockoutMs: 5_000 });
limiter.recordFailure("10.0.0.33");
limiter.recordFailure("10.0.0.33"); const locked = limiter.check("10.0.0.33");
expect(locked.allowed).toBe(false); const initialRetryAfter = locked.retryAfterMs;
// Move past the window so the two old failures expire.
vi.advanceTimersByTime(11_000);
expect(limiter.check("10.0.0.4").remaining).toBe(3);
} finally {
vi.useRealTimers();
}
});
// A different IP should be unaffected.
expect(limiter.check("10.0.0.11").allowed).toBe(true);
expect(limiter.check("10.0.0.11").remaining).toBe(2);
});
it("treats ipv4 and ipv4-mapped ipv6 forms as the same client", () => {
limiter = createAuthRateLimiter({ maxAttempts: 1, windowMs: 60_000, lockoutMs: 60_000 });
limiter.recordFailure("1.2.3.4");
expect(limiter.check("::ffff:1.2.3.4").allowed).toBe(false);
});
it.each([AUTH_RATE_LIMIT_SCOPE_DEVICE_TOKEN, AUTH_RATE_LIMIT_SCOPE_HOOK_AUTH])( "tracks %s independently from shared-secret for the same IP",
(otherScope) => {
limiter = createAuthRateLimiter({ maxAttempts: 1, windowMs: 60_000, lockoutMs: 60_000 });
limiter.recordFailure("10.0.0.12", AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET);
expect(limiter.check("10.0.0.12", AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET).allowed).toBe(false);
expect(limiter.check("10.0.0.12", otherScope).allowed).toBe(true);
},
);
it("clears tracking state when reset is called", () => {
createLimiter();
limiter.recordFailure("10.0.0.20");
limiter.recordFailure("10.0.0.20");
expect(limiter.check("10.0.0.20").allowed).toBe(false);
it("prune keeps entries that are still locked out", () => {
vi.useFakeTimers(); try {
limiter = createAuthRateLimiter({ maxAttempts: 1, windowMs: 5_000, lockoutMs: 30_000 });
limiter.recordFailure("10.0.0.31");
expect(limiter.check("10.0.0.31").allowed).toBe(false);
// Move past the window but NOT past the lockout.
vi.advanceTimersByTime(6_000);
limiter.prune();
expect(limiter.size()).toBe(1); // Still locked-out, not pruned.
} finally {
vi.useRealTimers();
}
});
// ---------- undefined / empty IP ----------
it("normalizes undefined IP to 'unknown'", () => {
createLimiter();
limiter.recordFailure(undefined);
limiter.recordFailure(undefined);
expect(limiter.check(undefined).allowed).toBe(false);
expect(limiter.size()).toBe(1);
});
it("normalizes empty-string IP to 'unknown'", () => {
createLimiter();
limiter.recordFailure("");
limiter.recordFailure("");
expect(limiter.check("").allowed).toBe(false);
});
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.