import { describe, it, expect, vi } from "vitest"; import { createCategory } from "../lib/steps/create-category.js"; describe("createCategory", () => { const forumDid = "did:plc:testforum"; // Builds a mock DB. If existingCategory is set, the first select() returns it. // The second select() (forum lookup) always returns a mock forum row. function mockDb(options: { existingCategory?: any } = {}) { let callCount = 0; return { select: vi.fn().mockImplementation(() => ({ from: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ limit: vi.fn().mockImplementation(() => { callCount++; if (callCount === 1) { // First select: category idempotency check return options.existingCategory ? [options.existingCategory] : []; } // Second select: forum lookup for forumId return [{ id: BigInt(1) }]; }), }), }), })), insert: vi.fn().mockReturnValue({ values: vi.fn().mockResolvedValue(undefined), }), } as any; } function mockAgent(overrides: Record = {}) { return { com: { atproto: { repo: { createRecord: vi.fn().mockResolvedValue({ data: { uri: `at://${forumDid}/space.atbb.forum.category/tid123`, cid: "bafytest", }, }), ...overrides, }, }, }, } as any; } it("creates category on PDS and inserts into DB", async () => { const db = mockDb(); const agent = mockAgent(); const result = await createCategory(db, agent, forumDid, { name: "General", description: "General discussion", }); expect(result.created).toBe(true); expect(result.skipped).toBe(false); expect(result.uri).toContain("space.atbb.forum.category/tid123"); expect(result.cid).toBe("bafytest"); expect(agent.com.atproto.repo.createRecord).toHaveBeenCalledWith( expect.objectContaining({ repo: forumDid, collection: "space.atbb.forum.category", record: expect.objectContaining({ $type: "space.atbb.forum.category", name: "General", description: "General discussion", }), }) ); expect(db.insert).toHaveBeenCalled(); }); it("derives slug from name when not provided", async () => { const db = mockDb(); const agent = mockAgent(); await createCategory(db, agent, forumDid, { name: "My Cool Category" }); expect(agent.com.atproto.repo.createRecord).toHaveBeenCalledWith( expect.objectContaining({ record: expect.objectContaining({ slug: "my-cool-category" }), }) ); }); it("uses provided slug instead of deriving one", async () => { const db = mockDb(); const agent = mockAgent(); await createCategory(db, agent, forumDid, { name: "General", slug: "gen" }); expect(agent.com.atproto.repo.createRecord).toHaveBeenCalledWith( expect.objectContaining({ record: expect.objectContaining({ slug: "gen" }), }) ); }); it("skips when category with same name already exists", async () => { const db = mockDb({ existingCategory: { did: forumDid, rkey: "existingtid", cid: "bafyexisting", name: "General", }, }); const agent = mockAgent(); const result = await createCategory(db, agent, forumDid, { name: "General" }); expect(result.created).toBe(false); expect(result.skipped).toBe(true); expect(result.existingName).toBe("General"); expect(agent.com.atproto.repo.createRecord).not.toHaveBeenCalled(); expect(db.insert).not.toHaveBeenCalled(); }); it("throws when PDS write fails", async () => { const db = mockDb(); const agent = mockAgent({ createRecord: vi.fn().mockRejectedValue(new Error("PDS write failed")), }); await expect( createCategory(db, agent, forumDid, { name: "General" }) ).rejects.toThrow("PDS write failed"); }); it("throws when DB insert fails after successful PDS write", async () => { let callCount = 0; const db = { select: vi.fn().mockImplementation(() => ({ from: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ limit: vi.fn().mockImplementation(() => { callCount++; if (callCount === 1) return []; // no existing category return [{ id: BigInt(1) }]; // forum lookup }), }), }), })), insert: vi.fn().mockReturnValue({ values: vi.fn().mockRejectedValue(new Error("DB insert failed")), }), } as any; const agent = mockAgent(); await expect( createCategory(db, agent, forumDid, { name: "General" }) ).rejects.toThrow("DB insert failed"); // PDS write happened before the failing DB insert expect(agent.com.atproto.repo.createRecord).toHaveBeenCalled(); }); it("includes sortOrder in PDS record and DB row when provided", async () => { const db = mockDb(); const agent = mockAgent(); await createCategory(db, agent, forumDid, { name: "General", sortOrder: 5 }); expect(agent.com.atproto.repo.createRecord).toHaveBeenCalledWith( expect.objectContaining({ record: expect.objectContaining({ sortOrder: 5 }), }) ); expect(db.insert().values).toHaveBeenCalledWith( expect.objectContaining({ sortOrder: 5 }) ); }); it("omits sortOrder from PDS record and DB row when not provided", async () => { const db = mockDb(); const agent = mockAgent(); await createCategory(db, agent, forumDid, { name: "General" }); const record = (agent.com.atproto.repo.createRecord as ReturnType).mock.calls[0][0].record; expect(record).not.toHaveProperty("sortOrder"); expect(db.insert().values).toHaveBeenCalledWith( expect.objectContaining({ sortOrder: null }) ); }); });