// postiz-poster integration test suite. // // Two gates: // 1. payloadGate -- pure assertions on buildPostPayload (always runs, no net). // 2. liveGate -- drives a real Postiz instance; runs only when both // POSTIZ_BASE_URL and POSTIZ_API_KEY are set. // // Run from the postiz/ directory: // // node test/e2e.js # payloadGate only // POSTIZ_BASE_URL=http://localhost:5000 \ // POSTIZ_API_KEY=xxxxxxxx node test/e2e.js # + liveGate // // Without the env vars the live gate is skipped (not failed), so the pure gate // still runs anywhere with no Postiz available. Assertion failures are caught // and counted; one failure does not stop the suite. const assert = require("node:assert/strict"); const { buildPostPayload } = require("../lib/action"); const { makePostizClient } = require("../lib/postizClient"); const { encryptSecret, decryptSecret, isEncryptedBlob } = require("../lib/secretsAtRest"); const { POST_TYPES } = require("../lib/constants"); let passed = 0; let failed = 0; const check = (name, fn) => { try { fn(); passed += 1; console.log(`ok - ${name}`); } catch (err) { failed += 1; console.log(`FAIL - ${name}: ${err && err.message ? err.message : err}`); } }; const checkAsync = async (name, fn) => { try { await fn(); passed += 1; console.log(`ok - ${name}`); } catch (err) { failed += 1; console.log(`FAIL - ${name}: ${err && err.message ? err.message : err}`); } }; const payloadGate = () => { const nowIso = "2026-06-08T20:00:00.000Z"; check("now post targets every channel id, content carried through", () => { const cfg = { contentField: "body", integrationIds: "a, b ,c", when: POST_TYPES.NOW }; const out = buildPostPayload(cfg, { body: "hi" }, [], nowIso); assert.equal(out.type, POST_TYPES.NOW); assert.equal(out.date, nowIso); assert.equal(out.posts.length, 3); assert.deepEqual(out.posts.map((p) => p.integration.id), ["a", "b", "c"]); assert.equal(out.posts[0].value[0].content, "hi"); }); check("media ids attach as image refs", () => { const cfg = { contentField: "body", integrationIds: "a", when: POST_TYPES.NOW }; const out = buildPostPayload(cfg, { body: "pic" }, ["m1", "m2"], nowIso); assert.deepEqual(out.posts[0].value[0].image, [{ id: "m1" }, { id: "m2" }]); }); check("scheduled post reads the date field, not now", () => { const cfg = { contentField: "body", integrationIds: "a", when: POST_TYPES.SCHEDULE, scheduleField: "at" }; const out = buildPostPayload(cfg, { body: "later", at: "2026-12-25T09:00:00Z" }, [], nowIso); assert.equal(out.type, POST_TYPES.SCHEDULE); assert.equal(out.date, "2026-12-25T09:00:00.000Z"); }); check("blank / whitespace channel ids are dropped", () => { const cfg = { contentField: "body", integrationIds: " , ,", when: POST_TYPES.DRAFT }; const out = buildPostPayload(cfg, { body: "x" }, [], nowIso); assert.equal(out.posts.length, 0); }); }; const secretsGate = () => { // secretsAtRest reads the key root from this env var (see getSessionSecret). process.env.SALTCORN_SESSION_SECRET = process.env.SALTCORN_SESSION_SECRET || "test-session-secret-do-not-use"; const info = "postiz-poster:apiKey"; check("seal -> open round-trips the plaintext", () => { const blob = encryptSecret("super-secret-key", info); assert.ok(isEncryptedBlob(blob), "blob should carry the scs1: prefix"); assert.notEqual(blob, "super-secret-key"); assert.equal(decryptSecret(blob, info), "super-secret-key"); }); check("ciphertext differs each call (random IV)", () => { assert.notEqual(encryptSecret("x", info), encryptSecret("x", info)); }); check("wrong domain label cannot open the blob", () => { const blob = encryptSecret("k", info); assert.equal(decryptSecret(blob, "postiz-poster:other"), null); }); check("tampered blob fails GCM auth -> null", () => { const blob = encryptSecret("k", info); const i = "scs1:".length + 2; const flipped = blob[i] === "A" ? "B" : "A"; const tampered = blob.slice(0, i) + flipped + blob.slice(i + 1); assert.equal(decryptSecret(tampered, info), null); }); check("plaintext is not mistaken for an encrypted blob", () => { assert.equal(isEncryptedBlob("plain"), false); assert.equal(decryptSecret("plain", info), null); }); }; const liveGate = async () => { const baseUrl = process.env.POSTIZ_BASE_URL; const apiKey = process.env.POSTIZ_API_KEY; if (!baseUrl || !apiKey) { console.log("skip - liveGate (set POSTIZ_BASE_URL + POSTIZ_API_KEY to enable)"); return; } const client = makePostizClient({ baseUrl, apiKey }); let firstChannel = null; await checkAsync("listIntegrations returns an array", async () => { const integrations = await client.listIntegrations(); assert.ok(Array.isArray(integrations), "expected an array of integrations"); firstChannel = integrations[0] || null; }); await checkAsync("createPost (draft) is accepted", async () => { if (!firstChannel) { console.log(" (no channels connected -- connect one in Postiz to exercise createPost)"); return; } const cfg = { contentField: "body", integrationIds: String(firstChannel.id), when: POST_TYPES.DRAFT }; const payload = buildPostPayload(cfg, { body: "postiz-poster e2e draft -- safe to delete" }, [], new Date().toISOString()); const res = await client.createPost(payload); assert.ok(res, "expected a create-post response"); }); }; const main = async () => { payloadGate(); secretsGate(); await liveGate(); console.log(`\n${passed} passed, ${failed} failed`); process.exit(failed === 0 ? 0 : 1); }; main();