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

Project Setup

No HTTP server needed

Hectoday HTTP’s app.fetch lets you call your app directly — no listen(), no ports, no network. This makes tests fast and isolated:

import { app } from "../src/app.js";

const res = await app.fetch(new Request("http://localhost:3000/health"));
const body = await res.json();
// { status: "ok" }

Each test constructs a Request, calls app.fetch, and checks the Response. No server startup, no teardown, no port conflicts.

Test file structure

tests/
  helpers.ts           # login, request, cookie extraction
  auth.test.ts         # login flows, sessions, 2FA
  authz.test.ts        # IDOR, roles, permissions, org scoping
  security.test.ts     # rate limiting, tokens, input handling

Helper functions

Create tests/helpers.ts:

// tests/helpers.ts
import { app } from "../src/app.js";

const BASE = "http://localhost:3000";

export async function request(path: string, options?: RequestInit): Promise<Response> {
  return app.fetch(new Request(`${BASE}${path}`, options));
}

export async function login(email: string, password: string): Promise<Response> {
  return request("/login", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ email, password }),
  });
}

export function getCookie(res: Response): string {
  const setCookie = res.headers.get("set-cookie") ?? "";
  const match = setCookie.match(/session=([^;]+)/);
  return match ? `session=${match[1]}` : "";
}

export async function authenticatedRequest(
  path: string,
  cookie: string,
  options?: RequestInit,
): Promise<Response> {
  return request(path, {
    ...options,
    headers: {
      ...options?.headers,
      cookie,
    },
  });
}

export async function loginAs(email: string, password: string = "password123"): Promise<string> {
  const res = await login(email, password);
  return getCookie(res);
}

loginAs is the most useful helper: log in and return the cookie in one call. Every test that needs an authenticated request starts with const cookie = await loginAs("[email protected]").

Running tests

# Run all tests
node --test tests/*.test.ts

# Run a specific file
node --test tests/auth.test.ts

# With tsx for TypeScript
npx tsx --test tests/*.test.ts

[!NOTE] Node.js 18+ includes a built-in test runner (node:test). No framework needed. If you prefer Vitest or Jest, the test patterns are the same — only the runner changes.

Database state

Tests need predictable data. Two approaches:

Seed before each test file: Reset the database to a known state. Every test starts from the same data.

// tests/helpers.ts
import db from "../src/db.js";

export function resetDatabase(): void {
  db.exec("DELETE FROM notes");
  db.exec("DELETE FROM memberships");
  db.exec("DELETE FROM users");
  // Re-run seed data...
}

Use unique data per test: Each test creates its own users and data with unique IDs. Tests do not interfere with each other.

For this course, we use the seed data from the auth course (Alice, Bob, Carol) and create additional data as needed per test.

Exercises

Exercise 1: Create tests/helpers.ts with the helper functions above. Write a simple test that calls GET /health and asserts the status is 200.

Exercise 2: Write a test that logs in as Alice using loginAs and accesses a protected route. Verify it returns 200.

Exercise 3: Write a test that accesses a protected route without logging in. Verify it returns 401.

Why do we test with app.fetch instead of starting an HTTP server?

← Testing Security, Not Just Functionality Testing Login Flows →

© 2026 hectoday. All rights reserved.