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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/7511-a-test-proves-a-mirrored-repository-s-br → main 2 commits fork 3 threads · 3 unresolved

  • Merged. scout/7511-a-test-proves-a-mirrored-repository-s-br was merged into main as c41897a.

Review

Sign in to review this change.
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 });

Name promises words, body asserts only the code. Assert the sentence (and that TOKEN is absent), or rename.

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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs