import { describe, expect, it, vi } from "vitest"; import { createHmac } from "node:crypto"; import { imagePublication } from "../scripts/image-publication.mjs"; import { notifyDockhand } from "../scripts/notify-dockhand.mjs"; const revision = "abcdef01".repeat(5); const publish = (event: string, ref: string, inputTag = "") => imagePublication({ event, ref, inputTag, revision }); describe("image publication channels", () => { it.each(["v1.2.3", "1.2.3", "v0.0.0", "v10.20.300"])( "publishes stable version %s to prod", (tag) => expect(publish("push", `refs/tags/${tag}`)).toEqual({ tag, channel: "prod", revision, }), ); it.each(["abcdef0", revision])( "publishes a matching manual commit %s only to dev", (commit) => { expect( publish("workflow_dispatch", "refs/heads/main", `dev-${commit}`), ).toEqual({ tag: `dev-${commit}`, channel: "dev", revision }); expect( publish("workflow_dispatch", "refs/tags/v1.2.3", `dev-${commit}`) .channel, ).toBe("dev"); }, ); it.each([ "v1.2.3-rc.1", "v1.2.3+build", "v01.2.3", "v1.2", "prod", "latest", "dev-abcdef0", "release/v1.2.3", "v1.2.3\nchannel=prod", "v1.2.3\n", ])("rejects non-release Git tag %s before publication", (tag) => expect(() => publish("push", `refs/tags/${tag}`)).toThrow(), ); it.each([ "v1.2.3", "prod", "latest", "dev", "", "dev-1234567", "dev-abcdef", "dev-ABCDEF0", "dev-abcdef0\nchannel=prod", "dev-abcdef0\n", "dev-$(exit 1)", ])("rejects manual tag %s instead of promoting it", (tag) => expect(() => publish("workflow_dispatch", "refs/heads/main", tag), ).toThrow(), ); it("does not publish branches or pull requests", () => { expect(() => publish("push", "refs/heads/main")).toThrow(); expect(() => publish("pull_request", "refs/tags/v1.2.3")).toThrow(); }); }); describe("optional Dockhand deployment", () => { const url = "https://dockhand.example.com/api/git/stacks/42/webhook"; const secret = "test-webhook-secret"; it("does not contact Dockhand when this channel has no configuration", async () => { const send = vi.fn(); await expect(notifyDockhand({}, send)).resolves.toBe(false); expect(send).not.toHaveBeenCalled(); }); it.each([{ url }, { secret }])( "rejects incomplete configuration", async (config) => { const send = vi.fn(); await expect(notifyDockhand(config, send)).rejects.toThrow(); expect(send).not.toHaveBeenCalled(); }, ); it("signs exactly the posted payload and refuses redirects", async () => { const send = vi.fn().mockResolvedValue(new Response("{}")); await expect(notifyDockhand({ url, secret }, send)).resolves.toBe(true); const [target, request] = send.mock.calls[0]; expect(target.href).toBe(url); expect(request.method).toBe("POST"); expect(request.body).toBe("{}"); expect(request.redirect).toBe("error"); expect(request.headers["X-Hub-Signature-256"]).toBe( `sha256=${createHmac("sha256", secret).update(request.body).digest("hex")}`, ); }); it("reports failed deployment requests", async () => { const send = vi .fn() .mockResolvedValue( new Response("private server details", { status: 403 }), ); await expect(notifyDockhand({ url, secret }, send)).rejects.toThrow( "HTTP 403", ); }); });