713 lines
24 KiB
TypeScript
713 lines
24 KiB
TypeScript
import { afterEach, describe, expect, test, spyOn } from "bun:test";
|
|
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { YAML } from "bun";
|
|
import { initializeProject } from "../../command/init";
|
|
import { managedHeader } from "../../lib/scaffold";
|
|
import { readTrust, resolveTrustIdentity, updateTrust } from "../../lib/trust";
|
|
import { KuberApiError } from "../../lib/api";
|
|
import type { ScaffoldQuestion } from "../../lib/scaffold-prompts";
|
|
|
|
const roots: string[] = [];
|
|
const originalConfig = process.env.XDG_CONFIG_HOME;
|
|
afterEach(async () => {
|
|
for (const root of roots.splice(0))
|
|
await rm(root, { recursive: true, force: true });
|
|
if (originalConfig === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
else process.env.XDG_CONFIG_HOME = originalConfig;
|
|
});
|
|
async function root() {
|
|
const path = await mkdtemp(join(tmpdir(), "kuber-init-test-"));
|
|
roots.push(path);
|
|
process.env.XDG_CONFIG_HOME = join(path, "config");
|
|
return path;
|
|
}
|
|
|
|
describe("kuber init", () => {
|
|
test("interactive YAML fields finish before login and automatic trust", async () => {
|
|
const cwd = await root();
|
|
const events: string[] = [];
|
|
const session = {
|
|
token: "test",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
};
|
|
const log = spyOn(console, "log").mockImplementation(() => {});
|
|
try {
|
|
await initializeProject(
|
|
cwd,
|
|
{},
|
|
{
|
|
prompt: async (question) => {
|
|
events.push(question.name);
|
|
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(
|
|
false,
|
|
);
|
|
if (question.name === "source") return "image";
|
|
if (question.name === "image") return "nginx:stable";
|
|
if (question.name === "path") return ".";
|
|
expect(question.type).toBe("snippet");
|
|
expect(question.template).toStartWith(
|
|
"name: ${project}\nservices:\n ${name}:",
|
|
);
|
|
expect(question.template).not.toContain("managedBy");
|
|
const project = question.fields?.find(
|
|
({ name }) => name === "project",
|
|
);
|
|
expect(project?.validate?.("Bad Project")).not.toBe(true);
|
|
expect(project?.validate?.("demo")).toBe(true);
|
|
return {
|
|
values: { project: "demo", name: "web", replicas: "1" },
|
|
result: "malicious preview",
|
|
};
|
|
},
|
|
session: async () => {
|
|
events.push("session");
|
|
return undefined;
|
|
},
|
|
login: async () => {
|
|
events.push("login");
|
|
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(
|
|
false,
|
|
);
|
|
return session;
|
|
},
|
|
request: async (_path, init) => {
|
|
events.push(init?.method ?? "GET");
|
|
return (init?.method ? undefined : { fingerprints: [] }) as never;
|
|
},
|
|
persistTrust: async () => {
|
|
events.push("trust");
|
|
},
|
|
},
|
|
);
|
|
expect(events).toEqual([
|
|
"source",
|
|
"image",
|
|
"path",
|
|
"compose",
|
|
"session",
|
|
"login",
|
|
"GET",
|
|
"POST",
|
|
"trust",
|
|
]);
|
|
const content = await readFile(join(cwd, "compose.yml"), "utf8");
|
|
expect(content.split("\n")[0]).toBe(managedHeader);
|
|
expect(YAML.parse(content)).toEqual({
|
|
managedBy: "kuber",
|
|
name: "demo",
|
|
services: { web: { image: "nginx:stable", deploy: { replicas: 1 } } },
|
|
});
|
|
expect(content).not.toContain("malicious");
|
|
} finally {
|
|
log.mockRestore();
|
|
}
|
|
});
|
|
|
|
test("flagged project and configured project bypass editable project field; existing login is reused", async () => {
|
|
for (const configured of [false, true]) {
|
|
const cwd = await root();
|
|
if (configured)
|
|
await writeFile(
|
|
join(cwd, ".kuberrc.ts"),
|
|
'export default { project: "fixed" };\n',
|
|
);
|
|
let logins = 0;
|
|
const questions: ScaffoldQuestion[] = [];
|
|
const log = spyOn(console, "log").mockImplementation(() => {});
|
|
try {
|
|
await initializeProject(
|
|
cwd,
|
|
{
|
|
source: "auto",
|
|
cnb: "",
|
|
path: ".",
|
|
...(configured ? {} : { project: "fixed" }),
|
|
},
|
|
{
|
|
prompt: async (question) => {
|
|
questions.push(question);
|
|
expect(question.template).toStartWith('name: "fixed"');
|
|
expect(
|
|
question.fields?.some(({ name }) => name === "project"),
|
|
).toBe(false);
|
|
return {
|
|
values: { project: "ignored", name: "web", replicas: "1" },
|
|
};
|
|
},
|
|
session: async () => ({
|
|
token: "test",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
}),
|
|
login: async () => {
|
|
logins++;
|
|
throw new Error("unneeded login");
|
|
},
|
|
request: async (_path, init) =>
|
|
(init?.method ? undefined : { fingerprints: [] }) as never,
|
|
persistTrust: async () => {},
|
|
},
|
|
);
|
|
expect(questions.map(({ type }) => type)).toEqual(["snippet"]);
|
|
expect(logins).toBe(0);
|
|
expect(
|
|
YAML.parse(await readFile(join(cwd, "compose.yml"), "utf8")),
|
|
).toMatchObject({
|
|
name: "fixed",
|
|
services: { web: { build: "auto" } },
|
|
});
|
|
} finally {
|
|
log.mockRestore();
|
|
}
|
|
}
|
|
});
|
|
|
|
test("cancel or invalid snippet performs no login, trust, or filesystem writes", async () => {
|
|
for (const failure of ["cancel", "invalid"]) {
|
|
const cwd = await root();
|
|
let downstream = 0;
|
|
await expect(
|
|
initializeProject(
|
|
cwd,
|
|
{ source: "template", template: "bun-service", path: "." },
|
|
{
|
|
prompt: async () => {
|
|
if (failure === "cancel") throw new Error("cancelled");
|
|
return {
|
|
values: {
|
|
project: "demo",
|
|
name: "web",
|
|
cpu: "-1",
|
|
replicas: "1",
|
|
},
|
|
};
|
|
},
|
|
session: async () => {
|
|
downstream++;
|
|
return undefined;
|
|
},
|
|
login: async () => {
|
|
downstream++;
|
|
throw new Error("unexpected login");
|
|
},
|
|
request: async () => {
|
|
downstream++;
|
|
return undefined as never;
|
|
},
|
|
},
|
|
),
|
|
).rejects.toThrow(
|
|
failure === "cancel" ? "cancelled" : "CPU must be a positive",
|
|
);
|
|
expect(downstream).toBe(0);
|
|
for (const name of ["compose.yml", "Dockerfile", ".dockerignore"])
|
|
expect(await Bun.file(join(cwd, name)).exists()).toBe(false);
|
|
}
|
|
});
|
|
|
|
test("uses an existing configured project for Compose and trust", async () => {
|
|
const cwd = await root();
|
|
await writeFile(
|
|
join(cwd, ".kuberrc.ts"),
|
|
'export default { project: "configured" };\n',
|
|
);
|
|
const paths: string[] = [];
|
|
const log = spyOn(console, "log").mockImplementation(() => {});
|
|
try {
|
|
await initializeProject(
|
|
cwd,
|
|
{ nonInteractive: true },
|
|
{
|
|
session: async () => ({
|
|
token: "test",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
}),
|
|
request: async (path, init) => {
|
|
paths.push(path);
|
|
return (
|
|
init?.method === "POST" ? undefined : { fingerprints: [] }
|
|
) as never;
|
|
},
|
|
},
|
|
);
|
|
expect(
|
|
YAML.parse(await readFile(join(cwd, "compose.yml"), "utf8")),
|
|
).toMatchObject({ name: "configured" });
|
|
expect(paths).toEqual([
|
|
"/workspaces/configured/trust",
|
|
"/workspaces/configured/trust",
|
|
]);
|
|
} finally {
|
|
log.mockRestore();
|
|
}
|
|
});
|
|
test("empty directory creates documented image-only example and trusts selected project", async () => {
|
|
const cwd = await root();
|
|
const calls: string[] = [];
|
|
const log = spyOn(console, "log").mockImplementation(() => {});
|
|
try {
|
|
await initializeProject(
|
|
cwd,
|
|
{ nonInteractive: true, project: "demo" },
|
|
{
|
|
session: async () => ({
|
|
token: "test",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
}),
|
|
request: async (path, init) => {
|
|
calls.push(path);
|
|
return (
|
|
init?.method === "POST" ? undefined : { fingerprints: [] }
|
|
) as never;
|
|
},
|
|
},
|
|
);
|
|
const content = await readFile(join(cwd, "compose.yml"), "utf8");
|
|
expect(content.split("\n")[0]).toBe(managedHeader);
|
|
const compose = YAML.parse(content);
|
|
expect(compose).toMatchObject({
|
|
managedBy: "kuber",
|
|
name: "demo",
|
|
services: {
|
|
[join(cwd).split("/").at(-1)!.toLowerCase()]: {
|
|
image: "nginx:stable",
|
|
},
|
|
},
|
|
});
|
|
expect(calls).toEqual([
|
|
"/workspaces/demo/trust",
|
|
"/workspaces/demo/trust",
|
|
]);
|
|
expect(await readTrust()).toHaveLength(1);
|
|
expect(await Bun.file(join(cwd, "Dockerfile")).exists()).toBe(false);
|
|
} finally {
|
|
log.mockRestore();
|
|
}
|
|
});
|
|
|
|
test("multiple manifests require explicit selection in noninteractive mode", async () => {
|
|
const cwd = await root();
|
|
for (const name of ["one", "two"]) {
|
|
await mkdir(join(cwd, name));
|
|
await writeFile(join(cwd, name, "package.json"), "{}");
|
|
}
|
|
await expect(
|
|
initializeProject(
|
|
cwd,
|
|
{ nonInteractive: true },
|
|
{ session: async () => undefined },
|
|
),
|
|
).rejects.toThrow("specify --path");
|
|
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(false);
|
|
});
|
|
|
|
test("revoked session retries once without duplicating trust; failed grant never writes local trust", async () => {
|
|
const cwd = await root();
|
|
let attempts = 0;
|
|
let logins = 0;
|
|
const log = spyOn(console, "log").mockImplementation(() => {});
|
|
try {
|
|
await initializeProject(
|
|
cwd,
|
|
{ nonInteractive: true, project: "demo" },
|
|
{
|
|
session: async () => ({
|
|
token: "old",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
}),
|
|
login: async () => {
|
|
logins++;
|
|
return {
|
|
token: "new",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
};
|
|
},
|
|
request: async (_path, init) => {
|
|
if (++attempts === 2) throw new KuberApiError("revoked", 401);
|
|
return (
|
|
init?.method === "POST" ? undefined : { fingerprints: [] }
|
|
) as never;
|
|
},
|
|
},
|
|
);
|
|
expect([attempts, logins]).toEqual([4, 1]);
|
|
expect(await readTrust()).toHaveLength(1);
|
|
} finally {
|
|
log.mockRestore();
|
|
}
|
|
const other = await root();
|
|
await expect(
|
|
initializeProject(
|
|
other,
|
|
{ nonInteractive: true, project: "bad" },
|
|
{
|
|
session: async () => undefined,
|
|
login: async () => ({
|
|
token: "new",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
}),
|
|
request: async () => {
|
|
throw new KuberApiError("failed", 503);
|
|
},
|
|
},
|
|
),
|
|
).rejects.toThrow("failed");
|
|
expect(await readTrust()).toEqual([]);
|
|
expect(await Bun.file(join(other, "compose.yml")).exists()).toBe(false);
|
|
});
|
|
|
|
test("successful grant with failed local persistence keeps registration and generated files", async () => {
|
|
const cwd = await root();
|
|
const identity = await resolveTrustIdentity("demo", cwd);
|
|
const trusted = new Set<string>();
|
|
const calls: string[] = [];
|
|
const log = spyOn(console, "log").mockImplementation(() => {});
|
|
try {
|
|
const failure = await initializeProject(
|
|
cwd,
|
|
{
|
|
nonInteractive: true,
|
|
project: "demo",
|
|
source: "template",
|
|
template: "static-nginx",
|
|
},
|
|
{
|
|
session: async () => ({
|
|
token: "test",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
}),
|
|
request: async (path, init) => {
|
|
calls.push(init?.method ?? "GET");
|
|
if (init?.method === "POST")
|
|
trusted.add((init.json as { fingerprint: string }).fingerprint);
|
|
expect(path).toBe("/workspaces/demo/trust");
|
|
return (
|
|
init?.method ? undefined : { fingerprints: [...trusted] }
|
|
) as never;
|
|
},
|
|
persistTrust: async () => {
|
|
throw new Error("disk full: secret-token");
|
|
},
|
|
},
|
|
).catch((error: unknown) => error);
|
|
expect(failure).toBeInstanceOf(Error);
|
|
expect((failure as Error).message).toMatch(
|
|
/Server registration exists.*kuber trust/,
|
|
);
|
|
expect((failure as Error).message).not.toContain("secret-token");
|
|
expect((failure as Error).cause).toBeUndefined();
|
|
expect(calls).toEqual(["GET", "POST"]);
|
|
expect(trusted.has(identity.fingerprint)).toBe(true);
|
|
expect(await readTrust()).toEqual([]);
|
|
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true);
|
|
expect(await Bun.file(join(cwd, "Dockerfile")).exists()).toBe(true);
|
|
expect(await Bun.file(join(cwd, ".dockerignore")).exists()).toBe(true);
|
|
expect(log).not.toHaveBeenCalled();
|
|
} finally {
|
|
log.mockRestore();
|
|
}
|
|
});
|
|
|
|
test("pre-existing server grant and local trust survive local persistence failure", async () => {
|
|
const cwd = await root();
|
|
const identity = await resolveTrustIdentity("demo", cwd);
|
|
const existing = await resolveTrustIdentity("other", cwd);
|
|
await updateTrust(() => [existing]);
|
|
const calls: string[] = [];
|
|
await expect(
|
|
initializeProject(
|
|
cwd,
|
|
{ nonInteractive: true, project: "demo" },
|
|
{
|
|
session: async () => ({
|
|
token: "test",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
}),
|
|
request: async (_path, init) => {
|
|
calls.push(init?.method ?? "GET");
|
|
return { fingerprints: [identity.fingerprint] } as never;
|
|
},
|
|
persistTrust: async () => {
|
|
throw new Error("disk full");
|
|
},
|
|
},
|
|
),
|
|
).rejects.toThrow(/Server registration exists.*kuber trust/);
|
|
expect(calls).toEqual(["GET"]);
|
|
expect(await readTrust()).toEqual([existing]);
|
|
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true);
|
|
});
|
|
|
|
test("concurrent grant between GET and POST is never revoked on local failure", async () => {
|
|
const cwd = await root();
|
|
const identity = await resolveTrustIdentity("demo", cwd);
|
|
const calls: string[] = [];
|
|
const trusted = new Set<string>();
|
|
await expect(
|
|
initializeProject(
|
|
cwd,
|
|
{ nonInteractive: true, project: "demo" },
|
|
{
|
|
session: async () => ({
|
|
token: "test",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
}),
|
|
request: async (_path, init) => {
|
|
calls.push(init?.method ?? "GET");
|
|
if (!init?.method) {
|
|
trusted.add(identity.fingerprint); // Another client grants after our GET snapshot.
|
|
return { fingerprints: [] } as never;
|
|
}
|
|
if (init.method === "POST") {
|
|
expect(trusted.has(identity.fingerprint)).toBe(true);
|
|
return undefined as never; // Idempotent POST did not create this grant.
|
|
}
|
|
throw new Error(`Unexpected ${init.method}`);
|
|
},
|
|
persistTrust: async () => {
|
|
throw new Error("disk full");
|
|
},
|
|
},
|
|
),
|
|
).rejects.toThrow(/Server registration exists.*kuber trust/);
|
|
expect(calls).toEqual(["GET", "POST"]);
|
|
expect(trusted.has(identity.fingerprint)).toBe(true);
|
|
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true);
|
|
});
|
|
|
|
test("partial local write still reports failure without revoking remote trust", async () => {
|
|
const cwd = await root();
|
|
const identity = await resolveTrustIdentity("demo", cwd);
|
|
const calls: string[] = [];
|
|
const log = spyOn(console, "log").mockImplementation(() => {});
|
|
try {
|
|
await expect(
|
|
initializeProject(
|
|
cwd,
|
|
{ nonInteractive: true, project: "demo" },
|
|
{
|
|
session: async () => ({
|
|
token: "test",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
}),
|
|
request: async (_path, init) => {
|
|
calls.push(init?.method ?? "GET");
|
|
return (init?.method ? undefined : { fingerprints: [] }) as never;
|
|
},
|
|
persistTrust: async (update) => {
|
|
await updateTrust(update); // Simulates rename succeeding before chmod fails.
|
|
throw new Error("chmod failed");
|
|
},
|
|
},
|
|
),
|
|
).rejects.toThrow(/Server registration exists.*kuber trust/);
|
|
expect(calls).toEqual(["GET", "POST"]);
|
|
expect(await readTrust()).toEqual([identity]);
|
|
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true);
|
|
expect(log).not.toHaveBeenCalled();
|
|
} finally {
|
|
log.mockRestore();
|
|
}
|
|
});
|
|
|
|
test("failed POST rolls back generated files without altering existing trust", async () => {
|
|
const cwd = await root();
|
|
const existing = await resolveTrustIdentity("other", cwd);
|
|
await updateTrust(() => [existing]);
|
|
const calls: string[] = [];
|
|
await expect(
|
|
initializeProject(
|
|
cwd,
|
|
{
|
|
nonInteractive: true,
|
|
project: "demo",
|
|
source: "template",
|
|
template: "static-nginx",
|
|
},
|
|
{
|
|
session: async () => ({
|
|
token: "test",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
}),
|
|
request: async (_path, init) => {
|
|
calls.push(init?.method ?? "GET");
|
|
if (init?.method === "POST")
|
|
throw new KuberApiError("grant failed", 400);
|
|
return { fingerprints: [] } as never;
|
|
},
|
|
},
|
|
),
|
|
).rejects.toThrow("grant failed");
|
|
expect(calls).toEqual(["GET", "POST"]);
|
|
expect(await readTrust()).toEqual([existing]);
|
|
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(false);
|
|
expect(await Bun.file(join(cwd, "Dockerfile")).exists()).toBe(false);
|
|
expect(await Bun.file(join(cwd, ".dockerignore")).exists()).toBe(false);
|
|
});
|
|
|
|
test("committed POST with lost response keeps generated files after reconciliation", async () => {
|
|
const cwd = await root();
|
|
const identity = await resolveTrustIdentity("demo", cwd);
|
|
const trusted = new Set<string>();
|
|
const calls: string[] = [];
|
|
await expect(
|
|
initializeProject(
|
|
cwd,
|
|
{
|
|
nonInteractive: true,
|
|
project: "demo",
|
|
source: "template",
|
|
template: "static-nginx",
|
|
},
|
|
{
|
|
session: async () => ({
|
|
token: "test",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
}),
|
|
request: async (_path, init) => {
|
|
calls.push(init?.method ?? "GET");
|
|
if (init?.method === "POST") {
|
|
trusted.add(identity.fingerprint);
|
|
throw new Error("socket closed after commit");
|
|
}
|
|
return { fingerprints: [...trusted] } as never;
|
|
},
|
|
},
|
|
),
|
|
).rejects.toThrow(/registration succeeded.*files were kept/);
|
|
expect(calls).toEqual(["GET", "POST", "GET"]);
|
|
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true);
|
|
expect(await Bun.file(join(cwd, "Dockerfile")).exists()).toBe(true);
|
|
expect(await Bun.file(join(cwd, ".dockerignore")).exists()).toBe(true);
|
|
});
|
|
|
|
test("unavailable reconciliation keeps generated files and explains repair", async () => {
|
|
const cwd = await root();
|
|
const calls: string[] = [];
|
|
await expect(
|
|
initializeProject(
|
|
cwd,
|
|
{
|
|
nonInteractive: true,
|
|
project: "demo",
|
|
},
|
|
{
|
|
session: async () => ({
|
|
token: "test",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
}),
|
|
request: async (_path, init) => {
|
|
calls.push(init?.method ?? "GET");
|
|
if (init?.method === "POST") throw new Error("response lost");
|
|
if (calls.length === 3)
|
|
throw new Error("trust service unavailable");
|
|
return { fingerprints: [] } as never;
|
|
},
|
|
},
|
|
),
|
|
).rejects.toThrow(/Could not confirm.*kuber trust status.*kuber trust/);
|
|
expect(calls).toEqual(["GET", "POST", "GET"]);
|
|
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true);
|
|
});
|
|
|
|
test("concurrent grant found during reconciliation keeps generated files", async () => {
|
|
const cwd = await root();
|
|
const identity = await resolveTrustIdentity("demo", cwd);
|
|
const calls: string[] = [];
|
|
await expect(
|
|
initializeProject(
|
|
cwd,
|
|
{
|
|
nonInteractive: true,
|
|
project: "demo",
|
|
},
|
|
{
|
|
session: async () => ({
|
|
token: "test",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
}),
|
|
request: async (_path, init) => {
|
|
calls.push(init?.method ?? "GET");
|
|
if (init?.method === "POST") throw new Error("response lost");
|
|
return {
|
|
fingerprints: calls.length === 3 ? [identity.fingerprint] : [],
|
|
} as never;
|
|
},
|
|
},
|
|
),
|
|
).rejects.toThrow(/registration succeeded.*files were kept/);
|
|
expect(calls).toEqual(["GET", "POST", "GET"]);
|
|
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(true);
|
|
});
|
|
|
|
test("existing Compose file is not overwritten and no grant is attempted", async () => {
|
|
const cwd = await root();
|
|
const composePath = join(cwd, "compose.yml");
|
|
const existing = "name: existing\nservices: {}\n";
|
|
await writeFile(composePath, existing);
|
|
let requested = false;
|
|
await expect(
|
|
initializeProject(
|
|
cwd,
|
|
{ nonInteractive: true, project: "demo" },
|
|
{
|
|
request: async () => {
|
|
requested = true;
|
|
return undefined as never;
|
|
},
|
|
},
|
|
),
|
|
).rejects.toThrow("A Compose file already exists");
|
|
expect(await readFile(composePath, "utf8")).toBe(existing);
|
|
expect(requested).toBe(false);
|
|
});
|
|
|
|
test("existing Dockerfile is not overwritten by template init", async () => {
|
|
const cwd = await root();
|
|
const dockerfile = join(cwd, "Dockerfile");
|
|
await writeFile(dockerfile, "FROM private-image\n");
|
|
let requested = false;
|
|
await expect(
|
|
initializeProject(
|
|
cwd,
|
|
{
|
|
nonInteractive: true,
|
|
project: "demo",
|
|
source: "template",
|
|
template: "static-nginx",
|
|
},
|
|
{
|
|
session: async () => ({
|
|
token: "test",
|
|
expiresAt: "2099-01-01",
|
|
user: { username: "a", roles: [] },
|
|
}),
|
|
request: async () => {
|
|
requested = true;
|
|
return undefined as never;
|
|
},
|
|
},
|
|
),
|
|
).rejects.toThrow();
|
|
expect(await readFile(dockerfile, "utf8")).toBe("FROM private-image\n");
|
|
expect(await Bun.file(join(cwd, "compose.yml")).exists()).toBe(false);
|
|
expect(requested).toBe(false);
|
|
});
|
|
});
|