mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
Blood-pressure fixtures in the in-target integration suite were copied from live readings; they are now synthetic with the same in/out split so every assertion holds. Hard-coded operator hostnames move behind env vars (DEPLOY_LOGS_URL, HEALTHLOG_PROD_URL), the codex client identifies via the public repository URL, the OpenAPI servers entry points at a neutral example host, and remaining workflow, docs and comment references to the operator's instance or working notes are generalised. The compose whitelist also gains DEPLOY_WEBHOOK_SECRET, which was missing and made the deploy webhook unusable on compose installs.
136 lines
4.8 KiB
TypeScript
136 lines
4.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { NextRequest } from "next/server";
|
|
|
|
/**
|
|
* F-5 (mobile security audit, 2026-05-16): two security headers
|
|
* needed to harden against first-visit MITM and to scope the
|
|
* Withings backend host to its surface only.
|
|
*
|
|
* - HSTS now carries `; preload` so the served host is
|
|
* eligible for the Chromium HSTS preload list.
|
|
* - The `wbsapi.withings.net` `connect-src` entry only ships on
|
|
* the Withings surfaces (`/settings/integrations/withings/*`
|
|
* and `/api/withings/*`) instead of globally — same gating
|
|
* pattern the AI hosts already use.
|
|
*
|
|
* Both checks lock the proxy in place so a future refactor cannot
|
|
* silently regress either one.
|
|
*/
|
|
|
|
vi.mock("@/lib/process-type", () => ({
|
|
shouldRunWeb: () => true,
|
|
}));
|
|
|
|
import { proxy } from "../proxy";
|
|
|
|
// `NODE_ENV` is a special read-only property on node's `process.env`
|
|
// proxy. `vi.stubEnv` knows how to patch it without tripping the
|
|
// "only accepts a configurable, writable, and enumerable data
|
|
// descriptor" guard that a direct `defineProperty` runs into.
|
|
function setNodeEnv(value: "development" | "production") {
|
|
vi.stubEnv("NODE_ENV", value);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
// Default to production so the production-only headers run; individual
|
|
// tests can flip back to development if they care about the dev shape.
|
|
setNodeEnv("production");
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
function makeRequest(
|
|
pathname: string,
|
|
cookies: Record<string, string> = {},
|
|
): NextRequest {
|
|
const cookieHeader = Object.entries(cookies)
|
|
.map(([k, v]) => `${k}=${v}`)
|
|
.join("; ");
|
|
return new NextRequest(`http://localhost${pathname}`, {
|
|
headers: cookieHeader ? { cookie: cookieHeader } : {},
|
|
});
|
|
}
|
|
|
|
describe("proxy.ts HSTS preload (F-5, 2026-05-16)", () => {
|
|
it("attaches `preload` to the Strict-Transport-Security header in production", () => {
|
|
const res = proxy(makeRequest("/", { healthlog_session: "sess-1" }));
|
|
expect(res.headers.get("strict-transport-security")).toBe(
|
|
"max-age=31536000; includeSubDomains; preload",
|
|
);
|
|
});
|
|
|
|
it("does not ship HSTS in development", () => {
|
|
setNodeEnv("development");
|
|
const res = proxy(makeRequest("/", { healthlog_session: "sess-1" }));
|
|
expect(res.headers.get("strict-transport-security")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("proxy.ts Withings CSP gating (F-5, 2026-05-16)", () => {
|
|
it("does NOT allow wbsapi.withings.net on a non-Withings page", () => {
|
|
const res = proxy(makeRequest("/", { healthlog_session: "sess-1" }));
|
|
const csp = res.headers.get("content-security-policy") ?? "";
|
|
expect(csp).not.toMatch(/wbsapi\.withings\.net/);
|
|
});
|
|
|
|
it("allows wbsapi.withings.net under /settings/integrations/withings/*", () => {
|
|
const res = proxy(
|
|
makeRequest("/settings/integrations/withings", {
|
|
healthlog_session: "sess-1",
|
|
}),
|
|
);
|
|
const csp = res.headers.get("content-security-policy") ?? "";
|
|
expect(csp).toMatch(/connect-src[^;]*https:\/\/wbsapi\.withings\.net/);
|
|
});
|
|
|
|
it("allows wbsapi.withings.net under /api/withings/*", () => {
|
|
const res = proxy(
|
|
makeRequest("/api/withings/callback", {
|
|
healthlog_session: "sess-1",
|
|
}),
|
|
);
|
|
const csp = res.headers.get("content-security-policy") ?? "";
|
|
expect(csp).toMatch(/connect-src[^;]*https:\/\/wbsapi\.withings\.net/);
|
|
});
|
|
|
|
it("never ships the Withings host on the auth surface (DOM-XSS exfil channel)", () => {
|
|
const res = proxy(makeRequest("/auth/login"));
|
|
const csp = res.headers.get("content-security-policy") ?? "";
|
|
expect(csp).not.toMatch(/wbsapi\.withings\.net/);
|
|
});
|
|
});
|
|
|
|
describe("proxy.ts WHOOP CSP gating (v1.11.0)", () => {
|
|
it("does NOT allow api.prod.whoop.com on a non-WHOOP page", () => {
|
|
const res = proxy(makeRequest("/", { healthlog_session: "sess-1" }));
|
|
const csp = res.headers.get("content-security-policy") ?? "";
|
|
expect(csp).not.toMatch(/api\.prod\.whoop\.com/);
|
|
});
|
|
|
|
it("allows api.prod.whoop.com under /settings/integrations/whoop/*", () => {
|
|
const res = proxy(
|
|
makeRequest("/settings/integrations/whoop", {
|
|
healthlog_session: "sess-1",
|
|
}),
|
|
);
|
|
const csp = res.headers.get("content-security-policy") ?? "";
|
|
expect(csp).toMatch(/connect-src[^;]*https:\/\/api\.prod\.whoop\.com/);
|
|
});
|
|
|
|
it("allows api.prod.whoop.com under /api/whoop/*", () => {
|
|
const res = proxy(
|
|
makeRequest("/api/whoop/callback", { healthlog_session: "sess-1" }),
|
|
);
|
|
const csp = res.headers.get("content-security-policy") ?? "";
|
|
expect(csp).toMatch(/connect-src[^;]*https:\/\/api\.prod\.whoop\.com/);
|
|
});
|
|
|
|
it("never ships the WHOOP host on the auth surface", () => {
|
|
const res = proxy(makeRequest("/auth/login"));
|
|
const csp = res.headers.get("content-security-policy") ?? "";
|
|
expect(csp).not.toMatch(/api\.prod\.whoop\.com/);
|
|
});
|
|
});
|