carepath-api / scribe /frontend /scripts /validate-deploy-env.test.mjs
tranth3truong's picture
Deploy CP-UX-17: public demo hub, same-origin API, one design world
aacc29a
Raw History Blame Contribute Delete
5.01 kB
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import { validateDeployEnv } from "./validate-deploy-env.mjs";
const validEnv = {
VITE_API_BASE: "https://carepath.hf.space",
};
test("accepts one HTTPS Space origin", () => {
assert.doesNotThrow(() => validateDeployEnv(validEnv));
});
test("accepts an empty API base when the websocket names its own host", () => {
// The production shape after DEC-0021: /api/* is rewritten same-origin by
// Vercel, so no CORS allow-list can take the site down again.
assert.doesNotThrow(() =>
validateDeployEnv({ VITE_API_BASE: "", VITE_WS_BASE: "https://carepath.hf.space" }),
);
});
test("rejects an empty API base with no websocket host to fall back on", () => {
// Both empty means the socket would target the Vercel origin, which cannot
// proxy the upgrade — the visit screen would connect to nothing.
assert.throws(() => validateDeployEnv({}), /VITE_WS_BASE is required/);
});
test("rejects a websocket base with a path", () => {
assert.throws(
() => validateDeployEnv({ VITE_WS_BASE: "https://carepath.hf.space/ws" }),
/VITE_WS_BASE pathname must be \//,
);
});
test("rejects HTTP", () => {
assert.throws(
() => validateDeployEnv({ VITE_API_BASE: "http://api.example.test" }),
/must use HTTPS/,
);
});
test("rejects paths, queries, and fragments that would corrupt runtime URLs", () => {
assert.throws(
() => validateDeployEnv({ VITE_API_BASE: "https://carepath.hf.space/wrong?token=secret" }),
/must not include a query or fragment[\s\S]*pathname must be \//,
);
});
test("Vercel runs validation before the production build", async () => {
const config = JSON.parse(
await readFile(new URL("../vercel.json", import.meta.url), "utf8"),
);
assert.equal(config.buildCommand, "npm run validate:deploy && npm run build");
const spa = config.rewrites.filter((rule) => rule.destination === "/index.html");
assert.deepEqual(spa, [
{ source: "/thu-nghiem", destination: "/index.html" },
{ source: "/thu-nghiem/", destination: "/index.html" },
{ source: "/thu-nghiem/:path*", destination: "/index.html" },
{ source: "/ghi-chep-lam-sang", destination: "/index.html" },
{ source: "/ghi-chep-lam-sang/", destination: "/index.html" },
{ source: "/ghi-chep-lam-sang/:path*", destination: "/index.html" },
{ source: "/kham-song-ngu", destination: "/index.html" },
{ source: "/kham-song-ngu/", destination: "/index.html" },
{ source: "/kham-song-ngu/:path*", destination: "/index.html" },
]);
});
test("the API proxy does not swallow the demo functions", async () => {
// /api/demo/* are serverless functions in this project. If the catch-all
// proxied them to the Space they would 404 there, and the quota that keeps
// anonymous traffic off the paid provider would never run.
const config = JSON.parse(
await readFile(new URL("../vercel.json", import.meta.url), "utf8"),
);
const api = config.rewrites.find((rule) => rule.source.startsWith("/api/"));
const pattern = new RegExp(`^${api.source}$`);
assert.ok(pattern.test("/api/v1/health"), "product API must still be proxied");
assert.ok(pattern.test("/api/sessions"), "product API must still be proxied");
assert.equal(pattern.test("/api/demo/document"), false);
assert.equal(pattern.test("/api/demo/session"), false);
});
test("the API is proxied same-origin so no CORS allow-list can break it", async () => {
// Regression guard for the outage this fixed: moving the site to
// carepath-medicaltranslation.vercel.app while the Space still allowed only
// carepath-omega.vercel.app returned 400 Disallowed CORS origin on every
// call, and both tool routes rendered "Mất kết nối máy chủ".
const config = JSON.parse(
await readFile(new URL("../vercel.json", import.meta.url), "utf8"),
);
const api = config.rewrites.find((rule) => rule.source.startsWith("/api/"));
assert.ok(api, "vercel.json must rewrite /api/* to the API host");
assert.match(api.destination, /^https:\/\/[^/]+\/api\//);
// It has to win before any SPA catch-all could swallow it.
assert.equal(config.rewrites.indexOf(api), 0);
});
test("every SPA route the app serves has a Vercel rewrite", async () => {
// A missing rewrite is a hard 404 on a direct visit, and /kham-song-ngu/ is
// the demo URL. Keep this derived from the app's own route table.
const app = await readFile(new URL("../src/App.tsx", import.meta.url), "utf8");
const routes = [...app.matchAll(/^const \w*_?PATH = "(\/[^"]+)";$/gm)].map((m) => m[1]);
assert.ok(routes.length >= 2, `expected app routes, found ${routes.join(", ")}`);
const config = JSON.parse(
await readFile(new URL("../vercel.json", import.meta.url), "utf8"),
);
const sources = new Set(config.rewrites.map((rule) => rule.source));
for (const route of routes) {
assert.ok(sources.has(route), `vercel.json has no rewrite for ${route}`);
}
});