mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
test(server): the new demo-gate, timestamp, JSON-column and maps tests add no ESLint warnings
Non-null assertions become explicit guards, the two demo-gate loops collect their answers with Promise.all, and the MapsService imports the maps tests no longer use are gone. The src counts the refactors lowered are taken off the warnings baseline.
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"src": {
|
||||
"@typescript-eslint/no-explicit-any": 93,
|
||||
"@typescript-eslint/no-explicit-any": 90,
|
||||
"@typescript-eslint/no-non-null-assertion": 297,
|
||||
"@typescript-eslint/no-require-imports": 18,
|
||||
"@typescript-eslint/no-unsafe-function-type": 1,
|
||||
"@typescript-eslint/no-unused-vars": 43,
|
||||
"no-await-in-loop": 566,
|
||||
"no-console": 237,
|
||||
"@typescript-eslint/no-unused-vars": 36,
|
||||
"no-await-in-loop": 563,
|
||||
"no-console": 236,
|
||||
"no-empty": 6
|
||||
},
|
||||
"suppressed": {
|
||||
|
||||
@@ -10,9 +10,16 @@
|
||||
* so a tool declared `mode: 'write'` but annotated read-only (or the reverse)
|
||||
* fails here instead of slipping past the gate.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach, afterAll } from 'vitest';
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
||||
import { ADDON_IDS } from '../../../src/addons';
|
||||
import { db as testDb } from '../../../src/db/database';
|
||||
import { isDemoGatedTool, trekDemoToolGate } from '../../../src/mcp/nest-mcp-policy';
|
||||
import type { McpContext, McpRegistry, McpRegistryListing } from '../../../src/nest-mcp';
|
||||
import { createUser } from '../../helpers/factories';
|
||||
import { createMcpTestRegistry } from '../../helpers/mcp-test-controllers';
|
||||
import { resetTestDb, setAddonEnabled } from '../../helpers/test-db';
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
||||
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach, afterAll } from 'vitest';
|
||||
|
||||
vi.mock('../../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../../helpers/db-mock');
|
||||
@@ -25,14 +32,10 @@ vi.mock('../../../src/config', () => ({
|
||||
}));
|
||||
vi.mock('../../../src/websocket', () => ({ broadcast: vi.fn() }));
|
||||
|
||||
import { resetTestDb, setAddonEnabled } from '../../helpers/test-db';
|
||||
import { createUser } from '../../helpers/factories';
|
||||
import { createMcpTestRegistry } from '../../helpers/mcp-test-controllers';
|
||||
import { ADDON_IDS } from '../../../src/addons';
|
||||
import { isDemoGatedTool, trekDemoToolGate } from '../../../src/mcp/nest-mcp-policy';
|
||||
import type { McpContext, McpRegistry, McpRegistryListing } from '../../../src/nest-mcp';
|
||||
|
||||
const DEMO_REFUSAL = { content: [{ type: 'text', text: 'Write operations are disabled in demo mode.' }], isError: true };
|
||||
const DEMO_REFUSAL = {
|
||||
content: [{ type: 'text', text: 'Write operations are disabled in demo mode.' }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
interface CapturedTool {
|
||||
annotations?: Record<string, unknown>;
|
||||
@@ -43,7 +46,11 @@ interface CapturedTool {
|
||||
async function captureTools(registry: McpRegistry, ctx: McpContext): Promise<Map<string, CapturedTool>> {
|
||||
const tools = new Map<string, CapturedTool>();
|
||||
const server = {
|
||||
registerTool: (name: string, config: { annotations?: Record<string, unknown> }, cb: (...a: unknown[]) => unknown) => {
|
||||
registerTool: (
|
||||
name: string,
|
||||
config: { annotations?: Record<string, unknown> },
|
||||
cb: (...a: unknown[]) => unknown,
|
||||
) => {
|
||||
// Arguments as a client would send nothing: the gate answers before
|
||||
// the handler ever reads them.
|
||||
tools.set(name, { annotations: config.annotations, call: async (args = {}) => cb(args, {}) });
|
||||
@@ -84,9 +91,8 @@ describe('demo tool gate over the whole registry', () => {
|
||||
|
||||
const writes = [...tools].filter(([, t]) => t.annotations?.readOnlyHint !== true);
|
||||
expect(writes.length).toBeGreaterThan(150);
|
||||
for (const [name, tool] of writes) {
|
||||
expect({ name, result: await tool.call() }).toEqual({ name, result: DEMO_REFUSAL });
|
||||
}
|
||||
const answers = await Promise.all(writes.map(async ([name, tool]) => ({ name, result: await tool.call() })));
|
||||
expect(answers).toEqual(writes.map(([name]) => ({ name, result: DEMO_REFUSAL })));
|
||||
});
|
||||
|
||||
it('DEMOGATE-002: no read-only tool is held back for the demo account', async () => {
|
||||
@@ -98,9 +104,10 @@ describe('demo tool gate over the whole registry', () => {
|
||||
|
||||
const reads = [...tools].filter(([, t]) => t.annotations?.readOnlyHint === true);
|
||||
expect(reads.length).toBeGreaterThan(80);
|
||||
for (const [name, tool] of reads) {
|
||||
expect({ name, refusal: await gate({ name, annotations: tool.annotations }, ctx) }).toEqual({ name, refusal: undefined });
|
||||
}
|
||||
const refusals = await Promise.all(
|
||||
reads.map(async ([name, tool]) => ({ name, refusal: await gate({ name, annotations: tool.annotations }, ctx) })),
|
||||
);
|
||||
expect(refusals).toEqual(reads.map(([name]) => ({ name, refusal: undefined })));
|
||||
});
|
||||
|
||||
it('DEMOGATE-003: an ordinary account reaches the write handlers in demo mode', async () => {
|
||||
@@ -108,7 +115,10 @@ describe('demo tool gate over the whole registry', () => {
|
||||
const { user } = createUser(testDb, { email: 'alice@example.com' });
|
||||
const tools = await captureTools(registry, { userId: user.id, scopes: null, isStaticToken: false });
|
||||
// create_tag has no trip to check and no addon gate: it writes and answers.
|
||||
const result = (await tools.get('create_tag')!.call({ name: 'Mine' })) as { isError?: boolean; content: { text: string }[] };
|
||||
const result = (await tools.get('create_tag')?.call({ name: 'Mine' })) as {
|
||||
isError?: boolean;
|
||||
content: { text: string }[];
|
||||
};
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(result.content[0].text).toContain('Mine');
|
||||
});
|
||||
@@ -116,7 +126,7 @@ describe('demo tool gate over the whole registry', () => {
|
||||
it('DEMOGATE-004: outside demo mode the demo account is an ordinary account', async () => {
|
||||
const { user: demo } = createUser(testDb, { email: 'demo@trek.app' });
|
||||
const tools = await captureTools(registry, { userId: demo.id, scopes: null, isStaticToken: false });
|
||||
const result = (await tools.get('create_tag')!.call({ name: 'Mine' })) as { isError?: boolean };
|
||||
const result = (await tools.get('create_tag')?.call({ name: 'Mine' })) as { isError?: boolean };
|
||||
expect(result.isError).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -153,7 +163,9 @@ describe('trekDemoToolGate', () => {
|
||||
|
||||
it('DEMOGATE-010: refuses a write tool for a demo account and asks with the caller id', async () => {
|
||||
const isDemo = vi.fn(async () => true);
|
||||
expect(await trekDemoToolGate(isDemo)({ name: 'w', annotations: { readOnlyHint: false } }, ctx)).toEqual(DEMO_REFUSAL);
|
||||
expect(await trekDemoToolGate(isDemo)({ name: 'w', annotations: { readOnlyHint: false } }, ctx)).toEqual(
|
||||
DEMO_REFUSAL,
|
||||
);
|
||||
expect(isDemo).toHaveBeenCalledWith(7);
|
||||
});
|
||||
|
||||
|
||||
@@ -238,7 +238,8 @@ describe('updateAccommodation', () => {
|
||||
const before = await linkedReservation(accom.id);
|
||||
await updateRows(await orm(), Reservations, { id: before.id }, { metadata: '{not json' });
|
||||
|
||||
const existing = (await svc.getAccommodation(accom.id, trip.id))!;
|
||||
const existing = await svc.getAccommodation(accom.id, trip.id);
|
||||
if (!existing) throw new Error('accommodation not found');
|
||||
await svc.updateAccommodation(accom.id, existing as Parameters<typeof svc.updateAccommodation>[1], { check_in: '16:00' });
|
||||
|
||||
expect(JSON.parse((await linkedReservation(accom.id)).metadata)).toEqual({ check_in_time: '16:00' });
|
||||
|
||||
@@ -1109,11 +1109,11 @@ describe('resetPassword', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const issued = await svc.requestPasswordReset(user.email, null);
|
||||
const row = await findRow(orm, PasswordResetTokens, { user: user.id });
|
||||
expect(row!.expires_at).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
|
||||
expect(row?.expires_at).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
|
||||
|
||||
const aMinuteAgo = new Date(Date.now() - 60_000).toISOString().slice(0, 19).replace('T', ' ');
|
||||
await updateRows(orm, PasswordResetTokens, { user: user.id }, { expires_at: aMinuteAgo });
|
||||
expect(await svc.resetPassword({ token: issued.tokenForDelivery!, new_password: 'Fresh123!' }))
|
||||
expect(await svc.resetPassword({ token: issued.tokenForDelivery ?? '', new_password: 'Fresh123!' }))
|
||||
.toEqual({ error: 'Reset link has expired. Please request a new one.', status: 400 });
|
||||
});
|
||||
|
||||
|
||||
@@ -225,11 +225,12 @@ describe('votePoll', () => {
|
||||
it('COLLAB-SVC-006b: a poll whose stored options are not JSON lists with no options and refuses every vote', async () => {
|
||||
const { user1, trip } = setup();
|
||||
const poll = await svc.createPoll(trip.id, user1.id, { question: 'Q?', options: ['A', 'B'] });
|
||||
await updateRows(orm, CollabPolls, { id: poll!.id }, { options: '{broken' });
|
||||
if (!poll) throw new Error('poll not created');
|
||||
await updateRows(orm, CollabPolls, { id: poll.id }, { options: '{broken' });
|
||||
|
||||
const [listed] = (await svc.listPolls(trip.id)) as Array<{ options: unknown[] }>;
|
||||
expect(listed.options).toEqual([]);
|
||||
expect((await svc.votePoll(trip.id, poll!.id, user1.id, 0)).error).toBe('invalid_index');
|
||||
expect((await svc.votePoll(trip.id, poll.id, user1.id, 0)).error).toBe('invalid_index');
|
||||
});
|
||||
|
||||
it('COLLAB-SVC-007: returns error "not_found" for nonexistent poll', async () => {
|
||||
|
||||
@@ -2740,9 +2740,9 @@ describe('entry enrichment', () => {
|
||||
|
||||
const names = (rows: Array<{ source_trip_id?: number | null; source_trip_name?: string | null }>) =>
|
||||
rows.map((e) => [e.source_trip_id ?? null, e.source_trip_name]).sort((x, y) => Number(x[0]) - Number(y[0]));
|
||||
const listed = names((await svc.listEntries(journey.id, user.id))!);
|
||||
const listed = names((await svc.listEntries(journey.id, user.id)) ?? []);
|
||||
expect(listed).toEqual([[null, null], [a.id, 'Alps'], [b.id, 'Baltic']].sort((x, y) => Number(x[0]) - Number(y[0])));
|
||||
const full = (await svc.getJourneyFull(journey.id, user.id))! as unknown as { entries: Array<{ source_trip_id?: number | null; source_trip_name?: string | null }> };
|
||||
const full = (await svc.getJourneyFull(journey.id, user.id)) as unknown as { entries: Array<{ source_trip_id?: number | null; source_trip_name?: string | null }> };
|
||||
expect(names(full.entries)).toEqual(listed);
|
||||
});
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@ vi.mock('../../../src/nest/maps/trek-places.client', async (importOriginal) => (
|
||||
trekPlacesSearch: vi.fn(async (): Promise<unknown[]> => []),
|
||||
}));
|
||||
|
||||
import { MapsService } from '../../../src/nest/maps/maps.service';
|
||||
import { buildMapsService } from '../../helpers/maps-service';
|
||||
import { OsmClient } from '../../../src/nest/maps/providers/osm.client';
|
||||
import { trekPlacesSearch } from '../../../src/nest/maps/trek-places.client';
|
||||
|
||||
@@ -17,7 +17,6 @@ vi.mock('../../../src/nest/maps/trek-places.client', async (importOriginal) => (
|
||||
|
||||
vi.mock('../../../src/config', () => ({ JWT_SECRET: 'test-secret', ENCRYPTION_KEY: '0'.repeat(64) }));
|
||||
|
||||
import { MapsService } from '../../../src/nest/maps/maps.service';
|
||||
import { buildMapsService } from '../../helpers/maps-service';
|
||||
import type { PlacePhotoCacheService } from '../../../src/nest/place-photos/place-photo-cache.service';
|
||||
import { noGoogleQuota } from '../../helpers/google-quota';
|
||||
|
||||
@@ -23,7 +23,6 @@ vi.mock('../../../src/nest/maps/trek-places.client', async (importOriginal) => (
|
||||
|
||||
vi.mock('../../../src/config', () => ({ JWT_SECRET: 'test-secret', ENCRYPTION_KEY: '0'.repeat(64) }));
|
||||
|
||||
import { MapsService } from '../../../src/nest/maps/maps.service';
|
||||
import { buildMapsService } from '../../helpers/maps-service';
|
||||
import type { PlacePhotoCacheService } from '../../../src/nest/place-photos/place-photo-cache.service';
|
||||
import { noGoogleQuota } from '../../helpers/google-quota';
|
||||
|
||||
@@ -30,7 +30,6 @@ vi.mock('../../../src/nest/geo/nominatim.client', async (importOriginal) => ({
|
||||
|
||||
vi.mock('../../../src/config', () => ({ JWT_SECRET: 'test-secret', ENCRYPTION_KEY: '0'.repeat(64) }));
|
||||
|
||||
import { MapsService } from '../../../src/nest/maps/maps.service';
|
||||
import { buildMapsService } from '../../helpers/maps-service';
|
||||
import type { PlacePhotoCacheService } from '../../../src/nest/place-photos/place-photo-cache.service';
|
||||
import { noGoogleQuota } from '../../helpers/google-quota';
|
||||
|
||||
@@ -156,6 +156,6 @@ describe('MapsService.getPlaceDetails for a gers: id', () => {
|
||||
await expect(svc.getPlaceDetails(1, 'gers:abc-123')).resolves.toEqual({ place: null });
|
||||
|
||||
expect(mockById).not.toHaveBeenCalled();
|
||||
expect(osmOf.get(svc)!.resolveOsmIdentity).not.toHaveBeenCalled();
|
||||
expect(osmOf.get(svc)?.resolveOsmIdentity).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,7 +129,9 @@ const osmOf = new WeakMap<MapsService, OsmClient>();
|
||||
// Overpass is the one network call this file must never make; stubbing it is
|
||||
// also what turns "dropped through" into something a case can assert.
|
||||
function stubOverpass(svc: MapsService) {
|
||||
return vi.spyOn(osmOf.get(svc)!, 'searchOverpassPois').mockResolvedValue(OVERPASS_ANSWER);
|
||||
const osm = osmOf.get(svc);
|
||||
if (!osm) throw new Error('no OSM client recorded for this service');
|
||||
return vi.spyOn(osm, 'searchOverpassPois').mockResolvedValue(OVERPASS_ANSWER);
|
||||
}
|
||||
|
||||
function rows(n: number) {
|
||||
|
||||
@@ -30,7 +30,6 @@ vi.mock('../../../src/nest/geo/nominatim.client', async (importOriginal) => ({
|
||||
|
||||
vi.mock('../../../src/config', () => ({ JWT_SECRET: 'test-secret', ENCRYPTION_KEY: '0'.repeat(64) }));
|
||||
|
||||
import { MapsService } from '../../../src/nest/maps/maps.service';
|
||||
import { buildMapsService } from '../../helpers/maps-service';
|
||||
import type { PlacePhotoCacheService } from '../../../src/nest/place-photos/place-photo-cache.service';
|
||||
import { noGoogleQuota } from '../../helpers/google-quota';
|
||||
|
||||
@@ -602,16 +602,17 @@ describe('listOAuthSessions + revokeSession', () => {
|
||||
it('stores both expiries in the canonical text and lists them in the ISO spelling the API has always answered', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const created = await makeClient(user.id);
|
||||
const clientId = created.client!.client_id as string;
|
||||
const clientId = created.client?.client_id as string;
|
||||
|
||||
await issueTokens(clientId, user.id, ['trips:read']);
|
||||
const row = await findRow(t, OauthTokens, { user: user.id });
|
||||
expect(row!.access_token_expires_at).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
|
||||
expect(row!.refresh_token_expires_at).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
|
||||
if (!row) throw new Error('token row not stored');
|
||||
expect(row.access_token_expires_at).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
|
||||
expect(row.refresh_token_expires_at).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
|
||||
|
||||
const [session] = await listOAuthSessions(user.id);
|
||||
expect(session.access_token_expires_at).toBe(`${row!.access_token_expires_at.replace(' ', 'T')}.000Z`);
|
||||
expect(session.refresh_token_expires_at).toBe(`${row!.refresh_token_expires_at.replace(' ', 'T')}.000Z`);
|
||||
expect(session.access_token_expires_at).toBe(`${row.access_token_expires_at.replace(' ', 'T')}.000Z`);
|
||||
expect(session.refresh_token_expires_at).toBe(`${row.refresh_token_expires_at.replace(' ', 'T')}.000Z`);
|
||||
const [adminRow] = (await svc.listAllOAuthSessions()) as Array<Record<string, unknown>>;
|
||||
expect(adminRow.access_token_expires_at).toBe(session.access_token_expires_at);
|
||||
});
|
||||
@@ -619,7 +620,7 @@ describe('listOAuthSessions + revokeSession', () => {
|
||||
it('a legacy ISO expiry still lists as stored', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const created = await makeClient(user.id);
|
||||
const clientId = created.client!.client_id as string;
|
||||
const clientId = created.client?.client_id as string;
|
||||
await issueTokens(clientId, user.id, ['trips:read']);
|
||||
await updateRows(t, OauthTokens, { user: user.id }, { access_token_expires_at: '2999-01-02T03:04:05.678Z', refresh_token_expires_at: '2999-01-02T03:04:05.678Z' });
|
||||
|
||||
@@ -1435,7 +1436,7 @@ describe('admin OAuth sessions', () => {
|
||||
it('a malformed consent row counts as consent to nothing, and a new grant replaces it', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const created = await makeClient(user.id);
|
||||
const clientId = created.client!.client_id as string;
|
||||
const clientId = created.client?.client_id as string;
|
||||
await saveConsent(clientId, user.id, ['trips:read']);
|
||||
await updateRows(t, OauthConsents, { user: user.id }, { scopes: '{broken' });
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@ import { TripMembersService } from '../../../src/nest/trip-members/trip-members.
|
||||
import { TripReadModelService } from '../../../src/nest/trip-read-model/trip-read-model.service';
|
||||
import { AccommodationsService } from '../../../src/nest/accommodations/accommodations.service';
|
||||
import { accommodationsOver, makeAccommodationsService } from '../../helpers/accommodations-service';
|
||||
import { MapsService } from '../../../src/nest/maps/maps.service';
|
||||
import { buildMapsService } from '../../helpers/maps-service';
|
||||
import { UnsplashService } from '../../../src/nest/unsplash/unsplash.service';
|
||||
import { PlacePhotoCacheService } from '../../../src/nest/place-photos/place-photo-cache.service';
|
||||
|
||||
Reference in New Issue
Block a user