87 lines
2.8 KiB
TypeScript
87 lines
2.8 KiB
TypeScript
/**
|
|
* Smoke test — boots the built app and asserts:
|
|
*
|
|
* 1. `/api/health` returns 200 JSON with `ok: true`.
|
|
* 2. `/` redirects to `/<locale>/home`.
|
|
* 3. Each public page (`/en/home`, `/en/about-us`,
|
|
* `/en/contact-us`, `/en/privacy-policy`, `/en/terms-of-service`)
|
|
* returns 200 with an HTML body containing the SPA root element.
|
|
*
|
|
* Run with `bun run e2e/smoke.test.ts`. Requires `bun run build`
|
|
* to have produced `dist/client/index.html` first.
|
|
*/
|
|
import { spawn } from "node:child_process";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
|
|
const PORT = 3399;
|
|
const BASE = `http://localhost:${PORT}`;
|
|
|
|
async function waitForReady(): Promise<void> {
|
|
for (let i = 0; i < 30; i++) {
|
|
try {
|
|
const res = await fetch(`${BASE}/api/health`);
|
|
if (res.ok) return;
|
|
} catch {
|
|
// not up yet
|
|
}
|
|
await delay(500);
|
|
}
|
|
throw new Error("server failed to start within 15s");
|
|
}
|
|
|
|
function assert(condition: unknown, message: string): asserts condition {
|
|
if (!condition) throw new Error(`SMOKE FAIL: ${message}`);
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const child = spawn("bun", ["run", "src/server/index.ts"], {
|
|
env: { ...process.env, PORT: String(PORT), NODE_ENV: "test" },
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
|
|
try {
|
|
await waitForReady();
|
|
|
|
const health = await (await fetch(`${BASE}/api/health`)).json();
|
|
// `@suaveplan/api`'s `registerHealthRoutes` emits
|
|
// `{ status, version, state, timestamp }` — accept either the new
|
|
// shape (`status: "healthy"`) or the legacy `{ ok: true }` shape so
|
|
// the smoke remains forward-compatible if the route is rewritten.
|
|
const healthy = health.status === "healthy" || health.ok === true;
|
|
assert(healthy, `health probe not healthy: ${JSON.stringify(health)}`);
|
|
|
|
const root = await fetch(`${BASE}/`, { redirect: "manual" });
|
|
// Server sends the SPA shell, client-side router handles the redirect.
|
|
// We accept either a 200 (shell) or a 3xx redirect.
|
|
assert(root.status === 200 || (root.status >= 300 && root.status < 400),
|
|
`/ expected 200 or 3xx, got ${root.status}`);
|
|
|
|
const paths = [
|
|
"/en/home",
|
|
"/en/about-us",
|
|
"/en/contact-us",
|
|
"/en/privacy-policy",
|
|
"/en/terms-of-service",
|
|
];
|
|
for (const path of paths) {
|
|
const res = await fetch(`${BASE}${path}`);
|
|
assert(res.ok, `${path} returned ${res.status}`);
|
|
const body = await res.text();
|
|
assert(body.includes('id="root"'), `${path} body missing #root`);
|
|
}
|
|
|
|
// Auth endpoint reachable.
|
|
const session = await fetch(`${BASE}/api/auth/get-session`);
|
|
assert(session.status === 200, `/api/auth/get-session status ${session.status}`);
|
|
|
|
console.log("SMOKE OK");
|
|
} finally {
|
|
child.kill("SIGTERM");
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|