Files
kuber/tests/lib/convert-artifacts.test.ts
2026-09-03 11:28:30 +07:00

192 lines
5.6 KiB
TypeScript

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