Files
kuber/tests/lib/scaffold-templates.test.ts
T
2026-10-06 15:31:51 +00:00

120 lines
4.0 KiB
TypeScript

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");
});
});