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)
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");
});
});
Playwright is best known for driving Chromium/Firefox/WebKit. It also ships APIRequest: a first-party HTTP client with the same expect() matchers. These scenarios launch no browser; they only use request.newContext().
beforeAll binds createApp() to listen(0). Each test opens an APIRequest context aimed at that base URL, issues GET, and asserts status + JSON. afterAll closes the server. Same contract as SuperTest, over a real port.
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/playwright
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/playwright/headers.spec.js · Apache-2.0
· run in examples/security/http-headers/playwright: npm test
// Playwright request API (no page / no browser) — same header contract as SuperTest
import { test, expect, request as playwrightRequest } from "@playwright/test";
import { createApp } from "../../../../samples/js-api/src/app.js";
let server;
let baseURL;
test.beforeAll(async () => {
const app = createApp();
server = await new Promise((resolve) => {
const s = app.listen(0, "127.0.0.1", () => resolve(s));
});
const { port } = server.address();
baseURL = `http://127.0.0.1:${port}`;
});
test.afterAll(async () => {
await new Promise((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
});
});
test("GET /health sends nosniff and DENY framing", async () => {
const context = await playwrightRequest.newContext({ baseURL });
const res = await context.get("/health");
expect(res.status()).toBe(200);
expect(res.headers()["x-content-type-options"]).toBe("nosniff");
expect(res.headers()["x-frame-options"]).toBe("DENY");
await context.dispose();
});
Uses Playwright's APIRequest context only — no browser is launched.
Testing tool
pytest + httpx · Python HTTP client used under pytest · MIT
httpx is a modern Python HTTP client (requests-like API, HTTP/2 capable). Combined with pytest it is the Python twin of SuperTest / Playwright request: a real GET/POST against a running server, then assert on status and JSON.
pytest starts a fixture that spawns samples/js-api on an ephemeral port (Node script prints the base URL). httpx then issues a real TCP request to that process. After the test, the fixture terminates the server.
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 pytest in examples/security/http-headers/pytest
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/pytest/test_headers.py · MIT
· run in examples/security/http-headers/pytest: pytest
# Same isolation-header contract as SuperTest, over a real TCP port
import sys
from pathlib import Path
import httpx
import pytest
# Shared spawn helper lives next to the integration examples
sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "integration"))
from _node_api import start_api, stop_api
@pytest.fixture()
def base_url():
proc, url = start_api()
yield url
stop_api(proc)
def test_health_sends_isolation_headers(base_url):
res = httpx.get(f"{base_url}/health")
assert res.status_code == 200
assert res.headers["x-content-type-options"] == "nosniff"
assert res.headers["x-frame-options"] == "DENY"
Spawns samples/js-api/scripts/serve-ephemeral.mjs so httpx talks to the same Express app.