Files
HealthLog/src/__tests__/proxy-server-action.test.ts
T
Marc-André Bombeck 7759729aca fix(proxy): refuse Server Action calls the app never makes
HealthLog defines no Server Actions, so no bundle of it, current or
stale, sends a Next-Action header. The requests that do arrive come from
the nightly scan probing that surface, and Next answered each with an
unstructured "Failed to find Server Action" at error level. The proxy
now refuses them with a 404 and one structured warn line, and a guard
keeps "use server" out of the tree so the refusal stays safe.
2026-10-20 20:29:46 +02:00

78 lines
2.3 KiB
TypeScript

/**
* A Server Action call is refused at the proxy.
*
* HealthLog defines no Server Actions, so the only requests that carry
* `Next-Action` are scanners probing for them. Next used to answer each with
* an unstructured "Failed to find Server Action" at error level; the proxy now
* refuses it with a 404 and one structured `warn` line, and a request without
* the header is untouched.
*/
import { NextRequest } from "next/server";
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi,
type MockInstance,
} from "vitest";
vi.mock("@/lib/process-type", () => ({ shouldRunWeb: () => true }));
import { proxy } from "../proxy";
let warn: MockInstance<(...args: unknown[]) => void>;
beforeEach(() => {
warn = vi.spyOn(console, "warn").mockImplementation(() => {});
});
afterEach(() => {
warn.mockRestore();
});
function probe(path: string, headers: Record<string, string> = {}) {
return new NextRequest(`https://health.example.test${path}`, {
method: "POST",
headers: { "content-type": "text/plain", ...headers },
body: "[]",
});
}
describe("proxy on a Server Action call", () => {
it("refuses it with 404 and one warn line", async () => {
const response = proxy(
probe("/", { "next-action": "7f3a9c", "user-agent": "scanner" }),
);
expect(response.status).toBe(404);
expect(await response.json()).toEqual({
data: null,
error: "Not found",
meta: { errorCode: "server_action.unsupported" },
});
expect(response.headers.get("X-Content-Type-Options")).toBe("nosniff");
expect(warn).toHaveBeenCalledTimes(1);
expect(JSON.parse(String(warn.mock.calls[0]![0]))).toMatchObject({
level: "warn",
action: { name: "proxy.server_action.refused" },
http: { method: "POST", path: "/", status: 404, user_agent: "scanner" },
});
});
it("never logs a secret path segment the probe aimed at", () => {
proxy(probe("/api/withings/webhook/s3cr3t-value", { "next-action": "x" }));
const line = String(warn.mock.calls[0]![0]);
expect(line).not.toContain("s3cr3t-value");
});
it("leaves a request without the header alone", () => {
const response = proxy(probe("/api/health"));
expect(response.status).not.toBe(404);
expect(warn).not.toHaveBeenCalled();
});
});