91 lines
3.5 KiB
TypeScript
91 lines
3.5 KiB
TypeScript
|
|
import { describe, expect, it, vi } from "vitest";
|
||
|
|
import { CoolifyClient } from "../src/coolify.js";
|
||
|
|
import { smoke } from "../src/smoke.js";
|
||
|
|
|
||
|
|
type EnvVar = {
|
||
|
|
key: string;
|
||
|
|
value: string;
|
||
|
|
is_buildtime: boolean;
|
||
|
|
uuid: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
// A stateful fetch mock standing in for Coolify's env-var store, so the
|
||
|
|
// bulk-write step's effect on previously-written keys is observable —
|
||
|
|
// `bulkMode: "upsert"` mirrors verified Coolify 4.1.2 behavior (see
|
||
|
|
// syncEnv's comment in cli.ts); `bulkMode: "replace"` simulates a
|
||
|
|
// regression to full-replace that smoke must catch.
|
||
|
|
function mockEnvStore(
|
||
|
|
appUuid: string,
|
||
|
|
bulkMode: "upsert" | "replace",
|
||
|
|
): typeof fetch {
|
||
|
|
let store: EnvVar[] = [];
|
||
|
|
let nextUuid = 1;
|
||
|
|
return vi.fn(async (url: string | URL, init?: RequestInit) => {
|
||
|
|
const method = init?.method ?? "GET";
|
||
|
|
const path = new URL(String(url)).pathname;
|
||
|
|
const base = `/api/v1/applications/${appUuid}/envs`;
|
||
|
|
if (method === "GET" && path === base) {
|
||
|
|
return new Response(JSON.stringify(store), { status: 200 });
|
||
|
|
}
|
||
|
|
if (method === "POST" && path === base) {
|
||
|
|
const body = JSON.parse(String(init?.body)) as {
|
||
|
|
key: string;
|
||
|
|
value: string;
|
||
|
|
is_buildtime: boolean;
|
||
|
|
};
|
||
|
|
const created: EnvVar = { ...body, uuid: `env-${nextUuid++}` };
|
||
|
|
store.push(created);
|
||
|
|
return new Response(JSON.stringify(created), { status: 200 });
|
||
|
|
}
|
||
|
|
if (method === "PATCH" && path === `${base}/bulk`) {
|
||
|
|
const body = JSON.parse(String(init?.body)) as {
|
||
|
|
data: Array<{ key: string; value: string; is_buildtime: boolean }>;
|
||
|
|
};
|
||
|
|
if (bulkMode === "replace") {
|
||
|
|
store = body.data.map((v) => ({ ...v, uuid: `env-${nextUuid++}` }));
|
||
|
|
} else {
|
||
|
|
for (const v of body.data) {
|
||
|
|
const existing = store.find((e) => e.key === v.key);
|
||
|
|
if (existing) Object.assign(existing, v);
|
||
|
|
else store.push({ ...v, uuid: `env-${nextUuid++}` });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
||
|
|
}
|
||
|
|
if (method === "DELETE" && path.startsWith(`${base}/`)) {
|
||
|
|
const uuid = path.slice(`${base}/`.length);
|
||
|
|
store = store.filter((e) => e.uuid !== uuid);
|
||
|
|
return new Response(null, { status: 204 });
|
||
|
|
}
|
||
|
|
if (method === "GET" && path === "/api/v1/version") {
|
||
|
|
return new Response("4.1.2", { status: 200 });
|
||
|
|
}
|
||
|
|
return new Response("not found", { status: 404 });
|
||
|
|
}) as unknown as typeof fetch;
|
||
|
|
}
|
||
|
|
|
||
|
|
describe("smoke", () => {
|
||
|
|
it("passes and leaves no residue when bulk env write is a true upsert", async () => {
|
||
|
|
const fetchImpl = mockEnvStore("app-1", "upsert");
|
||
|
|
const client = new CoolifyClient("https://coolify.test", "tok", fetchImpl);
|
||
|
|
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
||
|
|
await expect(smoke(client, "app-1")).resolves.toBeUndefined();
|
||
|
|
expect(log.mock.calls.at(-1)?.[0]).toMatch(/smoke OK/);
|
||
|
|
log.mockRestore();
|
||
|
|
// both probe vars cleaned up
|
||
|
|
const envsRes = await fetchImpl(
|
||
|
|
"https://coolify.test/api/v1/applications/app-1/envs",
|
||
|
|
{ method: "GET" },
|
||
|
|
);
|
||
|
|
expect(await envsRes.json()).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("fails loudly when the bulk env write is destructive (full-replace regression)", async () => {
|
||
|
|
const fetchImpl = mockEnvStore("app-1", "replace");
|
||
|
|
const client = new CoolifyClient("https://coolify.test", "tok", fetchImpl);
|
||
|
|
await expect(smoke(client, "app-1")).rejects.toThrow(
|
||
|
|
/bulk env write is destructive \(full-replace\) — never-delete broken/,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|