mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
583ba17606
The e2e suites for assignments, places, bookings, trips, the public API, doc sync, addons, transit, to-dos, admin, invite links, school holidays, categories, feeds and plugin search seeded and inspected their database with raw better-sqlite3 statements. They now go through the ORM factories and readers, the way the repositories write and read. Six of them (public API, transit, to-dos, admin, invite links, categories) still built a handful of hand-rolled tables. They move to the migrated snapshot, since an entity reads and writes every mapped column; where the real schema keeps usernames unique, the seeded users get distinct names and keep the ones the assertions read back. The dead canAccessTrip stubs left in two database mocks are dropped: nothing in src imports it from db/database any more.
150 lines
6.6 KiB
TypeScript
150 lines
6.6 KiB
TypeScript
/**
|
|
* GET /api/plugin-search and GET /api/plugin-search/suggest e2e (#2221): the real
|
|
* guard chain and the real contribution module against a temp SQLite db, with only
|
|
* the plugins played by spies.
|
|
*
|
|
* The unit tests pin the parsing and the normalizer. What only a booted container
|
|
* shows is that both routes sit behind JwtAuthGuard, that the static `suggest`
|
|
* segment is its own route rather than a query on the search, and that the typed-ahead
|
|
* route asks exactly the providers whose hook carries `suggest`.
|
|
*/
|
|
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
|
import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe';
|
|
import { PluginContributionsModule } from '../../src/nest/plugins/contributions/plugin-contributions.module';
|
|
import { PluginHooks } from '../../src/nest/plugins/plugin-hooks.service';
|
|
import { RealtimeModule } from '../../src/nest/realtime/realtime.module';
|
|
import { sessionCookie } from './harness';
|
|
import { pluginSuggestResultSchema } from '@trek/shared';
|
|
import { Test } from '@nestjs/testing';
|
|
|
|
import cookieParser from 'cookie-parser';
|
|
import type { Server } from 'http';
|
|
import request from 'supertest';
|
|
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi, type MockInstance } from 'vitest';
|
|
|
|
// Off while the app boots, so the runtime does not go looking for plugins on disk;
|
|
// the tests switch it on.
|
|
const { pluginsEnabled } = vi.hoisted(() => ({ pluginsEnabled: { value: false } }));
|
|
|
|
vi.mock('../../src/db/database', async () => {
|
|
const { createSnapshotTestDb } = await import('../helpers/db-mock');
|
|
const db = createSnapshotTestDb();
|
|
return {
|
|
db,
|
|
canAccessTrip: () => undefined,
|
|
isOwner: () => false,
|
|
getPlaceWithTags: () => null,
|
|
closeDb: () => {},
|
|
reinitialize: () => {},
|
|
};
|
|
});
|
|
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
|
|
vi.mock('../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled: () => pluginsEnabled.value }));
|
|
|
|
import { db } from '../../src/db/database';
|
|
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
|
import { createTestMikroOrmModule, createTestOrm, type TestOrm } from '../helpers/test-orm';
|
|
import { makeUser } from '../helpers/factories/users';
|
|
|
|
let orm: TestOrm;
|
|
|
|
const place = (id: string, name: string) => ({ id, name, lat: 35.66, lng: 139.7, address: 'Shibuya', rating: 4.4 });
|
|
|
|
describe('Plugin search e2e (real guard chain + temp SQLite)', () => {
|
|
let server: Server;
|
|
let app: Awaited<ReturnType<typeof build>>;
|
|
let providersOf: MockInstance;
|
|
let searchPlaces: MockInstance;
|
|
let suggestPlaces: MockInstance;
|
|
|
|
async function build() {
|
|
const moduleRef = await Test.createTestingModule({
|
|
imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), RealtimeModule, PluginContributionsModule],
|
|
}).compile();
|
|
const nest = moduleRef.createNestApplication();
|
|
nest.use(cookieParser());
|
|
nest.useGlobalPipes(new ZodValidationPipe());
|
|
nest.useGlobalFilters(new TrekExceptionFilter());
|
|
await nest.init();
|
|
return nest;
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
// harness.ts's seedUser() omits password_hash, which the migrated schema
|
|
// requires NOT NULL (addons.e2e.test.ts does the same) — the same SeededUser
|
|
// shape sessionCookie(1) needs, with the hash filled in.
|
|
orm = await createTestOrm(db);
|
|
await makeUser(orm, { id: 1, username: 'e2e-user', email: 'e2e@example.test' });
|
|
app = await build();
|
|
const hooks = app.get(PluginHooks, { strict: false });
|
|
providersOf = vi.spyOn(hooks, 'providersOf');
|
|
searchPlaces = vi.spyOn(hooks, 'searchPlaces');
|
|
suggestPlaces = vi.spyOn(hooks, 'suggestPlaces');
|
|
server = app.getHttpServer();
|
|
pluginsEnabled.value = true;
|
|
});
|
|
|
|
beforeEach(() => {
|
|
// Two installed indexes; only the local one takes a request per keystroke.
|
|
providersOf.mockReset().mockImplementation((_hook: string, fn?: string) =>
|
|
fn === 'suggest' ? ['local-index'] : ['local-index', 'remote-api'],
|
|
);
|
|
searchPlaces.mockReset().mockImplementation(async (pluginId: string) => [place('s1', `${pluginId} search`)]);
|
|
suggestPlaces.mockReset().mockResolvedValue([
|
|
place('a', 'Ichiran Shibuya'), place('b', 'Ichiran Harajuku'), place('c', 'Ichiran Ueno'), place('d', 'Ichiran Asakusa'),
|
|
]);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app?.close();
|
|
await orm?.close();
|
|
});
|
|
|
|
const cookie = () => sessionCookie(1);
|
|
|
|
it('refuses a caller without a session on both routes before any plugin is asked', async () => {
|
|
await request(server).get('/api/plugin-search').query({ q: 'ichiran' }).expect(401);
|
|
await request(server).get('/api/plugin-search/suggest').query({ q: 'ichiran' }).expect(401);
|
|
expect(searchPlaces).not.toHaveBeenCalled();
|
|
expect(suggestPlaces).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('suggests from the providers that implement suggest only, three rows at most, in the shared row', async () => {
|
|
const res = await request(server)
|
|
.get('/api/plugin-search/suggest')
|
|
.set('Cookie', cookie())
|
|
.query({ q: 'ichi', lat: '35.66', lng: '139.7', lang: 'ja' })
|
|
.expect(200);
|
|
expect(pluginSuggestResultSchema.parse(res.body).places.map((p) => p.osm_id)).toEqual([
|
|
'plugin:local-index:a', 'plugin:local-index:b', 'plugin:local-index:c',
|
|
]);
|
|
expect(providersOf).toHaveBeenCalledWith('searchProvider', 'suggest');
|
|
expect(suggestPlaces).toHaveBeenCalledTimes(1);
|
|
expect(suggestPlaces).toHaveBeenCalledWith('local-index', { query: 'ichi', limit: 3, lang: 'ja', near: { lat: 35.66, lng: 139.7 } }, 1);
|
|
expect(searchPlaces).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('asks nobody for a single letter', async () => {
|
|
const res = await request(server).get('/api/plugin-search/suggest').set('Cookie', cookie()).query({ q: 'i' }).expect(200);
|
|
expect(res.body).toEqual({ places: [] });
|
|
expect(suggestPlaces).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('keeps the explicit search on every provider, the typed-ahead route beside it', async () => {
|
|
const res = await request(server).get('/api/plugin-search').set('Cookie', cookie()).query({ q: 'ichiran' }).expect(200);
|
|
expect(res.body.places.map((p: { name: string }) => p.name)).toEqual(['local-index search', 'remote-api search']);
|
|
expect(suggestPlaces).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('answers an empty list while the plugin runtime is switched off', async () => {
|
|
pluginsEnabled.value = false;
|
|
try {
|
|
const res = await request(server).get('/api/plugin-search/suggest').set('Cookie', cookie()).query({ q: 'ichiran' }).expect(200);
|
|
expect(res.body).toEqual({ places: [] });
|
|
expect(suggestPlaces).not.toHaveBeenCalled();
|
|
} finally {
|
|
pluginsEnabled.value = true;
|
|
}
|
|
});
|
|
});
|