import { describe, expect, test } from "bun:test"; import { listDockerfileTemplates, renderDockerfileTemplate, templateIds, type DockerfileTemplateId, } from "../../lib/scaffold-templates"; // Parse each Dockerfile's instructions and JSON commands to catch malformed // templates without requiring a Docker daemon in the test environment. function parseDockerfile(source: string): { stages: string[]; commands: string[][]; } { const stages: string[] = []; const commands: string[][] = []; let stageCount = 0; for (const line of source.split("\n")) { const text = line.trim(); if (!text || text.startsWith("#")) continue; const match = /^([A-Z]+)\s+(.+)$/.exec(text); expect(match, `Invalid Dockerfile instruction: ${text}`).not.toBeNull(); const [, instruction, argument] = match!; expect([ "FROM", "WORKDIR", "COPY", "RUN", "ENV", "USER", "EXPOSE", "CMD", "ENTRYPOINT", ]).toContain(instruction!); if (instruction === "FROM") { stageCount++; const alias = /\s+AS\s+(\w+)$/i.exec(argument!)?.[1]; if (alias) stages.push(alias); } else { expect(stageCount, `${instruction} must follow FROM`).toBeGreaterThan(0); } if (instruction === "COPY") { const from = /^--from=(\w+)\s/.exec(argument!)?.[1]; if (from) expect(stages).toContain(from); } if (instruction === "CMD" || instruction === "ENTRYPOINT") { const parsed: unknown = JSON.parse(argument!); expect(Array.isArray(parsed)).toBe(true); expect( (parsed as unknown[]).every((value) => typeof value === "string"), ).toBe(true); commands.push(parsed as string[]); } } expect(stageCount).toBeGreaterThan(0); expect(commands).toHaveLength(1); return { stages, commands }; } describe("embedded scaffold templates", () => { test("lists eight selectable, distinct templates in stable order", () => { const options = listDockerfileTemplates(); expect(options.map(({ id }) => id)).toEqual([...templateIds]); expect(new Set(templateIds).size).toBe(8); for (const { id, label, description } of options) { expect(label.trim()).not.toBe(""); expect(description.trim()).not.toBe(""); expect(description).toMatch( /src\/|server\.js|dist\/|app:app|binary|port/i, ); expect(renderDockerfileTemplate(id).dockerfile).toContain("FROM "); } expect(listDockerfileTemplates()).toEqual(options); }); test.each([...templateIds])( "renders and parses %s with a safe build context", (id) => { const { dockerfile, dockerignore } = renderDockerfileTemplate(id); expect(dockerfile.length).toBeGreaterThan(50); expect(dockerignore.length).toBeGreaterThan(50); expect(dockerfile.endsWith("\n")).toBe(true); expect(dockerignore.endsWith("\n")).toBe(true); parseDockerfile(dockerfile); const patterns = dockerignore .split("\n") .filter((line) => line && !line.startsWith("#")); expect(patterns).toContain(".git"); expect(patterns).toContain(".env"); expect(patterns).toContain(".env.*"); expect(patterns).toContain("**/.env"); expect(patterns).toContain("**/.env.*"); expect(dockerfile).not.toMatch( /COPY\s+.*(?:\.env|credentials|\.pem|\.key)/i, ); expect(dockerfile).not.toMatch( /\/home\/dmgnr|\/home\/container|hamsterstore|kuber-server/, ); }, ); test("static site includes built output while app builders omit it", () => { expect(renderDockerfileTemplate("static-nginx").dockerignore).not.toMatch( /^dist\/?$/m, ); expect(renderDockerfileTemplate("static-nginx").dockerfile).toContain( "COPY dist/", ); for (const id of ["bun-compiled", "node-pnpm"] as const) { expect(renderDockerfileTemplate(id).dockerignore).toMatch(/^dist$/m); } }); test("rejects unknown selections", () => { expect(() => renderDockerfileTemplate("missing" as DockerfileTemplateId), ).toThrow("Unknown Dockerfile template: missing"); }); });