Files
Announcements/server/test/runtime.test.js
T

201 lines
6.5 KiB
JavaScript

const test = require("node:test");
const assert = require("node:assert/strict");
const net = require("node:net");
const { loadConfig } = require("../src/config");
const { createRuntime } = require("../src/runtime");
function testConfig(overrides = {}) {
return loadConfig({
NODE_ENV: "test",
HOST: "127.0.0.1",
PORT: "0",
DATABASE_PATH: ":memory:",
APP_TIMEZONE: "America/Chicago",
SHUTDOWN_TIMEOUT_MS: "1000",
S3_ENDPOINT: "http://seaweedfs:8333",
S3_REGION: "us-east-1",
S3_BUCKET: "announcements",
S3_ACCESS_KEY_ID: "access-key-that-must-not-leak",
S3_SECRET_ACCESS_KEY: "secret-key-that-must-not-leak",
S3_FORCE_PATH_STYLE: "true",
MQTT_URL: "mqtt://broker:1883",
MQTT_PASSWORD: "mqtt-password-that-must-not-leak",
HA_WEBHOOK_URL: "http://home-assistant:8123/api/webhook/private-id",
...overrides,
});
}
const silentLogger = {
info() {},
warn() {},
error() {},
};
function testStorage() {
return {
async start() {},
stop() {},
health() {
return {
status: "degraded",
checkedAt: "2026-08-16T00:00:00.000Z",
lastSuccessAt: null,
code: "STORAGE_UNAVAILABLE",
message: "Object storage is unavailable",
};
},
};
}
async function responseJson(baseUrl, path, init) {
const response = await fetch(`${baseUrl}${path}`, init);
return { response, body: await response.json() };
}
test("HTTP foundation exposes live, ready, detailed health, and persisted settings", async (t) => {
const runtime = createRuntime({
config: testConfig(),
logger: silentLogger,
storage: testStorage(),
});
await runtime.start();
t.after(() => runtime.stop("test cleanup"));
const baseUrl = `http://127.0.0.1:${runtime.address.port}`;
const live = await responseJson(baseUrl, "/health/live");
assert.equal(live.response.status, 200);
assert.deepEqual(live.body, { status: "ok" });
const ready = await responseJson(baseUrl, "/health/ready");
assert.equal(ready.response.status, 200);
assert.equal(ready.body.status, "ready");
const status = await responseJson(baseUrl, "/api/health");
assert.equal(status.response.status, 200);
assert.equal(status.body.status, "degraded");
assert.equal(status.body.services.database.currentSchemaVersion, 1);
assert.equal(status.body.services.storage.status, "degraded");
const statusText = JSON.stringify(status.body);
assert.doesNotMatch(statusText, /access-key-that-must-not-leak/);
assert.doesNotMatch(statusText, /secret-key-that-must-not-leak/);
assert.doesNotMatch(statusText, /mqtt-password-that-must-not-leak/);
assert.doesNotMatch(statusText, /private-id/);
const statusAlias = await responseJson(baseUrl, "/api/status");
assert.equal(statusAlias.response.status, 200);
assert.deepEqual(statusAlias.body, status.body);
const initial = await responseJson(baseUrl, "/api/settings");
assert.equal(initial.body.defaultBackgroundDarkened, false);
const changed = await responseJson(baseUrl, "/api/settings", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({
defaultBackgroundDarkened: true,
defaultDurationMs: 9000,
}),
});
assert.equal(changed.response.status, 200);
assert.equal(changed.body.revision, 2);
assert.equal(changed.body.defaultBackgroundDarkened, true);
const reread = await responseJson(baseUrl, "/api/settings");
assert.equal(reread.body.defaultDurationMs, 9000);
});
test("invalid settings receive a structured client error", async (t) => {
const runtime = createRuntime({
config: testConfig(),
logger: silentLogger,
storage: testStorage(),
});
await runtime.start();
t.after(() => runtime.stop("test cleanup"));
const baseUrl = `http://127.0.0.1:${runtime.address.port}`;
const result = await responseJson(baseUrl, "/api/settings", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ defaultBackgroundDarkened: 0.25 }),
});
assert.equal(result.response.status, 400);
assert.equal(result.body.error.code, "INVALID_SETTINGS");
});
test("liveness stays up while critical readiness failures return 503", async (t) => {
const runtime = createRuntime({
config: testConfig(),
logger: silentLogger,
storage: testStorage(),
});
await runtime.start();
t.after(() => runtime.stop("test cleanup"));
const baseUrl = `http://127.0.0.1:${runtime.address.port}`;
runtime.lifecycle.ready = false;
let live = await responseJson(baseUrl, "/health/live");
let ready = await responseJson(baseUrl, "/health/ready");
assert.equal(live.response.status, 200);
assert.equal(ready.response.status, 503);
assert.equal(ready.body.status, "not_ready");
runtime.lifecycle.ready = true;
runtime.migrations.pendingVersions.push(2);
ready = await responseJson(baseUrl, "/health/ready");
assert.equal(ready.response.status, 503);
assert.equal(ready.body.checks.migrations, "pending");
runtime.migrations.pendingVersions.length = 0;
runtime.database.close();
live = await responseJson(baseUrl, "/health/live");
ready = await responseJson(baseUrl, "/health/ready");
assert.equal(live.response.status, 200);
assert.equal(ready.response.status, 503);
assert.equal(ready.body.checks.database, "error");
});
test("runtime startup and shutdown are idempotent", async () => {
const runtime = createRuntime({
config: testConfig(),
logger: silentLogger,
storage: testStorage(),
});
const [firstAddress, secondAddress] = await Promise.all([
runtime.start(),
runtime.start(),
]);
assert.deepEqual(firstAddress, secondAddress);
assert.equal(runtime.lifecycle.ready, true);
await Promise.all([runtime.stop("test"), runtime.stop("test")]);
assert.equal(runtime.lifecycle.ready, false);
assert.equal(runtime.database.open, false);
});
test("a listen failure stops playback and closes startup resources", async (t) => {
const occupied = net.createServer();
occupied.listen(0, "127.0.0.1");
await new Promise((resolve, reject) => {
occupied.once("listening", resolve);
occupied.once("error", reject);
});
t.after(
() =>
new Promise((resolve) => {
occupied.close(resolve);
}),
);
const runtime = createRuntime({
config: testConfig({ PORT: String(occupied.address().port) }),
logger: silentLogger,
storage: testStorage(),
});
await assert.rejects(runtime.start(), (error) => error.code === "EADDRINUSE");
assert.equal(runtime.lifecycle.ready, false);
assert.equal(runtime.database.open, false);
});