Exosphere is a set of small, modular, self-hostable community tools built on the AT Protocol. app.exosphere.site
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

test: permissions

Hugo 0e777bd1 29e78481

+288
+288
packages/core/src/__tests__/permissions.test.ts
··· 1 + import { describe, it, expect, vi, beforeAll, beforeEach } from "vitest"; 2 + import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3"; 3 + 4 + import { createTestDb, seedSphere } from "./helpers/test-db.ts"; 5 + import { sphereMembers, spherePermissions } from "../db/schema/index.ts"; 6 + 7 + let db: BetterSQLite3Database; 8 + 9 + vi.mock("../db/index.ts", () => ({ 10 + getDb: () => db, 11 + })); 12 + 13 + import { hasMinimumRole, ROLE_LEVELS } from "../permissions/roles.ts"; 14 + import { 15 + registerModulePermissions, 16 + getDefaultRole, 17 + getAllModulePermissions, 18 + getModulePermissionsCollection, 19 + } from "../permissions/registry.ts"; 20 + import { getRequiredRole, checkPermission, computeUserPermissions } from "../permissions/check.ts"; 21 + import { CORE_MODULE, CORE_PERMISSIONS_COLLECTION, corePermissions } from "../permissions/core.ts"; 22 + 23 + const OWNER_DID = "did:plc:owner1"; 24 + const ADMIN_DID = "did:plc:admin1"; 25 + const MEMBER_DID = "did:plc:member1"; 26 + const SPHERE_ID = "sphere-1"; 27 + const SPHERE_HANDLE = "test.bsky.social"; 28 + 29 + // Register core permissions once (mirrors server startup) 30 + beforeAll(() => { 31 + registerModulePermissions(CORE_MODULE, corePermissions, CORE_PERMISSIONS_COLLECTION); 32 + }); 33 + 34 + function seedWithMembers() { 35 + seedSphere(db, { id: SPHERE_ID, handle: SPHERE_HANDLE, ownerDid: OWNER_DID }); 36 + db.insert(sphereMembers) 37 + .values({ sphereId: SPHERE_ID, did: ADMIN_DID, role: "admin", status: "active" }) 38 + .run(); 39 + db.insert(sphereMembers) 40 + .values({ sphereId: SPHERE_ID, did: MEMBER_DID, role: "member", status: "active" }) 41 + .run(); 42 + } 43 + 44 + function insertOverride(actionKey: string, minRole: string) { 45 + db.insert(spherePermissions).values({ sphereId: SPHERE_ID, actionKey, minRole }).run(); 46 + } 47 + 48 + beforeEach(() => { 49 + db = createTestDb(); 50 + }); 51 + 52 + // ---- hasMinimumRole ---- 53 + 54 + describe("hasMinimumRole", () => { 55 + it("returns true when user role meets the requirement", () => { 56 + expect(hasMinimumRole("owner", "owner")).toBe(true); 57 + expect(hasMinimumRole("owner", "admin")).toBe(true); 58 + expect(hasMinimumRole("admin", "admin")).toBe(true); 59 + expect(hasMinimumRole("admin", "member")).toBe(true); 60 + expect(hasMinimumRole("member", "member")).toBe(true); 61 + }); 62 + 63 + it("returns false when user role is below the requirement", () => { 64 + expect(hasMinimumRole("member", "admin")).toBe(false); 65 + expect(hasMinimumRole("member", "owner")).toBe(false); 66 + expect(hasMinimumRole("admin", "owner")).toBe(false); 67 + }); 68 + 69 + it("returns true for any member role when required is authenticated", () => { 70 + expect(hasMinimumRole("owner", "authenticated")).toBe(true); 71 + expect(hasMinimumRole("admin", "authenticated")).toBe(true); 72 + expect(hasMinimumRole("member", "authenticated")).toBe(true); 73 + }); 74 + 75 + it("returns true for null role when required is authenticated", () => { 76 + expect(hasMinimumRole(null, "authenticated")).toBe(true); 77 + }); 78 + 79 + it("returns false for null role when required is any member role", () => { 80 + expect(hasMinimumRole(null, "member")).toBe(false); 81 + expect(hasMinimumRole(null, "admin")).toBe(false); 82 + expect(hasMinimumRole(null, "owner")).toBe(false); 83 + }); 84 + }); 85 + 86 + // ---- ROLE_LEVELS ---- 87 + 88 + describe("ROLE_LEVELS", () => { 89 + it("maintains correct hierarchy ordering", () => { 90 + expect(ROLE_LEVELS.owner).toBeGreaterThan(ROLE_LEVELS.admin); 91 + expect(ROLE_LEVELS.admin).toBeGreaterThan(ROLE_LEVELS.member); 92 + expect(ROLE_LEVELS.member).toBeGreaterThan(ROLE_LEVELS.authenticated); 93 + }); 94 + }); 95 + 96 + // ---- Registry ---- 97 + 98 + describe("registerModulePermissions", () => { 99 + it("registers and retrieves module permissions", () => { 100 + // Core permissions are registered at import time — verify they exist 101 + const all = getAllModulePermissions(); 102 + expect(all.has(CORE_MODULE)).toBe(true); 103 + expect(all.get(CORE_MODULE)).toEqual(corePermissions); 104 + }); 105 + 106 + it("stores PDS collection for core module", () => { 107 + expect(getModulePermissionsCollection(CORE_MODULE)).toBe(CORE_PERMISSIONS_COLLECTION); 108 + }); 109 + }); 110 + 111 + describe("getDefaultRole", () => { 112 + it("returns the default role for a known action", () => { 113 + expect(getDefaultRole(CORE_MODULE, "invite-member")).toBe("admin"); 114 + expect(getDefaultRole(CORE_MODULE, "update-permissions")).toBe("owner"); 115 + }); 116 + 117 + it("returns null for an unknown action", () => { 118 + expect(getDefaultRole(CORE_MODULE, "nonexistent")).toBeNull(); 119 + }); 120 + 121 + it("returns null for an unknown module", () => { 122 + expect(getDefaultRole("nonexistent-module", "create")).toBeNull(); 123 + }); 124 + }); 125 + 126 + // ---- Core permissions ---- 127 + 128 + describe("corePermissions", () => { 129 + it("defines all expected sphere-level actions", () => { 130 + const actions = Object.keys(corePermissions); 131 + expect(actions).toContain("invite-member"); 132 + expect(actions).toContain("revoke-member"); 133 + expect(actions).toContain("update-member-role"); 134 + expect(actions).toContain("enable-module"); 135 + expect(actions).toContain("disable-module"); 136 + expect(actions).toContain("update-permissions"); 137 + }); 138 + 139 + it("restricts module and permissions management to owner by default", () => { 140 + expect(corePermissions["enable-module"].defaultRole).toBe("owner"); 141 + expect(corePermissions["disable-module"].defaultRole).toBe("owner"); 142 + expect(corePermissions["update-permissions"].defaultRole).toBe("owner"); 143 + }); 144 + 145 + it("allows admin for member management by default", () => { 146 + expect(corePermissions["invite-member"].defaultRole).toBe("admin"); 147 + expect(corePermissions["revoke-member"].defaultRole).toBe("admin"); 148 + expect(corePermissions["update-member-role"].defaultRole).toBe("admin"); 149 + }); 150 + }); 151 + 152 + // ---- getRequiredRole ---- 153 + 154 + describe("getRequiredRole", () => { 155 + it("returns module default when no override exists", () => { 156 + seedSphere(db, { id: SPHERE_ID, handle: SPHERE_HANDLE, ownerDid: OWNER_DID }); 157 + expect(getRequiredRole(SPHERE_ID, CORE_MODULE, "invite-member")).toBe("admin"); 158 + expect(getRequiredRole(SPHERE_ID, CORE_MODULE, "update-permissions")).toBe("owner"); 159 + }); 160 + 161 + it("returns override when one exists in DB", () => { 162 + seedSphere(db, { id: SPHERE_ID, handle: SPHERE_HANDLE, ownerDid: OWNER_DID }); 163 + insertOverride("sphere:invite-member", "owner"); 164 + expect(getRequiredRole(SPHERE_ID, CORE_MODULE, "invite-member")).toBe("owner"); 165 + }); 166 + 167 + it("falls back to admin for unknown module/action", () => { 168 + seedSphere(db, { id: SPHERE_ID, handle: SPHERE_HANDLE, ownerDid: OWNER_DID }); 169 + expect(getRequiredRole(SPHERE_ID, "unknown-module", "unknown-action")).toBe("admin"); 170 + }); 171 + 172 + it("falls back to admin for invalid role in DB override", () => { 173 + seedSphere(db, { id: SPHERE_ID, handle: SPHERE_HANDLE, ownerDid: OWNER_DID }); 174 + insertOverride("sphere:invite-member", "invalid-role"); 175 + expect(getRequiredRole(SPHERE_ID, CORE_MODULE, "invite-member")).toBe("admin"); 176 + }); 177 + }); 178 + 179 + // ---- checkPermission ---- 180 + 181 + describe("checkPermission", () => { 182 + beforeEach(() => { 183 + seedWithMembers(); 184 + }); 185 + 186 + it("allows owner for owner-level actions", () => { 187 + expect(checkPermission(SPHERE_ID, CORE_MODULE, "update-permissions", "owner")).toBe(true); 188 + }); 189 + 190 + it("denies admin for owner-level actions", () => { 191 + expect(checkPermission(SPHERE_ID, CORE_MODULE, "update-permissions", "admin")).toBe(false); 192 + }); 193 + 194 + it("allows admin for admin-level actions", () => { 195 + expect(checkPermission(SPHERE_ID, CORE_MODULE, "invite-member", "admin")).toBe(true); 196 + }); 197 + 198 + it("denies member for admin-level actions", () => { 199 + expect(checkPermission(SPHERE_ID, CORE_MODULE, "invite-member", "member")).toBe(false); 200 + }); 201 + 202 + it("denies null (unauthenticated) for member-level actions", () => { 203 + expect(checkPermission(SPHERE_ID, CORE_MODULE, "invite-member", null)).toBe(false); 204 + }); 205 + 206 + it("respects overrides — allows member when override lowers requirement", () => { 207 + insertOverride("sphere:invite-member", "member"); 208 + expect(checkPermission(SPHERE_ID, CORE_MODULE, "invite-member", "member")).toBe(true); 209 + }); 210 + 211 + it("respects overrides — denies admin when override raises requirement", () => { 212 + insertOverride("sphere:invite-member", "owner"); 213 + expect(checkPermission(SPHERE_ID, CORE_MODULE, "invite-member", "admin")).toBe(false); 214 + }); 215 + }); 216 + 217 + // ---- computeUserPermissions ---- 218 + 219 + describe("computeUserPermissions", () => { 220 + beforeEach(() => { 221 + seedWithMembers(); 222 + }); 223 + 224 + it("returns all core permissions for owner", () => { 225 + const perms = computeUserPermissions(SPHERE_ID, "owner", []); 226 + expect(perms["sphere:invite-member"]).toBe(true); 227 + expect(perms["sphere:update-permissions"]).toBe(true); 228 + expect(perms["sphere:enable-module"]).toBe(true); 229 + }); 230 + 231 + it("returns correct permissions for admin", () => { 232 + const perms = computeUserPermissions(SPHERE_ID, "admin", []); 233 + expect(perms["sphere:invite-member"]).toBe(true); 234 + expect(perms["sphere:update-permissions"]).toBe(false); 235 + expect(perms["sphere:enable-module"]).toBe(false); 236 + }); 237 + 238 + it("returns correct permissions for member", () => { 239 + const perms = computeUserPermissions(SPHERE_ID, "member", []); 240 + expect(perms["sphere:invite-member"]).toBe(false); 241 + expect(perms["sphere:update-permissions"]).toBe(false); 242 + }); 243 + 244 + it("applies DB overrides", () => { 245 + insertOverride("sphere:invite-member", "member"); 246 + const perms = computeUserPermissions(SPHERE_ID, "member", []); 247 + expect(perms["sphere:invite-member"]).toBe(true); 248 + }); 249 + 250 + it("includes enabled module permissions", () => { 251 + // Register a test module 252 + registerModulePermissions("test-mod", { 253 + create: { label: "Create", defaultRole: "authenticated" }, 254 + admin: { label: "Admin action", defaultRole: "admin" }, 255 + }); 256 + const perms = computeUserPermissions(SPHERE_ID, "member", ["test-mod"]); 257 + expect(perms["test-mod:create"]).toBe(true); 258 + expect(perms["test-mod:admin"]).toBe(false); 259 + }); 260 + 261 + it("does not include disabled module permissions", () => { 262 + const perms = computeUserPermissions(SPHERE_ID, "owner", []); 263 + // test-mod was registered above but not in enabledModules 264 + expect(perms["test-mod:create"]).toBeUndefined(); 265 + }); 266 + 267 + it("handles null role (unauthenticated user)", () => { 268 + registerModulePermissions("auth-test-mod", { 269 + open: { label: "Open action", defaultRole: "authenticated" }, 270 + restricted: { label: "Restricted", defaultRole: "member" }, 271 + }); 272 + const perms = computeUserPermissions(SPHERE_ID, null, ["auth-test-mod"]); 273 + expect(perms["auth-test-mod:open"]).toBe(true); 274 + expect(perms["auth-test-mod:restricted"]).toBe(false); 275 + }); 276 + 277 + it("always includes core permissions even if not in enabledModules", () => { 278 + const perms = computeUserPermissions(SPHERE_ID, "owner", ["some-other-module"]); 279 + expect(perms["sphere:invite-member"]).toBe(true); 280 + }); 281 + 282 + it("ignores invalid DB overrides", () => { 283 + insertOverride("sphere:invite-member", "bogus-role"); 284 + // Invalid override should be filtered out, falling back to default ("admin") 285 + const perms = computeUserPermissions(SPHERE_ID, "admin", []); 286 + expect(perms["sphere:invite-member"]).toBe(true); 287 + }); 288 + });