1import { describe, expect, it } from "vitest"; 2 3import { ROLE_ACTIONS, canRole, shareVisibility, type Share } from "./rules.js"; 4 5const user = { userId: "u_1", userTeams: ["tms_1", "tms_2"] }; 6const outsider = { userId: "u_2", userTeams: ["tms_3"] }; 7 8describe("canRole — static matrix", () => { 9 it("owner can do everything", () => { 10 for (const action of ROLE_ACTIONS) { 11 expect(canRole("owner", action), action).toBe(true); 12 } 13 }); 14 15 it("admin can manage but not delete the workspace", () => { 16 expect(canRole("admin", "manage_members")).toBe(true); 17 expect(canRole("admin", "invite")).toBe(true); 18 expect(canRole("admin", "manage_permissions")).toBe(true); 19 expect(canRole("admin", "manage_settings")).toBe(true); 20 expect(canRole("admin", "delete_workspace")).toBe(false); 21 expect(canRole("admin", "export_data")).toBe(true); 22 }); 23 24 it("member has no management actions", () => { 25 for (const action of ROLE_ACTIONS) { 26 expect(canRole("member", action), action).toBe(false); 27 } 28 }); 29 30 it("guest has no management actions", () => { 31 for (const action of ROLE_ACTIONS) { 32 expect(canRole("guest", action), action).toBe(false); 33 } 34 }); 35 36 it("throws on unknown roles", () => { 37 expect(() => canRole("superuser" as never, "invite")).toThrow(/unknown workspace role/); 38 }); 39}); 40 41describe("shareVisibility", () => { 42 const shares: [string, Share][] = [ 43 ["public", { scope: "public", level: "view", slug: "abc" }], 44 ["workspace", { scope: "workspace", level: "comment" }], 45 ["team (member)", { scope: "team", level: "edit", teamId: "tms_1" }], 46 ["team (other)", { scope: "team", level: "edit", teamId: "tms_9" }], 47 ["users (listed)", { scope: "users", level: "view", userIds: ["u_1", "u_3"] }], 48 ["users (unlisted)", { scope: "users", level: "view", userIds: ["u_3"] }], 49 ["private", { scope: "private", level: "full_access" }], 50 ]; 51 52 it.each(shares)( 53 "%s: visible to in-scope user, not to outsider (except public/workspace)", 54 (label, share) => { 55 const expectedUser = 56 share.scope === "public" || 57 share.scope === "workspace" || 58 (share.scope === "team" && share.teamId === "tms_1") || 59 (share.scope === "users" && (share.userIds ?? []).includes("u_1")); 60 const expectedOutsider = 61 share.scope === "public" || 62 share.scope === "workspace" || 63 (share.scope === "team" && share.teamId === "tms_3") || 64 (share.scope === "users" && (share.userIds ?? []).includes("u_2")); 65 expect(shareVisibility(share, user)).toBe(expectedUser); 66 expect(shareVisibility(share, outsider)).toBe(expectedOutsider); 67 }, 68 ); 69 70 it("private shares are never visible", () => { 71 expect(shareVisibility({ scope: "private", level: "view" }, user)).toBe(false); 72 expect(shareVisibility({ scope: "private", level: "view" }, outsider)).toBe(false); 73 }); 74 75 it("password/slug do not affect visibility", () => { 76 const base: Share = { scope: "public", level: "view" }; 77 const withExtras: Share = { scope: "public", level: "view", password: "hunter2", slug: "x" }; 78 expect(shareVisibility(withExtras, user)).toBe(shareVisibility(base, user)); 79 }); 80}); 81