security

Responses send isolation headers

The smallest useful security check on an API: after the handler runs, the response must carry X-Content-Type-Options: nosniff and X-Frame-Options: DENY. Compare SuperTest, Playwright request, and pytest + httpx on the same contract.

SUT: js-api · id: security.http-headers

Cached CI results from 10/3/2026, 10:14:14 AM (ci · d7784ca)

Testing tool

Supertest · HTTP integration helper on top of a JS runner · MIT

Supertest drives an Express/Connect/Fastify app without you opening a public port. You pass the app instance; SuperTest injects requests through the middleware stack and exposes .get/.post plus status and body.

This repo runs SuperTest inside Vitest. createApp() returns Express; request(app).get(...) walks routing and JSON handlers in-process. That is still an integration test (full HTTP stack) but cheaper than bind + curl.

Testing architecture

Security tests ask what an attacker can make the SUT do — not whether the happy-path JSON or heading is correct. Here that means isolation headers on js-api responses, and proving that a script-like signup name is assigned with textContent so it cannot run. Failures point at missing headers or unsafe DOM writes.

This is not a JSON-body integration test. The SUT is the Express middleware that sets isolation headers on every response. Each variant issues GET /health and asserts those two header values. SuperTest stays in-process; Playwright and pytest talk to a real port.

SUT: js-api · samples/js-api/src/app.js · run npm test in examples/security/http-headers/supertest

Code under test · samples/js-api/src/app.js
import express from "express";

/** In-memory catalog — no DB so integration tests stay local and cheap. */
const ITEMS = new Map([["1", { id: "1", name: "Notebook" }]]);

/**
 * Build the Express app (no listen). Tests import this and bind a port themselves.
 */
export function createApp() {
  const app = express();
  // Parse JSON bodies if a later POST example needs them
  app.use(express.json());
  // Baseline browser-isolation headers — asserted by security.http-headers
  app.use((_req, res, next) => {
    res.setHeader("X-Content-Type-Options", "nosniff");
    res.setHeader("X-Frame-Options", "DENY");
    next();
  });

  // Liveness probe — integration + load/microbench examples hit this
  app.get("/health", (_req, res) => {
    res.json({ ok: true });
  });

  // Read one item by id from the in-memory store
  app.get("/items/:id", (req, res) => {
    const item = ITEMS.get(req.params.id);
    if (!item) {
      // Stable error contract: same JSON shape for every missing id
      res.status(404).json({ error: "not_found", id: req.params.id });
      return;
    }
    res.json(item);
  });

  return app;
}
Test · examples/security/http-headers/supertest/headers.test.js · MIT · run in examples/security/http-headers/supertest: npm test
// Security contract: isolation headers on every JSON response
import { describe, it, expect } from "vitest";
import request from "supertest";
import { createApp } from "../../../../samples/js-api/src/app.js";

describe("security headers", () => {
  it("GET /health sends nosniff and DENY framing", async () => {
    const app = createApp();
    const res = await request(app).get("/health");
    expect(res.status).toBe(200);
    // MIME sniffing off — browsers must honor application/json
    expect(res.headers["x-content-type-options"]).toBe("nosniff");
    // Do not allow this API response to be framed
    expect(res.headers["x-frame-options"]).toBe("DENY");
  });
});