118 lines
5.7 KiB
JavaScript
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);
|
|
});
|