Public: anyone can read this; the crew can change it.
A test proves a mirrored repository's branch moves only after its origin took the push, for create and delete, in src/actions/branches.ts #7518
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/7511-a-test-proves-a-mirrored-repository-s-br → main 2 commits fork 3 threads · 3 unresolved
1 file
changed +193
−0
test/branches-action.test.ts added +193
| @@ -0,0 +1,193 @@ | ||
| 1 | /** createBranch / deleteBranch: a mirror's ref moves only after its origin took the push; a native repo's moves at once; a reader is refused. */ | |
| 2 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 3 | import { chmod, mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createBranch, deleteBranch } from "../src/actions/branches.ts"; | |
| 7 | import { commitToBranch } from "../src/actions/repos.ts"; | |
| 8 | import { createApp, type App } from "../src/app.ts"; | |
| 9 | import type { ForgeEvent } from "../src/core/events.ts"; | |
| 10 | import { createOrigin, linkRepoToOrigin, rememberOriginRepo } from "../src/core/origins.ts"; | |
| 11 | import { createRepo, getRepo, type Repo } from "../src/core/repos.ts"; | |
| 12 | import { createUser, type User } from "../src/core/users.ts"; | |
| 13 | import { resolveOid } from "../src/git/merge.ts"; | |
| 14 | import { repoDir } from "../src/git/repos.ts"; | |
| 15 | import { advertiseRefs, isGitService, serviceRpc } from "../src/git/smart-http.ts"; | |
| 16 | import { runGitCapture } from "../src/git/env.ts"; | |
| 17 | import { quietLogs } from "./helpers/globals.ts"; | |
| 18 | ||
| 19 | quietLogs(); | |
| 20 | ||
| 21 | const LOGIN = "octo"; | |
| 22 | const TOKEN = "origin-secret-token-7511"; | |
| 23 | const HEADER = `Basic ${Buffer.from(`${LOGIN}:${TOKEN}`).toString("base64")}`; | |
| 24 | ||
| 25 | let dataDir: string; | |
| 26 | let app: App; | |
| 27 | let ada: User; | |
| 28 | let bob: User; | |
| 29 | let native: Repo; | |
| 30 | let mirror: Repo; | |
| 31 | let server: ReturnType<typeof Bun.serve>; | |
| 32 | let originDir: string; | |
| 33 | let reject = ""; | |
| 34 | const events: ForgeEvent[] = []; | |
| 35 | ||
| 36 | const must = <T>(r: { ok: true; value: T } | { ok: false; error: unknown }): T => { | |
| 37 | if (!r.ok) throw new Error(JSON.stringify(r.error)); | |
| 38 | return r.value; | |
| 39 | }; | |
| 40 | const tip = (repo: Repo, branch: string) => | |
| 41 | resolveOid(repoDir(app.reposRoot, repo.ownerName, repo.name), `refs/heads/${branch}`); | |
| 42 | const originTip = (branch: string) => resolveOid(originDir, `refs/heads/${branch}`); | |
| 43 | ||
| 44 | beforeAll(async () => { | |
| 45 | dataDir = await mkdtemp(join(tmpdir(), "forge-branches-action-")); | |
| 46 | app = createApp({ dataDir, port: 0 }); | |
| 47 | app.bus.subscribe((e) => events.push(e)); | |
| 48 | const make = (username: string) => | |
| 49 | createUser(app.db, { origin: "seed", trusted: true, username, email: `${username}@forge.local`, password: "a-good-passphrase" }); | |
| 50 | ada = must(await make("ada")); | |
| 51 | bob = must(await make("bob")); | |
| 52 | ||
| 53 | // The origin: a local bare repository behind the forge's own smart-HTTP plumbing. | |
| 54 | originDir = join(dataDir, "origin.git"); | |
| 55 | await runGitCapture(["init", "-q", "--bare", "-b", "main", originDir], { cwd: dataDir }); | |
| 56 | const hook = join(originDir, "hooks", "pre-receive"); | |
| 57 | await Bun.write(hook, `#!/bin/sh\nif [ -n "$FAKE_REJECT" ]; then echo "$FAKE_REJECT" >&2; exit 1; fi\nexit 0\n`); | |
| 58 | await chmod(hook, 0o755); | |
| 59 | server = Bun.serve({ | |
| 60 | port: 0, | |
| 61 | fetch(request) { | |
| 62 | const url = new URL(request.url); | |
| 63 | const match = /^\/octo\/gears\.git\/(info\/refs|git-upload-pack|git-receive-pack)$/.exec(url.pathname); | |
| 64 | if (!match) return new Response("not found", { status: 404 }); | |
| 65 | if (request.headers.get("authorization") !== HEADER) return new Response("no", { status: 401, headers: { "www-authenticate": "Basic" } }); | |
| 66 | if (match[1] === "info/refs") { | |
| 67 | const service = url.searchParams.get("service"); | |
| 68 | return isGitService(service) ? advertiseRefs(originDir, service, request) : new Response("smart only", { status: 400 }); | |
| 69 | } | |
| 70 | if (match[1] === "git-upload-pack") return serviceRpc(originDir, "git-upload-pack", request); | |
| 71 | return serviceRpc(originDir, "git-receive-pack", request, { env: { FAKE_REJECT: reject } }); | |
| 72 | }, | |
| 73 | }); | |
| 74 | const base = `http://localhost:${server.port}`; | |
| 75 | ||
| 76 | native = must(await createRepo(app.db, app.reposRoot, { ownerId: ada.id, name: "plain", visibility: "private" })); | |
| 77 | mirror = must(await createRepo(app.db, app.reposRoot, { ownerId: ada.id, name: "gears", visibility: "private" })); | |
| 78 | for (const repo of [native, mirror]) | |
| 79 | must(await commitToBranch(app, ada, repo, { message: "seed", files: [{ path: "a.txt", content: "a\n" }] })); | |
| 80 | ||
| 81 | const origin = must( | |
| 82 | createOrigin(app.db, dataDir, { userId: ada.id, kind: "forgejo", apiUrl: `${base}/api/v1`, webUrl: base, login: LOGIN, token: TOKEN }), | |
| 83 | ); | |
| 84 | rememberOriginRepo(app.db, origin.id, { | |
| 85 | remoteId: "1", fullName: "octo/gears", owner: "octo", name: "gears", description: "", private: true, fork: false, | |
| 86 | parent: null, defaultBranch: "main", cloneUrl: `${base}/octo/gears.git`, sshUrl: "", htmlUrl: `${base}/octo/gears`, | |
| 87 | pushedAt: null, permissions: { push: true, admin: false }, | |
| 88 | openIssues: 0, openPulls: 0, openIssuesIncludesPulls: false, sizeKb: 0, | |
| 89 | }); | |
| 90 | linkRepoToOrigin(app.db, mirror.id, { originId: origin.id, fullName: "octo/gears", remoteId: "1", htmlUrl: `${base}/octo/gears` }); | |
| 91 | mirror = getRepo(app.db, "ada", "gears")!; | |
| 92 | }, 30_000); | |
| 93 | ||
| 94 | afterAll(async () => { | |
| 95 | server?.stop(true); | |
| 96 | app?.close(); | |
| 97 | await rm(dataDir, { recursive: true, force: true, maxRetries: 20, retryDelay: 100 }); | |
| 98 | }); | |
| 99 | ||
| 100 | describe("a native repository", () => { | |
| 101 | test("a branch is created and deleted locally and each is announced", async () => { | |
| 102 | const made = must(await createBranch(app, ada, native, { name: "feat" })); | |
| 103 | expect(await tip(native, "feat")).toBe(made.oid); | |
| 104 | expect(events.some((e) => e.kind === "branch.created" && e.repoId === native.id)).toBe(true); | |
| 105 | ||
| 106 | must(await deleteBranch(app, ada, native, "feat")); | |
| 107 | expect(await tip(native, "feat")).toBeUndefined(); | |
| 108 | expect(events.some((e) => e.kind === "branch.deleted" && e.repoId === native.id)).toBe(true); | |
| 109 | }); | |
| 110 | ||
| 111 | test("the default branch is never deleted", async () => { | |
| 112 | const r = await deleteBranch(app, ada, native, native.defaultBranch); | |
| 113 | expect(r).toEqual({ ok: false, error: "default_branch" }); | |
| 114 | }); | |
| 115 | }); | |
| 116 | ||
| 117 | describe("a reader", () => { | |
| 118 | test("of a public repository is told forbidden, and nothing moves", async () => { | |
| 119 | const open = must(await createRepo(app.db, app.reposRoot, { ownerId: ada.id, name: "open", visibility: "public" })); | |
| 120 | must(await commitToBranch(app, ada, open, { message: "seed", files: [{ path: "a.txt", content: "a\n" }] })); | |
| 121 | expect(await createBranch(app, bob, open, { name: "nope" })).toEqual({ ok: false, error: "forbidden" }); | |
| 122 | expect(await deleteBranch(app, bob, open, "nope")).toEqual({ ok: false, error: "forbidden" }); | |
| 123 | expect(await tip(open, "nope")).toBeUndefined(); | |
| 124 | }); | |
| 125 | ||
| 126 | test("is refused with not_found, and nothing moves", async () => { | |
| 127 | expect(await createBranch(app, bob, native, { name: "nope" })).toEqual({ ok: false, error: "not_found" }); | |
| 128 | expect(await createBranch(app, undefined, native, { name: "nope" })).toEqual({ ok: false, error: "not_found" }); | |
| 129 | expect(await deleteBranch(app, bob, native, "main")).toEqual({ ok: false, error: "not_found" }); | |
| 130 | expect(await tip(native, "nope")).toBeUndefined(); | |
| 131 | }); | |
| 132 | }); | |
| 133 | ||
| 134 | describe("a mirrored repository", () => { | |
| 135 | test("a refusing origin leaves the local ref where it was, and the refusal is a refusal, not a failure", async () => { | |
outdated
Name says origin already has it; only the local ref does. Rename or set it up on the origin only. |
||
| 136 | reject = "branch creation is closed here"; | |
| 137 | const before = events.length; | |
| 138 | const r = await createBranch(app, ada, mirror, { name: "feat" }); | |
| 139 | expect(r).toEqual({ ok: false, error: "origin_refused" }); | |
| 140 | expect(await tip(mirror, "feat")).toBeUndefined(); | |
| 141 | expect(await originTip("feat")).toBeUndefined(); | |
| 142 | expect(events.length).toBe(before); | |
| 143 | }); | |
| 144 | ||
| 145 | test("an accepting origin takes the ref first, then the local ref moves and it is announced", async () => { | |
| 146 | reject = ""; | |
| 147 | const made = must(await createBranch(app, ada, mirror, { name: "feat" })); | |
| 148 | expect(await originTip("feat")).toBe(made.oid); | |
| 149 | expect(await tip(mirror, "feat")).toBe(made.oid); | |
| 150 | expect(events.some((e) => e.kind === "branch.created" && e.repoId === mirror.id)).toBe(true); | |
| 151 | }); | |
| 152 | ||
| 153 | test("a branch that exists locally is not created again", async () => { | |
| 154 | expect(await createBranch(app, ada, mirror, { name: "feat" })).toEqual({ ok: false, error: "exists" }); | |
| 155 | }); | |
| 156 | ||
| 157 | test("a branch only the origin has is refused by its lease, and no local ref appears", async () => { | |
| 158 | const dir = repoDir(app.reposRoot, mirror.ownerName, mirror.name); | |
| 159 | // A commit the origin holds under that name and the mirror does not: same | |
| 160 | // tree, different message, so the oid differs from the mirror's start point. | |
| 161 | const other = await runGitCapture(["commit-tree", "main^{tree}", "-m", "theirs"], { | |
| 162 | cwd: dir, | |
| 163 | env: { GIT_AUTHOR_NAME: "o", GIT_AUTHOR_EMAIL: "o@o", GIT_COMMITTER_NAME: "o", GIT_COMMITTER_EMAIL: "o@o" }, | |
| 164 | }); | |
| 165 | const pushed = await runGitCapture(["push", originDir, `${other.stdout.trim()}:refs/heads/theirs`], { cwd: dir }); | |
| 166 | expect(pushed.code).toBe(0); | |
| 167 | expect(await createBranch(app, ada, mirror, { name: "theirs" })).toEqual({ ok: false, error: "origin_refused" }); | |
outdated
Assert the exact error, not just ok:false. |
||
| 168 | expect(await tip(mirror, "theirs")).toBeUndefined(); | |
| 169 | }); | |
| 170 | ||
| 171 | test("a refusing origin keeps the local branch on delete", async () => { | |
| 172 | reject = "deletes need review"; | |
| 173 | const before = await tip(mirror, "feat"); | |
| 174 | const r = await deleteBranch(app, ada, mirror, "feat"); | |
| 175 | expect(r).toEqual({ ok: false, error: "origin_refused" }); | |
| 176 | expect(await tip(mirror, "feat")).toBe(before); | |
| 177 | expect(await originTip("feat")).toBe(before); | |
| 178 | }); | |
| 179 | ||
| 180 | test("an accepting origin drops the ref, then the local one goes and it is announced", async () => { | |
| 181 | reject = ""; | |
| 182 | must(await deleteBranch(app, ada, mirror, "feat")); | |
| 183 | expect(await originTip("feat")).toBeUndefined(); | |
| 184 | expect(await tip(mirror, "feat")).toBeUndefined(); | |
| 185 | expect(events.some((e) => e.kind === "branch.deleted" && e.repoId === mirror.id)).toBe(true); | |
| 186 | }); | |
| 187 | ||
| 188 | test("only the origin's owner pushes with its credential", async () => { | |
| 189 | const other = must(await createUser(app.db, { origin: "seed", trusted: true, username: "cy", email: "[email protected]", password: "a-good-passphrase" })); | |
| 190 | expect(await createBranch(app, other, mirror, { name: "x" })).toEqual({ ok: false, error: "not_found" }); | |
| 191 | expect(await tip(mirror, "x")).toBeUndefined(); | |
| 192 | }); | |
| 193 | }); |
new above
offline
Name promises words, body asserts only the code. Assert the sentence (and that TOKEN is absent), or rename.