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
at main 190 lines 6.1 kB view raw
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});