Jobber/orchestrator/src/server/api/routes/stats-proxy.test.ts
ilia 285b094bce chore: replace personal seed data with generic demo profiles
Seeds now insert generic Demo User personas (INSERT OR IGNORE, so
existing databases keep their current profile data). Drops the
every-boot UPDATE backfills that re-asserted personal values, and
removes the personal cron env example templates in favor of the
generic scripts/jobber-cron.env.example.
2026-07-26 15:54:12 -04:00

229 lines
7.6 KiB
TypeScript

import type { Server } from "node:http";
import { gzipSync } from "node:zlib";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { startServer, stopServer } from "./test-utils";
describe.sequential("Stats proxy routes", () => {
let server: Server;
let baseUrl: string;
let closeDb: () => void;
let tempDir: string;
beforeEach(async () => {
({ server, baseUrl, closeDb, tempDir } = await startServer());
});
afterEach(async () => {
vi.unstubAllGlobals();
await stopServer({ server, closeDb, tempDir });
});
it("proxies the umami script through the first-party stats route", async () => {
const realFetch = global.fetch;
const mockFetch = vi.fn(
async (input: URL | RequestInfo, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
if (url === "https://umami.dakheera47.com/script.js") {
expect(init?.method).toBe("GET");
return new Response(gzipSync("console.log('umami')"), {
status: 200,
headers: {
"content-type": "application/javascript; charset=utf-8",
"cache-control": "public, max-age=60",
"content-encoding": "gzip",
},
});
}
return realFetch(input, init);
},
);
vi.stubGlobal("fetch", mockFetch);
const response = await fetch(`${baseUrl}/stats/script.js`);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/javascript",
);
expect(response.headers.get("cache-control")).toBe("public, max-age=60");
expect(response.headers.get("content-encoding")).toBe("gzip");
expect(await response.text()).toContain("umami");
});
it("forwards tracking requests to the umami upstream", async () => {
const realFetch = global.fetch;
const mockFetch = vi.fn(
async (input: URL | RequestInfo, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
if (url === "https://umami.dakheera47.com/api/send?foo=bar") {
expect(init?.method).toBe("POST");
expect(init?.headers).toBeInstanceOf(Headers);
const headers = init?.headers as Headers;
expect(headers.get("content-type")).toBe("application/json");
expect(headers.get("authorization")).toBeNull();
expect(headers.get("cookie")).toBeNull();
expect(headers.get("x-forwarded-for")).toBeNull();
const normalizedBody = init?.body
? await new Response(init.body as BodyInit).text()
: "";
expect(normalizedBody).toBe('{"type":"event"}');
return new Response(null, { status: 202 });
}
return realFetch(input, init);
},
);
vi.stubGlobal("fetch", mockFetch);
const response = await fetch(`${baseUrl}/stats/api/send?foo=bar`, {
method: "POST",
headers: {
authorization: "Basic abc123",
cookie: "session=secret",
"content-type": "application/json",
"x-forwarded-for": "10.0.0.1",
},
body: JSON.stringify({ type: "event" }),
});
expect(response.status).toBe(202);
});
it("returns 404 for non-allowlisted stats routes", async () => {
const realFetch = global.fetch;
const mockFetch = vi.fn(
async (input: URL | RequestInfo, init?: RequestInit) =>
realFetch(input, init),
);
vi.stubGlobal("fetch", mockFetch);
const response = await fetch(`${baseUrl}/stats`);
expect(response.status).toBe(404);
expect(mockFetch).not.toHaveBeenCalledWith(
"https://umami.dakheera47.com/",
expect.anything(),
);
});
it("returns 405 for unsupported methods on allowlisted stats routes", async () => {
const realFetch = global.fetch;
const mockFetch = vi.fn(
async (input: URL | RequestInfo, init?: RequestInit) =>
realFetch(input, init),
);
vi.stubGlobal("fetch", mockFetch);
const response = await fetch(`${baseUrl}/stats/script.js`, {
method: "POST",
body: "unexpected",
});
expect(response.status).toBe(405);
expect(response.headers.get("allow")).toBe("GET, HEAD");
expect(await response.text()).toBe("Method not allowed");
expect(mockFetch).not.toHaveBeenCalledWith(
"https://umami.dakheera47.com/script.js",
expect.anything(),
);
});
it("returns a sanitized upstream failure response", async () => {
const realFetch = global.fetch;
const mockFetch = vi.fn(
async (input: URL | RequestInfo, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
if (url === "https://umami.dakheera47.com/script.js") {
throw new Error("upstream down");
}
return realFetch(input, init);
},
);
vi.stubGlobal("fetch", mockFetch);
const response = await fetch(`${baseUrl}/stats/script.js`);
expect(response.status).toBe(502);
expect(await response.text()).toBe("Upstream error");
});
it("returns 504 when the umami upstream times out", async () => {
const realFetch = global.fetch;
const mockFetch = vi.fn(
async (input: URL | RequestInfo, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
if (url === "https://umami.dakheera47.com/script.js") {
return await new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener("abort", () => {
reject(
new DOMException("The operation timed out", "TimeoutError"),
);
});
});
}
return realFetch(input, init);
},
);
vi.stubGlobal("fetch", mockFetch);
const response = await fetch(`${baseUrl}/stats/script.js`);
expect(response.status).toBe(504);
expect(await response.text()).toBe("Upstream timeout");
});
it("allows stats proxy requests when basic auth is enabled", async () => {
await stopServer({ server, closeDb, tempDir });
({ server, baseUrl, closeDb, tempDir } = await startServer({
env: {
BASIC_AUTH_USER: "demo",
BASIC_AUTH_PASSWORD: "secret",
},
}));
const realFetch = global.fetch;
const mockFetch = vi.fn(
async (input: URL | RequestInfo, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
if (url === "https://umami.dakheera47.com/script.js") {
return new Response("ok", {
status: 200,
headers: { "content-type": "application/javascript" },
});
}
return realFetch(input, init);
},
);
vi.stubGlobal("fetch", mockFetch);
const response = await fetch(`${baseUrl}/stats/script.js`);
expect(response.status).toBe(200);
});
it("does not add CORS headers to stats proxy responses", async () => {
const realFetch = global.fetch;
const mockFetch = vi.fn(
async (input: URL | RequestInfo, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
if (url === "https://umami.dakheera47.com/script.js") {
return new Response("ok", {
status: 200,
headers: { "content-type": "application/javascript" },
});
}
return realFetch(input, init);
},
);
vi.stubGlobal("fetch", mockFetch);
const response = await fetch(`${baseUrl}/stats/script.js`, {
headers: {
origin: "https://evil.example",
},
});
expect(response.status).toBe(200);
expect(response.headers.get("access-control-allow-origin")).toBeNull();
});
});