// 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 = "coder@example.com"; 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", "owner@example.com"); 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 / 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", "other@example.com"); 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); });