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 root/atb-56-theme-caching-layer 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});