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, beforeEach, afterEach } from "vitest";
2import { seedDefaultRoles } from "../seed-roles.js";
3import { createTestContext, type TestContext } from "./test-context.js";
4import { roles } from "@atbb/db";
5
6describe("seedDefaultRoles", () => {
7 let ctx: TestContext;
8
9 beforeEach(async () => {
10 ctx = await createTestContext();
11 });
12
13 afterEach(async () => {
14 await ctx.cleanup();
15 });
16
17 function mockAgent(opts: {
18 failForRole?: string;
19 alwaysFail?: boolean;
20 } = {}) {
21 let callIndex = 0;
22 return {
23 com: {
24 atproto: {
25 repo: {
26 createRecord: vi.fn().mockImplementation(({ record }: { record: { name: string } }) => {
27 callIndex++;
28 if (opts.alwaysFail || record.name === opts.failForRole) {
29 throw new Error(`PDS write failed for role "${record.name}"`);
30 }
31 return Promise.resolve({
32 data: {
33 uri: `at://${ctx.config.forumDid}/space.atbb.forum.role/tid${callIndex}`,
34 cid: `bafynew${callIndex}`,
35 },
36 });
37 }),
38 },
39 },
40 },
41 } as any;
42 }
43
44 function withAgent(agent: ReturnType<typeof mockAgent>) {
45 return {
46 ...ctx,
47 forumAgent: {
48 getAgent: () => agent,
49 } as any,
50 };
51 }
52
53 it("creates all roles when none exist", async () => {
54 const agent = mockAgent();
55 const result = await seedDefaultRoles(withAgent(agent));
56
57 expect(result.created).toBe(4);
58 expect(result.skipped).toBe(0);
59 expect(agent.com.atproto.repo.createRecord).toHaveBeenCalledTimes(4);
60 });
61
62 it("skips roles that already exist in the database", async () => {
63 // Pre-insert Owner role so it gets skipped
64 await ctx.db.insert(roles).values({
65 did: ctx.config.forumDid,
66 rkey: "owner-rkey",
67 cid: "bafyexisting",
68 name: "Owner",
69 description: "Forum owner",
70 priority: 0,
71 createdAt: new Date(),
72 indexedAt: new Date(),
73 });
74
75 const agent = mockAgent();
76 const result = await seedDefaultRoles(withAgent(agent));
77
78 expect(result.skipped).toBe(1);
79 expect(result.created).toBe(3);
80 // Only 3 PDS writes — Owner was skipped
81 expect(agent.com.atproto.repo.createRecord).toHaveBeenCalledTimes(3);
82 });
83
84 it("throws when ForumAgent is null", async () => {
85 await expect(seedDefaultRoles({ ...ctx, forumAgent: null })).rejects.toThrow(
86 "ForumAgent not available"
87 );
88 });
89
90 it("throws when ForumAgent is not authenticated (getAgent returns null)", async () => {
91 const unauthenticatedAgent = { getAgent: () => null } as any;
92 await expect(
93 seedDefaultRoles({ ...ctx, forumAgent: unauthenticatedAgent })
94 ).rejects.toThrow("ForumAgent not authenticated");
95 });
96
97 it("throws when the Member role (critical) fails to seed", async () => {
98 const agent = mockAgent({ failForRole: "Member" });
99
100 await expect(seedDefaultRoles(withAgent(agent))).rejects.toThrow(
101 'Failed to seed critical role "Member"'
102 );
103 });
104
105 it("throws when any other role fails to seed", async () => {
106 const agent = mockAgent({ failForRole: "Owner" });
107
108 await expect(seedDefaultRoles(withAgent(agent))).rejects.toThrow(
109 'Failed to seed critical role "Owner"'
110 );
111 });
112
113 it("does not partially create roles when a critical role fails", async () => {
114 // Owner and Admin succeed, then Member fails
115 let callCount = 0;
116 const failingAgent = {
117 com: {
118 atproto: {
119 repo: {
120 createRecord: vi.fn().mockImplementation(({ record }: { record: { name: string } }) => {
121 callCount++;
122 if (record.name === "Member") {
123 throw new Error("PDS timeout");
124 }
125 return Promise.resolve({
126 data: {
127 uri: `at://${ctx.config.forumDid}/space.atbb.forum.role/tid${callCount}`,
128 cid: `bafynew${callCount}`,
129 },
130 });
131 }),
132 },
133 },
134 },
135 } as any;
136
137 await expect(seedDefaultRoles(withAgent(failingAgent))).rejects.toThrow(
138 'Failed to seed critical role "Member"'
139 );
140 });
141});