Public: anyone can read this; the crew can change it.

A pull's fast check tests the merge of the pull into its base, as the full tier does: 46 parked pulls fail head-only #1318

Merged opened this in answer to

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

Merged

scout/forge:scout/1316-a-pull-s-fast-check-tests-the-merge-of-t → main 6 commits fork 3 threads · 3 unresolved

  • Merged. scout/1316-a-pull-s-fast-check-tests-the-merge-of-t was merged into main as 0082539.

Review

Sign in to review this change.
2 files changed +132 −0
scripts/gate.ts +1
@@ -212,6 +212,7 @@ export const GATE: readonly string[] = [
212212 "test/outsiders-switch.test.ts",
213213 "test/check-list.test.ts",
214214 "test/check-tiers.test.ts",
215 "test/pull-check-again.test.ts",
215216 "test/holds.test.ts",
216217 "test/hollow.test.ts",
217218 "test/home-feed.test.ts",
test/pull-check-again.test.ts added +131
@@ -0,0 +1,131 @@
1 /**
2 * Asking for a check again over HTTP (#1318): only a writer may, a guest is
3 * told to sign in, a closed pull is refused, and the pull page offers the
4 * button to a writer on an open pull alone.
5 */
6 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7 import { mkdtemp, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { createApp, type App } from "../src/app.ts";
11 import { openPull } from "../src/actions/pulls.ts";
12 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
13 import { startChecks, type ChecksHandle } from "../src/checks/service.ts";
14 import { createToken } from "../src/core/tokens.ts";
15 import { createSession } from "../src/core/sessions.ts";
16 import { createUser, type User } from "../src/core/users.ts";
17 import { serve, type Servers } from "../src/http/server.ts";
18 import { quietLogs } from "./helpers/globals.ts";
19
20 quietLogs();
21
22 let dataDir: string;
23 let app: App;
24 let checks: ChecksHandle;
25 let servers: Servers;
26 let alice: User;
27 let bob: User;
28 let writerToken = "";
29 let readerToken = "";
30 let openNumber = 0;
31 let closedNumber = 0;
32 const cells = process.env["FORGE_CELLS"];
33
34 const mk = async (username: string, first = false): Promise<User> => {
35 const made = await createUser(app.db, {
36 trusted: true,
37 username,
38 email: `${username}@forge.local`,
39 password: "a-good-passphrase",
40 adminIfFirst: first,
41 });
42 if (!made.ok) throw new Error(String(made.error));
43 return made.value;
44 };
45
46 const post = (number: number, token?: string): Promise<Response> =>
47 fetch(`${servers.url}/api/v1/repos/alice/gate/pulls/${number}/checks`, {
48 method: "POST",
49 headers: token === undefined ? {} : { authorization: `Bearer ${token}` },
50 });
51
52 const page = async (user: User, number: number): Promise<string> => {
53 const cookie = `forge_session=${createSession(app.db, user.id).token}`;
54 const res = await fetch(`${servers.url}/alice/gate/pulls/${number}/files`, {
55 headers: { cookie },
56 });
57 expect(res.status).toBe(200);
58 return res.text();
59 };
60
61 beforeAll(async () => {
62 process.env["FORGE_CELLS"] = "off";
63 dataDir = await mkdtemp(join(tmpdir(), "forge-check-again-"));
64 app = createApp({ dataDir, port: 0 });
65 checks = startChecks(app, { forgeUrl: "http://127.0.0.1:1", concurrency: 1 });
66 servers = await serve(app);
67 alice = await mk("alice", true);
68 bob = await mk("bob");
69 const made = await createRepo(app, alice, { name: "gate", visibility: "public" });
70 if (!made.ok) throw new Error(String(made.error));
71 const repo = made.value;
72 const commit = async (branch: string | undefined, files: { path: string; content: string }[]) => {
73 const done = await commitToBranch(app, alice, repo, {
74 ...(branch === undefined ? {} : { branch }),
75 message: "change",
76 files,
77 });
78 if (!done.ok) throw new Error(JSON.stringify(done.error));
79 };
80 await commit(undefined, [
81 { path: "forge.json", content: JSON.stringify({ check: { run: "echo ok" } }) },
82 ]);
83 for (const branch of ["open", "closed"]) {
84 await commit(branch, [{ path: `${branch}.txt`, content: branch }]);
85 const pull = await openPull(app, alice, repo, {
86 title: branch,
87 body: "",
88 baseBranch: "main",
89 headBranch: branch,
90 });
91 if (!pull.ok) throw new Error(JSON.stringify(pull.error));
92 if (branch === "open") openNumber = pull.value.number;
93 else closedNumber = pull.value.number;
94 }
95 app.db.run("UPDATE issue SET state = 'closed' WHERE number = ?", [closedNumber]);
96 writerToken = createToken(app.db, alice.id, "w", "rw").token;
97 readerToken = createToken(app.db, bob.id, "r", "rw").token;
98 });
99
100 afterAll(async () => {
101 await checks.stop();
102 await servers.stop();
103 app.close();
104 if (cells === undefined) delete process.env["FORGE_CELLS"];
105 else process.env["FORGE_CELLS"] = cells;
106 await rm(dataDir, { recursive: true, force: true });
107 });
108
109 describe("asking again over HTTP", () => {
110 test("a guest is told to sign in; a reader is forbidden", async () => {
111 expect((await post(openNumber)).status).toBe(401);
112 expect((await post(openNumber, readerToken)).status).toBe(403);
113 });
114
115 test("a writer queues a check and gets the run", async () => {
116 const res = await post(openNumber, writerToken);
117 expect(res.status).toBe(202);
118 const body = (await res.json()) as { run: { reason: string } };
119 expect(body.run.reason).toBe("asked");
120 });
121
122 test("a closed pull is refused with 409", async () => {
123 expect((await post(closedNumber, writerToken)).status).toBe(409);
124 });
125
126 test("the page offers Check again to a writer on an open pull only", async () => {
127 expect(await page(alice, openNumber)).toContain("Check again");
128 expect(await page(bob, openNumber)).not.toContain("Check again");
129 expect(await page(alice, closedNumber)).not.toContain("Check again");
130 });
131 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs