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

A test pins a repository's events page: hidden events never shift a page, and a mostly hidden outbox comes back short with a cursor that resumes #6719

Merged opened this

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

Merged

scout/forge:scout/6699-a-test-pins-a-repository-s-events-page-h → main 1 commit fork

  • Merged. scout/6699-a-test-pins-a-repository-s-events-page-h was merged into main as 95cb19e.

Review

Sign in to review this change.
1 file changed +118 −0
test/events-page.test.ts added +118
@@ -0,0 +1,118 @@
1 /**
2 * A repository's events page. The audience filter runs before the page is
3 * cut, so hidden events never shift a page; one request reads at most 1,000
4 * rows, so a mostly hidden outbox comes back short with a cursor that resumes.
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 { createRepo } from "../src/actions/repos.ts";
12 import { eventsLimit, eventsPage, validEventsCursor } from "../src/core/events_page.ts";
13 import type { Repo } from "../src/core/repos.ts";
14 import { createUser, type User } from "../src/core/users.ts";
15
16 describe("events limit and cursor", () => {
17 test("limit defaults to 50 and accepts 1 to 200", () => {
18 expect(eventsLimit(undefined)).toBe(50);
19 expect(eventsLimit(null)).toBe(50);
20 expect(eventsLimit("1")).toBe(1);
21 expect(eventsLimit(200)).toBe(200);
22 for (const bad of [0, 201, "1.5", "-3", "12345", "abc"]) {
23 expect(eventsLimit(bad)).toBeUndefined();
24 }
25 });
26
27 test("a cursor is 1 to 64 id characters", () => {
28 expect(validEventsCursor("abc_DEF-123")).toBe(true);
29 expect(validEventsCursor("a".repeat(64))).toBe(true);
30 expect(validEventsCursor("")).toBe(false);
31 expect(validEventsCursor("a".repeat(65))).toBe(false);
32 expect(validEventsCursor("/")).toBe(false);
33 });
34 });
35
36 describe("a repository's events page", () => {
37 let dataDir: string;
38 let app: App;
39 let alice: User;
40 let bob: User;
41 let repo: Repo;
42
43 const emit = (kind: string, n: number) =>
44 app.bus.append({ kind, repoId: repo.id, actorId: alice.id, payload: { n } });
45
46 const user = async (username: string, first = false) => {
47 const made = await createUser(app.db, {
48 origin: "seed",
49 trusted: true,
50 username,
51 email: `${username}@forge.local`,
52 password: "a-good-passphrase",
53 adminIfFirst: first,
54 });
55 if (!made.ok) throw new Error(made.error);
56 return made.value;
57 };
58
59 const fresh = async (name: string) => {
60 const made = await createRepo(app, alice, { name, visibility: "public" });
61 if (!made.ok) throw new Error(String(made.error));
62 repo = made.value;
63 };
64
65 beforeAll(async () => {
66 dataDir = await mkdtemp(join(tmpdir(), "forge-events-page-"));
67 app = createApp({ dataDir, port: 0 });
68 alice = await user("alice", true);
69 bob = await user("bob");
70 }, 30_000);
71
72 afterAll(async () => {
73 app.close();
74 await rm(dataDir, { recursive: true, force: true });
75 });
76
77 test("hidden events never shift a page, and paging lists each visible event once", async () => {
78 await fresh("mixed");
79 const visible: number[] = [];
80 for (let i = 0; i < 12; i++) {
81 emit("push", i);
82 visible.push(i);
83 emit("webhook.created", i);
84 emit("webhook.created", i + 100);
85 }
86 const seen: number[] = [];
87 let cursor: string | undefined;
88 const sizes: number[] = [];
89 for (let guard = 0; guard < 10; guard++) {
90 const page = eventsPage(app.db, repo.id, bob, 5, cursor);
91 sizes.push(page.items.length);
92 for (const e of page.items) {
93 expect(e.kind).not.toBe("webhook.created");
94 if (e.kind === "push") seen.push((e.payload as { n: number }).n);
95 }
96 if (page.next_cursor === null) break;
97 cursor = page.next_cursor;
98 }
99 expect(sizes.slice(0, 2)).toEqual([5, 5]);
100 expect(seen).toEqual(visible.slice().reverse());
101 // The admin sees everything, hidden or not.
102 expect(eventsPage(app.db, repo.id, alice, 200).items.length).toBeGreaterThan(36);
103 });
104
105 test("1,200 hidden events in front of 1 visible: a short page, a cursor, then the event", async () => {
106 await fresh("buried");
107 const first = emit("push", 0);
108 for (let i = 0; i < 1200; i++) emit("webhook.created", i);
109 const one = eventsPage(app.db, repo.id, bob, 10);
110 expect(one.items.length).toBeLessThan(10);
111 expect(one.items).toEqual([]);
112 expect(one.next_cursor).not.toBeNull();
113 const two = eventsPage(app.db, repo.id, bob, 10, one.next_cursor!);
114 expect(two.items.map((e) => e.id)).toContain(first.id);
115 expect(two.items.every((e) => e.kind !== "webhook.created")).toBe(true);
116 expect(two.next_cursor).toBeNull();
117 });
118 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs