Add full application: receipt scanning, auth, billing, and account deletion
Brings the working codebase (Next.js app, auth system, Stripe billing, Docker/deploy config, tests, docs) into version control on top of the placeholder initial commit, and adds account self-deletion (Danger Zone in Settings, password + typed-email confirmation, cascading DB cleanup, Stripe cancellation) per GDPR right-to-erasure. Excludes local build caches, node_modules, and internal agent scratch files; .gitignore hardened to keep those out going forward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
122
tests/e2e/security_headers.test.ts
Normal file
122
tests/e2e/security_headers.test.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Security Headers Suite — Task A (HSTS / HTTPS-only)
|
||||
*
|
||||
* Verifies next.config.ts: the Strict-Transport-Security header is configured
|
||||
* with the full "max-age=63072000; includeSubDomains; preload" directive, is
|
||||
* emitted only when the request actually arrived over HTTPS
|
||||
* (x-forwarded-proto: https), and the remaining security headers stay
|
||||
* unconditional on the catch-all rule. Pure config logic — no database or
|
||||
* running server required.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from "./runner";
|
||||
import configModule from "../../next.config";
|
||||
|
||||
// Under tsx (ESM) the CJS-style default export can arrive wrapped as
|
||||
// `{ default: nextConfig }`; unwrap it so `config.headers()` is callable in
|
||||
// either interop mode.
|
||||
const config = ((configModule as { default?: typeof configModule }).default ??
|
||||
configModule) as typeof configModule;
|
||||
|
||||
interface HeaderItem {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface HeaderCondition {
|
||||
type: string;
|
||||
key: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
interface HeaderRule {
|
||||
source: string;
|
||||
has?: HeaderCondition[];
|
||||
headers: HeaderItem[];
|
||||
}
|
||||
|
||||
async function catchAllRules(): Promise<HeaderRule[]> {
|
||||
// `headers` is optional on the NextConfig type; next.config.ts always defines it.
|
||||
const rules = (await config.headers!()) as HeaderRule[];
|
||||
return rules.filter((rule) => rule.source === "/(.*)");
|
||||
}
|
||||
|
||||
function findHstsRule(rules: HeaderRule[]): HeaderRule | undefined {
|
||||
return rules.find((rule) =>
|
||||
rule.headers.some(
|
||||
(header) => header.key.toLowerCase() === "strict-transport-security"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
describe("Security headers — HSTS (HTTPS-only)", () => {
|
||||
test("the Strict-Transport-Security header is configured on the catch-all rule", async () => {
|
||||
const rules = await catchAllRules();
|
||||
expect(findHstsRule(rules)).toBeDefined();
|
||||
});
|
||||
|
||||
test("HSTS carries max-age=63072000, includeSubDomains and preload", async () => {
|
||||
const rule = findHstsRule(await catchAllRules());
|
||||
expect(rule).toBeDefined();
|
||||
const hsts = rule!.headers.find(
|
||||
(header) => header.key.toLowerCase() === "strict-transport-security"
|
||||
);
|
||||
expect(hsts).toBeDefined();
|
||||
expect(hsts!.value).toContain("max-age=63072000");
|
||||
expect(hsts!.value).toContain("includeSubDomains");
|
||||
expect(hsts!.value).toContain("preload");
|
||||
});
|
||||
|
||||
test("HSTS is emitted only when the request arrived over HTTPS (x-forwarded-proto)", async () => {
|
||||
const rule = findHstsRule(await catchAllRules());
|
||||
expect(rule).toBeDefined();
|
||||
expect(rule!.has).toBeDefined();
|
||||
const conditioned = rule!.has!.some(
|
||||
(condition) =>
|
||||
condition.type === "header" &&
|
||||
condition.key.toLowerCase() === "x-forwarded-proto" &&
|
||||
condition.value === "https"
|
||||
);
|
||||
expect(conditioned).toBe(true);
|
||||
});
|
||||
|
||||
test("the remaining security headers stay unconditional on the catch-all rule", async () => {
|
||||
const rules = await catchAllRules();
|
||||
const unconditional = rules.filter((rule) => !rule.has);
|
||||
expect(unconditional.length).toBeGreaterThan(0);
|
||||
|
||||
const allHeaders = unconditional.flatMap((rule) => rule.headers);
|
||||
const keys = new Set(allHeaders.map((header) => header.key.toLowerCase()));
|
||||
|
||||
expect(keys.has("x-frame-options")).toBe(true);
|
||||
expect(keys.has("x-content-type-options")).toBe(true);
|
||||
expect(keys.has("referrer-policy")).toBe(true);
|
||||
expect(keys.has("permissions-policy")).toBe(true);
|
||||
expect(keys.has("content-security-policy")).toBe(true);
|
||||
|
||||
const xfo = allHeaders.find(
|
||||
(header) => header.key.toLowerCase() === "x-frame-options"
|
||||
);
|
||||
const xcto = allHeaders.find(
|
||||
(header) => header.key.toLowerCase() === "x-content-type-options"
|
||||
);
|
||||
expect(xfo).toBeDefined();
|
||||
expect(xcto).toBeDefined();
|
||||
expect(xfo!.value).toBe("DENY");
|
||||
expect(xcto!.value).toBe("nosniff");
|
||||
expect(
|
||||
allHeaders.some(
|
||||
(header) =>
|
||||
header.key.toLowerCase() === "referrer-policy" &&
|
||||
header.value.includes("strict-origin-when-cross-origin")
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
allHeaders.some(
|
||||
(header) =>
|
||||
header.key.toLowerCase() === "content-security-policy" &&
|
||||
header.value.length > 0
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user