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, vi } from "vitest";
2import { createCategory } from "../lib/steps/create-category.js";
3
4describe("createCategory", () => {
5 const forumDid = "did:plc:testforum";
6
7 // Builds a mock DB. If existingCategory is set, the first select() returns it.
8 // The second select() (forum lookup) always returns a mock forum row.
9 function mockDb(options: { existingCategory?: any } = {}) {
10 let callCount = 0;
11 return {
12 select: vi.fn().mockImplementation(() => ({
13 from: vi.fn().mockReturnValue({
14 where: vi.fn().mockReturnValue({
15 limit: vi.fn().mockImplementation(() => {
16 callCount++;
17 if (callCount === 1) {
18 // First select: category idempotency check
19 return options.existingCategory ? [options.existingCategory] : [];
20 }
21 // Second select: forum lookup for forumId
22 return [{ id: BigInt(1) }];
23 }),
24 }),
25 }),
26 })),
27 insert: vi.fn().mockReturnValue({
28 values: vi.fn().mockResolvedValue(undefined),
29 }),
30 } as any;
31 }
32
33 function mockAgent(overrides: Record<string, any> = {}) {
34 return {
35 com: {
36 atproto: {
37 repo: {
38 createRecord: vi.fn().mockResolvedValue({
39 data: {
40 uri: `at://${forumDid}/space.atbb.forum.category/tid123`,
41 cid: "bafytest",
42 },
43 }),
44 ...overrides,
45 },
46 },
47 },
48 } as any;
49 }
50
51 it("creates category on PDS and inserts into DB", async () => {
52 const db = mockDb();
53 const agent = mockAgent();
54
55 const result = await createCategory(db, agent, forumDid, {
56 name: "General",
57 description: "General discussion",
58 });
59
60 expect(result.created).toBe(true);
61 expect(result.skipped).toBe(false);
62 expect(result.uri).toContain("space.atbb.forum.category/tid123");
63 expect(result.cid).toBe("bafytest");
64 expect(agent.com.atproto.repo.createRecord).toHaveBeenCalledWith(
65 expect.objectContaining({
66 repo: forumDid,
67 collection: "space.atbb.forum.category",
68 record: expect.objectContaining({
69 $type: "space.atbb.forum.category",
70 name: "General",
71 description: "General discussion",
72 }),
73 })
74 );
75 expect(db.insert).toHaveBeenCalled();
76 });
77
78 it("derives slug from name when not provided", async () => {
79 const db = mockDb();
80 const agent = mockAgent();
81
82 await createCategory(db, agent, forumDid, { name: "My Cool Category" });
83
84 expect(agent.com.atproto.repo.createRecord).toHaveBeenCalledWith(
85 expect.objectContaining({
86 record: expect.objectContaining({ slug: "my-cool-category" }),
87 })
88 );
89 });
90
91 it("uses provided slug instead of deriving one", async () => {
92 const db = mockDb();
93 const agent = mockAgent();
94
95 await createCategory(db, agent, forumDid, { name: "General", slug: "gen" });
96
97 expect(agent.com.atproto.repo.createRecord).toHaveBeenCalledWith(
98 expect.objectContaining({
99 record: expect.objectContaining({ slug: "gen" }),
100 })
101 );
102 });
103
104 it("skips when category with same name already exists", async () => {
105 const db = mockDb({
106 existingCategory: {
107 did: forumDid,
108 rkey: "existingtid",
109 cid: "bafyexisting",
110 name: "General",
111 },
112 });
113 const agent = mockAgent();
114
115 const result = await createCategory(db, agent, forumDid, { name: "General" });
116
117 expect(result.created).toBe(false);
118 expect(result.skipped).toBe(true);
119 expect(result.existingName).toBe("General");
120 expect(agent.com.atproto.repo.createRecord).not.toHaveBeenCalled();
121 expect(db.insert).not.toHaveBeenCalled();
122 });
123
124 it("throws when PDS write fails", async () => {
125 const db = mockDb();
126 const agent = mockAgent({
127 createRecord: vi.fn().mockRejectedValue(new Error("PDS write failed")),
128 });
129
130 await expect(
131 createCategory(db, agent, forumDid, { name: "General" })
132 ).rejects.toThrow("PDS write failed");
133 });
134
135 it("throws when DB insert fails after successful PDS write", async () => {
136 let callCount = 0;
137 const db = {
138 select: vi.fn().mockImplementation(() => ({
139 from: vi.fn().mockReturnValue({
140 where: vi.fn().mockReturnValue({
141 limit: vi.fn().mockImplementation(() => {
142 callCount++;
143 if (callCount === 1) return []; // no existing category
144 return [{ id: BigInt(1) }]; // forum lookup
145 }),
146 }),
147 }),
148 })),
149 insert: vi.fn().mockReturnValue({
150 values: vi.fn().mockRejectedValue(new Error("DB insert failed")),
151 }),
152 } as any;
153 const agent = mockAgent();
154
155 await expect(
156 createCategory(db, agent, forumDid, { name: "General" })
157 ).rejects.toThrow("DB insert failed");
158 // PDS write happened before the failing DB insert
159 expect(agent.com.atproto.repo.createRecord).toHaveBeenCalled();
160 });
161
162 it("includes sortOrder in PDS record and DB row when provided", async () => {
163 const db = mockDb();
164 const agent = mockAgent();
165
166 await createCategory(db, agent, forumDid, { name: "General", sortOrder: 5 });
167
168 expect(agent.com.atproto.repo.createRecord).toHaveBeenCalledWith(
169 expect.objectContaining({
170 record: expect.objectContaining({ sortOrder: 5 }),
171 })
172 );
173 expect(db.insert().values).toHaveBeenCalledWith(
174 expect.objectContaining({ sortOrder: 5 })
175 );
176 });
177
178 it("omits sortOrder from PDS record and DB row when not provided", async () => {
179 const db = mockDb();
180 const agent = mockAgent();
181
182 await createCategory(db, agent, forumDid, { name: "General" });
183
184 const record = (agent.com.atproto.repo.createRecord as ReturnType<typeof vi.fn>).mock.calls[0][0].record;
185 expect(record).not.toHaveProperty("sortOrder");
186 expect(db.insert().values).toHaveBeenCalledWith(
187 expect.objectContaining({ sortOrder: null })
188 );
189 });
190});