mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
test(server): suites record broadcasts on a FakeRealtimeService instead of mocking src/websocket
A hand-built service gets a FakeRealtimeService where it took a RealtimeService, a Nest testing module overrides the provider, and a suite over buildApp() spies on the container's instance (spyOnRealtime). The 167 vi.mock factories on src/websocket are gone, RealtimeService reads the socket registry in ws-state.ts directly, and the re-export stub src/websocket.ts is deleted. The facade's own suite now checks what reaches a socket instead of which module function it called.
This commit is contained in:
@@ -85,7 +85,7 @@ Nest owns everything: every domain is a DI module under `server/src/nest/<domain
|
||||
- **Auth is default-deny**: global guards run as `APP_GUARD`s; public routes opt out with `@Public()`, and a boot-time ratchet (`validate-route-guards.ts`) refuses any public route not on its allow-list.
|
||||
- **Trip-scoped routes** verify trip access (404) and the permission (403) — `@UseGuards(JwtAuthGuard, TripAccessGuard)` + `@RequirePermission('<action>')` per handler — and forward `X-Socket-Id` to the broadcast so the originating client doesn't echo its own change. **Never gate a multipart upload with a guard** (the client sees ECONNRESET instead of 403 — check in the handler).
|
||||
- **DB** (`server/src/db/`): `better-sqlite3`, WAL, FK on. MikroORM's migrations (`db/migrations/`) are the only schema source, **including for tests** — the hand-kept `schema.ts`/`migrations.ts`/`seeds.ts` test builders are gone. Seeding is MikroORM's too (`db/seeders/`), applied at boot from `buildApp()`; the ORM shares the one better-sqlite3 connection (bound in `server/src/db/orm-driver.ts`) rather than opening its own. Queries go through repositories (`server/src/db/repositories/`) and `UnitOfWork` (`server/src/nest/database/unit-of-work.ts`) for transactions — there is no `DatabaseService`. `better-sqlite3` itself is ESLint-restricted to `src/db/` plus a short, named allow-list (`server/eslint.config.mjs`), so a domain reaching for the driver directly fails lint. `NODE_ENV=test` gives each vitest worker an isolated `:memory:` DB.
|
||||
- **Realtime**: `/ws` is a Nest gateway in `server/src/nest/realtime/` — inject `RealtimeService`; `server/src/websocket.ts` is a legacy re-export stub.
|
||||
- **Realtime**: `/ws` is a Nest gateway in `server/src/nest/realtime/` — inject `RealtimeService`. Tests observe broadcasts through a `FakeRealtimeService` (`server/tests/helpers/fake-realtime.ts`), never by mocking the transport module.
|
||||
- **MCP**: tools are declared in `<domain>.mcp.ts` files beside each module, on the `server/src/nest-mcp/` decorator layer. A validation or permission change on a REST route must land in the parallel MCP tool in the same change. See `MCP.md`.
|
||||
- **Addons** are admin-toggleable feature modules keyed by `ADDON_IDS` in `server/src/addons.ts`.
|
||||
- **Plugins** (`server/src/nest/plugins/`): one forked child process per plugin, permission-gated RPC (a handler is registered only if the plugin holds the unlocking permission), install-time manifest/signature/egress/version-range gates. The protocol tables are **generated** into `plugin-sdk/` and `shared/` by `gen:plugin-facts`; `check:plugin-facts` is the CI gate. Author tooling is `plugin-sdk/`; the `trek-plugin-dev` skill covers authoring.
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ Single test: `npx vitest run tests/unit/nest/weather.controller.test.ts`, or `np
|
||||
Nest owns everything. Every domain is a DI module under `src/nest/<domain>/` (`controller` + `service` + `module`, registered in `src/nest/app.module.ts`); business logic lives in the injectable `.service.ts`, pure halves in `.helpers.ts`, SQL in a `.repository.ts` where one exists. There is no legacy service layer — ESLint errors on any `services/` import. **`weather/` is the reference module shape; `collections.controller.ts` is the reference for body validation and enumeration-safe access checks.**
|
||||
|
||||
- **`src/bootstrap.ts` — `buildApp()`** is the single builder for production and the test harness. Everything on the raw Express instance (global middleware, uploads, discovery metadata, statics, the named `jsonParser`/`urlencodedParser` wrappers) is registered **before `app.init()`**, because Nest throws `NotFoundException` on unmatched routes and nothing after init is reachable. The wrappers exempt `/mcp` so the MCP SDK reads the raw stream — **never `@Body()` a `/mcp` route**. Don't add routes to that pre-init shell; it bypasses every global guard, interceptor and filter.
|
||||
- **Realtime**: `/ws` is a Nest gateway in `src/nest/realtime/`. Inject `RealtimeService`; `src/websocket.ts` is a re-export stub for old call sites. Trip mutations forward `X-Socket-Id` so the originating client doesn't echo its own change.
|
||||
- **Realtime**: `/ws` is a Nest gateway in `src/nest/realtime/`. Inject `RealtimeService`; tests hand in a `FakeRealtimeService` (`tests/helpers/fake-realtime.ts`) or spy on the container's instance instead of mocking the transport module. Trip mutations forward `X-Socket-Id` so the originating client doesn't echo its own change.
|
||||
- **MCP**: `src/nest-mcp/` is the decorator/registry layer (`@McpController`/`@Tool`/`@Resource`/`@Prompt`; see its `README.md`); tools live in `<domain>.mcp.ts` files beside each module; the HTTP transport is `src/nest/mcp-transport/`; `src/mcp/` holds process-wide state only (scopes, session manager, boot policy). Every tool invocation writes an audit row through the registry's `onInvoke` seam. The demo-mode write block is the registry's `toolGate` (`trekDemoToolGate`): every tool not annotated read-only refuses a demo account there, so a tool never checks demo mode itself (ESLint refuses it in `*.mcp.ts`). MCP tools are adapters over the same services and permission checks as REST — a change on one side lands on the other in the same change.
|
||||
- **Scheduling**: `src/nest/scheduling/` (`CronRegistrarService`) is the one way to schedule a cron. Jobs are `*.job.ts` providers in their owning domain. The registrar refuses to schedule under `NODE_ENV=test` (an integration test pins it). **Never use a `@Cron` decorator**: it bypasses that gate. Every tick first takes the job's row in `scheduler_leases` (`SchedulerLeasesRepository.acquire`, one conditional upsert that decides the race), renews it while it runs and keeps it for a short settle window afterwards, so two processes on one database never run the same tick; a job needs nothing of its own for this. Claims on individual rows (the reminder jobs' `claimReminder`) stay where they are: the lease decides who runs a tick, the claim what a tick may send.
|
||||
- **Plugins** (`src/nest/plugins/`): the child-process runtime (`supervisor/`, `runtime/plugin-host-entry.ts`, `protocol/envelope.ts`) is **intentionally not Nest**; it is the security boundary. `host/rpc-host.ts` registers an RPC method at spawn only if the plugin holds the unlocking permission (registration = authorization). Install-time gates live in `install/`; the manifest's `trek` version range fails closed, and `TREK_PLUGINS_IGNORE_TREK_RANGE` downgrades that to a warning. The protocol tables in `protocol/envelope.ts` (+ `src/plugin-event-sink.ts`) are **generated** into `plugin-sdk/` and `shared/`: run `gen:plugin-facts` after touching them and commit the output. What a plugin gets back is `protocol/output-contract.ts`: every wire method has an entry, and an entity result (`trips.getById`, `todos.list`, `vacay.mine`, ...) is cut down to its published fields in `rpc-host.ts` on the way out, together with every child row it carries whose field is listed in `PLUGIN_ENTITY_NESTED` (a day's `notes_items`, a reservation's `endpoints`). A new column on any whole-row table behind those entities, child tables included, fails `tests/unit/plugins/output-contract.schema.test.ts` until it is listed as published or withheld (the SDK's `PLUGIN_ENTITY_FIELDS` and `PLUGIN_ENTITY_NESTED` are generated from it); a method left as `readModel` must return a value built field by field from named columns. A plugin's own SQL (`host/plugin-data.service.ts`) is held to a row cap, and `query`, `tx` and an `exec` script to a time budget checked between rows and statements (`migrate` is deliberately not timed). `runtime/egress-policy.ts` is a hand-kept mirror guarded by a parity test in `plugin-sdk/`; change both sides together.
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { TripEventSink, TripWriter } from '../common/trip-writer';
|
||||
* The impure MCP tool guards that used to live as plain functions in
|
||||
* src/mcp/tools/_shared.ts, reaching the db Proxy, a permissions.bridge
|
||||
* instance (no longer exists — see auth.service.ts:116) and the
|
||||
* src/websocket stub as module globals. The @McpController domain classes are
|
||||
* websocket broadcast functions as module globals. The @McpController domain classes are
|
||||
* ordinary Nest providers, so they inject this instead. The pure result
|
||||
* helpers (noAccess/permissionDenied/adminRequired and the
|
||||
* src/nest-mcp re-exports) stay in _shared.ts — they carry no dependencies.
|
||||
@@ -50,8 +50,8 @@ export class McpToolGuardsService {
|
||||
try {
|
||||
// Legacy MCP call sites pass event names as plain strings; route through
|
||||
// the facade's implementation signature (its typed overloads are for new
|
||||
// call sites). Runtime is a pure pass-through to src/websocket's
|
||||
// broadcast, so the 100+ per-file vi.mock stubs keep intercepting.
|
||||
// call sites). Runtime is a pure pass-through to the transport's
|
||||
// broadcast.
|
||||
const send = this.realtime.broadcast as (
|
||||
t: number | string, e: string, p: Record<string, unknown>, sid?: number | string, uid?: number,
|
||||
) => void;
|
||||
|
||||
@@ -33,7 +33,7 @@ import type { JourneyPhotosRepository } from '../../db/repositories/JourneyPhoto
|
||||
*
|
||||
* File/test renamed to match (Plan 4 Task 8a — 3e Task 6 renamed only the
|
||||
* class). The old path, `trek-photos.repository.ts`, was kept as a thin
|
||||
* re-export stub (the same `src/websocket.ts` shape) while its ~20
|
||||
* re-export stub while its ~20
|
||||
* importers sat in sibling tasks' exclusively-owned windows on this shared
|
||||
* branch; Plan 4 Task 4 repointed every one of them at this file directly
|
||||
* and deleted the stub.
|
||||
|
||||
@@ -47,7 +47,7 @@ const HEARTBEAT_INTERVAL = 30_000;
|
||||
* What did NOT move is the socket registry. It stays one process-wide
|
||||
* instance in ws-state.ts (the RoomRegistry port, installed in `roomsSlot` by
|
||||
* RealtimeGatewayModule) because out-of-container code (the no-Nest test
|
||||
* harnesses, the vi.mock'd src/websocket seam) must see the same rooms; see
|
||||
* harnesses, hand-built RealtimeService instances) must see the same rooms; see
|
||||
* the note there.
|
||||
*
|
||||
* The wire protocol is unchanged, down to the frame names. TrekWsAdapter maps
|
||||
|
||||
@@ -5,31 +5,28 @@ import type {
|
||||
TrekWsTripEventName,
|
||||
TrekWsUserEventName,
|
||||
} from '@trek/shared';
|
||||
import { broadcast, broadcastToUser, getOnlineUserIds } from '../../websocket';
|
||||
import { broadcast, broadcastToUser, getOnlineUserIds } from './ws-state';
|
||||
|
||||
/**
|
||||
* Injectable facade over the websocket module singleton (roadmap Phase 0 item 3).
|
||||
* Injectable facade over the socket registry in ws-state.ts.
|
||||
*
|
||||
* Migrated Nest services inject this instead of importing `broadcast`/
|
||||
* `broadcastToUser` module globals. The actual transport (rooms, auth,
|
||||
* heartbeat, rate limiting) stays in src/websocket.ts — a future
|
||||
* @WebSocketGateway swap happens behind this facade, invisibly.
|
||||
* Nest services inject this instead of importing the `broadcast`/
|
||||
* `broadcastToUser` module functions. The transport itself (rooms, the
|
||||
* handshake, the wire protocol) is RealtimeGateway, TrekWsAdapter and
|
||||
* ws-state.ts next door.
|
||||
*
|
||||
* Both methods delegate to the live module exports *at call time* (same
|
||||
* pattern as DatabaseService.canAccessTrip): tests vi.mock src/websocket
|
||||
* per file, and the stubs must keep flowing through here. Never capture
|
||||
* the functions at construction, and never dereference an export the
|
||||
* method wasn't asked for — many test mocks provide `broadcast` only.
|
||||
* Tests observe broadcasts through DI: a FakeRealtimeService
|
||||
* (tests/helpers/fake-realtime.ts) takes this class's place, so nothing has to
|
||||
* mock the transport module.
|
||||
*
|
||||
* The service is deliberately dependency-free so out-of-container code (the
|
||||
* auth.bridge graph, the no-Nest test harnesses) can construct it with a bare
|
||||
* `new RealtimeService()`.
|
||||
* no-Nest test harnesses) can construct it with a bare `new RealtimeService()`.
|
||||
*/
|
||||
@Injectable()
|
||||
export class RealtimeService {
|
||||
/**
|
||||
* Broadcast an event to all sockets in a trip room. Mirrors the
|
||||
* websocket.ts signature exactly: `excludeSid` is the X-Socket-Id
|
||||
* ws-state.ts signature exactly: `excludeSid` is the X-Socket-Id
|
||||
* echo-suppression contract (the originating client is skipped);
|
||||
* `onlyUserId` narrows delivery to one user's sockets (#858 private
|
||||
* packing items).
|
||||
@@ -40,9 +37,8 @@ export class RealtimeService {
|
||||
* ships silence. The `plugin:` overload is the deliberate escape hatch for
|
||||
* the host's force-namespaced `plugin:<id>:<event>` broadcasts.
|
||||
*
|
||||
* Rest-spread keeps the caller's exact argument arity — dozens of test
|
||||
* mocks assert the precise call shape (3, 4 or 5 args), so the facade
|
||||
* must not pad omitted optionals with explicit `undefined`s.
|
||||
* Rest-spread keeps the caller's exact argument arity: the facade must not
|
||||
* pad omitted optionals with explicit `undefined`s.
|
||||
*/
|
||||
broadcast<E extends TrekWsTripEventName>(
|
||||
tripId: number | string,
|
||||
@@ -94,12 +90,11 @@ export class RealtimeService {
|
||||
/**
|
||||
* Which users currently hold an open socket.
|
||||
*
|
||||
* AdminService read this through a lazy `require('../../websocket')` inside a
|
||||
* AdminService read this through a lazy `require()` of the transport inside a
|
||||
* try/catch, which is what the facade exists to avoid. Deliberately NOT
|
||||
* guarded here: a guard on the facade would swallow the failure for every
|
||||
* future caller, and the one caller that wants a degraded answer rather than
|
||||
* an error keeps its own catch. Delegates at call time, like its two
|
||||
* siblings, so per-file vi.mock stubs keep flowing through.
|
||||
* an error keeps its own catch.
|
||||
*/
|
||||
getOnlineUserIds(): Set<number> {
|
||||
return getOnlineUserIds();
|
||||
|
||||
@@ -9,11 +9,10 @@ import { ProcessStoreSlot } from '../common/process-store-slot';
|
||||
*
|
||||
* MODULE state, not provider state, and that is load-bearing: the rooms live
|
||||
* in the registry `roomsSlot` holds, which the container's provider replaces
|
||||
* when RealtimeGatewayModule is built and nothing else does. The no-Nest test
|
||||
* harnesses hand-build RealtimeService instances (mcp-test-controllers.ts and
|
||||
* ~60 unit suites), and the 115 vi.mock('src/websocket') seams assert on these
|
||||
* exact exports. If the rooms lived on a provider instance, an out-of-container
|
||||
* broadcast would go to an empty map: no error, no log, just a client that
|
||||
* when RealtimeGatewayModule is built and nothing else does. Hand-built
|
||||
* RealtimeService instances (the no-Nest test harnesses, out-of-container
|
||||
* consumers) deliver through these same functions. If the rooms lived on a
|
||||
* provider instance, an out-of-container broadcast would go to an empty map: no error, no log, just a client that
|
||||
* stops updating. Same reasoning as the geo throttle cursor and the
|
||||
* permissions cache.
|
||||
*
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* The transport's public surface, for code outside the Nest container.
|
||||
*
|
||||
* The implementation moved to nest/realtime: the connection lifecycle and the
|
||||
* handshake are RealtimeGateway, the wire protocol is TrekWsAdapter, and the
|
||||
* socket registry is ws-state.ts. This file stays because it is the import path
|
||||
* the rest of the tree knows, including the more than 160 test files that `vi.mock` it to
|
||||
* assert on broadcasts. Repointing them all would be a large diff that proves
|
||||
* nothing, and this re-export keeps every one of those mocks working.
|
||||
*
|
||||
* `setupWebSocket` is gone: the server is created by the adapter now, when Nest
|
||||
* initialises the gateway, rather than by index.ts after listen().
|
||||
*/
|
||||
export { broadcast, broadcastToUser, getOnlineUserIds } from './nest/realtime/ws-state';
|
||||
@@ -10,8 +10,8 @@
|
||||
* to resolve a hidden inverse relation this DDL never created — the same
|
||||
* class of failure `days.e2e.test.ts`'s own conversion (Task 2) fixed for
|
||||
* that file. AssignmentsService runs its real SQL (DI-injected, no service
|
||||
* mock); journeyService, the permission check and the WebSocket broadcast
|
||||
* stay mocked. Every `it(...)` body below is unchanged from before this
|
||||
* mock); journeyService and the permission check stay mocked, and the
|
||||
* broadcast goes to a FakeRealtimeService. Every `it(...)` body below is unchanged from before this
|
||||
* conversion — only the DB bootstrap changed.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi, type MockInstance } from 'vitest';
|
||||
@@ -19,6 +19,7 @@ import request from 'supertest';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import type { Server } from 'http';
|
||||
import { RealtimeModule } from '../../src/nest/realtime/realtime.module';
|
||||
import { RealtimeService } from '../../src/nest/realtime/realtime.service';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { sessionCookie } from './harness';
|
||||
|
||||
@@ -26,8 +27,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
const { broadcast } = vi.hoisted(() => ({ broadcast: vi.fn() }));
|
||||
vi.mock('../../src/websocket', () => ({ broadcast }));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
|
||||
@@ -55,6 +54,10 @@ import { Days } from '../../src/db/entities/Days.entity';
|
||||
import { Places } from '../../src/db/entities/Places.entity';
|
||||
import { Tours } from '../../src/db/entities/Tours.entity';
|
||||
import { Trips } from '../../src/db/entities/Trips.entity';
|
||||
import { FakeRealtimeService } from '../helpers/fake-realtime';
|
||||
|
||||
const realtime = new FakeRealtimeService();
|
||||
const broadcast = realtime.broadcastMock;
|
||||
|
||||
let orm: TestOrm;
|
||||
|
||||
@@ -78,6 +81,8 @@ describe('Assignments e2e (real auth guard + temp SQLite)', () => {
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), RealtimeModule, AssignmentsModule] })
|
||||
.overrideProvider(RealtimeService)
|
||||
.useValue(realtime)
|
||||
.overrideProvider(JourneyDomainService)
|
||||
.useValue({ reconcileTripSkeletons })
|
||||
.compile();
|
||||
|
||||
@@ -27,8 +27,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../src/websocket', () => ({ broadcastToUser: vi.fn(), broadcast: vi.fn() }));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
import { createUser, createTrip } from '../helpers/factories';
|
||||
import { AtlasModule } from '../../src/nest/atlas/atlas.module';
|
||||
|
||||
@@ -27,7 +27,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../src/websocket', () => ({ broadcastToUser: vi.fn(), broadcast: vi.fn() }));
|
||||
// The audit domain is DI-native: writeAudit runs for real against the temp
|
||||
// db's audit_log table; only the file logger is silenced.
|
||||
vi.mock('../../src/nest/audit/audit-log.logger', () => ({ LOG_LEVEL: 'error', logInfo: vi.fn(), logDebug: vi.fn(), logError: vi.fn(), logWarn: vi.fn() }));
|
||||
|
||||
@@ -35,7 +35,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
reinitialize: () => {},
|
||||
};
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
vi.mock('../../src/utils/ssrfGuard', async (orig) => ({ ...(await orig<Record<string, unknown>>()), safeFetchLlm }));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Budget module e2e — exercises the migrated /api/trips/:tripId/budget endpoints
|
||||
* through the real JwtAuthGuard against a temp SQLite db carrying the full real,
|
||||
* migrated schema (createSnapshotTestDb), so the folded BudgetService runs its
|
||||
* real SQL. Only the db singleton (trip access) and the WebSocket broadcast are
|
||||
* mocked; the permission check is a spy on the container's PermissionsService.
|
||||
* real SQL. Only the db singleton (trip access) is mocked, and broadcasts reach
|
||||
* the real transport, which has no sockets here; the permission check is a spy on the container's PermissionsService.
|
||||
* ReservationsModule is mounted beside it because an expense can be linked to a
|
||||
* booking (#2084), and deleting that booking has to take the expense with it.
|
||||
*/
|
||||
@@ -21,7 +21,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
// Trip access reads through TripsRepository now; the module only hands out the handle.
|
||||
return { db, closeDb: () => {}, reinitialize: () => {} };
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
|
||||
|
||||
import { PermissionsService } from '../../src/nest/permissions/permissions.service';
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Collab module e2e — exercises the migrated /api/trips/:tripId/collab endpoints
|
||||
* through the real JwtAuthGuard against a real migrated temp SQLite db
|
||||
* (createSnapshotTestDb(), no hand-rolled CREATE TABLEs), running CollabService's
|
||||
* real queries (DI-injected, no service mock). The permission check, the
|
||||
* WebSocket broadcast and the chat/note notification are mocked; this focuses
|
||||
* real queries (DI-injected, no service mock). The permission check and
|
||||
* the chat/note notification are mocked; this focuses
|
||||
* on auth, trip-access 404, permission 403, the create-201 status codes, the
|
||||
* vote/react 200 overrides and the persisted rows. Rows are seeded and read
|
||||
* through the factories in tests/helpers/factories.
|
||||
@@ -20,7 +20,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
|
||||
|
||||
import { PermissionsService } from '../../src/nest/permissions/permissions.service';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Collections e2e — drives /api/addons/collections through the REAL JwtAuthGuard
|
||||
* AND the real DI-native CollectionsService (DatabaseModule + RealtimeModule +
|
||||
* CollectionsModule) against a temp SQLite db (full schema). Only the addon
|
||||
* flag, websocket and notification send are mocked. Covers: the addon gate
|
||||
* flag and notification send are mocked. Covers: the addon gate
|
||||
* (404 before auth), auth, CRUD happy paths, invite/accept/decline, copy-to-trip,
|
||||
* cross-user 404s, the non-owner 403 on /:id/available-users (no enumeration), and
|
||||
* a list out as GPX and back in through the reader and the import (#2301).
|
||||
@@ -27,7 +27,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
});
|
||||
|
||||
const { isAddonEnabled } = vi.hoisted(() => ({ isAddonEnabled: vi.fn(() => true) }));
|
||||
vi.mock('../../src/websocket', () => ({ broadcastToUser: vi.fn(), broadcast: vi.fn() }));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
import { createUser, createTrip, createCategory, createDay, createPlace, createDayAssignment } from '../helpers/factories';
|
||||
|
||||
@@ -37,7 +37,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
});
|
||||
|
||||
const { isAddonEnabled } = vi.hoisted(() => ({ isAddonEnabled: vi.fn(() => true) }));
|
||||
vi.mock('../../src/websocket', () => ({ broadcastToUser: vi.fn(), broadcast: vi.fn() }));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
import { createUser } from '../helpers/factories';
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* `canAccessTrip` mock (the legacy override this file used to export from
|
||||
* `db/database.ts` — deleted there since Plan 3c Task 0b; a stale mock here
|
||||
* would silently do nothing, not fail loudly, which is worse than removing
|
||||
* it). Only the permission check and the WebSocket broadcast stay mocked.
|
||||
* it). Only the permission check stays mocked.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi, type MockInstance } from 'vitest';
|
||||
import request from 'supertest';
|
||||
@@ -29,7 +29,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
import { PermissionsService } from '../../src/nest/permissions/permissions.service';
|
||||
|
||||
@@ -37,7 +37,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
});
|
||||
|
||||
const { isAddonEnabled } = vi.hoisted(() => ({ isAddonEnabled: vi.fn(() => true) }));
|
||||
vi.mock('../../src/websocket', () => ({ broadcastToUser: vi.fn(), broadcast: vi.fn() }));
|
||||
|
||||
/**
|
||||
* The SSRF guard resolves DNS, and `paperless.example.com` does not exist. In
|
||||
|
||||
@@ -29,7 +29,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
import { resetTestDb } from '../helpers/test-db';
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Files + photos e2e — exercises the migrated /api/trips/:tripId/files and
|
||||
* /api/photos endpoints through the real JwtAuthGuard against a temp SQLite db.
|
||||
* FilesService is DI-native (no service mock): the file rows live in the temp
|
||||
* db and the SQL runs for real; only canAccessTrip, the permission check, the
|
||||
* photo services and the broadcast are mocked. Focuses on auth (incl. the
|
||||
* db and the SQL runs for real; only canAccessTrip, the permission check and the
|
||||
* photo services are mocked. Focuses on auth (incl. the
|
||||
* unguarded download's own token auth), trip-access 404, permission 403, the
|
||||
* photo id/access guards and status codes.
|
||||
*/
|
||||
@@ -56,7 +56,6 @@ const { canAccessTrip } = vi.hoisted(() => ({ canAccessTrip: vi.fn() }));
|
||||
vi.mock('../../src/db/database', () => ({
|
||||
db, canAccessTrip, getPlaceWithTags: vi.fn(), closeDb: () => {}, reinitialize: () => {},
|
||||
}));
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
|
||||
vi.mock('../../src/nest/common/demo', () => ({ isDemoEmail: vi.fn(() => false) }));
|
||||
|
||||
import { PermissionsService } from '../../src/nest/permissions/permissions.service';
|
||||
|
||||
@@ -27,7 +27,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
isOwner: () => false,
|
||||
};
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcastToUser: vi.fn(), broadcast: vi.fn() }));
|
||||
vi.mock('../../src/nest/audit/audit-log.logger', () => ({ LOG_LEVEL: 'error', logInfo: vi.fn(), logDebug: vi.fn(), logError: vi.fn(), logWarn: vi.fn() }));
|
||||
vi.mock('../../src/app-config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../src/app-config')>();
|
||||
|
||||
@@ -30,7 +30,6 @@ const { db } = vi.hoisted(() => {
|
||||
});
|
||||
|
||||
vi.mock('../../src/db/database', () => ({ db, canAccessTrip: vi.fn(), closeDb: () => {}, reinitialize: () => {} }));
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
|
||||
|
||||
// Provider services — fully mocked. fail/success/canAccessUserPhoto from the
|
||||
// helper module are kept real except canAccessUserPhoto which we override.
|
||||
|
||||
@@ -40,7 +40,6 @@ vi.mock('../../src/db/database', () => ({
|
||||
canAccessTrip: () => undefined,
|
||||
isOwner: () => false,
|
||||
}));
|
||||
vi.mock('../../src/websocket', () => ({ broadcastToUser: vi.fn(), broadcast: vi.fn() }));
|
||||
vi.mock('../../src/nest/audit/audit-log.logger', () => ({ LOG_LEVEL: 'error', logInfo: vi.fn(), logDebug: vi.fn(), logError: vi.fn(), logWarn: vi.fn() }));
|
||||
|
||||
const toggles = { oidc_login: true };
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* endpoints through the real JwtAuthGuard against a migrated temp SQLite db.
|
||||
* PackingService runs its real SQL via DatabaseModule (the DATABASE_CONNECTION
|
||||
* factory picks up the mocked db singleton); trip access resolves through the
|
||||
* real repositories over the temp db. Only the permission check, the
|
||||
* WebSocket broadcast and the notification sender stay mocked.
|
||||
* real repositories over the temp db. Only the permission check and
|
||||
* the notification sender stay mocked.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi, type MockInstance } from 'vitest';
|
||||
import request from 'supertest';
|
||||
@@ -17,7 +17,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
|
||||
|
||||
import { PermissionsService } from '../../src/nest/permissions/permissions.service';
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
* uses — the same class of failure `days.e2e.test.ts` (Task 2) and
|
||||
* `assignments.e2e.test.ts` (Task 3) already fixed for their own files.
|
||||
* PlacesService runs its real SQL (DI-injected, no service mock);
|
||||
* journeyService, the permission check and the WebSocket broadcast stay
|
||||
* mocked. Every `it(...)` body below is unchanged from before this
|
||||
* journeyService and the permission check stay mocked, and the broadcast
|
||||
* goes to a FakeRealtimeService. Every `it(...)` body below is unchanged from before this
|
||||
* conversion — only the DB bootstrap changed.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi, type MockInstance } from 'vitest';
|
||||
@@ -19,6 +19,7 @@ import request from 'supertest';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import type { Server } from 'http';
|
||||
import { RealtimeModule } from '../../src/nest/realtime/realtime.module';
|
||||
import { RealtimeService } from '../../src/nest/realtime/realtime.service';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { sessionCookie } from './harness';
|
||||
|
||||
@@ -26,8 +27,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
const { broadcast } = vi.hoisted(() => ({ broadcast: vi.fn() }));
|
||||
vi.mock('../../src/websocket', () => ({ broadcast }));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
|
||||
@@ -60,6 +59,10 @@ import { PlaceRatings } from '../../src/db/entities/PlaceRatings.entity';
|
||||
import { Places } from '../../src/db/entities/Places.entity';
|
||||
import { Tours } from '../../src/db/entities/Tours.entity';
|
||||
import { Trips } from '../../src/db/entities/Trips.entity';
|
||||
import { FakeRealtimeService } from '../helpers/fake-realtime';
|
||||
|
||||
const realtime = new FakeRealtimeService();
|
||||
const broadcast = realtime.broadcastMock;
|
||||
|
||||
let orm: TestOrm;
|
||||
|
||||
@@ -69,6 +72,8 @@ describe('Places e2e (real auth guard + temp SQLite)', () => {
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), RealtimeModule, PlacesModule] })
|
||||
.overrideProvider(RealtimeService)
|
||||
.useValue(realtime)
|
||||
.overrideProvider(JourneyDomainService)
|
||||
.useValue({ onPlaceCreated, onPlaceUpdated, onPlaceDeleted })
|
||||
.compile();
|
||||
|
||||
@@ -37,7 +37,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
reinitialize: () => {},
|
||||
};
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
|
||||
vi.mock('../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled: () => pluginsEnabled.value }));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
|
||||
@@ -38,7 +38,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
reinitialize: () => {},
|
||||
};
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
|
||||
vi.mock('../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled: () => pluginsEnabled.value }));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
|
||||
@@ -23,7 +23,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const tmp = createSnapshotTestDb();
|
||||
return { db: tmp, canAccessTrip: () => undefined, isOwner: () => false, getPlaceWithTags: () => null, closeDb: () => {}, reinitialize: () => {} };
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
vi.mock('../../src/utils/ssrfGuard', async (orig) => ({ ...(await orig<Record<string, unknown>>()), safeFetchLlm }));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* through the real JwtAuthGuard against a temp SQLite db. Reservation SQL runs
|
||||
* for real (ReservationsService is DI-native, no mock — the temp db carries the
|
||||
* full, real migrated schema via createSnapshotTestDb), and so does the accommodation
|
||||
* SQL (the injected DaysService is DI-native too); the budget service, the
|
||||
* permission check and the WebSocket broadcast stay mocked.
|
||||
* SQL (the injected DaysService is DI-native too); the budget service and the
|
||||
* permission check stay mocked.
|
||||
*/
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe';
|
||||
@@ -36,7 +36,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
reinitialize: () => {},
|
||||
};
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
|
||||
const { notificationSend } = vi.hoisted(() => ({ notificationSend: vi.fn().mockResolvedValue(undefined) }));
|
||||
import { PermissionsService } from '../../src/nest/permissions/permissions.service';
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ChargingService } from '../../src/nest/roadtrip/charging.service';
|
||||
* fixed for that file). Every service in this module now runs its real SQL
|
||||
* through repositories (DI-injected, no service mock) against the real
|
||||
* request-scoped `EntityManager` `createTestMikroOrmModule` wires in; only
|
||||
* the permission check and the WebSocket broadcast stay mocked.
|
||||
* the permission check stays mocked.
|
||||
*
|
||||
* The unit test next door pins the handler bodies. What only a booted container
|
||||
* can show is the thing the controller's own comment calls load-bearing: that
|
||||
@@ -47,7 +47,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
import { Addons } from '../../src/db/entities/Addons.entity';
|
||||
|
||||
@@ -20,7 +20,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -34,7 +34,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../src/websocket', () => ({ broadcastToUser: vi.fn(), broadcast: vi.fn() }));
|
||||
vi.mock('../../src/nest/audit/audit-log.logger', () => ({ LOG_LEVEL: 'error', logInfo: vi.fn(), logDebug: vi.fn(), logError: vi.fn(), logWarn: vi.fn() }));
|
||||
|
||||
import { MailerService } from '../../src/nest/notifications/mailer/mailer.service';
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* through the real JwtAuthGuard against a migrated temp SQLite db
|
||||
* (createSnapshotTestDb()). TodoService and the trip access check run their
|
||||
* real queries through the repositories; rows are seeded and read through the
|
||||
* factories in tests/helpers/factories. Only the permission check and the
|
||||
* WebSocket broadcast stay mocked.
|
||||
* factories in tests/helpers/factories. Only the permission check stays
|
||||
* mocked.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi, type MockInstance } from 'vitest';
|
||||
import request from 'supertest';
|
||||
@@ -17,7 +17,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
import { PermissionsService } from '../../src/nest/permissions/permissions.service';
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* JwtAuthGuard, AddonGuard and TripAccessGuard against a real migrated and
|
||||
* seeded temp SQLite db (createSnapshotTestDb()). ToursService, PlacesService,
|
||||
* AssignmentsService and TripsService run their real SQL; only the permission
|
||||
* check (a container spy), the journey hooks and the WebSocket broadcast are
|
||||
* stubbed.
|
||||
* check (a container spy) and the journey hooks are stubbed, and the broadcast
|
||||
* goes to a FakeRealtimeService.
|
||||
*
|
||||
* Fixtures: user 1 owns trips 5 and 6, user 2 is a member of trip 5 whom the
|
||||
* permission spy denies 'place_edit', user 3 has no access to either trip.
|
||||
@@ -16,6 +16,7 @@ import request from 'supertest';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import type { Server } from 'http';
|
||||
import { RealtimeModule } from '../../src/nest/realtime/realtime.module';
|
||||
import { RealtimeService } from '../../src/nest/realtime/realtime.service';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { sessionCookie } from './harness';
|
||||
|
||||
@@ -23,8 +24,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
const { broadcast } = vi.hoisted(() => ({ broadcast: vi.fn() }));
|
||||
vi.mock('../../src/websocket', () => ({ broadcast }));
|
||||
vi.mock('../../src/nest/audit/audit-log.logger', () => ({ LOG_LEVEL: 'error', logInfo: vi.fn(), logDebug: vi.fn(), logError: vi.fn(), logWarn: vi.fn() }));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
@@ -59,6 +58,10 @@ import { Places } from '../../src/db/entities/Places.entity';
|
||||
import { TourWaypoints } from '../../src/db/entities/TourWaypoints.entity';
|
||||
import { Tours } from '../../src/db/entities/Tours.entity';
|
||||
import { Trips } from '../../src/db/entities/Trips.entity';
|
||||
import { FakeRealtimeService } from '../helpers/fake-realtime';
|
||||
|
||||
const realtime = new FakeRealtimeService();
|
||||
const broadcast = realtime.broadcastMock;
|
||||
|
||||
let orm: TestOrm;
|
||||
|
||||
@@ -110,6 +113,8 @@ describe('Tours e2e (real guards + temp SQLite)', () => {
|
||||
TripsModule,
|
||||
],
|
||||
})
|
||||
.overrideProvider(RealtimeService)
|
||||
.useValue(realtime)
|
||||
.overrideProvider(JourneyDomainService)
|
||||
.useValue(journeyHooks)
|
||||
.compile();
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* not fail loudly, which is worse than removing it — the same reasoning
|
||||
* `days.e2e.test.ts`'s identical comment gives). Only the permission check,
|
||||
* `BudgetService` (the budget fold's own container spy, unrelated to this
|
||||
* conversion) and the WebSocket broadcast stay mocked.
|
||||
* conversion) stay mocked.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi, type MockInstance } from 'vitest';
|
||||
import request from 'supertest';
|
||||
@@ -33,7 +33,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn() }));
|
||||
// The audit domain is DI-native now: writeAudit runs for real against the temp
|
||||
// db's audit_log table; only the file logger is silenced.
|
||||
vi.mock('../../src/nest/audit/audit-log.logger', () => ({ LOG_LEVEL: 'error', logInfo: vi.fn(), logDebug: vi.fn(), logError: vi.fn(), logWarn: vi.fn() }));
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Vacay module e2e — exercises the migrated /api/addons/vacay endpoints through
|
||||
* the real JwtAuthGuard against a migrated temp SQLite db. DI-native: VacayService runs
|
||||
* its real SQL over the temp db (no legacy service mock); only the websocket
|
||||
* and notification side channels stay mocked. Focuses on auth, status codes
|
||||
* its real SQL over the temp db (no legacy service mock); the notification side
|
||||
* channel stays mocked and the broadcast goes to a FakeRealtimeService. Focuses on auth, status codes
|
||||
* (POSTs stay 200), the Zod-pipe 400 envelope and a 403 body.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
||||
@@ -16,7 +16,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db } from '../../src/db/database';
|
||||
import { VacayEntries } from '../../src/db/entities/VacayEntries.entity';
|
||||
@@ -25,20 +24,25 @@ import { RealtimeModule } from '../../src/nest/realtime/realtime.module';
|
||||
import { VacayModule } from '../../src/nest/vacay/vacay.module';
|
||||
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
|
||||
import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe';
|
||||
import { broadcastToUser } from '../../src/websocket';
|
||||
import { RealtimeService } from '../../src/nest/realtime/realtime.service';
|
||||
import { FakeRealtimeService } from '../helpers/fake-realtime';
|
||||
import { TestUnitOfWorkModule } from '../helpers/test-uow';
|
||||
import { createTestMikroOrmModule, createTestOrm, type TestOrm } from '../helpers/test-orm';
|
||||
import { makeUser } from '../helpers/factories/users';
|
||||
import { findRow } from '../helpers/factories/rows';
|
||||
|
||||
let orm: TestOrm;
|
||||
const realtime = new FakeRealtimeService();
|
||||
|
||||
describe('Vacay e2e (real auth guard + migrated temp SQLite)', () => {
|
||||
let server: Server;
|
||||
let app: Awaited<ReturnType<typeof build>>;
|
||||
|
||||
async function build() {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), RealtimeModule, VacayModule] }).compile();
|
||||
const moduleRef = await Test.createTestingModule({ imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), RealtimeModule, VacayModule] })
|
||||
.overrideProvider(RealtimeService)
|
||||
.useValue(realtime)
|
||||
.compile();
|
||||
const nest = moduleRef.createNestApplication();
|
||||
nest.use(cookieParser());
|
||||
nest.useGlobalFilters(new TrekExceptionFilter());
|
||||
@@ -81,7 +85,7 @@ describe('Vacay e2e (real auth guard + migrated temp SQLite)', () => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual({ action: 'added', fraction: 1, kind: 'vacation' });
|
||||
expect(await findRow(orm, VacayEntries, { user: 1, date: '2026-07-01' })).not.toBeNull();
|
||||
expect(vi.mocked(broadcastToUser)).toHaveBeenCalledWith(1, { type: 'vacay:update' }, 'sock-7');
|
||||
expect(realtime.broadcastToUserMock).toHaveBeenCalledWith(1, { type: 'vacay:update' }, 'sock-7');
|
||||
});
|
||||
|
||||
it('400 from the Zod pipe on entries/toggle without a date', async () => {
|
||||
|
||||
@@ -36,8 +36,11 @@ import { TripAccessService } from '../../src/nest/trip-membership/trip-access.se
|
||||
* instance). Kept as two functions for caller-shape continuity, though they
|
||||
* are now identical.
|
||||
*/
|
||||
export async function makeAccommodationsService(conn: Database): Promise<AccommodationsService> {
|
||||
return accommodationsOver(conn);
|
||||
export async function makeAccommodationsService(
|
||||
conn: Database,
|
||||
realtime: RealtimeService = new RealtimeService(),
|
||||
): Promise<AccommodationsService> {
|
||||
return accommodationsOver(conn, realtime);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,9 +50,11 @@ export async function makeAccommodationsService(conn: Database): Promise<Accommo
|
||||
* the same stop a night entered under Days does, and every suite that builds
|
||||
* that service by hand needs one to hand it.
|
||||
*/
|
||||
export async function accommodationsOver(conn: Database): Promise<AccommodationsService> {
|
||||
export async function accommodationsOver(
|
||||
conn: Database,
|
||||
realtime: RealtimeService = new RealtimeService(),
|
||||
): Promise<AccommodationsService> {
|
||||
const permissions = new PermissionsService(await createTestAppSettingsRepo(conn), await createTestUnitOfWork(conn));
|
||||
const realtime = new RealtimeService();
|
||||
const t = await sharedTestOrm(conn);
|
||||
const assignments = new AssignmentsService(
|
||||
new TripAccessService(await createTestTripsRepo(conn)), permissions, realtime,
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* A RealtimeService that records instead of delivering.
|
||||
*
|
||||
* Suites observe broadcasts through DI rather than by mocking a module path:
|
||||
* a hand-built service gets `new FakeRealtimeService()` where it took a
|
||||
* RealtimeService, and a booted Nest app gets its container instance swapped
|
||||
* (`.overrideProvider(RealtimeService).useValue(fake)`) or spied on with
|
||||
* `spyOnRealtime(app)`. Each method records into a `vi.fn`, so the assertions read the
|
||||
* same as they did against the old module mock:
|
||||
*
|
||||
* const realtime = new FakeRealtimeService();
|
||||
* const service = new TodoService(permissions, realtime, ...);
|
||||
* expect(realtime.broadcastMock).toHaveBeenCalledWith(tripId, 'todo:created', ...);
|
||||
*
|
||||
* The fake receives exactly the arguments the service handed RealtimeService
|
||||
* (the facade passes them through without padding), so call-shape assertions
|
||||
* keep their meaning.
|
||||
*/
|
||||
import { RealtimeService } from '../../src/nest/realtime/realtime.service';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
|
||||
import { vi, type Mock } from 'vitest';
|
||||
|
||||
type BroadcastArgs = [
|
||||
tripId: number | string,
|
||||
eventType: string,
|
||||
payload: Record<string, unknown>,
|
||||
excludeSid?: number | string,
|
||||
onlyUserId?: number,
|
||||
];
|
||||
type BroadcastToUserArgs = [userId: number, payload: Record<string, unknown>, excludeSid?: number | string];
|
||||
|
||||
export class FakeRealtimeService extends RealtimeService {
|
||||
readonly broadcastMock: Mock<(...args: BroadcastArgs) => void> = vi.fn();
|
||||
readonly broadcastToUserMock: Mock<(...args: BroadcastToUserArgs) => void> = vi.fn();
|
||||
/** Nobody is online unless a case says otherwise. */
|
||||
readonly getOnlineUserIdsMock: Mock<() => Set<number>> = vi.fn(() => new Set<number>());
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// Some callers take the method off the instance before calling it
|
||||
// (McpToolGuardsService.safeBroadcast does), as the real facade allows.
|
||||
this.broadcast = this.broadcast.bind(this);
|
||||
this.broadcastToUser = this.broadcastToUser.bind(this);
|
||||
this.getOnlineUserIds = this.getOnlineUserIds.bind(this);
|
||||
}
|
||||
|
||||
override broadcast(...args: BroadcastArgs): void {
|
||||
this.broadcastMock(...args);
|
||||
}
|
||||
|
||||
override broadcastToUser(...args: BroadcastToUserArgs): void {
|
||||
this.broadcastToUserMock(...args);
|
||||
}
|
||||
|
||||
override getOnlineUserIds(): Set<number> {
|
||||
return this.getOnlineUserIdsMock();
|
||||
}
|
||||
|
||||
/** Forget every recorded call (and any per-case return value). */
|
||||
reset(): void {
|
||||
this.broadcastMock.mockReset();
|
||||
this.broadcastToUserMock.mockReset();
|
||||
this.getOnlineUserIdsMock.mockReset();
|
||||
this.getOnlineUserIdsMock.mockImplementation(() => new Set<number>());
|
||||
}
|
||||
}
|
||||
|
||||
export interface RealtimeSpies {
|
||||
broadcast: Mock<(...args: BroadcastArgs) => void>;
|
||||
broadcastToUser: Mock<(...args: BroadcastToUserArgs) => void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spy on the RealtimeService a booted app resolved, for suites that build the
|
||||
* whole app (buildApp) and cannot override a provider. The spies do not
|
||||
* deliver: a test socket would otherwise receive every broadcast the case
|
||||
* caused, which is what the old module mock prevented too.
|
||||
*/
|
||||
export function spyOnRealtime(app: INestApplicationContext): RealtimeSpies {
|
||||
const realtime = app.get(RealtimeService);
|
||||
const broadcast = vi.spyOn(realtime, 'broadcast').mockImplementation(() => undefined);
|
||||
const broadcastToUser = vi.spyOn(realtime, 'broadcastToUser').mockImplementation(() => undefined);
|
||||
return {
|
||||
broadcast: broadcast as unknown as RealtimeSpies['broadcast'],
|
||||
broadcastToUser: broadcastToUser as unknown as RealtimeSpies['broadcastToUser'],
|
||||
};
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory';
|
||||
import { registerTools } from '../../src/mcp/tools';
|
||||
import type { McpAttachOptions } from '../../src/nest-mcp';
|
||||
import { createMcpTestRegistry } from './mcp-test-controllers';
|
||||
import type { RealtimeService } from '../../src/nest/realtime/realtime.service';
|
||||
|
||||
export interface McpHarness {
|
||||
client: Client;
|
||||
@@ -47,10 +48,16 @@ export interface McpHarnessOptions {
|
||||
* every suite that does not register one.
|
||||
*/
|
||||
dynamicTools?: McpAttachOptions['dynamicTools'];
|
||||
/**
|
||||
* The RealtimeService every tool broadcasts through. A suite that asserts on
|
||||
* broadcasts hands in a FakeRealtimeService (tests/helpers/fake-realtime.ts);
|
||||
* left out, the tools get a real one, whose transport has no sockets here.
|
||||
*/
|
||||
realtime?: RealtimeService;
|
||||
}
|
||||
|
||||
export async function createMcpHarness(options: McpHarnessOptions): Promise<McpHarness> {
|
||||
const { userId, withTools = true, scopes = null, isStaticToken = false, getDeprecationNotice, dynamicTools } = options;
|
||||
const { userId, withTools = true, scopes = null, isStaticToken = false, getDeprecationNotice, dynamicTools, realtime } = options;
|
||||
|
||||
const server = new McpServer({ name: 'trek-test', version: '1.0.0' });
|
||||
|
||||
@@ -59,7 +66,7 @@ export async function createMcpHarness(options: McpHarnessOptions): Promise<McpH
|
||||
// McpRegistryService to registerTools; the harness has no Nest app, so it
|
||||
// builds the same registry by hand (see mcp-test-controllers.ts).
|
||||
// registerTools' own ctx construction stays exercised.
|
||||
await registerTools(await createMcpTestRegistry(), server, userId, scopes ?? null, isStaticToken, getDeprecationNotice, undefined, dynamicTools);
|
||||
await registerTools(await createMcpTestRegistry(realtime), server, userId, scopes ?? null, isStaticToken, getDeprecationNotice, undefined, dynamicTools);
|
||||
}
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
@@ -194,7 +194,7 @@ import { TripAccessService } from '../../src/nest/trip-membership/trip-access.se
|
||||
* fan-out. Constructing against the `db` Proxy keeps per-file vi.mock's of
|
||||
* src/db/database flowing through (same pattern as todo.bridge.ts).
|
||||
*/
|
||||
export async function createMcpTestRegistry(): Promise<McpRegistry> {
|
||||
export async function createMcpTestRegistry(realtimeService: RealtimeService = new RealtimeService()): Promise<McpRegistry> {
|
||||
// Plan 3c Task 0b / Plan 4 Task 4: `mcpOrm` gives every repository below
|
||||
// its `EntityManager` — `canAccessTrip`/`isOwner`/`rosterUserIds`/
|
||||
// `getPlaceWithTags` resolve `TripsRepository`/`TripMembersRepository`/
|
||||
@@ -207,7 +207,6 @@ export async function createMcpTestRegistry(): Promise<McpRegistry> {
|
||||
const auditLogRepo = mcpOrm.repo(AuditLog);
|
||||
const usersRepo = mcpOrm.repo(Users);
|
||||
const permissionsService = new PermissionsService(await createTestAppSettingsRepo(db), await createTestUnitOfWork(db));
|
||||
const realtimeService = new RealtimeService();
|
||||
// Plan 4 Task 1 constructor-ripple: the trip `user_id`/user `role` reads
|
||||
// moved off `DatabaseService` onto `TripsRepository`/`UsersRepository`.
|
||||
const guards = new McpToolGuardsService(mcpOrm.repo(Trips), usersRepo, permissionsService, realtimeService);
|
||||
|
||||
@@ -11,7 +11,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
|
||||
@@ -30,7 +30,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn(), getOnlineUserIds: vi.fn(() => []) }));
|
||||
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
|
||||
@@ -12,7 +12,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { resetTestDb } from '../helpers/test-db';
|
||||
|
||||
@@ -11,7 +11,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { resetTestDb } from '../helpers/test-db';
|
||||
|
||||
@@ -11,7 +11,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -87,7 +87,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn(), getOnlineUserIds: vi.fn(() => []) }));
|
||||
|
||||
import request from 'supertest';
|
||||
import type { Application } from 'express';
|
||||
|
||||
@@ -28,7 +28,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
vi.mock('../../src/nest/atlas/atlas-geo', async (orig) => {
|
||||
const actual = await orig<typeof import('../../src/nest/atlas/atlas-geo')>();
|
||||
return {
|
||||
|
||||
@@ -12,7 +12,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -17,7 +17,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -15,7 +15,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
// Mock filesystem-dependent service functions to avoid real disk I/O in tests
|
||||
vi.mock('../../src/nest/backup/backup.impl', async () => {
|
||||
|
||||
@@ -63,7 +63,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn(), getOnlineUserIds: vi.fn(() => []) }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -14,7 +14,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
// BOOT-007 boots with NODE_ENV=production, which opens the cron gate — keep the
|
||||
// registrar inert so a bootstrap test never schedules real jobs or runs boot
|
||||
// sweeps against the real uploads/data dirs. The gate itself is covered by
|
||||
|
||||
@@ -11,7 +11,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -11,7 +11,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
|
||||
@@ -16,7 +16,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -17,7 +17,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn(), getOnlineUserIds: vi.fn(() => []) }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
|
||||
@@ -11,7 +11,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -15,7 +15,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -44,7 +44,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn(), getOnlineUserIds: vi.fn(() => []) }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -48,7 +48,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn(), getOnlineUserIds: vi.fn(() => []) }));
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
|
||||
@@ -18,7 +18,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
|
||||
@@ -14,7 +14,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -14,7 +14,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
// Mock SSRF guard: block loopback and private IPs, allow external hostnames without DNS.
|
||||
vi.mock('../../src/utils/ssrfGuard', async () => {
|
||||
|
||||
@@ -25,11 +25,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({
|
||||
broadcast: vi.fn(),
|
||||
broadcastToUser: vi.fn(),
|
||||
getOnlineUserIds: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -14,11 +14,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({
|
||||
broadcast: vi.fn(),
|
||||
broadcastToUser: vi.fn(),
|
||||
getOnlineUserIds: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -17,7 +17,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb2 } from '../../src/db/database';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
|
||||
@@ -16,11 +16,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { openLegacyFixture } = await import('../helpers/legacy-fixture');
|
||||
return buildDbMock(openLegacyFixture('legacy-v066'));
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({
|
||||
broadcast: vi.fn(),
|
||||
broadcastToUser: vi.fn(),
|
||||
getOnlineUserIds: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
describeLegacyUpgrade(
|
||||
{
|
||||
|
||||
@@ -13,11 +13,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { openLegacyFixture } = await import('../helpers/legacy-fixture');
|
||||
return buildDbMock(openLegacyFixture('legacy-v082'));
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({
|
||||
broadcast: vi.fn(),
|
||||
broadcastToUser: vi.fn(),
|
||||
getOnlineUserIds: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
describeLegacyUpgrade(
|
||||
{ fixture: 'legacy-v082', version: 82, reseated: 1, nightOrderIndex: 1, id: 'LEGACYUP-082' },
|
||||
|
||||
@@ -13,11 +13,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { openLegacyFixture } = await import('../helpers/legacy-fixture');
|
||||
return buildDbMock(openLegacyFixture('legacy-v205'));
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({
|
||||
broadcast: vi.fn(),
|
||||
broadcastToUser: vi.fn(),
|
||||
getOnlineUserIds: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
describeLegacyUpgrade(
|
||||
{ fixture: 'legacy-v205', version: 205, reseated: 1, nightOrderIndex: 1, id: 'LEGACYUP-205' },
|
||||
|
||||
@@ -13,11 +13,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { openLegacyFixture } = await import('../helpers/legacy-fixture');
|
||||
return buildDbMock(openLegacyFixture('legacy-v242'));
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({
|
||||
broadcast: vi.fn(),
|
||||
broadcastToUser: vi.fn(),
|
||||
getOnlineUserIds: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
describeLegacyUpgrade(
|
||||
{ fixture: 'legacy-v242', version: 242, reseated: 0, nightOrderIndex: 3, id: 'LEGACYUP-242' },
|
||||
|
||||
@@ -19,11 +19,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { openLegacyFixture } = await import('../helpers/legacy-fixture');
|
||||
return buildDbMock(openLegacyFixture('legacy-v244'));
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({
|
||||
broadcast: vi.fn(),
|
||||
broadcastToUser: vi.fn(),
|
||||
getOnlineUserIds: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
// Off by default under NODE_ENV=test; this suite is the one that asks for it.
|
||||
const previousSwitch = process.env.TREK_DB_PRE_MIGRATE_SNAPSHOT;
|
||||
|
||||
@@ -14,7 +14,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -14,7 +14,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -41,7 +41,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
// ── SSRF guard mock — routes all Immich API calls to fake responses ───────────
|
||||
vi.mock('../../src/utils/ssrfGuard', async () => {
|
||||
|
||||
@@ -19,7 +19,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
// ── SSRF guard mock — routes all Synology API calls to fake responses ─────────
|
||||
vi.mock('../../src/utils/ssrfGuard', async () => {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* /unified/trips/:tripId/album-links routes.
|
||||
*
|
||||
* No real HTTP is made — safeFetch is mocked to never be called.
|
||||
* The broadcast WebSocket call is no-op mocked.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
||||
import request from 'supertest';
|
||||
@@ -18,7 +17,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
vi.mock('../../src/utils/ssrfGuard', async () => {
|
||||
const actual = await vi.importActual<typeof import('../../src/utils/ssrfGuard')>('../../src/utils/ssrfGuard');
|
||||
return {
|
||||
|
||||
@@ -11,7 +11,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -15,7 +15,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
// The two test-send endpoints must not reach a real SMTP server or URL. Since
|
||||
// the fold they are provider methods, so they are stubbed on the prototype
|
||||
// rather than by module path — everything else in the domain stays real.
|
||||
|
||||
@@ -24,7 +24,6 @@ vi.mock('../../src/app-config', async (importOriginal) => {
|
||||
return { ...actual, getMcpSafeUrl: () => 'https://trek.example.com' };
|
||||
});
|
||||
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
vi.mock('../../src/mcp/sessionManager', () => ({ revokeUserSessions: vi.fn(), revokeUserSessionsForClient: vi.fn(), sessions: new Map() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
|
||||
@@ -19,7 +19,6 @@ vi.mock('../../src/app-config', async (importOriginal) => {
|
||||
return { ...actual, getMcpSafeUrl: () => 'https://trek.example.com' };
|
||||
});
|
||||
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
vi.mock('../../src/mcp/sessionManager', () => ({ revokeUserSessions: vi.fn(), revokeUserSessionsForClient: vi.fn(), sessions: new Map() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
|
||||
@@ -27,7 +27,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
// The provider's names are answered here; every other name goes to the real resolver.
|
||||
vi.mock('dns/promises', async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import('dns/promises')>();
|
||||
|
||||
@@ -19,7 +19,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import type { MockInstance } from 'vitest';
|
||||
|
||||
@@ -36,7 +36,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn(), getOnlineUserIds: vi.fn(() => []) }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
|
||||
@@ -11,7 +11,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
|
||||
@@ -27,7 +27,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -17,12 +17,10 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
import { DomainError } from '../../src/nest/common/domain-error';
|
||||
import { resetTestDb, resetRateLimits } from '../helpers/test-db';
|
||||
import type { FactoryOrm } from '../helpers/factories/context';
|
||||
import { createUser, createAdmin, createTrip, createPlace, addTripMember } from '../helpers/factories';
|
||||
@@ -38,7 +36,8 @@ import { PlaceRatings } from '../../src/db/entities/PlaceRatings.entity';
|
||||
import { Places } from '../../src/db/entities/Places.entity';
|
||||
import { PlacesService } from '../../src/nest/places/places.service';
|
||||
import { invalidatePermissionsCache } from '../../src/nest/permissions/permissions-cache';
|
||||
import { broadcast } from '../../src/websocket';
|
||||
import { spyOnRealtime, type RealtimeSpies } from '../helpers/fake-realtime';
|
||||
import { DomainError } from '../../src/nest/common/domain-error';
|
||||
|
||||
let nestApp: INestApplication;
|
||||
let app: Application;
|
||||
@@ -49,6 +48,7 @@ let orm: FactoryOrm;
|
||||
// implementation, so only the *Once overrides below change behaviour.
|
||||
let importGoogleList: MockInstance;
|
||||
let searchPlaceImage: MockInstance;
|
||||
let broadcast: RealtimeSpies['broadcast'];
|
||||
const GPX_FIXTURE = path.join(__dirname, '../fixtures/test.gpx');
|
||||
const KML_FIXTURE = path.join(__dirname, '../fixtures/test.kml');
|
||||
const KML_NESTED_FIXTURE = path.join(__dirname, '../fixtures/test-nested.kml');
|
||||
@@ -69,6 +69,7 @@ beforeEach(async () => {
|
||||
// afterEach, which would otherwise strip these for every later test.
|
||||
importGoogleList = vi.spyOn(nestApp.get(PlacesService), 'importGoogleList');
|
||||
searchPlaceImage = vi.spyOn(nestApp.get(PlacesService), 'searchImage');
|
||||
broadcast = spyOnRealtime(nestApp).broadcast;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -28,7 +28,6 @@ vi.mock('../../../src/db/database', async () => {
|
||||
const db = createSnapshotTestDb();
|
||||
return { db, canAccessTrip: () => undefined };
|
||||
});
|
||||
vi.mock('../../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../../src/db/database';
|
||||
import { UnitOfWork } from '../../../src/nest/database/unit-of-work';
|
||||
|
||||
@@ -18,7 +18,6 @@ import path from 'node:path';
|
||||
// (`createSnapshotTestDb` + `createTestOrm`) replaces the old hand-rolled table set,
|
||||
// same fix `registry.test.ts` needed.
|
||||
import { createSnapshotTestDb } from '../../helpers/db-mock';
|
||||
vi.mock('../../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { PluginRuntimeService } from '../../../src/nest/plugins/plugin-runtime.service';
|
||||
import { createPluginRuntime } from '../../helpers/plugin-host';
|
||||
|
||||
@@ -44,7 +44,6 @@ const { testDb } = vi.hoisted(() => {
|
||||
});
|
||||
vi.mock('../../../src/db/database', () => ({ db: testDb, canAccessTrip: () => undefined }));
|
||||
import { db as dbConn } from '../../../src/db/database';
|
||||
vi.mock('../../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { PluginRuntimeService, PluginDependencyError } from '../../../src/nest/plugins/plugin-runtime.service';
|
||||
import { createPluginRuntime } from '../../helpers/plugin-host';
|
||||
|
||||
@@ -12,7 +12,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -11,7 +11,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -15,7 +15,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
|
||||
@@ -19,7 +19,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -11,7 +11,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -11,7 +11,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
|
||||
@@ -27,7 +27,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn(), getOnlineUserIds: vi.fn(() => []) }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -14,7 +14,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
|
||||
@@ -11,7 +11,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { buildApp } from '../../src/bootstrap';
|
||||
|
||||
@@ -11,7 +11,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
|
||||
@@ -30,7 +30,6 @@ vi.mock('../../src/db/database', async () => {
|
||||
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
||||
return buildDbMock(createSnapshotTestDb());
|
||||
});
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
|
||||
import { db as testDb } from '../../src/db/database';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user