mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
cover roadtrip MCP authorization and search filters
This commit is contained in:
@@ -312,3 +312,68 @@ describe('Roadtrip MCP registration and search', () => {
|
||||
expect(service.broadcast).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP trip preferences authorization', () => {
|
||||
it('reads shared preferences and refuses inaccessible trips', async () => {
|
||||
const s = setup();
|
||||
const tool = new RoadtripPreferencesMcp(s.preferences, { isDemoUser: () => false } as never, {} as never, s.db as never, { hasTripPermission: () => true } as never);
|
||||
expect(JSON.stringify(await tool.read({ tripId: 10 }, ctx))).toContain('100');
|
||||
s.db.canAccessTrip.mockReturnValue(false);
|
||||
expect((await tool.read({ tripId: 10 }, ctx)).isError).toBe(true);
|
||||
expect((await tool.update({ tripId: 10, settings: {} }, ctx)).isError).toBe(true);
|
||||
});
|
||||
it('rejects demos and readers, but permits a trip editor', async () => {
|
||||
const s = setup();
|
||||
const auth = { isDemoUser: vi.fn(() => true) };
|
||||
const guards = { hasTripPermission: vi.fn(() => false) };
|
||||
const tool = new RoadtripPreferencesMcp(s.preferences, auth as never, {} as never, s.db as never, guards as never);
|
||||
expect((await tool.update({ tripId: 10, settings: {} }, ctx)).isError).toBe(true);
|
||||
auth.isDemoUser.mockReturnValue(false);
|
||||
expect((await tool.update({ tripId: 10, settings: {} }, ctx)).isError).toBe(true);
|
||||
guards.hasTripPermission.mockReturnValue(true);
|
||||
expect(JSON.stringify(await tool.update({ tripId: 10, settings: { roadtrip_range_km: 120 } }, ctx))).toContain('120');
|
||||
});
|
||||
});
|
||||
describe('corridor filtering', () => {
|
||||
const input = { tripId: 10, dayNumber: 1, category: 'charging' as const, widthKm: 5, offset: 0 };
|
||||
function tool() {
|
||||
const plan = { failures: [] as unknown[], omittedVisits: [] as number[], calculated: { dayWindowIssue: null as string | null, days: [{ dayNumber: 1, geometry: [[48, 10], [48, 10.1]] }] } };
|
||||
const maps = { search: vi.fn(async () => ({ pois: [
|
||||
{ osm_id: 'a', name: 'Fast', lat: 48, lng: 10.03, category: 'charging', charging: { sockets: [{ type: 'type2', kw: 150 }] } },
|
||||
{ osm_id: 'b', name: 'Slow', lat: 48, lng: 10.07, category: 'charging', charging: { sockets: [{ type: 'type2', kw: 11 }] } },
|
||||
{ osm_id: 'c', name: 'Unknown', lat: 48, lng: 10.05, category: 'charging', charging: { sockets: [{ type: 'ccs', kw: null }] } },
|
||||
{ osm_id: 'd', name: 'Far', lat: 50, lng: 12, category: 'charging' },
|
||||
], sources: ['osm'], failedSources: ['plugin'], truncated: false, clamped: false })) };
|
||||
return { plan, maps, mcp: new RoadtripPlanningMcp({ calculate: async () => plan } as never, maps as never, {} as never) };
|
||||
}
|
||||
it('refuses failed routes, absent days and reversed search ranges', async () => {
|
||||
const s = tool();
|
||||
s.plan.failures.push({});
|
||||
expect((await s.mcp.corridor(input, ctx)).isError).toBe(true);
|
||||
s.plan.failures = [];
|
||||
s.plan.omittedVisits = [1];
|
||||
expect((await s.mcp.corridor(input, ctx)).isError).toBe(true);
|
||||
s.plan.omittedVisits = [];
|
||||
s.plan.calculated.dayWindowIssue = 'conflict';
|
||||
expect((await s.mcp.corridor(input, ctx)).isError).toBe(true);
|
||||
s.plan.calculated.dayWindowIssue = null;
|
||||
expect((await s.mcp.corridor({ ...input, fromKm: 8, toKm: 2 }, ctx)).isError).toBe(true);
|
||||
expect((await s.mcp.corridor({ ...input, dayNumber: 2 }, ctx)).isError).toBe(true);
|
||||
expect(s.maps.search).not.toHaveBeenCalled();
|
||||
});
|
||||
it('filters sockets, power, name and along-route bounds without hiding source failures', async () => {
|
||||
const s = tool();
|
||||
const read = async (over: Partial<Parameters<RoadtripPlanningMcp['corridor']>[0]>) => JSON.parse((await s.mcp.corridor({ ...input, ...over }, ctx)).content[0].text as string);
|
||||
expect((await read({ socket: 'type2', minKw: 100 })).hits.map((h: { poi: { name: string } }) => h.poi.name)).toEqual(['Fast']);
|
||||
expect((await read({ name: 'unknown' })).hits).toHaveLength(1);
|
||||
expect((await read({ fromKm: 100 })).hits).toHaveLength(0);
|
||||
expect((await read({ toKm: 0 })).hits).toHaveLength(0);
|
||||
expect((await read({})).failedSources).toEqual(['plugin']);
|
||||
});
|
||||
});
|
||||
it('reads the saved roadtrip context without routing', async () => {
|
||||
const s = setup();
|
||||
const tool = new RoadtripPlanningMcp(s.plans, {} as never, {} as never);
|
||||
expect(JSON.stringify(await tool.context({ tripId: 10 }, ctx))).toContain('Stop 1');
|
||||
expect(s.router.route).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import type { McpContext } from '../../../src/nest-mcp';
|
||||
import { ChargingMcp } from '../../../src/nest/roadtrip/charging.mcp';
|
||||
import { RoadtripHazardsMcp } from '../../../src/nest/roadtrip/roadtrip-hazards.mcp';
|
||||
import { GoogleRouteMcp } from '../../../src/nest/roadtrip/google-route.mcp';
|
||||
import { DayBoundariesMcp } from '../../../src/nest/roadtrip/day-boundaries.mcp';
|
||||
|
||||
const ctx = { userId: 5 } as McpContext;
|
||||
describe('roadtrip read tools', () => {
|
||||
it('checks trip access before querying charging and hazards', async () => {
|
||||
const db = { canAccessTrip: vi.fn(() => false) };
|
||||
const source = { read: vi.fn(async () => ({ sources: ['public'] })) };
|
||||
const charging = new ChargingMcp(source as never, db as never, {} as never);
|
||||
const hazards = new RoadtripHazardsMcp(source as never, db as never, {} as never);
|
||||
expect((await charging.read({ tripId: 10, placeId: 2 }, ctx)).isError).toBe(true);
|
||||
expect((await hazards.read({ tripId: 10 }, ctx)).isError).toBe(true);
|
||||
expect(source.read).not.toHaveBeenCalled();
|
||||
db.canAccessTrip.mockReturnValue(true);
|
||||
expect(JSON.stringify(await charging.read({ tripId: 10, placeId: 2 }, ctx))).toContain('public');
|
||||
expect(source.read).toHaveBeenCalledWith(10, 2);
|
||||
expect(JSON.stringify(await hazards.read({ tripId: 10 }, ctx))).toContain('public');
|
||||
});
|
||||
it('passes reviewed Google stops and the authenticated user to the importer', async () => {
|
||||
const routes = { preview: vi.fn(async () => ({ stops: [] })), import: vi.fn(() => ({ imported: 2 })) };
|
||||
const tool = new GoogleRouteMcp(routes as never, {} as never);
|
||||
await tool.preview({ url: 'https://www.google.com/maps/dir/A/B' });
|
||||
expect(routes.preview).toHaveBeenCalledWith('https://www.google.com/maps/dir/A/B');
|
||||
const input = { tripId: 10, dayId: 1, stops: [{ name: 'A', lat: 1, lng: 2 }] };
|
||||
expect(JSON.stringify(tool.import(input, ctx))).toContain('2');
|
||||
expect(routes.import).toHaveBeenCalledWith(10, 5, input);
|
||||
});
|
||||
it('does not expose manual boundaries to nonmembers', async () => {
|
||||
const boundaries = { list: vi.fn(() => [{ day_number: 1 }]) };
|
||||
const db = { canAccessTrip: vi.fn(() => false) };
|
||||
const tool = new DayBoundariesMcp(boundaries as never, db as never, {} as never, {} as never, {} as never, {} as never);
|
||||
expect((await tool.list({ tripId: 10 }, ctx)).isError).toBe(true);
|
||||
expect(boundaries.list).not.toHaveBeenCalled();
|
||||
db.canAccessTrip.mockReturnValue(true);
|
||||
expect(JSON.stringify(await tool.list({ tripId: 10 }, ctx))).toContain('day_number');
|
||||
expect(boundaries.list).toHaveBeenCalledWith(10);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user