import { afterEach, describe, expect, test } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import type { V1Deployment } from "@kubernetes/client-node"; import { DEFAULT_REGISTRY, DEFAULT_ROLLOUT_TIMEOUT_MS, extractConfigArgument, loadConfig, resolveConfigArgument, } from "../../lib/config"; import { composeToKubernetes } from "../../lib/convert"; const directories: string[] = []; async function temporaryDirectory() { const directory = await mkdtemp(join(tmpdir(), "kuber-config-")); directories.push(directory); return directory; } afterEach(async () => { await Promise.all( directories .splice(0) .map((directory) => rm(directory, { recursive: true, force: true })), ); }); describe("kuber config", () => { test("uses operational defaults when .kuberrc.ts is absent", async () => { const cwd = await temporaryDirectory(); expect(await loadConfig(cwd)).toMatchObject({ project: cwd.split("/").at(-1), registry: DEFAULT_REGISTRY, rolloutTimeoutMs: DEFAULT_ROLLOUT_TIMEOUT_MS, configFile: undefined, }); }); test("loads TypeScript from a custom config path", async () => { const cwd = await temporaryDirectory(); const configFile = join(cwd, "production.kuberrc.ts"); await Bun.write( configFile, `export default { project: "configured-project", composeFile: "deploy/compose.yml", registry: "registry.example.com/", rolloutTimeoutMs: 42_000, async preBuild() {}, };`, ); const config = await loadConfig(cwd, "production.kuberrc.ts"); expect(config).toMatchObject({ project: "configured-project", composeFile: join(cwd, "deploy", "compose.yml"), registry: "registry.example.com", rolloutTimeoutMs: 42_000, configFile, }); expect(config.preBuild).toBeFunction(); }); test("requires explicit config paths to exist", async () => { const cwd = await temporaryDirectory(); await expect(loadConfig(cwd, "missing.ts")).rejects.toThrow( "Config file cannot be found", ); }); test("requires a default object export", async () => { const cwd = await temporaryDirectory(); await Bun.write(join(cwd, ".kuberrc.ts"), "export const project = 'bad';"); await expect(loadConfig(cwd)).rejects.toThrow( "must default export a configuration object", ); }); test("validates hook and operational values", async () => { const cwd = await temporaryDirectory(); await Bun.write( join(cwd, ".kuberrc.ts"), "export default { preBuild: true, rolloutTimeoutMs: -1 };", ); await expect(loadConfig(cwd)).rejects.toThrow("preBuild"); }); test("uses the configured registry in rendered build images", async () => { const resources = await composeToKubernetes( "project", { services: { app: { build: "." } } }, process.cwd(), {}, { app: `registry.example.com/team/kuber/project-app:latest@sha256:${"a".repeat(64)}` }, ); const deployment = resources.find( (resource) => resource.kind === "Deployment", ) as V1Deployment | undefined; expect(deployment?.spec?.template.spec?.containers[0]?.image).toBe( `registry.example.com/team/kuber/project-app:latest@sha256:${"a".repeat(64)}`, ); }); }); describe("global config argument", () => { test.each([ [["--config", "one.ts", "up"], "one.ts"], [["up", "--config", "two.ts"], "two.ts"], [["--config=three.ts", "up"], "three.ts"], ])("reads a config path from %j", (args, expected) => { expect(resolveConfigArgument(args)).toBe(expected); }); test("rejects a config option without a path", () => { expect(() => resolveConfigArgument(["up", "--config"])).toThrow( "requires a path", ); }); test("does not parse config arguments after --", () => { expect( resolveConfigArgument(["exec", "app", "--", "--config", "x.ts"]), ).toBeUndefined(); }); test("removes the global option before subcommand dispatch", () => { expect( extractConfigArgument(["logs", "--config", "production.ts"]), ).toEqual({ configPath: "production.ts", rawArgs: ["logs"], }); }); test("rejects duplicate config options", () => { expect(() => extractConfigArgument(["--config=one.ts", "--config=two.ts"]), ).toThrow("only be specified once"); }); });