Files

118 lines
5.7 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { gitEnv } from "../src/checkout.mjs";
import { readReviewedArtifact, ArtifactError } from "../src/artifact.mjs";
const LIMIT = 1024 * 1024;
function git(cwd, ...args) {
const result = spawnSync("git", ["-c", "user.name=test", "-c", "[email protected]", ...args],
{ cwd, encoding: "utf8", env: gitEnv() });
assert.equal(result.status, 0, `git ${args.join(" ")}: ${result.stderr}`);
return result.stdout.trim();
}
function world(t) {
const dir = mkdtempSync(join(tmpdir(), "mosaic-artifact-"));
t.after(() => rmSync(dir, { recursive: true, force: true }));
const work = join(dir, "work"), repo = join(dir, "working.git");
mkdirSync(work);
git(work, "init", "-q");
mkdirSync(join(work, "site"));
const original = Buffer.from("<html>first\0version</html>\n");
writeFileSync(join(work, "site", "index.html"), original);
writeFileSync(join(work, "site", "one.txt"), "plain");
writeFileSync(join(work, "site", "run.htm"), "executable");
chmodSync(join(work, "site", "run.htm"), 0o755);
writeFileSync(join(work, "site", "empty"), "");
writeFileSync(join(work, "site", "exact.bin"), Buffer.alloc(LIMIT, 0x61));
writeFileSync(join(work, "site", "large.bin"), Buffer.alloc(LIMIT + 1, 0x62));
symlinkSync("index.html", join(work, "site", "link.html"));
git(work, "add", ".");
git(work, "commit", "-qm", "first");
const first = git(work, "rev-parse", "HEAD");
const blob = git(work, "rev-parse", "HEAD:site/index.html");
git(work, "update-index", "--add", "--cacheinfo", `160000,${first},site/submodule`);
writeFileSync(join(work, "site", "index.html"), "later");
git(work, "add", "site/index.html");
git(work, "commit", "-qm", "second");
const second = git(work, "rev-parse", "HEAD");
git(dir, "clone", "-q", "--bare", work, repo);
return { repo, work, first, second, blob, original };
}
async function refuses(args, code) {
await assert.rejects(readReviewedArtifact(args), error => error instanceof ArtifactError && error.code === code);
}
test("reads exact committed HTML bytes, blob ID and SHA-256", async t => {
const { repo, first, blob, original } = world(t);
const result = await readReviewedArtifact({ repo, commit: first, path: "site/index.html" });
assert.deepEqual(result, { bytes: original, blob, sha256: createHash("sha256").update(original).digest("hex"), mime: "text/html" });
});
test("reads the reviewed commit after a later commit changes the file", async t => {
const { repo, first, second, original } = world(t);
assert.deepEqual((await readReviewedArtifact({ repo, commit: first, path: "site/index.html" })).bytes, original);
assert.equal((await readReviewedArtifact({ repo, commit: second, path: "site/index.html" })).bytes.toString(), "later");
});
test("accepts executable HTML and empty blobs under a zero-byte limit", async t => {
const { repo, first } = world(t);
const executable = await readReviewedArtifact({ repo, commit: first, path: "site/run.htm" });
assert.equal(executable.mime, "text/html");
assert.equal(executable.bytes.toString(), "executable");
assert.equal((await readReviewedArtifact({ repo, commit: first, path: "site/empty", maxBytes: 0 })).bytes.length, 0);
});
test("rejects traversal, empty segments, absolute paths, backslashes and NUL", async t => {
const { repo, first } = world(t);
for (const path of ["", "/site/index.html", "../site/index.html", "site/../site/index.html", "./site/index.html", "site/./index.html", "site//index.html", "site/", "site\\index.html", "site/\0index.html"]) {
await refuses({ repo, commit: first, path }, "artifact-path-invalid");
}
});
test("rejects symlink, submodule and directory entries", async t => {
const { repo, second } = world(t);
for (const path of ["site/link.html", "site/submodule", "site"]) {
await refuses({ repo, commit: second, path }, "artifact-not-regular");
}
});
test("accepts exactly one MiB and rejects larger blobs and invalid limits", async t => {
const { repo, first } = world(t);
const exact = await readReviewedArtifact({ repo, commit: first, path: "site/exact.bin" });
assert.deepEqual(exact.bytes, Buffer.alloc(LIMIT, 0x61));
assert.equal(exact.mime, "application/octet-stream");
await refuses({ repo, commit: first, path: "site/large.bin" }, "artifact-too-large");
await refuses({ repo, commit: first, path: "site/one.txt", maxBytes: 4 }, "artifact-too-large");
for (const maxBytes of [-1, LIMIT + 1, 1.5, NaN, "10", null]) {
await refuses({ repo, commit: first, path: "site/one.txt", maxBytes }, "artifact-limit-invalid");
}
});
test("rejects abbreviated IDs, refs, non-commit IDs and nonexistent commits", async t => {
const { repo, first, blob } = world(t);
for (const commit of [first.slice(0, 12), "HEAD", blob, "f".repeat(40)]) {
await refuses({ repo, commit, path: "site/index.html" }, "artifact-commit-invalid");
}
});
test("rejects missing paths and treats Git pathspec magic literally", async t => {
const { repo, first } = world(t);
for (const path of ["site/missing.html", "site/*.html", ":(glob)site/*.html"]) {
await refuses({ repo, commit: first, path }, "artifact-missing");
}
});
test("ignores replacement refs when resolving reviewed objects", async t => {
const { repo, first, second, original } = world(t);
git(repo, "replace", first, second);
assert.deepEqual((await readReviewedArtifact({ repo, commit: first, path: "site/index.html" })).bytes, original);
});