test/scripts/deploy-edge.test.ts
Rama main · réplica de lectura del repositorio original.
import { assertStringIncludes } from "@std/assert";
import { expect } from "@std/expect";
import { describe, it as test } from "@std/testing/bdd";
import {
ACCESS_KEY_ERROR,
BUNDLE_PATH,
BUNNY_API_BASE,
DEPLOY_BUILT_USAGE,
type DeployEdgeDeps,
deployScriptCode,
formatBunnyError,
getAccessKey,
parseDeployBuiltArgs,
parseScriptIdArg,
publishScript,
runDeployBuiltEdge,
runDeployEdge,
USAGE,
uploadScriptCode,
} from "#scripts/deploy-edge-lib.ts";
import type { FetchText, FetchTextResult } from "#scripts/fetch-text.ts";
interface FetchCall {
init: RequestInit;
url: string;
}
const envReader =
(vars: Record<string, string | undefined>) =>
(key: string): string | undefined =>
vars[key];
const fetchRecorder = (
responses: FetchTextResult[],
): { calls: FetchCall[]; fetchText: FetchText } => {
const calls: FetchCall[] = [];
const fetchText: FetchText = (url, init) => {
calls.push({ init, url });
return Promise.resolve(
responses[calls.length - 1] ?? { ok: true, status: 200, text: "{}" },
);
};
return { calls, fetchText };
};
const response = (
ok: boolean,
status: number,
text: string,
): FetchTextResult => ({ ok, status, text });
const headersFrom = (init: RequestInit): Record<string, string> =>
init.headers as Record<string, string>;
const bodyFrom = (init: RequestInit): Record<string, unknown> =>
JSON.parse(init.body as string);
const deployDeps = (
overrides: Partial<DeployEdgeDeps> = {},
): { deps: DeployEdgeDeps; stderr: string[]; stdout: string[] } => {
const stdout: string[] = [];
const stderr: string[] = [];
const recorder = fetchRecorder([
response(true, 200, "{}"),
response(true, 200, "{}"),
]);
return {
deps: {
args: ["42"],
bundlePath: "/repo/bunny-script.ts",
cwd: "/repo",
fetchText: recorder.fetchText,
getEnv: envReader({ BUNNY_ACCESS_KEY: "access-key" }),
readTextFile: () => Promise.resolve("console.log(1)"),
runBuildEdge: () => Promise.resolve({ code: 0, success: true }),
stderr: (line) => stderr.push(line),
stdout: (line) => stdout.push(line),
...overrides,
},
stderr,
stdout,
};
};
const expectDeployFailsBeforeBuild = async (
overrides: Partial<DeployEdgeDeps>,
): Promise<string[]> => {
let buildCalled = false;
const { deps, stderr } = deployDeps({
...overrides,
runBuildEdge: () => {
buildCalled = true;
return Promise.resolve({ code: 0, success: true });
},
});
await expect(runDeployEdge(deps)).resolves.toBe(1);
expect(buildCalled).toBe(false);
return stderr;
};
describe("deploy-edge argument parsing", () => {
test("accepts one script ID and trims surrounding whitespace", () => {
expect(parseScriptIdArg([" 12345 "])).toEqual({
ok: true,
scriptId: "12345",
});
});
test("rejects missing, blank, or extra script IDs", () => {
expect(parseScriptIdArg([])).toEqual({ error: USAGE, ok: false });
expect(parseScriptIdArg([" "])).toEqual({ error: USAGE, ok: false });
expect(parseScriptIdArg(["1", "2"])).toEqual({
error: USAGE,
ok: false,
});
});
});
describe("deploy-built-edge argument parsing", () => {
test("accepts a script ID and optional bundle path", () => {
expect(parseDeployBuiltArgs([" 12345 "])).toEqual({
bundlePath: BUNDLE_PATH,
ok: true,
scriptId: "12345",
});
expect(parseDeployBuiltArgs([" 12345 ", " built.ts "])).toEqual({
bundlePath: "built.ts",
ok: true,
scriptId: "12345",
});
});
test("rejects missing, blank, or extra inputs", () => {
const usageError = { error: DEPLOY_BUILT_USAGE, ok: false };
expect([
parseDeployBuiltArgs([]),
parseDeployBuiltArgs([" "]),
parseDeployBuiltArgs(["1", " "]),
parseDeployBuiltArgs(["1", "built.ts", "extra"]),
]).toEqual([usageError, usageError, usageError, usageError]);
});
});
describe("deploy-edge access key lookup", () => {
test("prefers BUNNY_ACCESS_KEY from .env", () => {
expect(
getAccessKey(
envReader({ BUNNY_ACCESS_KEY: " access ", BUNNY_API_KEY: "api" }),
),
).toBe("access");
});
test("falls back to BUNNY_API_KEY for existing local env files", () => {
expect(getAccessKey(envReader({ BUNNY_API_KEY: " api " }))).toBe("api");
});
test("ignores blank keys", () => {
expect(getAccessKey(envReader({ BUNNY_ACCESS_KEY: " " }))).toBeUndefined();
});
});
describe("deploy-edge Bunny API requests", () => {
test("formats JSON, raw, and empty Bunny API errors", () => {
expect(
formatBunnyError(
"Upload",
response(false, 400, JSON.stringify({ Message: "Bad request" })),
),
).toBe("Upload failed (400): Bad request");
expect(
formatBunnyError("Upload", response(false, 500, "Server Error")),
).toBe("Upload failed (500): Server Error");
expect(formatBunnyError("Upload", response(false, 502, ""))).toBe(
"Upload failed (502): empty response",
);
expect(
formatBunnyError("Upload", response(false, 422, '{"Message":" "}')),
).toBe('Upload failed (422): {"Message":" "}');
});
test("uploads code using the workflow Bunny endpoint", async () => {
const recorder = fetchRecorder([response(true, 200, "{}")]);
const result = await uploadScriptCode(
"abc/123",
"console.log(1)",
"key",
recorder.fetchText,
);
expect(result).toEqual({ ok: true });
expect(recorder.calls[0]!.url).toBe(
`${BUNNY_API_BASE}/compute/script/abc%2F123/code`,
);
expect(recorder.calls[0]!.init.method).toBe("POST");
expect(headersFrom(recorder.calls[0]!.init).AccessKey).toBe("key");
expect(headersFrom(recorder.calls[0]!.init)["Content-Type"]).toBe(
"application/json",
);
expect(bodyFrom(recorder.calls[0]!.init)).toEqual({
Code: "console.log(1)",
});
});
test("publishes code using an empty JSON body", async () => {
const recorder = fetchRecorder([response(true, 200, "{}")]);
const result = await publishScript("123", "key", recorder.fetchText);
expect(result).toEqual({ ok: true });
expect(recorder.calls[0]!.url).toBe(
`${BUNNY_API_BASE}/compute/script/123/publish`,
);
expect(bodyFrom(recorder.calls[0]!.init)).toEqual({});
});
test("uploads then publishes a script", async () => {
const recorder = fetchRecorder([
response(true, 200, "{}"),
response(true, 200, "{}"),
]);
const result = await deployScriptCode(
"123",
"code",
"key",
recorder.fetchText,
);
expect(result).toEqual({ ok: true });
expect(recorder.calls.map((call) => call.url)).toEqual([
`${BUNNY_API_BASE}/compute/script/123/code`,
`${BUNNY_API_BASE}/compute/script/123/publish`,
]);
});
test("stops before publishing when upload fails", async () => {
const recorder = fetchRecorder([response(false, 500, "Nope")]);
const result = await deployScriptCode(
"123",
"code",
"key",
recorder.fetchText,
);
expect(result).toEqual({
error: "Upload script code failed (500): Nope",
ok: false,
});
expect(recorder.calls).toHaveLength(1);
});
test("returns publish failures after a successful upload", async () => {
const recorder = fetchRecorder([
response(true, 200, "{}"),
response(false, 503, "Unavailable"),
]);
const result = await deployScriptCode(
"123",
"code",
"key",
recorder.fetchText,
);
expect(result).toEqual({
error: "Publish script failed (503): Unavailable",
ok: false,
});
expect(recorder.calls).toHaveLength(2);
});
});
describe("runDeployEdge", () => {
test("builds, reads, uploads, and publishes the bundle", async () => {
const recorder = fetchRecorder([
response(true, 200, "{}"),
response(true, 200, "{}"),
]);
const { deps, stderr, stdout } = deployDeps({
fetchText: recorder.fetchText,
});
await expect(runDeployEdge(deps)).resolves.toBe(0);
expect(stderr).toEqual([]);
expect(stdout).toEqual([
"Building edge bundle...",
`Uploading ${BUNDLE_PATH} to Bunny script 42...`,
"Published Bunny script 42.",
]);
expect(recorder.calls).toHaveLength(2);
});
test("returns usage errors before building", async () => {
const stderr = await expectDeployFailsBeforeBuild({ args: [] });
expect(stderr).toEqual([USAGE]);
});
test("requires a Bunny access key before building", async () => {
const stderr = await expectDeployFailsBeforeBuild({
getEnv: envReader({}),
});
expect(stderr).toEqual([ACCESS_KEY_ERROR]);
});
test("stops when the edge build fails", async () => {
let readCalled = false;
const { deps, stderr } = deployDeps({
readTextFile: () => {
readCalled = true;
return Promise.resolve("code");
},
runBuildEdge: () => Promise.resolve({ code: 3, success: false }),
});
await expect(runDeployEdge(deps)).resolves.toBe(1);
expect(stderr).toEqual(["build:edge failed with exit code 3"]);
expect(readCalled).toBe(false);
});
test("reports a missing bundle", async () => {
let fetchCalled = false;
const { deps, stderr } = deployDeps({
fetchText: () => {
fetchCalled = true;
return Promise.resolve(response(true, 200, "{}"));
},
readTextFile: () => Promise.reject(new Error("missing")),
});
await expect(runDeployEdge(deps)).resolves.toBe(1);
assertStringIncludes(
stderr[0] ?? "",
"Failed to read /repo/bunny-script.ts",
);
assertStringIncludes(stderr[0] ?? "", "Error: missing");
expect(fetchCalled).toBe(false);
});
test("reports deploy failures", async () => {
const recorder = fetchRecorder([response(false, 401, "Unauthorized")]);
const { deps, stderr } = deployDeps({
fetchText: recorder.fetchText,
});
await expect(runDeployEdge(deps)).resolves.toBe(1);
expect(stderr).toEqual(["Upload script code failed (401): Unauthorized"]);
expect(recorder.calls).toHaveLength(1);
});
});
describe("runDeployBuiltEdge", () => {
test("returns usage errors before reading", async () => {
const { deps, stderr } = deployDeps({ args: [] });
await expect(runDeployBuiltEdge(deps)).resolves.toBe(1);
expect(stderr).toEqual([DEPLOY_BUILT_USAGE]);
});
test("reads, uploads, and publishes an already-built bundle", async () => {
const recorder = fetchRecorder([
response(true, 200, "{}"),
response(true, 200, "{}"),
]);
const { deps, stderr, stdout } = deployDeps({
args: ["42", "built.ts"],
fetchText: recorder.fetchText,
});
await expect(runDeployBuiltEdge(deps)).resolves.toBe(0);
expect(stderr).toEqual([]);
expect(stdout).toEqual([
"Uploading built.ts to Bunny script 42...",
"Published Bunny script 42.",
]);
expect(recorder.calls).toHaveLength(2);
});
test("requires a Bunny access key before reading the bundle", async () => {
let readCalled = false;
const { deps, stderr } = deployDeps({
args: ["42", "built.ts"],
getEnv: envReader({}),
readTextFile: () => {
readCalled = true;
return Promise.resolve("code");
},
});
await expect(runDeployBuiltEdge(deps)).resolves.toBe(1);
expect(stderr).toEqual([ACCESS_KEY_ERROR]);
expect(readCalled).toBe(false);
});
});