diff --git a/apps/worker/src/processors/eoc-push.ts b/apps/worker/src/processors/eoc-push.ts index 34c808d..7e7881a 100644 --- a/apps/worker/src/processors/eoc-push.ts +++ b/apps/worker/src/processors/eoc-push.ts @@ -14,6 +14,44 @@ function ingestUrl(base: string): string { return `${trimmed}/api/v1/apps/cibello/ingest`; } +interface EocFeedbackPost { + typ?: string; + rubrik?: string; + status?: string; + created_at?: string; + skapad?: string; + [key: string]: unknown; +} + +interface EocFeedback { + oppna?: number; + senaste?: EocFeedbackPost[]; + [key: string]: unknown; +} + +/** + * Build a payload that matches EOC's authoritative contract: + * feedback.senaste[].created_at must be sent as feedback.senaste[].skapad. + * All other keys are forwarded unchanged. + */ +function mapPayloadForEoc(parsed: Record): Record { + const mapped: Record = { ...parsed }; + const feedback = mapped.feedback as EocFeedback | undefined; + if (feedback && Array.isArray(feedback.senaste)) { + mapped.feedback = { + ...feedback, + senaste: feedback.senaste.map((post) => { + const { created_at, ...rest } = post; + return { + ...rest, + ...(created_at !== undefined ? { skapad: created_at } : {}), + }; + }), + }; + } + return mapped; +} + /** * Push the cached ops summary to EOC. Never throws; failures are returned * as `ok: false` and must be logged by the caller. @@ -42,6 +80,8 @@ export async function pushOpsSummaryToEoc(redis: Redis): Promise return { ok: false, status: null, error: "ops summary cache is not a JSON object" }; } + const body = JSON.stringify(mapPayloadForEoc(parsed as Record)); + const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), EOC_PUSH_TIMEOUT_MS); @@ -52,7 +92,7 @@ export async function pushOpsSummaryToEoc(redis: Redis): Promise Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, - body: json, + body, signal: controller.signal, }); clearTimeout(timeout); diff --git a/apps/worker/test/eoc-push.test.ts b/apps/worker/test/eoc-push.test.ts index 637988f..524121e 100644 --- a/apps/worker/test/eoc-push.test.ts +++ b/apps/worker/test/eoc-push.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, beforeAll, afterAll } from "vitest"; +import { describe, expect, it, beforeAll, afterAll, vi } from "vitest"; import IORedis from "ioredis"; import { pushOpsSummaryToEoc } from "../src/processors/eoc-push.js"; @@ -21,6 +21,43 @@ describe("pushOpsSummaryToEoc", () => { await redis.quit(); }); + it("maps feedback.created_at to skapad in the POST body", async () => { + process.env.EOC_URL = "https://eoc.example.com"; + process.env.EOC_PUSH_TOKEN = "tok"; + const ts = "2026-08-11T10:00:00.000Z"; + await redis.set( + "ops:summary:cache", + JSON.stringify({ + as_of: ts, + app: { app: "cibello" }, + feedback: { + oppna: 1, + senaste: [{ typ: "bug", rubrik: "exempel", status: "oppen", created_at: ts }], + }, + }), + "EX", + 60, + ); + + const originalFetch = globalThis.fetch; + const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 204 })); + globalThis.fetch = fetchMock as unknown as typeof fetch; + + try { + const result = await pushOpsSummaryToEoc(redis); + expect(result.ok).toBe(true); + expect(fetchMock).toHaveBeenCalledTimes(1); + + const init = fetchMock.mock.calls[0][1] as RequestInit; + const body = JSON.parse(init.body as string); + expect(body.feedback.senaste[0].skapad).toBe(ts); + expect(body.feedback.senaste[0]).not.toHaveProperty("created_at"); + expect(body.as_of).toBe(ts); + } finally { + globalThis.fetch = originalFetch; + } + }); + it("returns disabled result when EOC_URL is missing", async () => { delete process.env.EOC_URL; process.env.EOC_PUSH_TOKEN = "tok";