Files
MineGeo/tests/deploy-vercel.test.ts

231 lines
7.0 KiB
TypeScript

import assert from "node:assert/strict";
import { test } from "node:test";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { zipSync, strToU8 } from "fflate";
// The command is also an importable module, so tests never trigger a real upload.
import {
deploymentFiles,
deploymentPayload,
deploy,
} from "../scripts/deploy-vercel.mjs";
function fixture(
run: (
root: string,
write: (file: string, data: string | Uint8Array) => void,
) => void,
) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "minegeo-deploy-test-"));
function write(file: string, data: string | Uint8Array) {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.writeFileSync(path.join(root, file), data);
}
try {
write(
"package.json",
JSON.stringify({
name: "minegeo",
scripts: { worker: "private worker" },
repository: "https://private.invalid/repository",
}),
);
write("app/layout.tsx", "export default function Layout() {}");
run(root, write);
} finally {
assert.ok(root.startsWith(path.join(os.tmpdir(), "minegeo-deploy-test-")));
fs.rmSync(root, { recursive: true, force: true });
}
}
test("deployment collects web assets and excludes local secrets, Git and build outputs", () => {
fixture((root, write) => {
write(".env.local", "SUPABASE_SERVICE_ROLE_KEY=secret");
write(".env.deploy.local", "VERCEL_TOKEN=secret");
write(".git/config", "private Git configuration");
write("public/.hidden", "secret");
write("public/generated/capture.png", "private capture");
write("public/downloads/old.jar", "generated artifact");
write("app/downloads/page.tsx", "export default function Downloads() {}");
write("worker/main.ts", "private worker");
write("public/images/map.svg", "<svg/>");
write("components/Example.tsx", "export const Example = 1;");
const files = deploymentFiles(root);
assert.deepEqual(
files.map((entry: { file: string }) => entry.file).sort(),
[
"app/downloads/page.tsx",
"app/layout.tsx",
"components/Example.tsx",
"package.json",
"public/images/map.svg",
],
);
const pkg = JSON.parse(
Buffer.from(
files.find((entry: { file: string }) => entry.file === "package.json")
.data,
"base64",
).toString(),
);
assert.equal(pkg.repository, undefined);
assert.equal(pkg.scripts.worker, undefined);
const payload = deploymentPayload(files);
assert.equal(payload.target, "production");
assert.equal(deploymentPayload(files, true).target, undefined);
assert.equal(Object.hasOwn(payload, "gitSource"), false);
assert.equal(Object.hasOwn(payload, "gitMetadata"), false);
assert.equal(
JSON.stringify(payload).includes("SUPABASE_SERVICE_ROLE_KEY"),
false,
);
});
});
test("privacy scan rejects the private Git hostname in public files and bundled JARs", () => {
fixture((root, write) => {
execFileSync("git", ["init", root], { windowsHide: true, stdio: "ignore" });
execFileSync(
"git",
[
"-C",
root,
"remote",
"add",
"origin",
"https://private.example.invalid/user/minegeo.git",
],
{ windowsHide: true },
);
write("public/leak.txt", "https://private.example.invalid");
assert.throws(
() => deploymentFiles(root),
/Private Git server reference found in public\/leak.txt/,
);
fs.unlinkSync(path.join(root, "public/leak.txt"));
write(
"public/tools/mod.jar",
zipSync({ "metadata.json": strToU8('"private.example.invalid"') }),
);
assert.throws(
() => deploymentFiles(root),
/Private Git server reference found in public\/tools\/mod.jar/,
);
});
});
test("deploy sends every file in one POST, then polls until READY", async () => {
const requests: { url: string; init: RequestInit }[] = [];
const states = ["QUEUED", "BUILDING", "READY"];
const logs: string[] = [];
const payload = deploymentPayload([
{ file: "app/page.tsx", data: "YWJj", encoding: "base64" },
]);
const result = await deploy(payload, {
token: "test-token",
wait: async () => {},
log: (message: string) => logs.push(message),
request: async (url, init = {}) => {
requests.push({ url: String(url), init });
return new Response(
JSON.stringify({
id: "test-deployment",
readyState: states.shift(),
url: "preview.example.invalid",
}),
{ status: 200 },
);
},
});
assert.equal(result.readyState, "READY");
assert.equal(
requests.filter((item) => item.init.method === "POST").length,
1,
);
assert.deepEqual(
JSON.parse(requests[0].init.body as string).files,
payload.files,
);
assert.equal(
requests.slice(1).every((item) => item.init.method === "GET"),
true,
);
assert.equal(
logs.some((message) => message.includes("test-token")),
false,
);
});
test("failed builds and rejected authentication never report success", async () => {
const payload = deploymentPayload([]);
await assert.rejects(
deploy(payload, {
token: "test",
log: () => {},
request: async () =>
new Response(
JSON.stringify({
id: "failed",
readyState: "ERROR",
errorMessage: "Build failed",
}),
{ status: 200 },
),
}),
/Build failed/,
);
await assert.rejects(
deploy(payload, {
token: "test",
log: () => {},
request: async () =>
new Response(JSON.stringify({ error: { message: "Unauthorized" } }), {
status: 401,
}),
}),
/Vercel 401/,
);
});
test("large desktop releases upload as binary before deployment and failures stop release", async () => {
const binary = Buffer.alloc(10 * 1024 * 1024 + 1, 7);
const payload = deploymentPayload([
{
file: "public/desktop/MineGeo.exe",
data: binary.toString("base64"),
encoding: "base64",
},
]);
const requests: { url: string; init: RequestInit }[] = [];
await deploy(payload, {
token: "test",
log: () => {},
request: async (url, init = {}) => {
requests.push({ url: String(url), init });
return new Response(
JSON.stringify(
String(url).includes("/v2/files")
? {}
: { id: "release", readyState: "READY", url: "example.invalid" },
),
);
},
});
assert.ok(requests[0].url.includes("/v2/files"));
assert.deepEqual(requests[0].init.body, binary);
const file = JSON.parse(requests[1].init.body as string).files[0];
assert.equal(file.size, binary.length);
assert.match(file.sha, /^[a-f0-9]{40}$/);
assert.equal(file.data, undefined);
await assert.rejects(
deploy(payload, {
token: "test",
log: () => {},
request: async () => new Response("", { status: 403 }),
}),
/file upload failed/,
);
});