1376 lines
40 KiB
TypeScript
1376 lines
40 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
ACTION_METADATA,
|
|
ALL_SCOPES,
|
|
checkScopes,
|
|
expandScopes,
|
|
formatActions,
|
|
formatResourcePermission,
|
|
getResourceDescription,
|
|
groupAndFormatScopes,
|
|
groupScopesByResource,
|
|
isLegacyScope,
|
|
isMetaScope,
|
|
isModernScope,
|
|
isOpenIdScope,
|
|
isScopeSubset,
|
|
isValidScope,
|
|
LEGACY_SCOPE_ALIASES,
|
|
LEGACY_SCOPES,
|
|
META_SCOPES,
|
|
MODERN_SCOPES,
|
|
makeScopeChecker,
|
|
OPENID_SCOPES,
|
|
parseScope,
|
|
RESOURCE_METADATA,
|
|
RESOURCES,
|
|
ROLE_SCOPES,
|
|
type ScopeString,
|
|
Scopes,
|
|
validateScopes,
|
|
} from "./scopeDefinitions";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 1. Constants & invariants
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("constants & invariants", () => {
|
|
test("RESOURCES has 11 entries in canonical order", () => {
|
|
expect(RESOURCES.length).toBe(11);
|
|
expect([...RESOURCES]).toEqual([
|
|
"organisation",
|
|
"customers",
|
|
"features",
|
|
"plans",
|
|
"rewards",
|
|
"balances",
|
|
"billing",
|
|
"migrations",
|
|
"analytics",
|
|
"apiKeys",
|
|
"platform",
|
|
]);
|
|
});
|
|
|
|
test("MODERN_SCOPES count equals 2*len(resources) - 1 (analytics read-only)", () => {
|
|
const expectedCount = RESOURCES.length * 2 - 1;
|
|
expect(MODERN_SCOPES.length).toBe(expectedCount);
|
|
expect(MODERN_SCOPES.length).toBe(21);
|
|
});
|
|
|
|
test("MODERN_SCOPES contains every Scopes.X.Read/Write entry", () => {
|
|
const expected = [
|
|
Scopes.Organisation.Read,
|
|
Scopes.Organisation.Write,
|
|
Scopes.Customers.Read,
|
|
Scopes.Customers.Write,
|
|
Scopes.Features.Read,
|
|
Scopes.Features.Write,
|
|
Scopes.Plans.Read,
|
|
Scopes.Plans.Write,
|
|
Scopes.Rewards.Read,
|
|
Scopes.Rewards.Write,
|
|
Scopes.Balances.Read,
|
|
Scopes.Balances.Write,
|
|
Scopes.Billing.Read,
|
|
Scopes.Billing.Write,
|
|
Scopes.Migrations.Read,
|
|
Scopes.Migrations.Write,
|
|
Scopes.Analytics.Read,
|
|
Scopes.ApiKeys.Read,
|
|
Scopes.ApiKeys.Write,
|
|
Scopes.Platform.Read,
|
|
Scopes.Platform.Write,
|
|
];
|
|
expect([...MODERN_SCOPES].sort()).toEqual([...expected].sort());
|
|
});
|
|
|
|
test("MODERN_SCOPES does NOT include analytics:write", () => {
|
|
expect(
|
|
(MODERN_SCOPES as readonly string[]).includes("analytics:write"),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("ALL_SCOPES contains OPENID_SCOPES + MODERN_SCOPES + META_SCOPES + LEGACY_SCOPES", () => {
|
|
const expected = [
|
|
...OPENID_SCOPES,
|
|
...MODERN_SCOPES,
|
|
...META_SCOPES,
|
|
...LEGACY_SCOPES,
|
|
];
|
|
expect([...ALL_SCOPES].sort()).toEqual([...expected].sort());
|
|
expect(ALL_SCOPES.length).toBe(
|
|
OPENID_SCOPES.length +
|
|
MODERN_SCOPES.length +
|
|
META_SCOPES.length +
|
|
LEGACY_SCOPES.length,
|
|
);
|
|
});
|
|
|
|
test("LEGACY_SCOPES includes bare 'apiKeys' token", () => {
|
|
expect((LEGACY_SCOPES as readonly string[]).includes("apiKeys")).toBe(true);
|
|
});
|
|
|
|
test("Scopes.Analytics has only Read (no Write property)", () => {
|
|
expect(Scopes.Analytics.Read).toBe("analytics:read");
|
|
expect((Scopes.Analytics as Record<string, unknown>).Write).toBeUndefined();
|
|
});
|
|
|
|
test("Scopes.Platform.Read === 'platform:read'", () => {
|
|
expect(Scopes.Platform.Read).toBe("platform:read");
|
|
expect(Scopes.Platform.Write).toBe("platform:write");
|
|
});
|
|
|
|
test("Meta-scope constants have expected values", () => {
|
|
expect(Scopes.Admin).toBe("admin");
|
|
expect(Scopes.Superuser).toBe("superuser");
|
|
expect(Scopes.Owner).toBe("owner");
|
|
expect(Scopes.Public).toBe("public");
|
|
});
|
|
|
|
test("META_SCOPES contains exactly the 4 meta-scopes", () => {
|
|
expect([...META_SCOPES].sort()).toEqual([
|
|
"admin",
|
|
"owner",
|
|
"public",
|
|
"superuser",
|
|
]);
|
|
});
|
|
|
|
test("OPENID_SCOPES contains exactly the 4 OIDC scopes", () => {
|
|
expect([...OPENID_SCOPES].sort()).toEqual([
|
|
"email",
|
|
"offline_access",
|
|
"openid",
|
|
"profile",
|
|
]);
|
|
});
|
|
|
|
test("RESOURCE_METADATA has an entry for every resource", () => {
|
|
for (const r of RESOURCES) {
|
|
expect(RESOURCE_METADATA[r]).toBeDefined();
|
|
expect(typeof RESOURCE_METADATA[r].name).toBe("string");
|
|
expect(typeof RESOURCE_METADATA[r].namePlural).toBe("string");
|
|
expect(typeof RESOURCE_METADATA[r].description).toBe("string");
|
|
}
|
|
});
|
|
|
|
test("ACTION_METADATA has read (order 1) and write (order 2)", () => {
|
|
expect(ACTION_METADATA.read.order).toBe(1);
|
|
expect(ACTION_METADATA.write.order).toBe(2);
|
|
expect(ACTION_METADATA.read.verb).toBe("Read");
|
|
expect(ACTION_METADATA.write.verb).toBe("Write");
|
|
});
|
|
|
|
test("LEGACY_SCOPE_ALIASES maps bare 'apiKeys' to apiKeys:write", () => {
|
|
expect(LEGACY_SCOPE_ALIASES.apiKeys).toBe("apiKeys:write");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 2. Predicates
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("isOpenIdScope", () => {
|
|
test("returns true for every OPENID_SCOPES entry", () => {
|
|
for (const s of OPENID_SCOPES) {
|
|
expect(isOpenIdScope(s)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("returns false for resource scopes", () => {
|
|
expect(isOpenIdScope("customers:read")).toBe(false);
|
|
expect(isOpenIdScope("plans:write")).toBe(false);
|
|
});
|
|
|
|
test("returns false for empty string and garbage", () => {
|
|
expect(isOpenIdScope("")).toBe(false);
|
|
expect(isOpenIdScope("garbage")).toBe(false);
|
|
expect(isOpenIdScope("OPENID")).toBe(false);
|
|
});
|
|
|
|
test("returns false for meta scopes", () => {
|
|
expect(isOpenIdScope("admin")).toBe(false);
|
|
expect(isOpenIdScope("superuser")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isModernScope", () => {
|
|
test("returns true for every MODERN_SCOPES entry", () => {
|
|
for (const s of MODERN_SCOPES) {
|
|
expect(isModernScope(s)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("returns false for every LEGACY_SCOPES entry", () => {
|
|
for (const s of LEGACY_SCOPES) {
|
|
expect(isModernScope(s)).toBe(false);
|
|
}
|
|
});
|
|
|
|
test("returns false for every META_SCOPES entry", () => {
|
|
for (const s of META_SCOPES) {
|
|
expect(isModernScope(s)).toBe(false);
|
|
}
|
|
});
|
|
|
|
test("returns false for every OPENID_SCOPES entry", () => {
|
|
for (const s of OPENID_SCOPES) {
|
|
expect(isModernScope(s)).toBe(false);
|
|
}
|
|
});
|
|
|
|
test("returns false for analytics:write (intentionally absent)", () => {
|
|
expect(isModernScope("analytics:write")).toBe(false);
|
|
});
|
|
|
|
test("returns false for garbage input", () => {
|
|
expect(isModernScope("")).toBe(false);
|
|
expect(isModernScope("garbage")).toBe(false);
|
|
expect(isModernScope("customers")).toBe(false);
|
|
expect(isModernScope("customers:")).toBe(false);
|
|
expect(isModernScope(":read")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isLegacyScope", () => {
|
|
test("returns true for every LEGACY_SCOPES entry", () => {
|
|
for (const s of LEGACY_SCOPES) {
|
|
expect(isLegacyScope(s)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("returns true for 'customers:create'", () => {
|
|
expect(isLegacyScope("customers:create")).toBe(true);
|
|
});
|
|
|
|
test("returns false for modern scopes", () => {
|
|
expect(isLegacyScope("customers:read")).toBe(false);
|
|
expect(isLegacyScope("customers:write")).toBe(false);
|
|
});
|
|
|
|
test("returns false for meta, openid, and garbage", () => {
|
|
expect(isLegacyScope("admin")).toBe(false);
|
|
expect(isLegacyScope("openid")).toBe(false);
|
|
expect(isLegacyScope("")).toBe(false);
|
|
expect(isLegacyScope("garbage")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isMetaScope", () => {
|
|
test("returns true for every META_SCOPES entry", () => {
|
|
for (const s of META_SCOPES) {
|
|
expect(isMetaScope(s)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("returns true for specific meta scopes", () => {
|
|
expect(isMetaScope("admin")).toBe(true);
|
|
expect(isMetaScope("owner")).toBe(true);
|
|
expect(isMetaScope("superuser")).toBe(true);
|
|
expect(isMetaScope("public")).toBe(true);
|
|
});
|
|
|
|
test("returns false for resource scopes", () => {
|
|
expect(isMetaScope("customers:read")).toBe(false);
|
|
expect(isMetaScope("plans:write")).toBe(false);
|
|
});
|
|
|
|
test("returns false for empty and garbage", () => {
|
|
expect(isMetaScope("")).toBe(false);
|
|
expect(isMetaScope("ADMIN")).toBe(false);
|
|
expect(isMetaScope("garbage")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isValidScope", () => {
|
|
test("returns true for any ALL_SCOPES entry", () => {
|
|
for (const s of ALL_SCOPES) {
|
|
expect(isValidScope(s)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("returns true across each category", () => {
|
|
expect(isValidScope("openid")).toBe(true);
|
|
expect(isValidScope("customers:read")).toBe(true);
|
|
expect(isValidScope("admin")).toBe(true);
|
|
expect(isValidScope("customers:create")).toBe(true);
|
|
});
|
|
|
|
test("returns false for garbage", () => {
|
|
expect(isValidScope("")).toBe(false);
|
|
expect(isValidScope("garbage")).toBe(false);
|
|
expect(isValidScope("customers:garbage")).toBe(false);
|
|
expect(isValidScope("analytics:write")).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 3. parseScope
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("parseScope", () => {
|
|
test("parses every MODERN_SCOPES entry correctly", () => {
|
|
for (const s of MODERN_SCOPES) {
|
|
const { resource, action } = parseScope(s);
|
|
expect(resource).not.toBeNull();
|
|
expect(action).not.toBeNull();
|
|
expect(`${resource}:${action}`).toBe(s);
|
|
}
|
|
});
|
|
|
|
test("parses customers:read correctly", () => {
|
|
expect(parseScope("customers:read")).toEqual({
|
|
resource: "customers",
|
|
action: "read",
|
|
});
|
|
});
|
|
|
|
test("parses customers:write correctly", () => {
|
|
expect(parseScope("customers:write")).toEqual({
|
|
resource: "customers",
|
|
action: "write",
|
|
});
|
|
});
|
|
|
|
test("parses analytics:read correctly", () => {
|
|
expect(parseScope("analytics:read")).toEqual({
|
|
resource: "analytics",
|
|
action: "read",
|
|
});
|
|
});
|
|
|
|
test("parses platform:write correctly", () => {
|
|
expect(parseScope("platform:write")).toEqual({
|
|
resource: "platform",
|
|
action: "write",
|
|
});
|
|
});
|
|
|
|
test("returns null/null for legacy 'customers:create'", () => {
|
|
expect(parseScope("customers:create")).toEqual({
|
|
resource: null,
|
|
action: null,
|
|
});
|
|
});
|
|
|
|
test("returns null/null for meta 'admin'", () => {
|
|
expect(parseScope("admin")).toEqual({
|
|
resource: null,
|
|
action: null,
|
|
});
|
|
});
|
|
|
|
test("returns null/null for analytics:write (not modern)", () => {
|
|
expect(parseScope("analytics:write")).toEqual({
|
|
resource: null,
|
|
action: null,
|
|
});
|
|
});
|
|
|
|
test("returns null/null for malformed input: 'customers'", () => {
|
|
expect(parseScope("customers")).toEqual({
|
|
resource: null,
|
|
action: null,
|
|
});
|
|
});
|
|
|
|
test("returns null/null for empty string", () => {
|
|
expect(parseScope("")).toEqual({ resource: null, action: null });
|
|
});
|
|
|
|
test("returns null/null for bare colon", () => {
|
|
expect(parseScope(":")).toEqual({ resource: null, action: null });
|
|
});
|
|
|
|
test("returns null/null for three-part scope", () => {
|
|
expect(parseScope("a:b:c")).toEqual({ resource: null, action: null });
|
|
});
|
|
|
|
test("returns null/null for 'customers:unknown'", () => {
|
|
expect(parseScope("customers:unknown")).toEqual({
|
|
resource: null,
|
|
action: null,
|
|
});
|
|
});
|
|
|
|
test("returns null/null for openid scopes", () => {
|
|
expect(parseScope("openid")).toEqual({ resource: null, action: null });
|
|
expect(parseScope("email")).toEqual({ resource: null, action: null });
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 4. expandScopes
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("expandScopes", () => {
|
|
test("empty input → empty set", () => {
|
|
expect([...expandScopes([])]).toEqual([]);
|
|
});
|
|
|
|
test("customers:read → just customers:read", () => {
|
|
expect([...expandScopes(["customers:read"])].sort()).toEqual([
|
|
"customers:read",
|
|
]);
|
|
});
|
|
|
|
test("customers:write → customers:write + customers:read", () => {
|
|
expect([...expandScopes(["customers:write"])].sort()).toEqual([
|
|
"customers:read",
|
|
"customers:write",
|
|
]);
|
|
});
|
|
|
|
test("customers:write + customers:read → same (no duplication)", () => {
|
|
const result = expandScopes(["customers:write", "customers:read"]);
|
|
expect([...result].sort()).toEqual(["customers:read", "customers:write"]);
|
|
expect(result.size).toBe(2);
|
|
});
|
|
|
|
test("legacy customers:list → customers:read", () => {
|
|
expect([...expandScopes(["customers:list"])].sort()).toEqual([
|
|
"customers:read",
|
|
]);
|
|
});
|
|
|
|
test("legacy customers:create → customers:write + customers:read", () => {
|
|
expect([...expandScopes(["customers:create"])].sort()).toEqual([
|
|
"customers:read",
|
|
"customers:write",
|
|
]);
|
|
});
|
|
|
|
test("legacy customers:update → customers:write + customers:read", () => {
|
|
expect([...expandScopes(["customers:update"])].sort()).toEqual([
|
|
"customers:read",
|
|
"customers:write",
|
|
]);
|
|
});
|
|
|
|
test("legacy customers:delete → customers:write + customers:read", () => {
|
|
expect([...expandScopes(["customers:delete"])].sort()).toEqual([
|
|
"customers:read",
|
|
"customers:write",
|
|
]);
|
|
});
|
|
|
|
test("bare 'apiKeys' → apiKeys:write + apiKeys:read", () => {
|
|
expect([...expandScopes(["apiKeys"])].sort()).toEqual([
|
|
"apiKeys:read",
|
|
"apiKeys:write",
|
|
]);
|
|
});
|
|
|
|
test("meta 'admin' expands to admin + every modern scope", () => {
|
|
const expanded = expandScopes(["admin"]);
|
|
expect(expanded.has("admin")).toBe(true);
|
|
for (const scope of MODERN_SCOPES) {
|
|
expect(expanded.has(scope)).toBe(true);
|
|
}
|
|
// admin does NOT grant owner or superuser
|
|
expect(expanded.has("owner" as ScopeString)).toBe(false);
|
|
expect(expanded.has("superuser" as ScopeString)).toBe(false);
|
|
});
|
|
|
|
test("meta 'superuser' expands to superuser + owner + admin + every modern scope", () => {
|
|
const expanded = expandScopes(["superuser"]);
|
|
expect(expanded.has("superuser")).toBe(true);
|
|
expect(expanded.has("owner" as ScopeString)).toBe(true);
|
|
expect(expanded.has("admin" as ScopeString)).toBe(true);
|
|
for (const scope of MODERN_SCOPES) {
|
|
expect(expanded.has(scope)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("meta 'owner' expands to owner + admin + every modern scope (NOT superuser)", () => {
|
|
const expanded = expandScopes(["owner"]);
|
|
expect(expanded.has("owner")).toBe(true);
|
|
expect(expanded.has("admin" as ScopeString)).toBe(true);
|
|
expect(expanded.has("superuser" as ScopeString)).toBe(false);
|
|
for (const scope of MODERN_SCOPES) {
|
|
expect(expanded.has(scope)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("meta 'public' passes through as-is and does NOT expand modern scopes", () => {
|
|
const expanded = expandScopes(["public"]);
|
|
expect([...expanded]).toEqual(["public"]);
|
|
});
|
|
|
|
test("unknown scopes silently dropped", () => {
|
|
expect([...expandScopes(["garbage"])]).toEqual([]);
|
|
});
|
|
|
|
test("OpenID scopes silently dropped but valid modern scope retained", () => {
|
|
expect(
|
|
[...expandScopes(["openid", "email", "customers:read"])].sort(),
|
|
).toEqual(["customers:read"]);
|
|
});
|
|
|
|
test("mixed input expanded correctly (admin grants every modern scope)", () => {
|
|
const result = expandScopes([
|
|
"customers:write",
|
|
"admin",
|
|
"junk",
|
|
"plans:list",
|
|
]);
|
|
// admin grants every modern scope; individual write→read expansion
|
|
// is subsumed by the admin grant.
|
|
expect(result.has("admin")).toBe(true);
|
|
for (const scope of MODERN_SCOPES) {
|
|
expect(result.has(scope)).toBe(true);
|
|
}
|
|
// junk dropped.
|
|
expect(result.has("junk" as ScopeString)).toBe(false);
|
|
});
|
|
|
|
test("all legacy aliases expand to a known modern scope (+ optional read)", () => {
|
|
for (const [legacy, modern] of Object.entries(LEGACY_SCOPE_ALIASES)) {
|
|
const expanded = expandScopes([legacy]);
|
|
expect(expanded.has(modern)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("analytics:write is dropped (not modern, not legacy)", () => {
|
|
expect([...expandScopes(["analytics:write"])]).toEqual([]);
|
|
});
|
|
|
|
test("organisation:create → organisation:write + organisation:read", () => {
|
|
expect([...expandScopes(["organisation:create"])].sort()).toEqual([
|
|
"organisation:read",
|
|
"organisation:write",
|
|
]);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 5. ROLE_SCOPES
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("ROLE_SCOPES", () => {
|
|
test("owner contains 'owner', 'admin', and every MODERN_SCOPES entry", () => {
|
|
expect(ROLE_SCOPES.owner).toContain("owner");
|
|
expect(ROLE_SCOPES.owner).toContain("admin");
|
|
for (const s of MODERN_SCOPES) {
|
|
expect(ROLE_SCOPES.owner).toContain(s);
|
|
}
|
|
});
|
|
|
|
test("admin contains 'admin' and every MODERN_SCOPES entry, but NOT 'owner'", () => {
|
|
expect(ROLE_SCOPES.admin).toContain("admin");
|
|
for (const s of MODERN_SCOPES) {
|
|
expect(ROLE_SCOPES.admin).toContain(s);
|
|
}
|
|
expect(ROLE_SCOPES.admin).not.toContain("owner");
|
|
});
|
|
|
|
test("developer has the exact 11 expected scopes", () => {
|
|
expect([...ROLE_SCOPES.developer].sort()).toEqual(
|
|
[
|
|
"organisation:read",
|
|
"customers:write",
|
|
"features:write",
|
|
"plans:write",
|
|
"rewards:write",
|
|
"balances:write",
|
|
"billing:write",
|
|
"migrations:write",
|
|
"analytics:read",
|
|
"apiKeys:write",
|
|
"platform:write",
|
|
].sort(),
|
|
);
|
|
expect(ROLE_SCOPES.developer.length).toBe(11);
|
|
});
|
|
|
|
test("sales has the exact 7 expected scopes", () => {
|
|
expect([...ROLE_SCOPES.sales].sort()).toEqual(
|
|
[
|
|
"customers:write",
|
|
"billing:write",
|
|
"rewards:write",
|
|
"balances:write",
|
|
"plans:read",
|
|
"features:read",
|
|
"analytics:read",
|
|
].sort(),
|
|
);
|
|
expect(ROLE_SCOPES.sales.length).toBe(7);
|
|
});
|
|
|
|
test("member contains all :read scopes, no :write", () => {
|
|
expect(ROLE_SCOPES.member.length).toBe(RESOURCES.length);
|
|
for (const s of ROLE_SCOPES.member) {
|
|
expect(s.endsWith(":read")).toBe(true);
|
|
expect(s.endsWith(":write")).toBe(false);
|
|
}
|
|
for (const r of RESOURCES) {
|
|
expect(ROLE_SCOPES.member).toContain(`${r}:read` as ScopeString);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 6. checkScopes
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("checkScopes — shorthand array (ALL semantics)", () => {
|
|
test("empty required + empty granted → allowed", () => {
|
|
expect(checkScopes([], [])).toEqual({ allowed: true, missing: [] });
|
|
});
|
|
|
|
test("customers:read required, customers:read granted → allowed", () => {
|
|
expect(checkScopes(["customers:read"], ["customers:read"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("customers:read required, customers:write granted → allowed (write implies read)", () => {
|
|
expect(checkScopes(["customers:read"], ["customers:write"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("customers:write required, customers:read granted → blocked, missing customers:write", () => {
|
|
expect(checkScopes(["customers:write"], ["customers:read"])).toEqual({
|
|
allowed: false,
|
|
missing: ["customers:write"],
|
|
});
|
|
});
|
|
|
|
test("partial miss → blocked, missing contains only the missing one", () => {
|
|
expect(
|
|
checkScopes(["customers:read", "plans:read"], ["customers:read"]),
|
|
).toEqual({ allowed: false, missing: ["plans:read"] });
|
|
});
|
|
});
|
|
|
|
describe("checkScopes — { ALL } semantics", () => {
|
|
test("empty ALL → allowed vacuously", () => {
|
|
expect(checkScopes({ ALL: [] }, [])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("ALL satisfied → allowed", () => {
|
|
expect(
|
|
checkScopes({ ALL: ["customers:read", "plans:read"] }, [
|
|
"customers:read",
|
|
"plans:read",
|
|
]),
|
|
).toEqual({ allowed: true, missing: [] });
|
|
});
|
|
|
|
test("ALL with one missing → blocked", () => {
|
|
expect(
|
|
checkScopes({ ALL: ["customers:read", "plans:read"] }, [
|
|
"customers:read",
|
|
]),
|
|
).toEqual({ allowed: false, missing: ["plans:read"] });
|
|
});
|
|
});
|
|
|
|
describe("checkScopes — { ANY } semantics", () => {
|
|
test("one of the ANY matches → allowed", () => {
|
|
expect(
|
|
checkScopes({ ANY: ["customers:read", "plans:read"] }, [
|
|
"customers:read",
|
|
]),
|
|
).toEqual({ allowed: true, missing: [] });
|
|
});
|
|
|
|
test("none match → blocked, missing = full ANY list", () => {
|
|
expect(
|
|
checkScopes({ ANY: ["customers:read", "plans:read"] }, ["rewards:read"]),
|
|
).toEqual({
|
|
allowed: false,
|
|
missing: ["customers:read", "plans:read"],
|
|
});
|
|
});
|
|
|
|
test("empty ANY with any granted → allowed (vacuous truth)", () => {
|
|
expect(checkScopes({ ANY: [] }, ["customers:read"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("empty ANY with empty granted → allowed", () => {
|
|
expect(checkScopes({ ANY: [] }, [])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("checkScopes — { ALL, ANY } semantics", () => {
|
|
test("both satisfied → allowed", () => {
|
|
expect(
|
|
checkScopes(
|
|
{
|
|
ALL: ["customers:read"],
|
|
ANY: ["plans:read", "rewards:read"],
|
|
},
|
|
["customers:read", "plans:read"],
|
|
),
|
|
).toEqual({ allowed: true, missing: [] });
|
|
});
|
|
|
|
test("ALL satisfied, ANY not → blocked with ANY list in missing", () => {
|
|
expect(
|
|
checkScopes(
|
|
{
|
|
ALL: ["customers:read"],
|
|
ANY: ["plans:read", "rewards:read"],
|
|
},
|
|
["customers:read"],
|
|
),
|
|
).toEqual({
|
|
allowed: false,
|
|
missing: ["plans:read", "rewards:read"],
|
|
});
|
|
});
|
|
|
|
test("ANY satisfied, ALL not → blocked with ALL-misses only", () => {
|
|
expect(
|
|
checkScopes(
|
|
{
|
|
ALL: ["customers:read", "features:read"],
|
|
ANY: ["plans:read"],
|
|
},
|
|
["plans:read"],
|
|
),
|
|
).toEqual({
|
|
allowed: false,
|
|
missing: ["customers:read", "features:read"],
|
|
});
|
|
});
|
|
|
|
test("neither satisfied → blocked with ALL-misses + ANY list", () => {
|
|
expect(
|
|
checkScopes(
|
|
{
|
|
ALL: ["customers:read", "features:read"],
|
|
ANY: ["plans:read", "rewards:read"],
|
|
},
|
|
[],
|
|
),
|
|
).toEqual({
|
|
allowed: false,
|
|
missing: [
|
|
"customers:read",
|
|
"features:read",
|
|
"plans:read",
|
|
"rewards:read",
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("checkScopes — public bypass", () => {
|
|
test("required = ['public'], granted = [] → allowed", () => {
|
|
expect(checkScopes(["public"], [])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("required = { ANY: ['public'] }, granted = [] → allowed", () => {
|
|
expect(checkScopes({ ANY: ["public"] }, [])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("required = { ALL: ['public'] }, granted = [] → allowed", () => {
|
|
expect(checkScopes({ ALL: ["public"] }, [])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("public short-circuits even when other scopes also required", () => {
|
|
expect(checkScopes(["customers:write", "public"], [])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("checkScopes — meta-scope hierarchy", () => {
|
|
// Hierarchy: superuser > owner > admin > product-scopes
|
|
//
|
|
// Satisfaction semantics:
|
|
// - superuser req → ONLY superuser
|
|
// - owner req → superuser OR owner
|
|
// - admin req → superuser OR owner OR admin
|
|
// - product req → admin / owner / superuser / exact product scope
|
|
|
|
test("admin grants every product scope", () => {
|
|
expect(checkScopes(["customers:write"], ["admin"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
expect(
|
|
checkScopes(
|
|
["customers:write", "plans:write", "rewards:write"],
|
|
["admin"],
|
|
),
|
|
).toEqual({ allowed: true, missing: [] });
|
|
});
|
|
|
|
test("admin does NOT satisfy owner requirement", () => {
|
|
expect(checkScopes(["owner"], ["admin"])).toEqual({
|
|
allowed: false,
|
|
missing: ["owner"],
|
|
});
|
|
});
|
|
|
|
test("admin does NOT satisfy superuser requirement", () => {
|
|
expect(checkScopes(["superuser"], ["admin"])).toEqual({
|
|
allowed: false,
|
|
missing: ["superuser"],
|
|
});
|
|
});
|
|
|
|
test("owner satisfies admin requirement (owner > admin)", () => {
|
|
expect(checkScopes(["admin"], ["owner"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("owner grants every product scope (inherits admin → all products)", () => {
|
|
expect(checkScopes(["customers:write"], ["owner"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("owner does NOT satisfy superuser requirement", () => {
|
|
expect(checkScopes(["superuser"], ["owner"])).toEqual({
|
|
allowed: false,
|
|
missing: ["superuser"],
|
|
});
|
|
});
|
|
|
|
test("superuser satisfies owner requirement (superuser > owner)", () => {
|
|
expect(checkScopes(["owner"], ["superuser"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("superuser satisfies admin requirement", () => {
|
|
expect(checkScopes(["admin"], ["superuser"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("superuser grants every product scope", () => {
|
|
expect(checkScopes(["customers:write"], ["superuser"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("owner granted, owner required → allowed", () => {
|
|
expect(checkScopes(["owner"], ["owner"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("superuser granted, superuser required → allowed", () => {
|
|
expect(checkScopes(["superuser"], ["superuser"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("admin satisfies an ANY requirement alongside superuser (via product-scope alternative)", () => {
|
|
// admin grants `customers:read` via expansion, so the ANY condition
|
|
// is satisfied by the non-superuser alternative.
|
|
expect(
|
|
checkScopes({ ANY: ["superuser", "customers:read"] }, ["admin"]),
|
|
).toEqual({ allowed: true, missing: [] });
|
|
});
|
|
|
|
test("admin denied when ALL requires superuser", () => {
|
|
const res = checkScopes({ ALL: ["superuser"] }, ["admin"]);
|
|
expect(res.allowed).toBe(false);
|
|
expect(res.missing).toContain("superuser" as ScopeString);
|
|
});
|
|
|
|
test("admin denied when ALL requires owner", () => {
|
|
const res = checkScopes({ ALL: ["owner"] }, ["admin"]);
|
|
expect(res.allowed).toBe(false);
|
|
expect(res.missing).toContain("owner" as ScopeString);
|
|
});
|
|
});
|
|
|
|
describe("checkScopes — legacy & isolation", () => {
|
|
test("legacy customers:list grants customers:read", () => {
|
|
expect(checkScopes(["customers:read"], ["customers:list"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("legacy customers:create grants customers:write (and read)", () => {
|
|
expect(checkScopes(["customers:write"], ["customers:create"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
expect(checkScopes(["customers:read"], ["customers:create"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("bare legacy 'apiKeys' grants apiKeys:write", () => {
|
|
expect(checkScopes(["apiKeys:write"], ["apiKeys"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("cross-resource isolation: plans:read requested, customers:write granted → blocked", () => {
|
|
expect(checkScopes(["plans:read"], ["customers:write"])).toEqual({
|
|
allowed: false,
|
|
missing: ["plans:read"],
|
|
});
|
|
});
|
|
|
|
test("openid scope alone does not satisfy a resource requirement", () => {
|
|
expect(checkScopes(["customers:read"], ["openid"])).toEqual({
|
|
allowed: false,
|
|
missing: ["customers:read"],
|
|
});
|
|
});
|
|
|
|
test("garbage scope alone does not satisfy a resource requirement", () => {
|
|
expect(checkScopes(["customers:read"], ["junk"])).toEqual({
|
|
allowed: false,
|
|
missing: ["customers:read"],
|
|
});
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 7. isScopeSubset
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("isScopeSubset", () => {
|
|
test("empty requested + empty granted → true", () => {
|
|
expect(isScopeSubset([], [])).toBe(true);
|
|
});
|
|
|
|
test("empty requested + anything granted → true", () => {
|
|
expect(isScopeSubset([], ["customers:read"])).toBe(true);
|
|
expect(isScopeSubset([], ["admin"])).toBe(true);
|
|
});
|
|
|
|
test("identical single scope → true", () => {
|
|
expect(isScopeSubset(["customers:read"], ["customers:read"])).toBe(true);
|
|
});
|
|
|
|
test("customers:read ⊆ customers:write (write implies read)", () => {
|
|
expect(isScopeSubset(["customers:read"], ["customers:write"])).toBe(true);
|
|
});
|
|
|
|
test("customers:write NOT ⊆ customers:read", () => {
|
|
expect(isScopeSubset(["customers:write"], ["customers:read"])).toBe(false);
|
|
});
|
|
|
|
test("multi-scope requested vs multi-scope write grant", () => {
|
|
expect(
|
|
isScopeSubset(
|
|
["customers:read", "plans:read"],
|
|
["customers:write", "plans:write"],
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("cross-resource isolation", () => {
|
|
expect(isScopeSubset(["customers:write"], ["plans:write"])).toBe(false);
|
|
});
|
|
|
|
test("admin in granted: grants every product scope + admin, but NOT owner or superuser", () => {
|
|
expect(isScopeSubset(["customers:write"], ["admin"])).toBe(true);
|
|
expect(isScopeSubset(["admin"], ["admin"])).toBe(true);
|
|
expect(isScopeSubset(["owner"], ["admin"])).toBe(false);
|
|
expect(isScopeSubset(["superuser"], ["admin"])).toBe(false);
|
|
});
|
|
|
|
test("owner in granted: grants every product scope + admin + owner, but NOT superuser", () => {
|
|
expect(isScopeSubset(["customers:write"], ["owner"])).toBe(true);
|
|
expect(isScopeSubset(["admin"], ["owner"])).toBe(true);
|
|
expect(isScopeSubset(["owner"], ["owner"])).toBe(true);
|
|
expect(isScopeSubset(["superuser"], ["owner"])).toBe(false);
|
|
});
|
|
|
|
test("superuser in granted: grants everything including owner and admin", () => {
|
|
expect(isScopeSubset(["customers:write"], ["superuser"])).toBe(true);
|
|
expect(isScopeSubset(["admin"], ["superuser"])).toBe(true);
|
|
expect(isScopeSubset(["owner"], ["superuser"])).toBe(true);
|
|
expect(isScopeSubset(["superuser"], ["superuser"])).toBe(true);
|
|
});
|
|
|
|
test("legacy granted covers modern requested", () => {
|
|
expect(isScopeSubset(["customers:write"], ["customers:create"])).toBe(true);
|
|
});
|
|
|
|
test("requested contains unknown scope → dropped via expansion", () => {
|
|
// both requested and granted go through expandScopes; garbage is dropped
|
|
// so an all-garbage requested becomes empty set → subset trivially.
|
|
expect(isScopeSubset(["garbage"], [])).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 8. makeScopeChecker
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("makeScopeChecker", () => {
|
|
test("empty grant — nothing passes", () => {
|
|
const c = makeScopeChecker([]);
|
|
expect(c.has("customers:read")).toBe(false);
|
|
expect(c.has("customers:write")).toBe(false);
|
|
expect(c.isAdmin).toBe(false);
|
|
expect(c.isOwner).toBe(false);
|
|
expect(c.isSuperuser).toBe(false);
|
|
});
|
|
|
|
test("admin grant — has() true for every product scope", () => {
|
|
const c = makeScopeChecker(["admin"]);
|
|
expect(c.has("customers:read")).toBe(true);
|
|
expect(c.has("customers:write")).toBe(true);
|
|
expect(c.has("analytics:read")).toBe(true);
|
|
expect(c.has("platform:write")).toBe(true);
|
|
});
|
|
|
|
test("admin grant — has('superuser') → false", () => {
|
|
const c = makeScopeChecker(["admin"]);
|
|
expect(c.has("superuser")).toBe(false);
|
|
expect(c.isSuperuser).toBe(false);
|
|
});
|
|
|
|
test("admin grant — has('owner') → false", () => {
|
|
const c = makeScopeChecker(["admin"]);
|
|
expect(c.has("owner")).toBe(false);
|
|
expect(c.isOwner).toBe(false);
|
|
});
|
|
|
|
test("admin grant — has('admin') → true, isAdmin true", () => {
|
|
const c = makeScopeChecker(["admin"]);
|
|
expect(c.has("admin")).toBe(true);
|
|
expect(c.isAdmin).toBe(true);
|
|
});
|
|
|
|
test("owner grant — has() true for admin + product scopes", () => {
|
|
const c = makeScopeChecker(["owner"]);
|
|
expect(c.has("customers:write")).toBe(true);
|
|
expect(c.has("admin")).toBe(true);
|
|
expect(c.has("owner")).toBe(true);
|
|
expect(c.isOwner).toBe(true);
|
|
// owner is a capability flag — isAdmin is semantic (can satisfy admin reqs)
|
|
expect(c.isAdmin).toBe(true);
|
|
});
|
|
|
|
test("owner grant — has('superuser') → false (owner does NOT grant superuser)", () => {
|
|
const c = makeScopeChecker(["owner"]);
|
|
expect(c.has("superuser")).toBe(false);
|
|
expect(c.isSuperuser).toBe(false);
|
|
});
|
|
|
|
test("superuser grant — has() true for everything", () => {
|
|
const c = makeScopeChecker(["superuser"]);
|
|
expect(c.has("customers:write")).toBe(true);
|
|
expect(c.has("admin")).toBe(true);
|
|
expect(c.has("owner")).toBe(true);
|
|
expect(c.has("superuser")).toBe(true);
|
|
expect(c.isAdmin).toBe(true);
|
|
expect(c.isOwner).toBe(true);
|
|
expect(c.isSuperuser).toBe(true);
|
|
});
|
|
|
|
test("role capability flags are SEMANTIC (what the caller can satisfy), not literal grant membership", () => {
|
|
// A superuser grant SEMANTICALLY makes the caller an admin and an
|
|
// owner (they can satisfy any admin or owner requirement). The
|
|
// isAdmin/isOwner booleans reflect capability, not literal grant.
|
|
const c = makeScopeChecker(["superuser"]);
|
|
expect(c.isAdmin).toBe(true);
|
|
expect(c.isOwner).toBe(true);
|
|
expect(c.isSuperuser).toBe(true);
|
|
});
|
|
|
|
test("customers:write expands to include customers:read via has()", () => {
|
|
const c = makeScopeChecker(["customers:write"]);
|
|
expect(c.has("customers:read")).toBe(true);
|
|
expect(c.has("customers:write")).toBe(true);
|
|
});
|
|
|
|
test("customers:read does NOT imply customers:write via has()", () => {
|
|
const c = makeScopeChecker(["customers:read"]);
|
|
expect(c.has("customers:read")).toBe(true);
|
|
expect(c.has("customers:write")).toBe(false);
|
|
});
|
|
|
|
test("hasAll — all present → true", () => {
|
|
const c = makeScopeChecker(["customers:read", "plans:read"]);
|
|
expect(c.hasAll(["customers:read", "plans:read"])).toBe(true);
|
|
});
|
|
|
|
test("hasAll — one missing → false", () => {
|
|
const c = makeScopeChecker(["customers:read"]);
|
|
expect(c.hasAll(["customers:read", "plans:read"])).toBe(false);
|
|
});
|
|
|
|
test("hasAll — empty list → true (vacuous)", () => {
|
|
const c = makeScopeChecker([]);
|
|
expect(c.hasAll([])).toBe(true);
|
|
});
|
|
|
|
test("hasAny — at least one present → true", () => {
|
|
const c = makeScopeChecker(["customers:read"]);
|
|
expect(c.hasAny(["customers:read", "plans:read"])).toBe(true);
|
|
});
|
|
|
|
test("hasAny — none present → false", () => {
|
|
const c = makeScopeChecker(["customers:read"]);
|
|
expect(c.hasAny(["plans:read", "rewards:read"])).toBe(false);
|
|
});
|
|
|
|
test("hasAny — empty list → false (vacuous)", () => {
|
|
const c = makeScopeChecker(["customers:read"]);
|
|
expect(c.hasAny([])).toBe(false);
|
|
});
|
|
|
|
test("check() delegates to checkScopes — shorthand array", () => {
|
|
const c = makeScopeChecker(["customers:read"]);
|
|
expect(c.check(["customers:read"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("check() delegates — public bypass works", () => {
|
|
const c = makeScopeChecker([]);
|
|
expect(c.check(["public"])).toEqual({ allowed: true, missing: [] });
|
|
});
|
|
|
|
test("check() delegates — admin bypass works", () => {
|
|
const c = makeScopeChecker(["admin"]);
|
|
expect(c.check(["customers:write"])).toEqual({
|
|
allowed: true,
|
|
missing: [],
|
|
});
|
|
});
|
|
|
|
test("check() — blocked path returns missing", () => {
|
|
const c = makeScopeChecker(["customers:read"]);
|
|
expect(c.check(["plans:write"])).toEqual({
|
|
allowed: false,
|
|
missing: ["plans:write"],
|
|
});
|
|
});
|
|
|
|
test("expanded set contains write→read expansion", () => {
|
|
const c = makeScopeChecker(["customers:write"]);
|
|
expect(c.expanded.has("customers:read")).toBe(true);
|
|
expect(c.expanded.has("customers:write")).toBe(true);
|
|
});
|
|
|
|
test("legacy input normalised in expanded set", () => {
|
|
const c = makeScopeChecker(["customers:create"]);
|
|
expect(c.expanded.has("customers:write")).toBe(true);
|
|
expect(c.expanded.has("customers:read")).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 9. Display helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("groupScopesByResource", () => {
|
|
test("groups scopes by resource with actions sorted read-before-write", () => {
|
|
const result = groupScopesByResource([
|
|
"customers:write",
|
|
"customers:read",
|
|
"plans:read",
|
|
]);
|
|
expect(result.get("customers")).toEqual(["read", "write"]);
|
|
expect(result.get("plans")).toEqual(["read"]);
|
|
});
|
|
|
|
test("customers:write alone → read + write (expansion)", () => {
|
|
const result = groupScopesByResource(["customers:write"]);
|
|
expect(result.get("customers")).toEqual(["read", "write"]);
|
|
});
|
|
|
|
test("legacy customers:list → customers: read only", () => {
|
|
const result = groupScopesByResource(["customers:list"]);
|
|
expect(result.get("customers")).toEqual(["read"]);
|
|
});
|
|
|
|
test("openid dropped; admin grant expands to all modern resources (via hierarchy)", () => {
|
|
// admin in input now expands to every modern product scope
|
|
// (superuser > owner > admin > products). Grouping reflects that:
|
|
// every resource has read+write except analytics (read-only).
|
|
const result = groupScopesByResource(["openid", "admin", "customers:read"]);
|
|
// 11 resources: organisation, customers, features, plans, rewards,
|
|
// balances, billing, migrations, analytics, apiKeys, platform
|
|
expect(result.size).toBe(11);
|
|
// customers ends up with both read and write (from admin expansion).
|
|
expect(result.get("customers")).toEqual(["read", "write"]);
|
|
// analytics is read-only.
|
|
expect(result.get("analytics")).toEqual(["read"]);
|
|
});
|
|
|
|
test("openid + single modern scope (no meta) — only the literal resource", () => {
|
|
const result = groupScopesByResource(["openid", "customers:read"]);
|
|
expect(result.size).toBe(1);
|
|
expect(result.get("customers")).toEqual(["read"]);
|
|
});
|
|
|
|
test("empty input → empty map", () => {
|
|
const result = groupScopesByResource([]);
|
|
expect(result.size).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("formatActions", () => {
|
|
test("empty → ''", () => {
|
|
expect(formatActions([])).toBe("");
|
|
});
|
|
|
|
test("['read'] → 'Read'", () => {
|
|
expect(formatActions(["read"])).toBe("Read");
|
|
});
|
|
|
|
test("['write'] → 'Write'", () => {
|
|
expect(formatActions(["write"])).toBe("Write");
|
|
});
|
|
|
|
test("['read', 'write'] → 'read and write' (source returns lowercase for 2+)", () => {
|
|
// NOTE: despite the jsdoc claiming "Read and write", the actual
|
|
// implementation lowercases every verb when there are 2+ actions
|
|
// (see line 826: `sorted.map(a => ACTION_METADATA[a].verb.toLowerCase())`).
|
|
// formatResourcePermission then capitalises the first char. This test
|
|
// pins current behaviour; capitalisation is re-asserted at the
|
|
// formatResourcePermission level.
|
|
expect(formatActions(["read", "write"])).toBe("read and write");
|
|
});
|
|
|
|
test("['write', 'read'] also returns 'read and write' (sorted)", () => {
|
|
expect(formatActions(["write", "read"])).toBe("read and write");
|
|
});
|
|
});
|
|
|
|
describe("formatResourcePermission", () => {
|
|
test("customers + ['read'] → 'Read customers'", () => {
|
|
expect(formatResourcePermission("customers", ["read"])).toBe(
|
|
"Read customers",
|
|
);
|
|
});
|
|
|
|
test("plans + ['read', 'write'] → 'Read and write plans'", () => {
|
|
expect(formatResourcePermission("plans", ["read", "write"])).toBe(
|
|
"Read and write plans",
|
|
);
|
|
});
|
|
|
|
test("apiKeys is pluralised via metadata ('API Keys')", () => {
|
|
expect(formatResourcePermission("apiKeys", ["read"])).toBe("Read api keys");
|
|
});
|
|
});
|
|
|
|
describe("groupAndFormatScopes", () => {
|
|
test("produces one entry per resource with correct shape", () => {
|
|
const result = groupAndFormatScopes(["customers:read", "plans:write"]);
|
|
expect(result.length).toBe(2);
|
|
|
|
const customers = result.find((e) => e.resource === "customers");
|
|
expect(customers).toBeDefined();
|
|
expect(customers?.resourceName).toBe("Customers");
|
|
expect(customers?.actions).toEqual(["read"]);
|
|
expect(customers?.formattedPermission).toBe("Read customers");
|
|
expect(customers?.description).toBe(
|
|
RESOURCE_METADATA.customers.description,
|
|
);
|
|
|
|
const plans = result.find((e) => e.resource === "plans");
|
|
expect(plans).toBeDefined();
|
|
expect(plans?.actions).toEqual(["read", "write"]); // write expands to read
|
|
expect(plans?.formattedPermission).toBe("Read and write plans");
|
|
});
|
|
|
|
test("empty input → empty array", () => {
|
|
expect(groupAndFormatScopes([])).toEqual([]);
|
|
});
|
|
|
|
test("garbage input → empty array", () => {
|
|
expect(groupAndFormatScopes(["garbage", "openid"])).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("validateScopes", () => {
|
|
test("partitions valid vs invalid", () => {
|
|
const result = validateScopes([
|
|
"customers:read",
|
|
"customers:garbage",
|
|
"openid",
|
|
]);
|
|
expect([...result.valid].sort()).toEqual(
|
|
["customers:read", "openid"].sort(),
|
|
);
|
|
expect(result.invalid).toEqual(["customers:garbage"]);
|
|
});
|
|
|
|
test("all valid inputs → empty invalid", () => {
|
|
const result = validateScopes(["customers:read", "admin", "openid"]);
|
|
expect(result.invalid).toEqual([]);
|
|
expect([...result.valid].sort()).toEqual(
|
|
["admin", "customers:read", "openid"].sort(),
|
|
);
|
|
});
|
|
|
|
test("all invalid inputs → empty valid", () => {
|
|
const result = validateScopes(["garbage", "nonsense"]);
|
|
expect(result.valid).toEqual([]);
|
|
expect([...result.invalid].sort()).toEqual(["garbage", "nonsense"]);
|
|
});
|
|
|
|
test("legacy scopes count as valid", () => {
|
|
const result = validateScopes(["customers:create", "apiKeys"]);
|
|
expect(result.invalid).toEqual([]);
|
|
expect([...result.valid].sort()).toEqual(
|
|
["apiKeys", "customers:create"].sort(),
|
|
);
|
|
});
|
|
|
|
test("empty input → both buckets empty", () => {
|
|
expect(validateScopes([])).toEqual({ valid: [], invalid: [] });
|
|
});
|
|
|
|
test("analytics:write classified as invalid", () => {
|
|
const result = validateScopes(["analytics:write"]);
|
|
expect(result.valid).toEqual([]);
|
|
expect(result.invalid).toEqual(["analytics:write"]);
|
|
});
|
|
});
|
|
|
|
describe("getResourceDescription", () => {
|
|
test("returns the metadata description for each resource", () => {
|
|
for (const r of RESOURCES) {
|
|
expect(getResourceDescription(r)).toBe(RESOURCE_METADATA[r].description);
|
|
}
|
|
});
|
|
});
|