88 lines
3.0 KiB
JavaScript
88 lines
3.0 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import http from "node:http";
|
|
import path from "node:path";
|
|
import { execSync } from "node:child_process";
|
|
import { createServer } from "../server.js";
|
|
|
|
const TEST_PORT = 8999;
|
|
const BASE_URL = `http://127.0.0.1:${TEST_PORT}`;
|
|
|
|
// Helper to perform HTTP GET requests
|
|
function get(urlPath) {
|
|
return new Promise((resolve, reject) => {
|
|
http.get(`${BASE_URL}${urlPath}`, (res) => {
|
|
let data = "";
|
|
res.on("data", (chunk) => (data += chunk));
|
|
res.on("end", () => {
|
|
resolve({
|
|
statusCode: res.statusCode,
|
|
headers: res.headers,
|
|
body: data
|
|
});
|
|
});
|
|
}).on("error", reject);
|
|
});
|
|
}
|
|
|
|
test("Node Static Server: lifecycle, routing, and adversarial security tests", async (t) => {
|
|
// Ensure _site exists before running server tests
|
|
if (!fs.existsSync(path.resolve(process.cwd(), "_site/index.html"))) {
|
|
execSync("npm run build", {
|
|
stdio: "pipe",
|
|
env: { ...process.env, SITE_DOMAIN: "riverrooks.dev" }
|
|
});
|
|
}
|
|
|
|
const server = createServer();
|
|
await new Promise((resolve) => server.listen(TEST_PORT, "127.0.0.1", resolve));
|
|
|
|
t.after(() => {
|
|
return new Promise((resolve) => server.close(resolve));
|
|
});
|
|
|
|
await t.test("serves root index.html with 200 OK and text/html", async () => {
|
|
const res = await get("/");
|
|
assert.equal(res.statusCode, 200);
|
|
assert.match(res.headers["content-type"], /text\/html/);
|
|
assert.match(res.body, /<!DOCTYPE html>/i);
|
|
// Verify security headers
|
|
assert.equal(res.headers["x-content-type-options"], "nosniff");
|
|
assert.equal(res.headers["x-frame-options"], "DENY");
|
|
});
|
|
|
|
await t.test("serves clean directory URL (/dispatches/)", async () => {
|
|
const res = await get("/dispatches/");
|
|
assert.equal(res.statusCode, 200);
|
|
assert.match(res.headers["content-type"], /text\/html/);
|
|
});
|
|
|
|
await t.test("serves static assets with appropriate Content-Type", async () => {
|
|
const res = await get("/assets/css/main.css");
|
|
assert.equal(res.statusCode, 200);
|
|
assert.match(res.headers["content-type"], /text\/css/);
|
|
});
|
|
|
|
await t.test("responds with 200 OK JSON on /healthz", async () => {
|
|
const res = await get("/healthz");
|
|
assert.equal(res.statusCode, 200);
|
|
assert.match(res.headers["content-type"], /application\/json/);
|
|
const body = JSON.parse(res.body);
|
|
assert.equal(body.status, "ok");
|
|
});
|
|
|
|
await t.test("returns 404 Not Found for non-existent routes", async () => {
|
|
const res = await get("/non-existent-file-xyz.html");
|
|
assert.equal(res.statusCode, 404);
|
|
});
|
|
|
|
await t.test("adversarial: blocks path traversal attempts", async () => {
|
|
const res = await get("/../../../../etc/passwd");
|
|
// Must either return 403 Forbidden or 404 Not Found, never 200
|
|
assert.notEqual(res.statusCode, 200);
|
|
assert.ok(res.statusCode === 403 || res.statusCode === 404);
|
|
assert.doesNotMatch(res.body, /root:x:/);
|
|
});
|
|
});
|