import { afterEach, describe, expect, test } from "bun:test"; import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { composeToKubernetes, envFromToSecrets } from "../../lib/convert"; import type { ComposeSpecification, Service } from "../../schema/docker.d"; import { BundleArtifactProvider, LocalArtifactProvider, createArtifactBundle, } from "../../shared/artifacts"; const temporaryDirectories: string[] = []; async function temporaryDirectory(): Promise { const path = await mkdtemp(join(tmpdir(), "kuber-artifacts-")); temporaryDirectories.push(path); return path; } afterEach(async () => { await Promise.all( temporaryDirectories .splice(0) .map((path) => rm(path, { recursive: true, force: true })), ); }); describe("conversion artifacts", () => { test("bundle rendering matches the default local filesystem rendering", async () => { const workspace = await temporaryDirectory(); const env = "MODE=production\nTOKEN=a=b\n"; const config = "enabled=true\n"; await writeFile(join(workspace, ".env"), env); await writeFile(join(workspace, "app.conf"), config); const compose = { services: { app: { image: "app", env_file: ".env", volumes: ["./app.conf:/etc/app.conf:ro"], }, }, } as ComposeSpecification; const local = await composeToKubernetes("project", compose, workspace); const bundle = JSON.parse( JSON.stringify(createArtifactBundle({ ".env": env, "app.conf": config })), ); const bundled = await composeToKubernetes( "project", compose, workspace, {}, {}, new BundleArtifactProvider(bundle), ); expect(bundled).toEqual(local); }); test("bundle providers preserve optional and required missing-file behavior", async () => { const provider = new BundleArtifactProvider(createArtifactBundle({})); expect( await envFromToSecrets( "project", "app", { env_file: [{ path: "missing.env", required: false }] } as Service, process.cwd(), {}, provider, ), ).toEqual([]); await expect( envFromToSecrets( "project", "app", { env_file: "missing.env" } as Service, process.cwd(), {}, provider, ), ).rejects.toThrow("Artifact not found"); }); test("default local providers preserve env-file tilde path behavior", async () => { const workspace = await temporaryDirectory(); await mkdir(join(workspace, "~")); await writeFile(join(workspace, "~", "legacy.env"), "LEGACY=yes\n"); const [secret] = await envFromToSecrets( "project", "app", { env_file: "~/legacy.env" } as Service, workspace, ); expect(secret?.stringData).toEqual({ LEGACY: "yes" }); }); test("strict local providers reject traversal and absolute paths", async () => { const workspace = await temporaryDirectory(); const provider = new LocalArtifactProvider({ workspace, strict: true }); await expect( envFromToSecrets( "project", "app", { env_file: "../outside.env" } as Service, workspace, {}, provider, ), ).rejects.toThrow("escapes the workspace"); await expect( envFromToSecrets( "project", "app", { env_file: join(workspace, "absolute.env") } as Service, workspace, {}, provider, ), ).rejects.toThrow("workspace-relative"); }); test("strict local providers reject symlinks escaping the workspace", async () => { const workspace = await temporaryDirectory(); const outside = await temporaryDirectory(); await writeFile(join(outside, "secret.env"), "TOKEN=secret\n"); await symlink(join(outside, "secret.env"), join(workspace, "secret.env")); await expect( envFromToSecrets( "project", "app", { env_file: "secret.env" } as Service, workspace, {}, new LocalArtifactProvider({ workspace, strict: true }), ), ).rejects.toThrow("escapes the workspace"); }); test("strict local providers reject missing files below escaping symlinks", async () => { const workspace = await temporaryDirectory(); const outside = await temporaryDirectory(); await symlink(outside, join(workspace, "outside")); await expect( envFromToSecrets( "project", "app", { env_file: [{ path: "outside/missing.env", required: false }], } as Service, workspace, {}, new LocalArtifactProvider({ workspace, strict: true }), ), ).rejects.toThrow("escapes the workspace"); }); test("bundle providers reject unsafe paths and byte or count excesses", () => { expect( () => new BundleArtifactProvider(createArtifactBundle({ "../secret": "x" })), ).toThrow("escapes the workspace"); expect( () => new BundleArtifactProvider(createArtifactBundle({ config: "four" }), { maxArtifactBytes: 3, }), ).toThrow("exceeds the 3 byte limit"); expect( () => new BundleArtifactProvider( createArtifactBundle({ first: "1", second: "2" }), { maxArtifactCount: 1 }, ), ).toThrow("Artifact count exceeds the 1 limit"); expect( () => new BundleArtifactProvider( createArtifactBundle({ first: "12", second: "34" }), { maxTotalBytes: 3 }, ), ).toThrow("Artifact bytes exceed the 3 byte limit"); }); });