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, beforeEach, afterEach } from "vitest";
2import { Indexer } from "../indexer.js";
3import { createTestContext, type TestContext } from "./test-context.js";
4import { roles, rolePermissions } from "@atbb/db";
5import { eq } from "drizzle-orm";
6import type {
7 CommitCreateEvent,
8 CommitUpdateEvent,
9 CommitDeleteEvent,
10} from "@skyware/jetstream";
11
12describe("Indexer - Role Handlers", () => {
13 let ctx: TestContext;
14 let indexer: Indexer;
15
16 beforeEach(async () => {
17 ctx = await createTestContext();
18 indexer = new Indexer(ctx.db, ctx.logger);
19 });
20
21 afterEach(async () => {
22 await ctx.db.delete(roles).where(eq(roles.did, "did:plc:test-forum"));
23 await ctx.cleanup();
24 });
25
26 it("handleRoleCreate indexes role record with all fields", async () => {
27 const event: CommitCreateEvent<"space.atbb.forum.role"> = {
28 kind: "commit",
29 commit: {
30 rev: "abc123",
31 operation: "create",
32 collection: "space.atbb.forum.role",
33 rkey: "role1",
34 record: {
35 $type: "space.atbb.forum.role",
36 name: "Moderator",
37 description: "Can moderate posts",
38 permissions: ["space.atbb.permission.moderatePosts"],
39 priority: 10,
40 createdAt: "2026-02-15T00:00:00Z",
41 } as any,
42 cid: "bafyrole",
43 },
44 did: "did:plc:test-forum",
45 time_us: 1000000,
46 };
47
48 await indexer.handleRoleCreate(event);
49
50 const [role] = await ctx.db
51 .select()
52 .from(roles)
53 .where(eq(roles.rkey, "role1"))
54 .limit(1);
55
56 expect(role).toBeDefined();
57 expect(role.name).toBe("Moderator");
58 expect(role.description).toBe("Can moderate posts");
59 expect(role.priority).toBe(10);
60
61 const perms = await ctx.db
62 .select({ permission: rolePermissions.permission })
63 .from(rolePermissions)
64 .where(eq(rolePermissions.roleId, role.id));
65 expect(perms.map((p) => p.permission)).toEqual([
66 "space.atbb.permission.moderatePosts",
67 ]);
68 });
69
70 it("handleRoleCreate indexes role without optional description", async () => {
71 const event: CommitCreateEvent<"space.atbb.forum.role"> = {
72 kind: "commit",
73 commit: {
74 rev: "abc123",
75 operation: "create",
76 collection: "space.atbb.forum.role",
77 rkey: "role2",
78 record: {
79 $type: "space.atbb.forum.role",
80 name: "Member",
81 permissions: ["space.atbb.permission.createPosts"],
82 priority: 50,
83 createdAt: "2026-02-15T00:00:00Z",
84 } as any,
85 cid: "bafyrole2",
86 },
87 did: "did:plc:test-forum",
88 time_us: 1000000,
89 };
90
91 await indexer.handleRoleCreate(event);
92
93 const [role] = await ctx.db
94 .select()
95 .from(roles)
96 .where(eq(roles.rkey, "role2"))
97 .limit(1);
98
99 expect(role).toBeDefined();
100 expect(role.description).toBeNull();
101 });
102
103 it("handleRoleUpdate updates role fields", async () => {
104 // First create a role (no permissions column — permissions live in role_permissions)
105 await ctx.db.insert(roles).values({
106 did: "did:plc:test-forum",
107 rkey: "role3",
108 cid: "bafyold",
109 name: "Old Name",
110 description: "Old description",
111 priority: 30,
112 createdAt: new Date(),
113 indexedAt: new Date(),
114 });
115
116 const event: CommitUpdateEvent<"space.atbb.forum.role"> = {
117 kind: "commit",
118 commit: {
119 rev: "def456",
120 operation: "update",
121 collection: "space.atbb.forum.role",
122 rkey: "role3",
123 record: {
124 $type: "space.atbb.forum.role",
125 name: "Updated Name",
126 description: "Updated description",
127 permissions: [
128 "space.atbb.permission.createPosts",
129 "space.atbb.permission.moderatePosts",
130 ],
131 priority: 20,
132 createdAt: "2026-02-15T00:00:00Z",
133 } as any,
134 cid: "bafynew",
135 },
136 did: "did:plc:test-forum",
137 time_us: 2000000,
138 };
139
140 await indexer.handleRoleUpdate(event);
141
142 const [role] = await ctx.db
143 .select()
144 .from(roles)
145 .where(eq(roles.rkey, "role3"))
146 .limit(1);
147
148 expect(role.name).toBe("Updated Name");
149 expect(role.description).toBe("Updated description");
150 expect(role.priority).toBe(20);
151 expect(role.cid).toBe("bafynew");
152
153 const perms = await ctx.db
154 .select({ permission: rolePermissions.permission })
155 .from(rolePermissions)
156 .where(eq(rolePermissions.roleId, role.id));
157 expect(perms).toHaveLength(2);
158 expect(perms.map((p) => p.permission)).toEqual(
159 expect.arrayContaining([
160 "space.atbb.permission.createPosts",
161 "space.atbb.permission.moderatePosts",
162 ])
163 );
164 });
165
166 it("handleRoleDelete removes role record", async () => {
167 // First create a role (no permissions column — permissions live in role_permissions)
168 await ctx.db.insert(roles).values({
169 did: "did:plc:test-forum",
170 rkey: "role4",
171 cid: "bafyrole4",
172 name: "To Delete",
173 priority: 99,
174 createdAt: new Date(),
175 indexedAt: new Date(),
176 });
177
178 const event: CommitDeleteEvent<"space.atbb.forum.role"> = {
179 kind: "commit",
180 commit: {
181 rev: "ghi789",
182 operation: "delete",
183 collection: "space.atbb.forum.role",
184 rkey: "role4",
185 },
186 did: "did:plc:test-forum",
187 time_us: 3000000,
188 };
189
190 await indexer.handleRoleDelete(event);
191
192 const result = await ctx.db
193 .select()
194 .from(roles)
195 .where(eq(roles.rkey, "role4"))
196 .limit(1);
197
198 expect(result).toHaveLength(0);
199 });
200});