WIP! A BB-style forum, on the ATmosphere!
We're still working... we'll be back soon when we have something to show off!
node
typescript
hono
htmx
atproto
1import { describe, it, expect, beforeEach, afterEach } from "vitest";
2import { Indexer } from "../indexer.js";
3import { createTestContext, type TestContext } from "./test-context.js";
4import { boards, categories, forums } from "@atbb/db";
5import { eq } from "drizzle-orm";
6import type {
7 CommitCreateEvent,
8 CommitUpdateEvent,
9 CommitDeleteEvent,
10} from "@skyware/jetstream";
11
12describe("Indexer - Board Handlers", () => {
13 let ctx: TestContext;
14 let indexer: Indexer;
15
16 let testCategoryId: bigint;
17
18 beforeEach(async () => {
19 ctx = await createTestContext();
20 indexer = new Indexer(ctx.db, ctx.logger);
21
22 // Get the forum ID created by createTestContext
23 const [forum] = await ctx.db
24 .select({ id: forums.id })
25 .from(forums)
26 .where(eq(forums.did, "did:plc:test-forum"))
27 .limit(1);
28
29 // Insert test category and save the ID
30 const [category] = await ctx.db.insert(categories).values({
31 did: "did:plc:test-forum",
32 rkey: "cat1",
33 cid: "bafycat",
34 name: "Test Category",
35 forumId: forum.id,
36 createdAt: new Date(),
37 indexedAt: new Date(),
38 }).returning();
39
40 testCategoryId = category.id;
41 });
42
43 afterEach(async () => {
44 // Clean up in correct FK order: boards -> categories -> forums
45 await ctx.db.delete(boards).where(eq(boards.did, "did:plc:test-forum"));
46 await ctx.cleanup();
47 });
48
49 it("handleBoardCreate indexes board record", async () => {
50 const event: CommitCreateEvent<"space.atbb.forum.board"> = {
51 kind: "commit",
52 commit: {
53 rev: "abc123",
54 operation: "create",
55 collection: "space.atbb.forum.board",
56 rkey: "board1",
57 record: {
58 $type: "space.atbb.forum.board",
59 name: "General Discussion",
60 description: "Talk about anything",
61 slug: "general",
62 sortOrder: 1,
63 category: {
64 category: {
65 uri: "at://did:plc:test-forum/space.atbb.forum.category/cat1",
66 cid: "bafycat",
67 },
68 },
69 createdAt: "2026-02-13T00:00:00Z",
70 } as any,
71 cid: "bafyboard",
72 },
73 did: "did:plc:test-forum",
74 time_us: 1000000,
75 };
76
77 await indexer.handleBoardCreate(event);
78
79 const [board] = await ctx.db
80 .select()
81 .from(boards)
82 .where(eq(boards.rkey, "board1"));
83
84 expect(board).toBeDefined();
85 expect(board.name).toBe("General Discussion");
86 expect(board.slug).toBe("general");
87 expect(board.sortOrder).toBe(1);
88 expect(board.categoryId).toBe(testCategoryId);
89 expect(board.categoryUri).toBe(
90 "at://did:plc:test-forum/space.atbb.forum.category/cat1"
91 );
92 });
93
94 it("handleBoardCreate throws when category not found", async () => {
95 const event: CommitCreateEvent<"space.atbb.forum.board"> = {
96 kind: "commit",
97 commit: {
98 rev: "abc123",
99 operation: "create",
100 collection: "space.atbb.forum.board",
101 rkey: "board2",
102 record: {
103 $type: "space.atbb.forum.board",
104 name: "Orphan Board",
105 category: {
106 category: {
107 uri: "at://did:plc:test-forum/space.atbb.forum.category/nonexistent",
108 cid: "bafynonexistent",
109 },
110 },
111 createdAt: "2026-02-13T00:00:00Z",
112 } as any,
113 cid: "bafyboard2",
114 },
115 did: "did:plc:test-forum",
116 time_us: 1000000,
117 };
118
119 await expect(indexer.handleBoardCreate(event)).rejects.toThrow(
120 "Category not found: at://did:plc:test-forum/space.atbb.forum.category/nonexistent"
121 );
122
123 const results = await ctx.db.select().from(boards);
124 expect(results).toHaveLength(0);
125 });
126
127 it("handleBoardUpdate updates existing board", async () => {
128 // First create a board
129 const createEvent: CommitCreateEvent<"space.atbb.forum.board"> = {
130 kind: "commit",
131 commit: {
132 rev: "abc123",
133 operation: "create",
134 collection: "space.atbb.forum.board",
135 rkey: "board1",
136 record: {
137 $type: "space.atbb.forum.board",
138 name: "General Discussion",
139 description: "Talk about anything",
140 slug: "general",
141 sortOrder: 1,
142 category: {
143 category: {
144 uri: "at://did:plc:test-forum/space.atbb.forum.category/cat1",
145 cid: "bafycat",
146 },
147 },
148 createdAt: "2026-02-13T00:00:00Z",
149 } as any,
150 cid: "bafyboard",
151 },
152 did: "did:plc:test-forum",
153 time_us: 1000000,
154 };
155
156 await indexer.handleBoardCreate(createEvent);
157
158 // Now update it
159 const updateEvent: CommitUpdateEvent<"space.atbb.forum.board"> = {
160 kind: "commit",
161 commit: {
162 rev: "def456",
163 operation: "update",
164 collection: "space.atbb.forum.board",
165 rkey: "board1",
166 record: {
167 $type: "space.atbb.forum.board",
168 name: "Updated Board Name",
169 description: "Updated description",
170 slug: "updated-general",
171 sortOrder: 2,
172 category: {
173 category: {
174 uri: "at://did:plc:test-forum/space.atbb.forum.category/cat1",
175 cid: "bafycat",
176 },
177 },
178 createdAt: "2026-02-13T00:00:00Z",
179 } as any,
180 cid: "bafyboard-updated",
181 },
182 did: "did:plc:test-forum",
183 time_us: 2000000,
184 };
185
186 await indexer.handleBoardUpdate(updateEvent);
187
188 const [board] = await ctx.db
189 .select()
190 .from(boards)
191 .where(eq(boards.rkey, "board1"));
192
193 expect(board).toBeDefined();
194 expect(board.name).toBe("Updated Board Name");
195 expect(board.description).toBe("Updated description");
196 expect(board.slug).toBe("updated-general");
197 expect(board.sortOrder).toBe(2);
198 expect(board.cid).toBe("bafyboard-updated");
199 });
200
201 it("handleBoardDelete removes board", async () => {
202 // First create a board
203 const createEvent: CommitCreateEvent<"space.atbb.forum.board"> = {
204 kind: "commit",
205 commit: {
206 rev: "abc123",
207 operation: "create",
208 collection: "space.atbb.forum.board",
209 rkey: "board1",
210 record: {
211 $type: "space.atbb.forum.board",
212 name: "General Discussion",
213 category: {
214 category: {
215 uri: "at://did:plc:test-forum/space.atbb.forum.category/cat1",
216 cid: "bafycat",
217 },
218 },
219 createdAt: "2026-02-13T00:00:00Z",
220 } as any,
221 cid: "bafyboard",
222 },
223 did: "did:plc:test-forum",
224 time_us: 1000000,
225 };
226
227 await indexer.handleBoardCreate(createEvent);
228
229 // Verify it exists
230 let results = await ctx.db.select().from(boards);
231 expect(results).toHaveLength(1);
232
233 // Now delete it
234 const deleteEvent: CommitDeleteEvent<"space.atbb.forum.board"> = {
235 kind: "commit",
236 commit: {
237 rev: "ghi789",
238 operation: "delete",
239 collection: "space.atbb.forum.board",
240 rkey: "board1",
241 },
242 did: "did:plc:test-forum",
243 time_us: 3000000,
244 };
245
246 await indexer.handleBoardDelete(deleteEvent);
247
248 // Verify it's gone
249 results = await ctx.db.select().from(boards);
250 expect(results).toHaveLength(0);
251 });
252});