import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { Hono } from "hono"; import { createTestContext, type TestContext } from "../../lib/__tests__/test-context.js"; import { roles, rolePermissions, memberships, users } from "@atbb/db"; import { checkPermission, checkMinRole, canActOnUser, requireAnyPermission, } from "../permissions.js"; import type { Variables } from "../../types.js"; describe("Permission Helper Functions", () => { let ctx: TestContext; beforeEach(async () => { ctx = await createTestContext(); }); afterEach(async () => { await ctx.cleanup(); }); describe("checkPermission", () => { it("returns true when user has required permission", async () => { // Create a test role with createTopics permission const [memberRole] = await ctx.db.insert(roles).values({ did: ctx.config.forumDid, rkey: "test-role-123", cid: "test-cid", name: "Member", description: "Test member role", priority: 30, createdAt: new Date(), indexedAt: new Date(), }).returning({ id: roles.id }); await ctx.db.insert(rolePermissions).values([ { roleId: memberRole.id, permission: "space.atbb.permission.createTopics" }, ]); // Create a test user await ctx.db.insert(users).values({ did: "did:plc:test-testuser", handle: "testuser.bsky.social", indexedAt: new Date(), }); // Create membership with roleUri pointing to test role await ctx.db.insert(memberships).values({ did: "did:plc:test-testuser", rkey: "membership-123", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: `at://${ctx.config.forumDid}/space.atbb.forum.role/test-role-123`, createdAt: new Date(), indexedAt: new Date(), }); const result = await checkPermission( ctx, "did:plc:test-testuser", "space.atbb.permission.createTopics" ); expect(result).toBe(true); }); it("returns true for Owner role with wildcard permission", async () => { // Create Owner role with wildcard const [ownerRole] = await ctx.db.insert(roles).values({ did: ctx.config.forumDid, rkey: "owner-role", cid: "test-cid", name: "Owner", description: "Forum owner", priority: 0, createdAt: new Date(), indexedAt: new Date(), }).returning({ id: roles.id }); await ctx.db.insert(rolePermissions).values([ { roleId: ownerRole.id, permission: "*" }, ]); await ctx.db.insert(users).values({ did: "did:plc:test-owner", handle: "owner.bsky.social", indexedAt: new Date(), }); await ctx.db.insert(memberships).values({ did: "did:plc:test-owner", rkey: "membership-123", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: `at://${ctx.config.forumDid}/space.atbb.forum.role/owner-role`, createdAt: new Date(), indexedAt: new Date(), }); // Should return true for ANY permission const result = await checkPermission( ctx, "did:plc:test-owner", "space.atbb.permission.someRandomPermission" ); expect(result).toBe(true); }); it("returns false when user has no role assigned", async () => { await ctx.db.insert(users).values({ did: "did:plc:test-norole", handle: "norole.bsky.social", indexedAt: new Date(), }); // Create membership with roleUri = null await ctx.db.insert(memberships).values({ did: "did:plc:test-norole", rkey: "membership-123", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: null, // No role assigned createdAt: new Date(), indexedAt: new Date(), }); const result = await checkPermission( ctx, "did:plc:test-norole", "space.atbb.permission.createTopics" ); expect(result).toBe(false); }); it("returns false when user's role is deleted (fail closed)", async () => { await ctx.db.insert(users).values({ did: "did:plc:test-deletedrole", handle: "deletedrole.bsky.social", indexedAt: new Date(), }); // Create membership with roleUri pointing to non-existent role await ctx.db.insert(memberships).values({ did: "did:plc:test-deletedrole", rkey: "membership-123", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: `at://${ctx.config.forumDid}/space.atbb.forum.role/deleted-role`, createdAt: new Date(), indexedAt: new Date(), }); const result = await checkPermission( ctx, "did:plc:test-deletedrole", "space.atbb.permission.createTopics" ); expect(result).toBe(false); // Fail closed }); it("returns false when user has no membership", async () => { await ctx.db.insert(users).values({ did: "did:plc:test-nomembership", handle: "nomembership.bsky.social", indexedAt: new Date(), }); // No membership record created const result = await checkPermission( ctx, "did:plc:test-nomembership", "space.atbb.permission.createTopics" ); expect(result).toBe(false); }); }); describe("checkMinRole", () => { it("returns true when user has exact role match", async () => { await ctx.db.insert(roles).values({ did: ctx.config.forumDid, rkey: "admin-role", cid: "test-cid", name: "Admin", priority: 10, createdAt: new Date(), indexedAt: new Date(), }); await ctx.db.insert(users).values({ did: "did:plc:test-admin", handle: "admin.bsky.social", indexedAt: new Date(), }); await ctx.db.insert(memberships).values({ did: "did:plc:test-admin", rkey: "membership-123", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: `at://${ctx.config.forumDid}/space.atbb.forum.role/admin-role`, createdAt: new Date(), indexedAt: new Date(), }); const result = await checkMinRole(ctx, "did:plc:test-admin", "admin"); expect(result).toBe(true); }); it("returns true when user has higher authority role", async () => { // Owner (priority 0) should pass admin check (priority 10) await ctx.db.insert(roles).values({ did: ctx.config.forumDid, rkey: "owner-role-2", cid: "test-cid", name: "Owner", priority: 0, createdAt: new Date(), indexedAt: new Date(), }); await ctx.db.insert(users).values({ did: "did:plc:test-owner2", handle: "owner2.bsky.social", indexedAt: new Date(), }); await ctx.db.insert(memberships).values({ did: "did:plc:test-owner2", rkey: "membership-owner2", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: `at://${ctx.config.forumDid}/space.atbb.forum.role/owner-role-2`, createdAt: new Date(), indexedAt: new Date(), }); const result = await checkMinRole(ctx, "did:plc:test-owner2", "admin"); expect(result).toBe(true); // Owner > Admin }); it("returns false when user has lower authority role", async () => { // Moderator (priority 20) should fail admin check (priority 10) await ctx.db.insert(roles).values({ did: ctx.config.forumDid, rkey: "mod-role", cid: "test-cid", name: "Moderator", priority: 20, createdAt: new Date(), indexedAt: new Date(), }); await ctx.db.insert(users).values({ did: "did:plc:test-mod", handle: "mod.bsky.social", indexedAt: new Date(), }); await ctx.db.insert(memberships).values({ did: "did:plc:test-mod", rkey: "membership-123", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: `at://${ctx.config.forumDid}/space.atbb.forum.role/mod-role`, createdAt: new Date(), indexedAt: new Date(), }); const result = await checkMinRole(ctx, "did:plc:test-mod", "admin"); expect(result).toBe(false); // Moderator < Admin }); }); describe("canActOnUser", () => { it("returns true when actor is acting on themselves", async () => { const result = await canActOnUser( ctx, "did:plc:test-testuser", "did:plc:test-testuser" // Same DID ); expect(result).toBe(true); // Self-action bypass }); it("returns true when actor has higher authority", async () => { // Create Admin role (priority 10) await ctx.db.insert(roles).values({ did: ctx.config.forumDid, rkey: "admin-role-2", cid: "test-cid", name: "Admin", priority: 10, createdAt: new Date(), indexedAt: new Date(), }); // Create Moderator role (priority 20) await ctx.db.insert(roles).values({ did: ctx.config.forumDid, rkey: "mod-role-2", cid: "test-cid", name: "Moderator", priority: 20, createdAt: new Date(), indexedAt: new Date(), }); // Admin user await ctx.db.insert(users).values({ did: "did:plc:test-admin2", handle: "admin2.bsky.social", indexedAt: new Date(), }); await ctx.db.insert(memberships).values({ did: "did:plc:test-admin2", rkey: "membership-admin2", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: `at://${ctx.config.forumDid}/space.atbb.forum.role/admin-role-2`, createdAt: new Date(), indexedAt: new Date(), }); // Moderator user await ctx.db.insert(users).values({ did: "did:plc:test-mod2", handle: "mod2.bsky.social", indexedAt: new Date(), }); await ctx.db.insert(memberships).values({ did: "did:plc:test-mod2", rkey: "membership-mod2", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: `at://${ctx.config.forumDid}/space.atbb.forum.role/mod-role-2`, createdAt: new Date(), indexedAt: new Date(), }); const result = await canActOnUser(ctx, "did:plc:test-admin2", "did:plc:test-mod2"); expect(result).toBe(true); // Admin (10) can act on Moderator (20) }); it("returns false when actor has equal authority", async () => { // Create Admin role await ctx.db.insert(roles).values({ did: ctx.config.forumDid, rkey: "admin-role-3", cid: "test-cid", name: "Admin", priority: 10, createdAt: new Date(), indexedAt: new Date(), }); // Admin user 1 await ctx.db.insert(users).values({ did: "did:plc:test-admin3", handle: "admin3.bsky.social", indexedAt: new Date(), }); await ctx.db.insert(memberships).values({ did: "did:plc:test-admin3", rkey: "membership-admin3", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: `at://${ctx.config.forumDid}/space.atbb.forum.role/admin-role-3`, createdAt: new Date(), indexedAt: new Date(), }); // Admin user 2 await ctx.db.insert(users).values({ did: "did:plc:test-admin4", handle: "admin4.bsky.social", indexedAt: new Date(), }); await ctx.db.insert(memberships).values({ did: "did:plc:test-admin4", rkey: "membership-admin4", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: `at://${ctx.config.forumDid}/space.atbb.forum.role/admin-role-3`, createdAt: new Date(), indexedAt: new Date(), }); const result = await canActOnUser(ctx, "did:plc:test-admin3", "did:plc:test-admin4"); expect(result).toBe(false); // Admin (10) cannot act on Admin (10) }); it("returns false when actor has lower authority", async () => { // Create Admin role (priority 10) await ctx.db.insert(roles).values({ did: ctx.config.forumDid, rkey: "admin-role-4", cid: "test-cid", name: "Admin", priority: 10, createdAt: new Date(), indexedAt: new Date(), }); // Create Moderator role (priority 20) await ctx.db.insert(roles).values({ did: ctx.config.forumDid, rkey: "mod-role-4", cid: "test-cid", name: "Moderator", priority: 20, createdAt: new Date(), indexedAt: new Date(), }); // Admin user await ctx.db.insert(users).values({ did: "did:plc:test-admin5", handle: "admin5.bsky.social", indexedAt: new Date(), }); await ctx.db.insert(memberships).values({ did: "did:plc:test-admin5", rkey: "membership-admin5", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: `at://${ctx.config.forumDid}/space.atbb.forum.role/admin-role-4`, createdAt: new Date(), indexedAt: new Date(), }); // Moderator user await ctx.db.insert(users).values({ did: "did:plc:test-mod5", handle: "mod5.bsky.social", indexedAt: new Date(), }); await ctx.db.insert(memberships).values({ did: "did:plc:test-mod5", rkey: "membership-mod5", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: `at://${ctx.config.forumDid}/space.atbb.forum.role/mod-role-4`, createdAt: new Date(), indexedAt: new Date(), }); const result = await canActOnUser(ctx, "did:plc:test-mod5", "did:plc:test-admin5"); expect(result).toBe(false); // Moderator (20) cannot act on Admin (10) }); }); describe("requireAnyPermission", () => { it("returns 200 when user has one of the required permissions", async () => { // Create a role with moderatePosts permission const [modRole] = await ctx.db.insert(roles).values({ did: ctx.config.forumDid, rkey: "mod-role-anyperm-1", cid: "test-cid", name: "Moderator", description: "Moderator role", priority: 20, createdAt: new Date(), indexedAt: new Date(), }).returning({ id: roles.id }); await ctx.db.insert(rolePermissions).values([ { roleId: modRole.id, permission: "space.atbb.permission.moderatePosts" }, ]); await ctx.db.insert(users).values({ did: "did:plc:test-anyperm-1", handle: "anyperm1.bsky.social", indexedAt: new Date(), }); await ctx.db.insert(memberships).values({ did: "did:plc:test-anyperm-1", rkey: "membership-anyperm-1", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: `at://${ctx.config.forumDid}/space.atbb.forum.role/mod-role-anyperm-1`, createdAt: new Date(), indexedAt: new Date(), }); const testApp = new Hono<{ Variables: Variables }>(); testApp.use("*", async (c, next) => { c.set("user", { did: "did:plc:test-anyperm-1", handle: "anyperm1.bsky.social", pdsUrl: "https://pds.example.com", agent: {} as any, }); await next(); }); testApp.get( "/test", requireAnyPermission(ctx, [ "space.atbb.permission.moderatePosts", "space.atbb.permission.banUsers", ]), (c) => c.json({ ok: true }) ); const res = await testApp.request("/test"); expect(res.status).toBe(200); const body = await res.json(); expect(body).toEqual({ ok: true }); }); it("returns 403 when user has none of the required permissions", async () => { // Create a role with only createTopics permission const [memberRole] = await ctx.db.insert(roles).values({ did: ctx.config.forumDid, rkey: "mod-role-anyperm-2", cid: "test-cid", name: "Member", description: "Member role", priority: 30, createdAt: new Date(), indexedAt: new Date(), }).returning({ id: roles.id }); await ctx.db.insert(rolePermissions).values([ { roleId: memberRole.id, permission: "space.atbb.permission.createTopics" }, ]); await ctx.db.insert(users).values({ did: "did:plc:test-anyperm-2", handle: "anyperm2.bsky.social", indexedAt: new Date(), }); await ctx.db.insert(memberships).values({ did: "did:plc:test-anyperm-2", rkey: "membership-anyperm-2", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: `at://${ctx.config.forumDid}/space.atbb.forum.role/mod-role-anyperm-2`, createdAt: new Date(), indexedAt: new Date(), }); const testApp = new Hono<{ Variables: Variables }>(); testApp.use("*", async (c, next) => { c.set("user", { did: "did:plc:test-anyperm-2", handle: "anyperm2.bsky.social", pdsUrl: "https://pds.example.com", agent: {} as any, }); await next(); }); testApp.get( "/test", requireAnyPermission(ctx, [ "space.atbb.permission.moderatePosts", "space.atbb.permission.banUsers", ]), (c) => c.json({ ok: true }) ); const res = await testApp.request("/test"); expect(res.status).toBe(403); const body = await res.json(); expect(body).toEqual({ error: "Insufficient permissions" }); }); it("returns 401 when user is not authenticated", async () => { const testApp = new Hono<{ Variables: Variables }>(); // No auth middleware — user is not set testApp.get( "/test", requireAnyPermission(ctx, [ "space.atbb.permission.moderatePosts", "space.atbb.permission.banUsers", ]), (c) => c.json({ ok: true }) ); const res = await testApp.request("/test"); expect(res.status).toBe(401); }); it("short-circuits on second permission if first fails", async () => { // Create a role with banUsers but NOT moderatePosts const [banRole] = await ctx.db.insert(roles).values({ did: ctx.config.forumDid, rkey: "mod-role-anyperm-3", cid: "test-cid", name: "BanRole", description: "Role with banUsers only", priority: 15, createdAt: new Date(), indexedAt: new Date(), }).returning({ id: roles.id }); await ctx.db.insert(rolePermissions).values([ { roleId: banRole.id, permission: "space.atbb.permission.banUsers" }, ]); await ctx.db.insert(users).values({ did: "did:plc:test-anyperm-3", handle: "anyperm3.bsky.social", indexedAt: new Date(), }); await ctx.db.insert(memberships).values({ did: "did:plc:test-anyperm-3", rkey: "membership-anyperm-3", cid: "test-cid", forumUri: `at://${ctx.config.forumDid}/space.atbb.forum.forum/self`, roleUri: `at://${ctx.config.forumDid}/space.atbb.forum.role/mod-role-anyperm-3`, createdAt: new Date(), indexedAt: new Date(), }); const testApp = new Hono<{ Variables: Variables }>(); testApp.use("*", async (c, next) => { c.set("user", { did: "did:plc:test-anyperm-3", handle: "anyperm3.bsky.social", pdsUrl: "https://pds.example.com", agent: {} as any, }); await next(); }); // First perm (moderatePosts) will fail, second (banUsers) will succeed testApp.get( "/test", requireAnyPermission(ctx, [ "space.atbb.permission.moderatePosts", "space.atbb.permission.banUsers", ]), (c) => c.json({ ok: true }) ); const res = await testApp.request("/test"); expect(res.status).toBe(200); const body = await res.json(); expect(body).toEqual({ ok: true }); }); }); });