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 Access Boundaries

The most important authorization tests

Access boundary tests verify that users cannot see, modify, or delete things they should not. These are the tests that catch IDOR bugs, broken role checks, and org scoping leaks.

IDOR tests

The pattern: log in as user A, try to access user B’s resource.

describe("IDOR prevention", () => {
  it("user cannot view another user's note by ID", async () => {
    const aliceCookie = await loginAs("[email protected]");
    const bobCookie = await loginAs("[email protected]");

    // Alice creates a note
    const createRes = await authenticatedRequest("/orgs/org-acme/notes", aliceCookie, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ title: "Private", body: "Alice only" }),
    });
    const { id: noteId } = await createRes.json();

    // Bob tries to access it (in a user-scoped app, this should fail)
    // In an org-scoped app, Bob can access it if he's a member of the same org
    // Adjust based on your app's access model
  });

  it("non-member cannot access org resources", async () => {
    const bobCookie = await loginAs("[email protected]");

    // Bob is not a member of Globex
    const res = await authenticatedRequest("/orgs/org-globex/notes", bobCookie);
    assert.strictEqual(res.status, 404, "Non-member should get 404, not 403");
  });

  it("non-member gets 404, not 403 (no org existence leak)", async () => {
    const bobCookie = await loginAs("[email protected]");

    // Try a real org Bob is not in
    const realOrg = await authenticatedRequest("/orgs/org-globex/notes", bobCookie);

    // Try a fake org
    const fakeOrg = await authenticatedRequest("/orgs/org-doesnt-exist/notes", bobCookie);

    // Both should return 404 — no way to tell which orgs exist
    assert.strictEqual(realOrg.status, 404);
    assert.strictEqual(fakeOrg.status, 404);
  });
});

The third test is subtle: it verifies that real and non-existent orgs return the same status code for non-members. This prevents org enumeration.

Role and permission tests

Test every role against every action:

describe("role-based access", () => {
  it("viewer can read notes", async () => {
    const carolCookie = await loginAs("[email protected]"); // viewer at Acme
    const res = await authenticatedRequest("/orgs/org-acme/notes", carolCookie);
    assert.strictEqual(res.status, 200);
  });

  it("viewer cannot create notes", async () => {
    const carolCookie = await loginAs("[email protected]");
    const res = await authenticatedRequest("/orgs/org-acme/notes", carolCookie, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ title: "Test", body: "Should fail" }),
    });
    assert.strictEqual(res.status, 403);
  });

  it("editor can create notes", async () => {
    const bobCookie = await loginAs("[email protected]"); // editor at Acme
    const res = await authenticatedRequest("/orgs/org-acme/notes", bobCookie, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ title: "Bob's Note", body: "Should work" }),
    });
    assert.strictEqual(res.status, 201);
  });

  it("editor cannot delete notes", async () => {
    const bobCookie = await loginAs("[email protected]");
    const res = await authenticatedRequest("/orgs/org-acme/notes/note-1", bobCookie, {
      method: "DELETE",
    });
    assert.strictEqual(res.status, 403);
  });

  it("owner can delete notes", async () => {
    const aliceCookie = await loginAs("[email protected]"); // owner at Acme
    // Create a note to delete (don't delete seed data)
    const createRes = await authenticatedRequest("/orgs/org-acme/notes", aliceCookie, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ title: "Deletable", body: "Will be deleted" }),
    });
    const { id } = await createRes.json();

    const res = await authenticatedRequest(`/orgs/org-acme/notes/${id}`, aliceCookie, {
      method: "DELETE",
    });
    assert.strictEqual(res.status, 200);
  });
});

This is a permission matrix test. Each row tests one role against one action. The complete matrix for three roles and three actions (read, create, delete) is nine tests. Write them all.

The access boundary matrix

A useful way to plan these tests:

Read notesCreate notesDelete notes
Owner (Alice)✅ 200✅ 201✅ 200
Editor (Bob)✅ 200✅ 201❌ 403
Viewer (Carol)✅ 200❌ 403❌ 403
Non-member❌ 404❌ 404❌ 404

Every cell is a test. The matrix makes it obvious what is missing.

Exercises

Exercise 1: Write the full 4×3 matrix of tests (4 access levels × 3 actions). Run them all.

Exercise 2: Add a test for Alice accessing Globex (where she is a viewer). She should be able to read but not create or delete.

Exercise 3: Temporarily remove the user_id check from a route (break IDOR prevention). Run the tests. Which ones fail?

Why do we test that non-members get 404 for both real and non-existent orgs?

← Testing 2FA Flows Testing API Keys and Scopes →

© 2026 hectoday. All rights reserved.