Files
Ekklesia/packages/contracts/tests/authorization.test.ts
T

129 lines
5.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
AUTHORIZATION_ALLOWED,
AUTHORIZATION_DENIED,
type AuthorizationActor,
type AuthorizationDecision,
createUserActor,
type GrantId,
isAuthorizationAllowed,
isUserActor,
parseAuthorizationScope,
parseGrantId,
parsePermissionId,
type PermissionId,
safeParseAuthorizationScope,
safeParseGrantId,
safeParsePermissionId,
SYSTEM_ACTOR,
} from "../src/index.js";
describe("PermissionId", () => {
it("parses a valid lowercase dot-namespaced permission id", () => {
expect(parsePermissionId("calendar.events.read")).toBe("calendar.events.read");
});
it("is opaque at the type level: a plain string is not assignable without parsing", () => {
// @ts-expect-error a bare string must not satisfy the branded PermissionId type.
const notAllowed: PermissionId = "calendar.events.read";
expect(notAllowed).toBe("calendar.events.read");
});
it("rejects a non-namespaced, empty, oversized, or non-string permission id", () => {
expect(() => parsePermissionId("read")).toThrow();
expect(() => parsePermissionId("")).toThrow();
expect(() => parsePermissionId("Calendar.Events.Read")).toThrow();
expect(() => parsePermissionId("x".repeat(161))).toThrow();
expect(() => parsePermissionId(42)).toThrow();
expect(() => parsePermissionId(null)).toThrow();
});
it("rejects a wildcard segment: there is no such thing as a wildcard permission", () => {
expect(() => parsePermissionId("media.*")).toThrow();
expect(() => parsePermissionId("media.*.read")).toThrow();
expect(() => parsePermissionId("*")).toThrow();
});
it("exposes a safe-parse variant that never throws", () => {
expect(safeParsePermissionId("calendar.events.read").success).toBe(true);
expect(safeParsePermissionId("*").success).toBe(false);
});
});
describe("GrantId", () => {
it("parses a non-empty opaque grant id and rejects empty/oversized input", () => {
const id: GrantId = parseGrantId("11111111-1111-1111-1111-111111111111");
expect(id).toBe("11111111-1111-1111-1111-111111111111");
expect(() => parseGrantId("")).toThrow();
expect(() => parseGrantId("x".repeat(129))).toThrow();
});
it("exposes a safe-parse variant that never throws", () => {
expect(safeParseGrantId("abc").success).toBe(true);
expect(safeParseGrantId("").success).toBe(false);
});
});
describe("AuthorizationScope", () => {
it("accepts a non-empty opaque scope and rejects empty/whitespace/oversized scope", () => {
expect(parseAuthorizationScope("calendar-event:123")).toBe("calendar-event:123");
expect(safeParseAuthorizationScope("").success).toBe(false);
expect(safeParseAuthorizationScope(" ").success).toBe(false);
expect(safeParseAuthorizationScope("x".repeat(257)).success).toBe(false);
});
});
describe("AuthorizationActor", () => {
it("creates a frozen user actor carrying only the opaque user id", () => {
const actor = createUserActor("user_123");
expect(actor).toEqual({ type: "user", userId: "user_123" });
expect(Object.isFrozen(actor)).toBe(true);
expect(isUserActor(actor)).toBe(true);
});
it("rejects an invalid user id when creating a user actor", () => {
expect(() => createUserActor("")).toThrow();
expect(() => createUserActor(42)).toThrow();
});
it("exposes a frozen system actor singleton distinguishable from a user actor", () => {
expect(SYSTEM_ACTOR).toEqual({ type: "system" });
expect(Object.isFrozen(SYSTEM_ACTOR)).toBe(true);
expect(isUserActor(SYSTEM_ACTOR)).toBe(false);
});
it("discriminates actors on type without leaking extra fields", () => {
const actors: AuthorizationActor[] = [SYSTEM_ACTOR, createUserActor("user_123")];
for (const actor of actors) {
expect(["user", "system"]).toContain(actor.type);
}
expect(Object.keys(SYSTEM_ACTOR)).toEqual(["type"]);
});
});
describe("AuthorizationDecision", () => {
it("exposes exactly two frozen, minimal decision singletons", () => {
expect(AUTHORIZATION_ALLOWED).toEqual({ allowed: true });
expect(AUTHORIZATION_DENIED).toEqual({ allowed: false });
expect(Object.isFrozen(AUTHORIZATION_ALLOWED)).toBe(true);
expect(Object.isFrozen(AUTHORIZATION_DENIED)).toBe(true);
expect(Object.keys(AUTHORIZATION_ALLOWED)).toEqual(["allowed"]);
expect(Object.keys(AUTHORIZATION_DENIED)).toEqual(["allowed"]);
});
it("never carries a reason, matched grant, permission echo, or any other detail", () => {
const decisions: AuthorizationDecision[] = [AUTHORIZATION_ALLOWED, AUTHORIZATION_DENIED];
for (const decision of decisions) {
expect(Object.keys(decision)).not.toContain("reason");
expect(Object.keys(decision)).not.toContain("grantId");
expect(Object.keys(decision)).not.toContain("permission");
expect(Object.keys(decision)).not.toContain("code");
}
});
it("exposes a boolean helper matching the allowed field", () => {
expect(isAuthorizationAllowed(AUTHORIZATION_ALLOWED)).toBe(true);
expect(isAuthorizationAllowed(AUTHORIZATION_DENIED)).toBe(false);
});
});