191 lines
5.7 KiB
TypeScript
191 lines
5.7 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/,
|
|
);
|
|
});
|