hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Testing Auth and Security with @hectoday/http

Why Auth Tests Are Different

  • Testing Security, Not Just Functionality
  • Project Setup

Testing Authentication

  • Testing Login Flows
  • Testing Sessions and Cookies
  • Testing 2FA Flows

Testing Authorization

  • Testing Access Boundaries
  • Testing API Keys and Scopes

Testing Security Properties

  • Testing Rate Limiting and Lockout
  • Testing Token Security
  • Testing Input Handling

Putting It Together

  • A Security Test Suite

Testing Rate Limiting and Lockout

Testing defenses under abuse

Rate limiting and lockout are defenses that only activate under abuse. If you do not test them with actual abuse patterns, you do not know they work.

Rate limit tests

describe("rate limiting", () => {
  it("allows requests under the limit", async () => {
    // Per-email limit is 5 per minute
    for (let i = 0; i < 5; i++) {
      const res = await login("[email protected]", "wrong");
      assert.strictEqual(res.status, 401, `Request ${i + 1} should get 401, not 429`);
    }
  });

  it("blocks requests over the limit with 429", async () => {
    // Send 6 requests (5 allowed + 1 over)
    for (let i = 0; i < 5; i++) {
      await login("[email protected]", "wrong");
    }

    const res = await login("[email protected]", "wrong");
    assert.strictEqual(res.status, 429);
  });

  it("includes Retry-After header on 429", async () => {
    // Exhaust the limit
    for (let i = 0; i < 6; i++) {
      await login("[email protected]", "wrong");
    }

    const res = await login("[email protected]", "wrong");
    const retryAfter = res.headers.get("retry-after");
    assert.ok(retryAfter, "429 response must include Retry-After header");
    assert.ok(parseInt(retryAfter!) > 0, "Retry-After must be a positive number");
  });

  it("same error message for IP and email rate limits", async () => {
    // Exhaust email limit
    for (let i = 0; i < 6; i++) {
      await login("[email protected]", "wrong");
    }
    const emailLimited = await login("[email protected]", "wrong");
    const emailBody = await emailLimited.json();

    // Both should use the same generic message
    assert.ok(
      emailBody.error.includes("Too many") || emailBody.error.includes("try again"),
      "Error should not reveal which limit was hit",
    );
  });
});

The last test verifies that the error message does not distinguish between IP and email rate limiting. If it did, an attacker could learn which limit they hit and adjust their strategy.

Lockout tests

describe("account lockout", () => {
  it("locks account after 10 failed attempts", async () => {
    const email = "[email protected]";
    // Create this user in test seed

    for (let i = 0; i < 10; i++) {
      await login(email, "wrong");
    }

    const res = await login(email, "wrong");
    assert.strictEqual(res.status, 429);
    const body = await res.json();
    assert.ok(body.error.includes("locked") || body.error.includes("Too many"));
  });

  it("correct password fails during lockout", async () => {
    const email = "[email protected]";
    // Create this user with known password

    // Trigger lockout
    for (let i = 0; i < 10; i++) {
      await login(email, "wrong");
    }

    // Even the correct password should fail
    const res = await login(email, "password123");
    assert.strictEqual(res.status, 429, "Correct password must fail during lockout");
  });

  it("successful login clears failed attempt counter", async () => {
    const email = "[email protected]";

    // 9 failed attempts (one below lockout threshold)
    for (let i = 0; i < 9; i++) {
      await login(email, "wrong");
    }

    // Successful login should clear the counter
    await login(email, "password123");

    // One more failure should not trigger lockout (counter was reset)
    const res = await login(email, "wrong");
    assert.strictEqual(res.status, 401, "Counter should have been cleared by successful login");
  });
});

The “correct password fails during lockout” test is important. Without it, an attacker who guesses the correct password during lockout could still log in, defeating the lockout mechanism.

The “successful login clears counter” test verifies that legitimate users who occasionally mistype their password do not accumulate failures toward lockout.

Exercises

Exercise 1: Write the rate limit tests. You will need unique email addresses per test to avoid interfering with each other’s rate limit counters.

Exercise 2: Write the lockout tests. Verify that lockout engages at exactly the threshold (10 attempts), not before.

Exercise 3: Add a test that verifies lockout expires after the configured duration. Temporarily set the lockout duration to 1 second, wait 2 seconds, and verify login works again.

Why should the correct password fail during lockout?

← Testing API Keys and Scopes Testing Token Security →

© 2026 hectoday. All rights reserved.