mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-10-03 19:03:13 +00:00
76 lines
2.7 KiB
TypeScript
76 lines
2.7 KiB
TypeScript
|
|
import { describe, it, expect } from 'vitest';
|
|||
|
|
|
|||
|
|
import {
|
|||
|
|
computeScopesToSuppress,
|
|||
|
|
entitledScopes,
|
|||
|
|
ALL_TIER_SCOPES,
|
|||
|
|
BASE_SCOPES,
|
|||
|
|
OPS_READ,
|
|||
|
|
OPS_TASKS,
|
|||
|
|
FINANCE_READ,
|
|||
|
|
FINANCE_ADMIN,
|
|||
|
|
OPS_GMAIL,
|
|||
|
|
OPS_CALENDAR,
|
|||
|
|
} from '../src/scopes.js';
|
|||
|
|
|
|||
|
|
describe('entitledScopes', () => {
|
|||
|
|
it('unions the scopes across a user’s groups', () => {
|
|||
|
|
// Lauren: assistant + finance (design.md §2.3 worked example).
|
|||
|
|
const got = entitledScopes(['sh-mcp-assistant', 'sh-mcp-finance']).sort();
|
|||
|
|
expect(got).toEqual([OPS_CALENDAR, OPS_GMAIL, OPS_READ, OPS_TASKS, FINANCE_READ].sort());
|
|||
|
|
// She does NOT get finance:admin.
|
|||
|
|
expect(got).not.toContain(FINANCE_ADMIN);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('ignores unknown groups (a stray group grants nothing)', () => {
|
|||
|
|
expect(entitledScopes(['sh-mcp-not-a-real-group'])).toEqual([]);
|
|||
|
|
expect(entitledScopes([])).toEqual([]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('admin gets every tier scope', () => {
|
|||
|
|
expect(entitledScopes(['sh-mcp-admin']).sort()).toEqual([...ALL_TIER_SCOPES].sort());
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('computeScopesToSuppress (suppress-only)', () => {
|
|||
|
|
it('suppresses exactly the tier scopes the groups do not grant', () => {
|
|||
|
|
// ops-only user: keeps ops:read, loses everything else.
|
|||
|
|
const suppress = computeScopesToSuppress(['sh-mcp-ops'], true);
|
|||
|
|
expect(suppress).toContain(OPS_TASKS);
|
|||
|
|
expect(suppress).toContain(FINANCE_READ);
|
|||
|
|
expect(suppress).toContain(FINANCE_ADMIN);
|
|||
|
|
expect(suppress).not.toContain(OPS_READ);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('finance group keeps finance:read but never finance:admin', () => {
|
|||
|
|
const suppress = computeScopesToSuppress(['sh-mcp-finance'], true);
|
|||
|
|
expect(suppress).not.toContain(FINANCE_READ);
|
|||
|
|
expect(suppress).not.toContain(OPS_READ);
|
|||
|
|
expect(suppress).toContain(FINANCE_ADMIN);
|
|||
|
|
expect(suppress).toContain(OPS_TASKS);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('admin suppresses nothing', () => {
|
|||
|
|
expect(computeScopesToSuppress(['sh-mcp-admin'], true)).toEqual([]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('FAIL CLOSED: stale sync drops everyone to base ops:read regardless of groups', () => {
|
|||
|
|
// Even a finance admin is reduced to ops:read when sync is stale.
|
|||
|
|
const suppress = computeScopesToSuppress(['sh-mcp-admin'], false);
|
|||
|
|
const kept = ALL_TIER_SCOPES.filter((s) => !suppress.includes(s));
|
|||
|
|
expect(kept).toEqual([...BASE_SCOPES]);
|
|||
|
|
expect(suppress).toContain(FINANCE_READ);
|
|||
|
|
expect(suppress).toContain(FINANCE_ADMIN);
|
|||
|
|
expect(suppress).toContain(OPS_TASKS);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('only ever returns scopes drawn from the issued tier-scope set (never invents one)', () => {
|
|||
|
|
for (const groups of [[], ['sh-mcp-ops'], ['sh-mcp-finance'], ['sh-mcp-admin']]) {
|
|||
|
|
for (const fresh of [true, false]) {
|
|||
|
|
const suppress = computeScopesToSuppress(groups, fresh);
|
|||
|
|
for (const s of suppress) expect(ALL_TIER_SCOPES).toContain(s);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|