Implement Phase 2 asset storage pipeline
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const express = require("express");
|
||||
const { openDatabase } = require("../src/database");
|
||||
const { runMigrations } = require("../src/database/migrate");
|
||||
const {
|
||||
createDetailedHealthRouter,
|
||||
createHealthRouter,
|
||||
} = require("../src/health/routes");
|
||||
|
||||
function config() {
|
||||
return {
|
||||
environment: "test",
|
||||
host: "127.0.0.1",
|
||||
port: 0,
|
||||
timeZone: "America/Chicago",
|
||||
trustProxy: true,
|
||||
database: { path: ":memory:" },
|
||||
storage: {
|
||||
endpoint: "http://private-storage:8333",
|
||||
region: "us-east-1",
|
||||
bucket: "announcements-v2",
|
||||
accessKeyId: "must-not-leak",
|
||||
secretAccessKey: "must-not-leak-either",
|
||||
forcePathStyle: true,
|
||||
},
|
||||
processing: {
|
||||
tempDirectory: "/tmp/announcements-uploads",
|
||||
maxFileBytes: 1024,
|
||||
maxFiles: 2,
|
||||
maxInputPixels: 1000,
|
||||
},
|
||||
mqtt: { url: null },
|
||||
notifications: { homeAssistantWebhookUrl: null },
|
||||
};
|
||||
}
|
||||
|
||||
async function withServer(storage, callback) {
|
||||
const database = openDatabase({ path: ":memory:", busyTimeoutMs: 1000 });
|
||||
const migrations = runMigrations(database);
|
||||
const lifecycle = { ready: true };
|
||||
const app = express();
|
||||
const options = { config: config(), database, lifecycle, migrations, storage };
|
||||
app.use("/health", createHealthRouter(options));
|
||||
app.use("/api/health", createDetailedHealthRouter(options));
|
||||
const server = app.listen(0, "127.0.0.1");
|
||||
await new Promise((resolve) => server.once("listening", resolve));
|
||||
|
||||
try {
|
||||
await callback(`http://127.0.0.1:${server.address().port}`);
|
||||
} finally {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
||||
test("storage degradation is reported but does not fail API readiness", async () => {
|
||||
const storage = {
|
||||
health() {
|
||||
return {
|
||||
status: "degraded",
|
||||
checkedAt: "2026-08-16T12:00:00.000Z",
|
||||
lastSuccessAt: "2026-08-16T11:59:00.000Z",
|
||||
code: "STORAGE_UNAVAILABLE",
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
await withServer(storage, async (baseUrl) => {
|
||||
const ready = await fetch(`${baseUrl}/health/ready`);
|
||||
const readyBody = await ready.json();
|
||||
assert.equal(ready.status, 200);
|
||||
assert.equal(readyBody.status, "ready");
|
||||
assert.equal(readyBody.checks.storage, "degraded");
|
||||
|
||||
const detailed = await fetch(`${baseUrl}/api/health`);
|
||||
const detailedBody = await detailed.json();
|
||||
assert.equal(detailed.status, 200);
|
||||
assert.equal(detailedBody.status, "degraded");
|
||||
assert.equal(detailedBody.services.storage.status, "degraded");
|
||||
assert.equal(
|
||||
detailedBody.services.storage.lastSuccessAt,
|
||||
"2026-08-16T11:59:00.000Z",
|
||||
);
|
||||
assert.equal(detailedBody.services.storage.code, "STORAGE_UNAVAILABLE");
|
||||
assert.doesNotMatch(JSON.stringify(detailedBody), /must-not-leak/);
|
||||
assert.doesNotMatch(JSON.stringify(detailedBody), /private-storage/);
|
||||
});
|
||||
});
|
||||
|
||||
test("healthy storage produces healthy detailed status", async () => {
|
||||
const storage = {
|
||||
health() {
|
||||
return { status: "healthy", checkedAt: "2026-08-16T12:00:00.000Z" };
|
||||
},
|
||||
};
|
||||
|
||||
await withServer(storage, async (baseUrl) => {
|
||||
const detailed = await fetch(`${baseUrl}/api/health`);
|
||||
const body = await detailed.json();
|
||||
assert.equal(detailed.status, 200);
|
||||
assert.equal(body.status, "ok");
|
||||
assert.equal(body.services.storage.status, "healthy");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user