Files
Epicnabbo-Catalogus-Updated…/src/lib/docker-build-contract.test.ts
T

81 lines
3.2 KiB
TypeScript

import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dockerfile = readFileSync("Dockerfile", "utf8");
describe("Docker build cache", () => {
it("installs frozen dependencies before copying application source", () => {
const manifests = dockerfile.indexOf(
"COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* .npmrc* ./",
);
const fetch = dockerfile.indexOf("pnpm fetch --ignore-scripts");
const install = dockerfile.indexOf("pnpm install --frozen-lockfile");
const source = dockerfile.indexOf("COPY . .");
expect(manifests).toBeGreaterThan(-1);
expect(fetch).toBeGreaterThan(manifests);
expect(install).toBeGreaterThan(fetch);
expect(source).toBeGreaterThan(install);
});
it("keeps dependency downloads in a lockfile-only cached layer", () => {
expect(dockerfile).toContain("pnpm fetch --ignore-scripts");
expect(dockerfile).toContain(
"pnpm install --frozen-lockfile --ignore-scripts --offline",
);
});
it("ships standalone output without a redundant dependency pruning step", () => {
expect(dockerfile).toContain("/app/.next/standalone ./");
expect(dockerfile).not.toContain("pnpm prune --prod");
expect(dockerfile).not.toContain("npm prune --production");
expect(dockerfile).not.toContain("yarn install --production");
});
});
it("passes a compiled release to both the application build and final image", () => {
expect(dockerfile.indexOf("ARG NEXT_DEPLOYMENT_ID")).toBeGreaterThan(
dockerfile.indexOf("COPY . ."),
);
expect(dockerfile).toContain(
'LABEL org.opencontainers.image.revision="$NEXT_DEPLOYMENT_ID"',
);
expect(dockerfile.match(/FROM node:26\.8\.1-alpine/g)).toHaveLength(2);
const compose = readFileSync("docker-compose.yml", "utf8");
// biome-ignore lint/suspicious/noTemplateCurlyInString: Docker Compose interpolation, not JavaScript.
expect(compose).toContain("NEXT_DEPLOYMENT_ID: ${CMS_RELEASE:-unknown}");
});
it("builds with fixtures and excludes installation secrets from every stage", () => {
const ignored = readFileSync(".dockerignore", "utf8");
expect(ignored).toMatch(/^\.env$/m);
expect(ignored).toMatch(/^\.env\.\*$/m);
expect(dockerfile).toContain("FROM migrations AS builder");
expect(dockerfile).toContain('HOTEL_NAME="Build fixture"');
expect(dockerfile).not.toMatch(
/^ENV.*(?:AUTH_SECRET|DATABASE_URL|HOTEL_NAME)/m,
);
expect(dockerfile).toContain('CMD ["node", "docker-start.mjs"]');
const updater = readFileSync("scripts/docker-update.sh", "utf8");
expect(updater).toContain("target=/app/.env,readonly");
expect(updater).toContain("--target migrations");
});
it("verifies portability before publishing and uses committed build context", () => {
const publish = readFileSync("scripts/publish-container.sh", "utf8");
expect(publish).toContain("git archive HEAD");
expect(
publish.indexOf("node scripts/verify-portable-image.mjs"),
).toBeLessThan(publish.indexOf("docker push"));
expect(publish).toContain("--password-stdin");
expect(publish).not.toContain(":latest");
});
it("does not prerender installation metadata into a shared image", () => {
for (const path of [
"src/app/robots.ts",
"src/app/sitemap.ts",
"src/app/manifest.ts",
]) {
expect(readFileSync(path, "utf8")).toContain(
'export const dynamic = "force-dynamic"',
);
}
});