Files
ToolCTRL-website/server.test.js
T

327 lines
10 KiB
JavaScript

"use strict";
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { after, describe, it } = require("node:test");
const testDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "toolctrl-leads-test-"));
process.env.DATA_DIR = testDataDir;
process.env.CLAMAV_ENABLED = "false";
const {
bindSubmissionId,
createLeadFingerprint
} = require("./public/submission-id");
const {
PlatformLeadConfigurationError,
PlatformLeadSubmissionError,
createServer,
forwardLeadToPlatform
} = require("./server");
const ENV = {
PLATFORM_LEADS_API_URL: "https://api.hivegrid.de",
PLATFORM_LEADS_API_KEY: "t".repeat(32)
};
const LEAD = {
submissionId: "334955b4-269d-4f18-b51b-7b1d93ed2103",
firstName: "Erika",
lastName: "Musterfrau",
email: "erika@example.com",
phone: "+49 123 456",
company: "Beispiel GmbH",
message: "Bitte melden."
};
function successResponse() {
return new Response(JSON.stringify({
id: "lead_789",
status: "new",
createdAt: "2026-07-16T12:00:00.000Z"
}), {
status: 201,
headers: { "Content-Type": "application/json" }
});
}
async function withServer(submitLead, callback) {
const server = createServer({ submitLead: submitLead });
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolve);
});
const address = server.address();
try {
return await callback("http://127.0.0.1:" + address.port);
} finally {
await new Promise((resolve) => server.close(resolve));
}
}
after(() => {
fs.rmSync(testDataDir, { recursive: true, force: true });
});
describe("forwardLeadToPlatform", () => {
it("sends the strict native contract without a source field", async () => {
const calls = [];
const fetchImpl = async (input, init) => {
calls.push({ input: String(input), init: init });
return successResponse();
};
const result = await forwardLeadToPlatform(LEAD, {
env: ENV,
fetchImpl: fetchImpl
});
assert.equal(result.id, "lead_789");
assert.equal(calls.length, 1);
assert.equal(calls[0].input, "https://api.hivegrid.de/v1/platform/leads");
assert.deepEqual(calls[0].init.headers, {
"Content-Type": "application/json",
Authorization: "Bearer " + "t".repeat(32),
"Idempotency-Key": LEAD.submissionId
});
const body = JSON.parse(calls[0].init.body);
assert.deepEqual(body, {
externalId: LEAD.submissionId,
sourceDetail: "Messe — Landing Page",
firstName: "Erika",
lastName: "Musterfrau",
email: "erika@example.com",
phone: "+49 123 456",
company: "Beispiel GmbH",
message: "Bitte melden.",
utmCampaign: "messe-landing-page",
metadata: {
consent: true,
formId: "contact-form",
formName: "ToolCTRL Kontaktformular",
locale: "de-DE",
submissionId: LEAD.submissionId
}
});
assert.equal(Object.hasOwn(body, "source"), false);
});
it("reuses the exact key and body across a retry", async () => {
const calls = [];
const fetchImpl = async (_input, init) => {
calls.push(init);
return calls.length === 1
? new Response(null, { status: 503 })
: successResponse();
};
await forwardLeadToPlatform(LEAD, { env: ENV, fetchImpl: fetchImpl });
assert.equal(calls.length, 2);
assert.equal(calls[0].body, calls[1].body);
assert.deepEqual(calls[0].headers, calls[1].headers);
});
it("rejects an API error instead of reporting success", async () => {
await assert.rejects(
forwardLeadToPlatform(LEAD, {
env: ENV,
fetchImpl: async () => new Response(null, { status: 401 })
}),
(error) => error instanceof PlatformLeadSubmissionError &&
error.code === "upstream" && error.upstreamStatus === 401
);
});
it("fails closed without runtime credentials", async () => {
await assert.rejects(
forwardLeadToPlatform(LEAD, { env: {} }),
PlatformLeadConfigurationError
);
});
});
describe("browser submission id binding", () => {
it("keeps the id for an equivalent retry and renews it after a semantic edit", () => {
let sequence = 0;
const createId = () => "submission-" + (++sequence);
const original = {
name: " Erika Musterfrau ",
firma: " Beispiel GmbH ",
email: " ERIKA@example.com ",
telefon: " +49 123 456 ",
nachricht: " Bitte melden. "
};
const first = bindSubmissionId(null, createLeadFingerprint(original), createId);
const retry = bindSubmissionId(first, createLeadFingerprint({
...original,
name: "Erika Musterfrau",
email: "erika@EXAMPLE.com"
}), createId);
const edited = bindSubmissionId(retry, createLeadFingerprint({
...original,
nachricht: "Bitte morgen melden."
}), createId);
assert.equal(retry, first);
assert.equal(retry.id, "submission-1");
assert.equal(edited.id, "submission-2");
});
it("loads the binding helper before the form client", () => {
const index = fs.readFileSync(path.join(__dirname, "public/index.html"), "utf8");
const helperPosition = index.indexOf('<script src="/submission-id.js?v=');
const clientPosition = index.indexOf('<script src="/script.js?v=');
assert.notEqual(helperPosition, -1);
assert.notEqual(clientPosition, -1);
assert.equal(helperPosition < clientPosition, true);
});
});
describe("runtime readiness", () => {
it("fails without platform credentials and succeeds with valid runtime configuration", async () => {
const previousUrl = process.env.PLATFORM_LEADS_API_URL;
const previousKey = process.env.PLATFORM_LEADS_API_KEY;
try {
delete process.env.PLATFORM_LEADS_API_URL;
delete process.env.PLATFORM_LEADS_API_KEY;
await withServer(async () => ({ id: "unused" }), async (baseUrl) => {
const response = await fetch(baseUrl + "/readyz");
assert.equal(response.status, 503);
assert.equal((await response.json()).checks.platformLeadsConfigured, false);
});
process.env.PLATFORM_LEADS_API_URL = ENV.PLATFORM_LEADS_API_URL;
process.env.PLATFORM_LEADS_API_KEY = ENV.PLATFORM_LEADS_API_KEY;
await withServer(async () => ({ id: "unused" }), async (baseUrl) => {
const response = await fetch(baseUrl + "/readyz");
assert.equal(response.status, 200);
assert.equal((await response.json()).checks.platformLeadsConfigured, true);
});
} finally {
if (previousUrl == null) delete process.env.PLATFORM_LEADS_API_URL;
else process.env.PLATFORM_LEADS_API_URL = previousUrl;
if (previousKey == null) delete process.env.PLATFORM_LEADS_API_KEY;
else process.env.PLATFORM_LEADS_API_KEY = previousKey;
}
});
it("uses readiness rather than liveness for the container healthcheck", () => {
const dockerfile = fs.readFileSync(path.join(__dirname, "Dockerfile"), "utf8");
assert.match(dockerfile, /HEALTHCHECK[^]*\/readyz/);
});
});
describe("deployment contract", () => {
it("uses Coolify pull-deploy and verifies the ready revision", () => {
const workflow = fs.readFileSync(
path.join(__dirname, ".gitea/workflows/deploy.yml"),
"utf8"
);
assert.equal(
workflow.includes(
"/api/v1/deploy?uuid=${COOLIFY_RESOURCE_UUID}&force=true"
),
true
);
assert.doesNotMatch(
workflow,
/\/api\/v1\/applications\/[^\s"']+\/(?:start|restart)/
);
assert.equal(workflow.includes('-t "$IMAGE_LC:$SHA"'), true);
assert.equal(workflow.includes("EXPECTED_REVISION: ${{ github.sha }}"), true);
assert.equal(workflow.includes("READY_URL: https://tool-ctrl.de/readyz"), true);
assert.equal(
workflow.includes(
'if [ "$DEPLOYED_REVISION" = "$EXPECTED_REVISION" ]; then'
),
true
);
});
});
describe("POST /api/lead", () => {
const formBody = {
name: " Erika Musterfrau ",
firma: " Beispiel GmbH ",
email: " ERIKA@example.com ",
telefon: " +49 123 456 ",
nachricht: " Bitte melden. ",
website: "",
submissionId: LEAD.submissionId
};
it("returns success only after the platform accepted the normalized lead", async () => {
const captured = [];
await withServer(async (lead) => {
captured.push(lead);
return { id: "lead_route", status: "new", createdAt: "now" };
}, async (baseUrl) => {
const response = await fetch(baseUrl + "/api/lead", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formBody)
});
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), { ok: true, id: "lead_route" });
});
assert.deepEqual(captured, [LEAD]);
assert.equal(fs.existsSync(path.join(testDataDir, "leads")), false);
});
it("returns an error and does not write NDJSON/CSV when the platform fails", async () => {
await withServer(async () => {
throw new PlatformLeadSubmissionError("network");
}, async (baseUrl) => {
const response = await fetch(baseUrl + "/api/lead", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formBody)
});
assert.equal(response.status, 502);
assert.equal((await response.json()).error.includes("erneut"), true);
});
assert.equal(fs.existsSync(path.join(testDataDir, "leads")), false);
});
it("requires a stable submission id before calling the platform", async () => {
let calls = 0;
await withServer(async () => {
calls += 1;
return { id: "unexpected" };
}, async (baseUrl) => {
const response = await fetch(baseUrl + "/api/lead", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...formBody, submissionId: undefined })
});
assert.equal(response.status, 400);
});
assert.equal(calls, 0);
});
it("keeps the honeypot response silent without creating a lead", async () => {
let calls = 0;
await withServer(async () => {
calls += 1;
return { id: "unexpected" };
}, async (baseUrl) => {
const response = await fetch(baseUrl + "/api/lead", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ website: "bot.example" })
});
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), { ok: true });
});
assert.equal(calls, 0);
});
});