mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
724c88c201
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them.
284 lines
10 KiB
TypeScript
284 lines
10 KiB
TypeScript
/**
|
|
* Unit tests for MCP addon gating and scope enforcement in tools.
|
|
*/
|
|
import { ADDON_IDS } from '../../../src/addons';
|
|
import { db as testDb } from '../../../src/db/database';
|
|
import { createUser, createTrip } from '../../helpers/factories';
|
|
import { FakeRealtimeService } from '../../helpers/fake-realtime';
|
|
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
|
|
import { resetTestDb } from '../../helpers/test-db';
|
|
import { setAddonEnabled } from '../../helpers/test-db';
|
|
|
|
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
|
|
|
vi.mock('../../../src/db/database', async () => {
|
|
const { createSnapshotTestDb, buildDbMock } = await import('../../helpers/db-mock');
|
|
return buildDbMock(createSnapshotTestDb());
|
|
});
|
|
|
|
const realtime = new FakeRealtimeService();
|
|
const broadcastMock = realtime.broadcastMock;
|
|
|
|
beforeEach(() => {
|
|
resetTestDb(testDb);
|
|
broadcastMock.mockClear();
|
|
// The summary reads these three off the injected AddonsService, so the case
|
|
// that wants one off writes the row the admin panel writes. resetTestDb
|
|
// leaves the addons table alone, so each case restates what it needs.
|
|
for (const id of [ADDON_IDS.BUDGET, ADDON_IDS.PACKING, ADDON_IDS.COLLAB]) {
|
|
setAddonEnabled(testDb, id, true);
|
|
}
|
|
});
|
|
|
|
afterAll(() => {
|
|
testDb.close();
|
|
});
|
|
|
|
async function withHarness(userId: number, fn: (h: McpHarness) => Promise<void>, scopes?: string[] | null) {
|
|
const h = await createMcpHarness({ realtime, userId, withResources: false, scopes: scopes ?? null });
|
|
try {
|
|
await fn(h);
|
|
} finally {
|
|
await h.cleanup();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// get_trip_summary — addon gating
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('get_trip_summary — addon gating', () => {
|
|
it('when all addons enabled: packing, budget, collab_notes, todos are present', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id, { title: 'Full Trip' });
|
|
|
|
await withHarness(user.id, async (h) => {
|
|
const result = await h.client.callTool({ name: 'get_trip_summary', arguments: { tripId: trip.id } });
|
|
expect(result.isError).toBeFalsy();
|
|
const data = parseToolResult(result) as any;
|
|
expect(data.packing).toBeDefined();
|
|
expect(data.budget).toBeDefined();
|
|
expect(Array.isArray(data.collab_notes)).toBe(true);
|
|
expect(Array.isArray(data.todos)).toBe(true);
|
|
});
|
|
});
|
|
|
|
it('when budget disabled: budget is undefined in response', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id, { title: 'No Budget Trip' });
|
|
|
|
setAddonEnabled(testDb, ADDON_IDS.BUDGET, false);
|
|
|
|
await withHarness(user.id, async (h) => {
|
|
const result = await h.client.callTool({ name: 'get_trip_summary', arguments: { tripId: trip.id } });
|
|
expect(result.isError).toBeFalsy();
|
|
const data = parseToolResult(result) as any;
|
|
expect(data.budget).toBeUndefined();
|
|
// packing and collab still present
|
|
expect(data.packing).toBeDefined();
|
|
expect(Array.isArray(data.collab_notes)).toBe(true);
|
|
});
|
|
});
|
|
|
|
it('when packing disabled: packing is undefined and todos is empty array', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id, { title: 'No Packing Trip' });
|
|
|
|
setAddonEnabled(testDb, ADDON_IDS.PACKING, false);
|
|
|
|
await withHarness(user.id, async (h) => {
|
|
const result = await h.client.callTool({ name: 'get_trip_summary', arguments: { tripId: trip.id } });
|
|
expect(result.isError).toBeFalsy();
|
|
const data = parseToolResult(result) as any;
|
|
expect(data.packing).toBeUndefined();
|
|
expect(Array.isArray(data.todos)).toBe(true);
|
|
expect(data.todos).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
it('when collab disabled: collab_notes is empty array, pollCount is 0, messageCount is 0', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id, { title: 'No Collab Trip' });
|
|
|
|
setAddonEnabled(testDb, ADDON_IDS.COLLAB, false);
|
|
|
|
await withHarness(user.id, async (h) => {
|
|
const result = await h.client.callTool({ name: 'get_trip_summary', arguments: { tripId: trip.id } });
|
|
expect(result.isError).toBeFalsy();
|
|
const data = parseToolResult(result) as any;
|
|
expect(Array.isArray(data.collab_notes)).toBe(true);
|
|
expect(data.collab_notes).toHaveLength(0);
|
|
expect(data.pollCount).toBe(0);
|
|
expect(data.messageCount).toBe(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Budget tools — addon gating
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Budget tools — addon gating', () => {
|
|
it('when budget addon disabled, create_budget_item is not registered', async () => {
|
|
const { user } = createUser(testDb);
|
|
|
|
setAddonEnabled(testDb, ADDON_IDS.BUDGET, false);
|
|
|
|
await withHarness(user.id, async (h) => {
|
|
const result = await h.client.callTool({
|
|
name: 'create_budget_item',
|
|
arguments: { tripId: 1, name: 'Test', total_price: 100 },
|
|
});
|
|
expect(result.isError).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Packing tools — addon gating
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Packing tools — addon gating', () => {
|
|
it('when packing addon disabled, create_packing_item is not registered', async () => {
|
|
const { user } = createUser(testDb);
|
|
|
|
setAddonEnabled(testDb, ADDON_IDS.PACKING, false);
|
|
|
|
await withHarness(user.id, async (h) => {
|
|
const result = await h.client.callTool({
|
|
name: 'create_packing_item',
|
|
arguments: { tripId: 1, name: 'Sunscreen' },
|
|
});
|
|
expect(result.isError).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Collab tools — addon gating
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Collab tools — addon gating', () => {
|
|
it('when collab addon disabled, create_collab_note is not registered', async () => {
|
|
const { user } = createUser(testDb);
|
|
|
|
setAddonEnabled(testDb, ADDON_IDS.COLLAB, false);
|
|
|
|
await withHarness(user.id, async (h) => {
|
|
const result = await h.client.callTool({
|
|
name: 'create_collab_note',
|
|
arguments: { tripId: 1, title: 'Test Note' },
|
|
});
|
|
expect(result.isError).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Atlas tools — addon gating
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Atlas tools — addon gating', () => {
|
|
it('when atlas addon disabled, mark_country_visited is not registered', async () => {
|
|
const { user } = createUser(testDb);
|
|
|
|
setAddonEnabled(testDb, ADDON_IDS.ATLAS, false);
|
|
|
|
await withHarness(user.id, async (h) => {
|
|
const result = await h.client.callTool({ name: 'mark_country_visited', arguments: { country_code: 'FR' } });
|
|
expect(result.isError).toBe(true);
|
|
});
|
|
});
|
|
|
|
it('when atlas addon disabled, create_bucket_list_item is not registered', async () => {
|
|
const { user } = createUser(testDb);
|
|
|
|
setAddonEnabled(testDb, ADDON_IDS.ATLAS, false);
|
|
|
|
await withHarness(user.id, async (h) => {
|
|
const result = await h.client.callTool({ name: 'create_bucket_list_item', arguments: { name: 'Paris' } });
|
|
expect(result.isError).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Scope enforcement in tools
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Scope enforcement in tools', () => {
|
|
it('with scopes trips:read, create_trip is not registered (write not in scopes)', async () => {
|
|
const { user } = createUser(testDb);
|
|
|
|
await withHarness(
|
|
user.id,
|
|
async (h) => {
|
|
const result = await h.client.callTool({ name: 'create_trip', arguments: { title: 'Should Fail' } });
|
|
expect(result.isError).toBe(true);
|
|
},
|
|
['trips:read'],
|
|
);
|
|
});
|
|
|
|
it('with scopes trips:write, create_trip is registered and works', async () => {
|
|
const { user } = createUser(testDb);
|
|
|
|
await withHarness(
|
|
user.id,
|
|
async (h) => {
|
|
const result = await h.client.callTool({ name: 'create_trip', arguments: { title: 'My Trip' } });
|
|
expect(result.isError).toBeFalsy();
|
|
const data = parseToolResult(result) as any;
|
|
expect(data.trip.title).toBe('My Trip');
|
|
},
|
|
['trips:write'],
|
|
);
|
|
});
|
|
|
|
it('with scopes null (full access), create_trip is registered', async () => {
|
|
const { user } = createUser(testDb);
|
|
|
|
await withHarness(
|
|
user.id,
|
|
async (h) => {
|
|
const result = await h.client.callTool({ name: 'create_trip', arguments: { title: 'Full Access Trip' } });
|
|
expect(result.isError).toBeFalsy();
|
|
},
|
|
null,
|
|
);
|
|
});
|
|
|
|
it('with scopes trips:read, create_budget_item is not registered (budget:write not in scopes)', async () => {
|
|
const { user } = createUser(testDb);
|
|
|
|
await withHarness(
|
|
user.id,
|
|
async (h) => {
|
|
const result = await h.client.callTool({
|
|
name: 'create_budget_item',
|
|
arguments: { tripId: 1, name: 'Hotel', total_price: 200 },
|
|
});
|
|
expect(result.isError).toBe(true);
|
|
},
|
|
['trips:read'],
|
|
);
|
|
});
|
|
|
|
it('with scopes budget:write and trips:read, create_budget_item is registered (budget addon enabled)', async () => {
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id, { title: 'Budget Trip' });
|
|
|
|
await withHarness(
|
|
user.id,
|
|
async (h) => {
|
|
const result = await h.client.callTool({
|
|
name: 'create_budget_item',
|
|
arguments: { tripId: trip.id, name: 'Hotel', total_price: 200 },
|
|
});
|
|
expect(result.isError).toBeFalsy();
|
|
},
|
|
['budget:write', 'trips:read'],
|
|
);
|
|
});
|
|
});
|