feat(docker): build and serve with Node and npm instead of Nginx
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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:/);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user