feat(docker): build and serve with Node and npm instead of Nginx

This commit is contained in:
venus
2026-09-13 01:59:49 -05:00
parent e733f479dd
commit 78b1e0b485
12 changed files with 294 additions and 200 deletions
+5 -2
View File
@@ -23,8 +23,11 @@ function getAllHtmlFiles(dir) {
test("Build & Link Integrity: Output inspection and link verification", () => {
// Ensure fresh build exists
if (!fs.existsSync(siteDir)) {
execSync("npm run build", { stdio: "pipe" });
if (!fs.existsSync(path.join(siteDir, "index.html"))) {
execSync("npm run build", {
stdio: "pipe",
env: { ...process.env, SITE_DOMAIN: process.env.SITE_DOMAIN || "riverrooks.dev" }
});
}
// 1. Core Pages Exist
+39
View File
@@ -0,0 +1,39 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
test("Docker Configuration: Dockerfile enforces Node runtime and excludes Nginx", () => {
const dockerfilePath = path.resolve(process.cwd(), "Dockerfile");
assert.ok(fs.existsSync(dockerfilePath), "Dockerfile must exist");
const content = fs.readFileSync(dockerfilePath, "utf-8");
// Must use Node 22 alpine
assert.match(content, /FROM\s+node:22-alpine/i, "Dockerfile must use node:22-alpine base image");
// Must NOT contain nginx base images or references
assert.doesNotMatch(content, /FROM\s+nginx/i, "Dockerfile must not use nginx base image");
assert.doesNotMatch(content, /nginx/i, "Dockerfile must not contain nginx directives");
// Must build static site and serve with npm
assert.match(content, /RUN\s+npm\s+run\s+build/, "Dockerfile must run npm build");
assert.match(content, /EXPOSE\s+8080/, "Dockerfile must expose port 8080");
assert.match(content, /CMD\s*\[\s*["']npm["']\s*,\s*["']start["']\s*\]/, "Dockerfile must start via npm start");
});
test("Docker Configuration: docker-compose.yml configures Node service correctly", () => {
const composePath = path.resolve(process.cwd(), "docker-compose.yml");
assert.ok(fs.existsSync(composePath), "docker-compose.yml must exist");
const content = fs.readFileSync(composePath, "utf-8");
// Verifies port mapping to 8080
assert.match(content, /8080:8080/, "docker-compose.yml must map port 8080 to container 8080");
// Verifies healthcheck targets 8080
assert.match(content, /localhost:8080/, "Healthcheck must probe port 8080");
// Verifies domain argument enforcement
assert.match(content, /SITE_DOMAIN/, "docker-compose.yml must require SITE_DOMAIN");
});
+87
View File
@@ -0,0 +1,87 @@
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:/);
});
});
+3 -3
View File
@@ -48,9 +48,9 @@ test("Site Config: correctly parses SITE_DOMAIN and derives service subdomains",
assert.equal(chefService.url, "https://chef.testcluster.internal");
// Verify navigation links
const navGateway = site.nav.find((n) => n.label === "Gateway");
assert.ok(navGateway, "Nav Gateway link exists");
assert.equal(navGateway.url, "/");
const navHome = site.nav.find((n) => n.label === "Home");
assert.ok(navHome, "Nav Home link exists");
assert.equal(navHome.url, "/");
const navGit = site.nav.find((n) => n.label === "Gitea");
assert.ok(navGit, "Nav Gitea link exists");