Files
TREK/server/tests/unit/mcp/scopes.test.ts
T
jubnl f2908fdd65 test(mcp): add tests for OAuth 2.1, addon gating, and budget reorder
Covers OAuth integration flow, scope enforcement, addon-gated tool access,
oauthService unit tests, and budget reorder/permission/reservation-sync scenarios.
2026-04-09 23:12:59 +02:00

217 lines
6.9 KiB
TypeScript

/**
* Unit tests for MCP scope helper functions in server/src/mcp/scopes.ts.
* No DB or mocks needed — pure functions only.
*/
import { describe, it, expect } from 'vitest';
import {
validateScopes,
canReadTrips,
canWrite,
canRead,
canDeleteTrips,
ALL_SCOPES,
SCOPE_INFO,
} from '../../../src/mcp/scopes';
// ---------------------------------------------------------------------------
// ALL_SCOPES
// ---------------------------------------------------------------------------
describe('ALL_SCOPES', () => {
it('contains expected scope strings', () => {
expect(ALL_SCOPES).toContain('trips:read');
expect(ALL_SCOPES).toContain('trips:write');
expect(ALL_SCOPES).toContain('trips:delete');
expect(ALL_SCOPES).toContain('budget:read');
expect(ALL_SCOPES).toContain('budget:write');
expect(ALL_SCOPES).toContain('packing:read');
expect(ALL_SCOPES).toContain('packing:write');
expect(ALL_SCOPES).toContain('collab:read');
expect(ALL_SCOPES).toContain('collab:write');
expect(ALL_SCOPES).toContain('places:read');
expect(ALL_SCOPES).toContain('places:write');
});
it('is a non-empty array', () => {
expect(Array.isArray(ALL_SCOPES)).toBe(true);
expect(ALL_SCOPES.length).toBeGreaterThan(0);
});
});
// ---------------------------------------------------------------------------
// SCOPE_INFO
// ---------------------------------------------------------------------------
describe('SCOPE_INFO', () => {
it('has label, description, and group for trips:read', () => {
const info = SCOPE_INFO['trips:read'];
expect(typeof info.label).toBe('string');
expect(typeof info.description).toBe('string');
expect(typeof info.group).toBe('string');
expect(info.group).toBe('Trips');
});
it('has label, description, and group for budget:write', () => {
const info = SCOPE_INFO['budget:write'];
expect(typeof info.label).toBe('string');
expect(typeof info.description).toBe('string');
expect(info.group).toBe('Budget');
});
it('has label, description, and group for packing:read', () => {
const info = SCOPE_INFO['packing:read'];
expect(info.group).toBe('Packing');
});
it('has an entry for every scope in ALL_SCOPES', () => {
for (const scope of ALL_SCOPES) {
expect(SCOPE_INFO[scope]).toBeDefined();
}
});
});
// ---------------------------------------------------------------------------
// validateScopes
// ---------------------------------------------------------------------------
describe('validateScopes', () => {
it('returns valid=true and empty invalid array for all valid scopes', () => {
const result = validateScopes(['trips:read', 'budget:write']);
expect(result.valid).toBe(true);
expect(result.invalid).toEqual([]);
});
it('returns valid=false and lists invalid scopes', () => {
const result = validateScopes(['trips:read', 'invalid:scope']);
expect(result.valid).toBe(false);
expect(result.invalid).toContain('invalid:scope');
expect(result.invalid).not.toContain('trips:read');
});
it('returns valid=false for completely unknown scopes', () => {
const result = validateScopes(['foo:bar', 'baz:qux']);
expect(result.valid).toBe(false);
expect(result.invalid).toEqual(['foo:bar', 'baz:qux']);
});
it('returns valid=true for empty array', () => {
const result = validateScopes([]);
expect(result.valid).toBe(true);
expect(result.invalid).toEqual([]);
});
});
// ---------------------------------------------------------------------------
// canReadTrips
// ---------------------------------------------------------------------------
describe('canReadTrips', () => {
it('returns true when scopes is null (full access)', () => {
expect(canReadTrips(null)).toBe(true);
});
it('returns true when trips:read is present', () => {
expect(canReadTrips(['trips:read'])).toBe(true);
});
it('returns true when trips:write is present', () => {
expect(canReadTrips(['trips:write'])).toBe(true);
});
it('returns true when trips:delete is present', () => {
expect(canReadTrips(['trips:delete'])).toBe(true);
});
it('returns false when only unrelated scopes are present', () => {
expect(canReadTrips(['budget:read', 'packing:write'])).toBe(false);
});
it('returns false for empty scopes array', () => {
expect(canReadTrips([])).toBe(false);
});
});
// ---------------------------------------------------------------------------
// canWrite
// ---------------------------------------------------------------------------
describe('canWrite', () => {
it('returns true when scopes is null', () => {
expect(canWrite(null, 'trips')).toBe(true);
});
it('returns true when group:write is present', () => {
expect(canWrite(['trips:write'], 'trips')).toBe(true);
expect(canWrite(['budget:write'], 'budget')).toBe(true);
expect(canWrite(['packing:write'], 'packing')).toBe(true);
});
it('returns false when only group:read is present', () => {
expect(canWrite(['trips:read'], 'trips')).toBe(false);
});
it('returns false when a different group write is present', () => {
expect(canWrite(['budget:write'], 'trips')).toBe(false);
});
it('returns false for empty scopes array', () => {
expect(canWrite([], 'trips')).toBe(false);
});
});
// ---------------------------------------------------------------------------
// canRead
// ---------------------------------------------------------------------------
describe('canRead', () => {
it('returns true when scopes is null', () => {
expect(canRead(null, 'budget')).toBe(true);
});
it('returns true when group:read is present', () => {
expect(canRead(['budget:read'], 'budget')).toBe(true);
});
it('returns true when group:write is present (write implies read)', () => {
expect(canRead(['budget:write'], 'budget')).toBe(true);
});
it('returns false when neither read nor write for group is present', () => {
expect(canRead(['trips:read', 'packing:write'], 'budget')).toBe(false);
});
it('returns false for empty scopes array', () => {
expect(canRead([], 'collab')).toBe(false);
});
});
// ---------------------------------------------------------------------------
// canDeleteTrips
// ---------------------------------------------------------------------------
describe('canDeleteTrips', () => {
it('returns true when scopes is null', () => {
expect(canDeleteTrips(null)).toBe(true);
});
it('returns true when trips:delete is present', () => {
expect(canDeleteTrips(['trips:delete'])).toBe(true);
});
it('returns false when only trips:write is present', () => {
expect(canDeleteTrips(['trips:write'])).toBe(false);
});
it('returns false when only trips:read is present', () => {
expect(canDeleteTrips(['trips:read'])).toBe(false);
});
it('returns false for unrelated scopes', () => {
expect(canDeleteTrips(['budget:write', 'packing:read'])).toBe(false);
});
it('returns false for empty scopes array', () => {
expect(canDeleteTrips([])).toBe(false);
});
});