Squash of Megavolt's 627979a17..86963d281 on origin/megavolt/row199-on-190 (base00d4cc1f6, row 190's landing), the 37 manifest paths, build.patch 05383075, manifest 35f900fd (Duckworth approve round 1, comment 29779, rev 2540). Mutant batches 1 and 2 in agents/megavolt/work/queue-199/BUILD.md: batch 2 26 of 28 killed, M17 and M27 equivalent. docs/TOOLS.md is a 3-way merge with row 200 (9e1085816): row 199's hunk applies unchanged (patch-id d88379111e2e) and no test reads the file. Gate on9e1085816plus the patch: 15 package suites 2735 pass 0 fail, 10 scripts/test-*.sh suites 429 pass 0 fail (plus 156 node tests in the queue suite). Refactor moved to76134dfdesince, a brief touching no manifest path. No live change: the stack runs from copy4429a104funtil the 10-24 re-pin window.
206 lines
11 KiB
JavaScript
206 lines
11 KiB
JavaScript
// Row 199 (#1697, design D9 step 3 to 5, D9a, T3): publish and publishRemote (src/working.mjs)
|
|
// against a TLS fake Gitea on 127.0.0.1 (fake-gitea.mjs) with a scratch CA, passed only through
|
|
// the `tls` parameter. Fake tokens in a scratch root. Each refusal that should reach the network
|
|
// checks the fake saw the request, so no case passes because git refused https first.
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { gitEnv } from "../src/checkout.mjs";
|
|
import { publish, publishRemote, push, workingRepo } from "../src/working.mjs";
|
|
import { fakeGitea, scratchCa } from "./fake-gitea.mjs";
|
|
|
|
const TASK = "vikunja:1/2";
|
|
const TOKEN = "fake_publish_token_0123456789";
|
|
const USER = "coder";
|
|
const EMAIL = "[email protected]";
|
|
const DST = "refs/heads/coder/feat";
|
|
|
|
function git(cwd, ...args) {
|
|
const r = spawnSync("git", args, { cwd, encoding: "utf8", env: gitEnv() });
|
|
assert.equal(r.status, 0, `git ${args.join(" ")}: ${r.stderr}`);
|
|
return r.stdout.trim();
|
|
}
|
|
function commit(dir, text, email = EMAIL) {
|
|
writeFileSync(join(dir, "README"), `${text}\n`);
|
|
git(dir, "add", "README");
|
|
git(dir, "-c", `user.name=${text}`, "-c", `user.email=${email}`, "commit", "-q", "-m", text);
|
|
return git(dir, "rev-parse", "HEAD");
|
|
}
|
|
const rejects = (p, code) => assert.rejects(p, (e) => e.code === code || assert.fail(`${e.code}: ${e.message}, wanted ${code}`));
|
|
|
|
// A root, a coder's clone with work on feat pushed to the working repo, and a fake Gitea whose
|
|
// coder/stack fork is a bare clone of the root.
|
|
async function world(t, { fork = true, redirect = null } = {}) {
|
|
const dir = mkdtempSync(join(tmpdir(), "mosaic-publish-"));
|
|
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
const root = join(dir, "root");
|
|
mkdirSync(root);
|
|
git(root, "init", "-q", "-b", "main");
|
|
const first = commit(root, "one", "[email protected]");
|
|
const clone = join(dir, "clone");
|
|
git(dir, "clone", "-q", root, clone);
|
|
git(clone, "checkout", "-q", "-b", "feat");
|
|
const work = commit(clone, "work");
|
|
const dataRoot = join(dir, "data");
|
|
mkdirSync(dataRoot, { mode: 0o700 });
|
|
const file = join(dir, "b.bundle");
|
|
git(clone, "bundle", "create", "-q", file, "refs/heads/feat", "--not", "--remotes=origin");
|
|
await push({ dataRoot, business: "acme", project: "stack", root, taskRef: TASK, branch: "feat", bundle: file });
|
|
const ca = scratchCa(join(dir, "ca"));
|
|
const gitea = await fakeGitea({ dir: join(dir, "gitea"), tls: ca, user: USER, token: TOKEN, redirect });
|
|
t.after(() => gitea.close());
|
|
const forkRepo = gitea.repo(USER, "stack");
|
|
if (fork) {
|
|
mkdirSync(join(forkRepo, ".."), { recursive: true });
|
|
git(dir, "clone", "-q", "--bare", root, forkRepo);
|
|
}
|
|
const tls = ["-c", `http.sslCAInfo=${ca.ca}`];
|
|
const repo = workingRepo(dataRoot, "acme", "stack");
|
|
const remote = (extra = {}) => publishRemote({ repo, fork: `${gitea.origin}/${USER}/stack`, user: USER, source: work, dst: DST, token: TOKEN + "\n", tls, ...extra });
|
|
const go = (extra = {}) => publish({ dataRoot, business: "acme", project: "stack", root, taskRef: TASK, source: work, email: EMAIL, remote: () => remote(), ...extra });
|
|
return { dir, root, first, clone, work, dataRoot, repo, gitea, forkRepo, tls, remote, go };
|
|
}
|
|
|
|
test("T3: publish pushes the work commit by oid to <user>/<branch> on the fork, with the token only through the helper", async (t) => {
|
|
const w = await world(t);
|
|
assert.deepEqual(await w.go(), { ref: DST, commit: w.work, previous: null });
|
|
assert.equal(git(w.dir, `--git-dir=${w.forkRepo}`, "rev-parse", DST), w.work);
|
|
// Every request went to the fork's path, and each one that carried auth carried the agent's user.
|
|
assert.ok(w.gitea.requests.length > 0);
|
|
for (const r of w.gitea.requests) assert.match(r.path, /^\/coder\/stack\/(info\/refs|git-receive-pack|git-upload-pack)$/);
|
|
assert.ok(w.gitea.requests.some((r) => r.user === USER && r.path.endsWith("git-receive-pack")));
|
|
// A second publish of the same commit is a no-op push and names the previous tip.
|
|
assert.deepEqual(await w.go(), { ref: DST, commit: w.work, previous: w.work });
|
|
// The working repo holds no remote and no credential config.
|
|
assert.equal(spawnSync("git", [`--git-dir=${w.repo}`, "config", "--get-regexp", "^(remote|credential)\\."], { env: gitEnv() }).stdout.toString(), "");
|
|
});
|
|
|
|
test("T3: the token is never in git's argv, the fork URL included; it reaches git on fd 3 only", async (t) => {
|
|
const w = await world(t);
|
|
// A git first on PATH that logs its argv and execs the real one; exec keeps fd 3.
|
|
const real = spawnSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).stdout.trim();
|
|
const shim = join(w.dir, "shim");
|
|
mkdirSync(shim);
|
|
const log = join(w.dir, "argv.log");
|
|
writeFileSync(join(shim, "git"), `#!/bin/sh\nprintf '%s\\n' "$@" >> '${log}'\nexec '${real}' "$@"\n`);
|
|
chmodSync(join(shim, "git"), 0o755);
|
|
assert.deepEqual(await w.remote({ env: { PATH: `${shim}:${process.env.PATH}` } }), { ref: DST, commit: w.work, previous: null });
|
|
const argv = readFileSync(log, "utf8");
|
|
assert.match(argv, /^ls-remote$/m);
|
|
assert.match(argv, /^push$/m);
|
|
assert.equal(argv.includes(TOKEN), false);
|
|
assert.equal(argv.includes(`${USER}:`), false, "no userinfo in a URL");
|
|
});
|
|
|
|
test("T3: a moved fork branch refuses non-fast-forward; no + and no force", async (t) => {
|
|
const w = await world(t);
|
|
const other = join(w.dir, "other");
|
|
git(w.dir, "clone", "-q", w.root, other);
|
|
const elsewhere = commit(other, "elsewhere");
|
|
git(other, "push", "-q", w.forkRepo, `${elsewhere}:${DST}`);
|
|
await rejects(w.go(), "non-fast-forward");
|
|
assert.equal(git(w.dir, `--git-dir=${w.forkRepo}`, "rev-parse", DST), elsewhere);
|
|
});
|
|
|
|
test("T3: D9a: an empty fork and an unrelated fork refuse remote-unrelated; a missing fork refuses fork-unavailable", async (t) => {
|
|
const empty = await world(t, { fork: false });
|
|
git(empty.dir, "init", "-q", "--bare", empty.forkRepo);
|
|
await rejects(empty.go(), "remote-unrelated");
|
|
assert.ok(empty.gitea.requests.length > 0);
|
|
assert.equal(empty.gitea.requests.some((r) => r.path.endsWith("git-receive-pack")), false);
|
|
|
|
const unrelated = await world(t, { fork: false });
|
|
const alone = join(unrelated.dir, "alone");
|
|
mkdirSync(alone);
|
|
git(alone, "init", "-q", "-b", "main");
|
|
commit(alone, "unrelated");
|
|
git(unrelated.dir, "clone", "-q", "--bare", alone, unrelated.forkRepo);
|
|
await rejects(unrelated.go(), "remote-unrelated");
|
|
assert.equal(unrelated.gitea.requests.some((r) => r.path.endsWith("git-receive-pack")), false);
|
|
|
|
const missing = await world(t, { fork: false });
|
|
await rejects(missing.go(), "fork-unavailable");
|
|
assert.ok(missing.gitea.requests.length > 0);
|
|
});
|
|
|
|
test("T3: a fork past main refuses remote-unrelated until root catches up", async (t) => {
|
|
const w = await world(t);
|
|
const other = join(w.dir, "other");
|
|
git(w.dir, "clone", "-q", w.forkRepo, other);
|
|
const ahead = commit(other, "ahead");
|
|
git(other, "push", "-q", w.forkRepo, `${ahead}:refs/heads/main`);
|
|
await rejects(w.go(), "remote-unrelated");
|
|
});
|
|
|
|
test("T3: a wrong token refuses fork-unavailable; git's stderr isn't in the refusal", async (t) => {
|
|
const w = await world(t);
|
|
await assert.rejects(w.remote({ token: "fake_wrong_token_0123456789" }), (e) => e.code === "fork-unavailable" && e.message === "fork-unavailable");
|
|
assert.ok(w.gitea.requests.length > 0);
|
|
assert.equal(git(w.dir, `--git-dir=${w.forkRepo}`, "for-each-ref", DST), "");
|
|
});
|
|
|
|
test("T3: a 302 is not followed: fork-unavailable, and the target sees no request", async (t) => {
|
|
const w = await world(t);
|
|
// One CA for both, which git trusts, so only http.followRedirects=false keeps git off the target;
|
|
// with a CA git doesn't trust, a followed redirect would fail at TLS and the target would see nothing anyway.
|
|
const ca = scratchCa(join(w.dir, "ca2"));
|
|
const target = await fakeGitea({ dir: join(w.dir, "target"), tls: ca, user: USER, token: TOKEN });
|
|
t.after(() => target.close());
|
|
const bounce = await fakeGitea({ dir: join(w.dir, "bounce"), tls: ca, user: USER, token: TOKEN, redirect: target.origin });
|
|
t.after(() => bounce.close());
|
|
const tls = ["-c", `http.sslCAInfo=${ca.ca}`];
|
|
await rejects(w.remote({ fork: `${bounce.origin}/${USER}/stack`, tls }), "fork-unavailable");
|
|
assert.ok(bounce.requests.length > 0);
|
|
assert.equal(target.requests.length, 0);
|
|
});
|
|
|
|
test("T3: the helper answers only the fork's host and port: a fork on another port gets no token", async (t) => {
|
|
const w = await world(t);
|
|
// The fork names port 1; insteadOf (through the tests' tls parameter) sends git to the fake's
|
|
// port. The helper was told port 1, so it answers nothing and no password goes out.
|
|
const before = w.gitea.requests.length;
|
|
await rejects(w.remote({ fork: `https://127.0.0.1:1/${USER}/stack`, tls: [...w.tls, "-c", `url.https://127.0.0.1:${w.gitea.port}/.insteadOf=https://127.0.0.1:1/`] }), "fork-unavailable");
|
|
assert.ok(w.gitea.requests.length > before, "the rewritten URL reached the fake");
|
|
assert.equal(w.gitea.requests.slice(before).some((r) => r.user !== null), false, "no credential went to a host the helper wasn't told");
|
|
});
|
|
|
|
test("T3: a fork that echoes the token refuses credential-leak", async (t) => {
|
|
const w = await world(t);
|
|
const hook = join(w.forkRepo, "hooks", "pre-receive");
|
|
writeFileSync(hook, `#!/bin/sh\necho ${TOKEN} >&2\nexit 0\n`);
|
|
chmodSync(hook, 0o755);
|
|
await rejects(w.go(), "credential-leak");
|
|
});
|
|
|
|
test("T3: no tls parameter, no trust: the scratch CA reaches git only through it", async (t) => {
|
|
const w = await world(t);
|
|
await rejects(w.remote({ tls: [] }), "fork-unavailable");
|
|
});
|
|
|
|
test("D9 steps 3 and 4: ref-moved and identity-mismatch refuse before any request", async (t) => {
|
|
const w = await world(t);
|
|
await rejects(w.go({ source: w.first }), "ref-moved");
|
|
const other = commit(w.clone, "by someone else", "[email protected]");
|
|
const file = join(w.dir, "b2.bundle");
|
|
git(w.clone, "bundle", "create", "-q", file, "refs/heads/feat", "--not", "--remotes=origin");
|
|
await push({ dataRoot: w.dataRoot, business: "acme", project: "stack", root: w.root, taskRef: TASK, branch: "feat", bundle: file });
|
|
await rejects(w.go({ source: other }), "identity-mismatch");
|
|
assert.equal(w.gitea.requests.length, 0);
|
|
});
|
|
|
|
test("publishRemote refuses a fork, user or destination that isn't shell- and ref-safe before any git", async (t) => {
|
|
const w = await world(t);
|
|
for (const extra of [
|
|
{ fork: `http://127.0.0.1:${w.gitea.port}/${USER}/stack` },
|
|
{ fork: `https://127.0.0.1:${w.gitea.port}'/${USER}/stack` },
|
|
{ user: "co'der" },
|
|
{ dst: "refs/heads/main" },
|
|
{ dst: "refs/heads/pm/feat" },
|
|
{ source: "HEAD" },
|
|
]) await rejects(w.remote(extra), "invalid-request");
|
|
assert.equal(w.gitea.requests.length, 0);
|
|
});
|