diff --git a/server/src/nest/addons/addons.bridge.ts b/server/src/nest/addons/addons.bridge.ts index 2b8b5a7a1..64ce7998e 100644 --- a/server/src/nest/addons/addons.bridge.ts +++ b/server/src/nest/addons/addons.bridge.ts @@ -3,19 +3,18 @@ import { DatabaseService } from '../database/database.service'; import { AddonsService } from './addons.service'; /** - * Non-Nest entry point for the addons domain — for code running OUTSIDE the - * Nest container (the legacy MCP registrars/resources, systemNotices, the - * pre-init platform.routes Express router, the module-level `when:` gate - * closures in the DI-discovered *.mcp.ts files, and the legacy - * oauthService/airtrailSync/thumbnailService modules). Exports the legacy - * services/adminService names 1:1 so repointing a consumer is an - * import-path-only diff. Inside the container, inject AddonsService instead. - * Delete this file when the last legacy consumer migrates. + * Non-Nest entry point for the addons domain — pinned by the MCP/OAuth + * transport (src/mcp/index.ts) and the pre-init platform.routes Express + * router, both mounted BEFORE app.init(), so the container is not available + * to them; systemNotices/conditions.ts reads it too until its enablement + * check is threaded in from the DI side. Inside the container, inject + * AddonsService instead (the `when:` gates in *.mcp.ts files do, via + * addon-gate.ts). Delete this file when the MCP/OAuth mount moves behind the + * container. * - * Only the enablement READS are bridged — the paired writers - * (updateBagTracking/updateCollabFeatures) have in-container consumers only. - * The reads are uncached per-call queries on purpose: admin toggles must stay - * immediately visible without invalidation wiring. + * Only the enablement READ is bridged, as an uncached per-call query on + * purpose: admin toggles must stay immediately visible without invalidation + * wiring. * * Module-level construction is safe: `db` is the reinitialize-proof Proxy onto * the shared better-sqlite3 singleton. @@ -25,11 +24,3 @@ const addons = new AddonsService(new DatabaseService(db)); export function isAddonEnabled(addonId: string): boolean { return addons.isAddonEnabled(addonId); } - -export function getBagTracking(): { enabled: boolean } { - return addons.getBagTracking(); -} - -export function getCollabFeatures(): { chat: boolean; notes: boolean; polls: boolean; whatsnext: boolean } { - return addons.getCollabFeatures(); -} diff --git a/server/src/nest/atlas/atlas-geo.ts b/server/src/nest/atlas/atlas-geo.ts index 2ed65401e..6b7776dbe 100644 --- a/server/src/nest/atlas/atlas-geo.ts +++ b/server/src/nest/atlas/atlas-geo.ts @@ -12,8 +12,8 @@ import { cacheKeyFor, getCached, nominatimFetch, setCached } from '../geo/nomina // per-country store, geocode/region caches, country poly/box indexes, the // shared Nominatim throttle and the geocodingInFlight dedup set) are // file-scoped so their lifetime stays process-global no matter how many -// AtlasService instances exist (container singleton + auth.bridge.ts + -// test helpers) — instance state would duplicate the multi-MB indexes the +// AtlasService instances exist (container singleton + test helpers) — +// instance state would duplicate the multi-MB indexes the // #1576 OOM fix exists to avoid and split the ≥1.1s Nominatim throttle. // Relocated verbatim from the legacy services/atlasService.ts fold. diff --git a/server/src/nest/atlas/atlas.service.ts b/server/src/nest/atlas/atlas.service.ts index 9d6178bef..2b3f41161 100644 --- a/server/src/nest/atlas/atlas.service.ts +++ b/server/src/nest/atlas/atlas.service.ts @@ -46,7 +46,7 @@ function countryCodeFromRegionCode(regionCode: string): string { * The pure-geo machinery (bundled admin0/admin1 boundaries, point-in-polygon * indexes, Nominatim geocoding and its caches) lives in ./atlas-geo — a plain * module so the multi-MB caches stay process-global across the container - * instance, the auth.bridge.ts instance, and test helpers (#1576). + * instance and test helpers (#1576). * Returns native service shapes; the client-facing contracts live in * @trek/shared. No broadcasts: atlas rows are uid-scoped, not trip-scoped. */ diff --git a/server/src/nest/audit/audit.bridge.ts b/server/src/nest/audit/audit.bridge.ts index 9f32835cb..e3598b4ab 100644 --- a/server/src/nest/audit/audit.bridge.ts +++ b/server/src/nest/audit/audit.bridge.ts @@ -3,13 +3,12 @@ import { DatabaseService } from '../database/database.service'; import { AuditService } from './audit.service'; /** - * Non-Nest entry point for the audit domain — for code running OUTSIDE the - * Nest container (src/index.ts lazy-requires, src/mcp/index.ts and - * oauthProvider.ts, and the legacy airtrail/immich/oauth services). Exports - * the full legacy services/auditLog surface 1:1 so repointing a consumer is an - * import-path-only diff (log*-only consumers import audit-log.logger - * directly). Inside the container, inject AuditService instead. Delete this - * file when the last legacy consumer migrates. + * Non-Nest entry point for the audit domain — pinned by the MCP/OAuth + * transport (src/mcp/index.ts and oauthProvider.ts), which platform.routes.ts + * mounts on the Express app BEFORE app.init(), so the container is not + * available to it. Inside the container, inject AuditService instead; log*-only + * consumers import audit-log.logger directly. Delete this file when the + * MCP/OAuth mount moves behind the container. * * Module-level construction is safe: `db` is the reinitialize-proof Proxy onto * the shared better-sqlite3 singleton. @@ -21,6 +20,3 @@ export function writeAudit(entry: Parameters[0]): vo } export { getClientIp } from './client-ip'; -// LOG_LEVEL has no external consumers today — re-exported purely for 1:1 -// surface parity with the legacy module. -export { LOG_LEVEL, logInfo, logDebug, logError, logWarn } from './audit-log.logger'; diff --git a/server/src/nest/auth/auth.bridge.ts b/server/src/nest/auth/auth.bridge.ts index 6127a243f..81fae1ce1 100644 --- a/server/src/nest/auth/auth.bridge.ts +++ b/server/src/nest/auth/auth.bridge.ts @@ -11,31 +11,23 @@ import { User } from '../../types'; import { EphemeralTokenService } from './ephemeral-token.service'; /** - * Non-Nest entry point for the auth domain — for code running OUTSIDE the - * Nest container: the legacy MCP transport (mcp/index.ts token verification), - * the legacy tool registrars (mcp/tools/journey|notifications|transports), the - * not-yet-migrated services/adminService, and — as the one in-container - * exception — - * atlas.mcp.ts. The cycle that used to make that mandatory is gone - * (getTravelStats moved to AtlasService, so AuthModule no longer imports - * AtlasModule); what keeps it here is cost, not correctness: a direct import - * would drag the whole auth chain into every Atlas consumer for one - * isDemoUser call. Same open trade-off as places.mcp.ts keeping its - * assignments.bridge imports. + * Non-Nest entry point for the auth domain — pinned by the MCP transport + * (src/mcp/index.ts token verification), which platform.routes.ts mounts on + * the Express app BEFORE app.init(), so the container is not available to it. + * Inside the container, inject AuthService/TokenService instead. Delete this + * file when the MCP/OAuth mount moves behind the container. * - * Exports the legacy services/authService names 1:1 so repointing a consumer - * is an import-path-only diff. Inside the container, inject AuthService - * instead. Delete exports as their last legacy consumer migrates. - * - * stripUserForClient is pure and re-exported straight from auth.helpers.ts; - * avatarUrl keeps the legacy re-export shape (services/avatarUrl). The - * pending-MFA and reset-throttle maps are module-scoped in auth.service.ts, - * so this instance and the container singleton share one copy. + * Only two exports remain: verifyMcpToken lives on TokenService, but + * verifyJwtToken is login identity and deliberately stays on AuthService + * (see token.service.ts), so the whole AuthService collaborator graph below + * exists to serve that one call. WebauthnConfigService and UserCleanupService + * are constructed only because AuthService takes them — nothing this file + * exports reaches either one. The pending-MFA and reset-throttle maps are + * module-scoped in auth.service.ts, so this instance and the container + * singleton share one copy. * * Module-level construction is safe: `db` is the reinitialize-proof Proxy onto * the shared better-sqlite3 singleton (same pattern as permissions.bridge.ts). - * WebauthnConfigService and UserCleanupService are constructed only because - * AuthService takes them — nothing this file exports reaches either one. */ const dbs = new DatabaseService(db); const permissions = new PermissionsService(dbs); @@ -51,13 +43,6 @@ const auth = new AuthService( const tokens = new TokenService(dbs, new EphemeralTokenService()); -export { stripUserForClient } from './auth.helpers'; -export { avatarUrl } from '../common/avatarUrl'; - -export function isDemoUser(userId: number): boolean { - return auth.isDemoUser(userId); -} - export function verifyMcpToken(rawToken: string): User | null { return tokens.verifyMcpToken(rawToken); } @@ -65,15 +50,3 @@ export function verifyMcpToken(rawToken: string): User | null { export function verifyJwtToken(token: string): User | null { return auth.verifyJwtToken(token); } - -export function resolveAuthToggles() { - return auth.resolveAuthToggles(); -} - -export function generateToken(user: { id: number | bigint; password_version?: number }, rememberMe = false) { - return auth.generateToken(user, rememberMe); -} - -export function createWsToken(userId: number) { - return tokens.createWsToken(userId); -} diff --git a/server/src/nest/oauth/oauth.bridge.ts b/server/src/nest/oauth/oauth.bridge.ts index 6b3ceb6bd..4c2075e75 100644 --- a/server/src/nest/oauth/oauth.bridge.ts +++ b/server/src/nest/oauth/oauth.bridge.ts @@ -19,13 +19,12 @@ import { OauthService } from './oauth.service'; * pending authorization codes — is module-scoped in oauth.pending-codes.ts, so * this instance and the container singleton see the same map. That is load- * bearing: the consent controller writes the code through the container and - * the SDK reads it back through here. + * the SDK reads it back through here (which is why consumeAuthCode is bridged + * and createAuthCode is not). */ const dbs = new DatabaseService(db); const oauth = new OauthService(dbs, new AddonsService(dbs), new AuditService(dbs)); -export type { OAuthTokenInfo, PendingCode } from './oauth.service'; - export function getUserByAccessToken(rawToken: string) { return oauth.getUserByAccessToken(rawToken); } @@ -34,10 +33,6 @@ export function createOAuthClient(...args: Parameters[0]) { - return oauth.createAuthCode(params); -} - export function consumeAuthCode(code: string) { return oauth.consumeAuthCode(code); } diff --git a/server/src/nest/permissions/permissions.bridge.ts b/server/src/nest/permissions/permissions.bridge.ts index 9064fac6c..16fdde12c 100644 --- a/server/src/nest/permissions/permissions.bridge.ts +++ b/server/src/nest/permissions/permissions.bridge.ts @@ -1,44 +1,27 @@ import { db } from '../../db/database'; import { DatabaseService } from '../database/database.service'; import { PermissionsService } from './permissions.service'; -import type { PermissionLevel } from './permissions.service'; /** - * Non-Nest entry point for the permissions domain — for code running OUTSIDE - * the Nest container (the legacy MCP _shared.ts hasTripPermission helper and - * the legacy adminService/authService/backupService). - * Exports the legacy services/permissions names 1:1 so repointing a consumer - * is an import-path-only diff. Inside the container, inject PermissionsService - * instead. Delete this file when the last legacy consumer migrates. + * Non-Nest entry point for the permissions domain — pinned by the MCP + * transport's hasTripPermission helper (src/mcp/tools/_shared.ts), which the + * pre-app.init() MCP/OAuth mount evaluates before the container exists. + * Inside the container, inject PermissionsService instead. Delete this file + * when the MCP/OAuth mount moves behind the container. * * The permissions cache is module-scoped in permissions.service.ts, so this * instance and the container singleton share one cache — invalidating through - * either flushes both (backupService restores depend on this). + * either flushes both (backup restores depend on this). * * Module-level construction is safe: `db` is the reinitialize-proof Proxy onto * the shared better-sqlite3 singleton. */ const permissions = new PermissionsService(new DatabaseService(db)); -export { PERMISSION_ACTIONS } from './permissions.service'; -export type { PermissionLevel, PermissionAction } from './permissions.service'; - export function invalidatePermissionsCache(): void { permissions.invalidatePermissionsCache(); } -export function getPermissionLevel(actionKey: string): PermissionLevel { - return permissions.getPermissionLevel(actionKey); -} - -export function getAllPermissions(): Record { - return permissions.getAllPermissions(); -} - -export function savePermissions(settings: Record): { skipped: string[] } { - return permissions.savePermissions(settings); -} - export function checkPermission( actionKey: string, userRole: string, diff --git a/server/tests/integration/oauth.test.ts b/server/tests/integration/oauth.test.ts index 8041beeff..3b9e956e9 100644 --- a/server/tests/integration/oauth.test.ts +++ b/server/tests/integration/oauth.test.ts @@ -58,7 +58,21 @@ import { runMigrations } from '../../src/db/migrations'; import { resetTestDb, resetRateLimits } from '../helpers/test-db'; import { createUser } from '../helpers/factories'; import { authCookie } from '../helpers/auth'; -import { createOAuthClient, createAuthCode, getUserByAccessToken } from '../../src/nest/oauth/oauth.bridge'; +import { createOAuthClient, getUserByAccessToken } from '../../src/nest/oauth/oauth.bridge'; +import { OauthService } from '../../src/nest/oauth/oauth.service'; +import { DatabaseService } from '../../src/nest/database/database.service'; +import { AddonsService } from '../../src/nest/addons/addons.service'; +import { AuditService } from '../../src/nest/audit/audit.service'; + +// In production the consent controller writes pending codes through the +// container instance and the SDK-mounted routes read them back through the +// bridge. createAuthCode is not bridged (only the read side is), so the tests +// write codes the same way the consent controller does: through a service +// instance. The pending-code map is module-scoped in oauth.pending-codes.ts, +// so the routes under test see every code written here. +const oauthDbs = new DatabaseService(testDb); +const containerSideOauth = new OauthService(oauthDbs, new AddonsService(oauthDbs), new AuditService(oauthDbs)); +const createAuthCode = containerSideOauth.createAuthCode.bind(containerSideOauth); let nestApp: INestApplication; let app: Application; diff --git a/server/tests/unit/mcp/tools-transit.test.ts b/server/tests/unit/mcp/tools-transit.test.ts index 42382f5d2..b29a3573c 100644 --- a/server/tests/unit/mcp/tools-transit.test.ts +++ b/server/tests/unit/mcp/tools-transit.test.ts @@ -1,6 +1,8 @@ import { runMigrations } from '../../../src/db/migrations'; import { createTables } from '../../../src/db/schema'; -import { invalidatePermissionsCache, savePermissions } from '../../../src/nest/permissions/permissions.bridge'; +import { invalidatePermissionsCache } from '../../../src/nest/permissions/permissions.bridge'; +import { PermissionsService } from '../../../src/nest/permissions/permissions.service'; +import { DatabaseService } from '../../../src/nest/database/database.service'; import { addTripMember, createDay, createTrip, createUser } from '../../helpers/factories'; import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness'; import { resetTestDb } from '../../helpers/test-db'; @@ -44,6 +46,12 @@ import { ReservationsService } from '../../../src/nest/reservations/reservations import type { TransitPlace } from '../../../src/nest/transit/transit.helpers'; import { TransitService } from '../../../src/nest/transit/transit.service'; +// savePermissions is no longer bridged; write through a service instance — the +// permissions cache is module-scoped, so the MCP _shared checkPermission path +// sees the write immediately. +const permissionsService = new PermissionsService(new DatabaseService(testDb)); +const savePermissions = permissionsService.savePermissions.bind(permissionsService); + // The transit tools live on the DI-discovered transit.mcp.ts since the transit // fold; the test registry builds a real TransitService (and injects a real // ReservationsService over the mocked db proxy), so stub the provider methods diff --git a/server/tests/unit/nest/addons.bridge.test.ts b/server/tests/unit/nest/addons.bridge.test.ts index aaf896d82..720df607e 100644 --- a/server/tests/unit/nest/addons.bridge.test.ts +++ b/server/tests/unit/nest/addons.bridge.test.ts @@ -2,12 +2,11 @@ * addons.bridge delegation — one case per bridged export (the permissions * bridge-test precedent). The bridge constructs a module-level AddonsService * over the reinitialize-proof db Proxy, so a real in-memory SQLite exercises - * the byte-identical SQL relocated from services/adminService (`admin-1`), - * including the polarity quirks: isAddonEnabled reads the addons integer - * column, bag tracking is opt-in (=== 'true'), collab flags opt-out - * (!== 'false'). + * the byte-identical SQL relocated from services/adminService (`admin-1`). + * The bag-tracking/collab polarity quirks are pinned on the service itself + * (addons.service.test.ts) — those reads are no longer bridged. */ -import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest'; +import { describe, it, expect, vi, beforeAll } from 'vitest'; // ── DB setup ────────────────────────────────────────────────────────────────── // The db/database mock builds a real in-memory SQLite; the test body reaches it @@ -25,17 +24,13 @@ vi.mock('../../../src/db/database', async () => { import { db as testDb } from '../../../src/db/database'; import { createTables } from '../../../src/db/schema'; import { runMigrations } from '../../../src/db/migrations'; -import { isAddonEnabled, getBagTracking, getCollabFeatures } from '../../../src/nest/addons/addons.bridge'; +import { isAddonEnabled } from '../../../src/nest/addons/addons.bridge'; beforeAll(() => { createTables(testDb); runMigrations(testDb); }); -beforeEach(() => { - testDb.prepare("DELETE FROM app_settings WHERE key IN ('bag_tracking_enabled', 'collab_chat_enabled', 'collab_notes_enabled', 'collab_polls_enabled', 'collab_whatsnext_enabled')").run(); -}); - describe('addons.bridge', () => { it('isAddonEnabled reflects the addons row and is false for an unknown id', () => { testDb @@ -46,16 +41,4 @@ describe('addons.bridge', () => { expect(isAddonEnabled('test-addon')).toBe(false); expect(isAddonEnabled('no-such-addon')).toBe(false); }); - - it('getBagTracking is opt-in: absent row is OFF, only the string true enables', () => { - expect(getBagTracking()).toEqual({ enabled: false }); - testDb.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('bag_tracking_enabled', 'true')").run(); - expect(getBagTracking()).toEqual({ enabled: true }); - }); - - it('getCollabFeatures is opt-out: absent rows are ON, only the string false disables', () => { - expect(getCollabFeatures()).toEqual({ chat: true, notes: true, polls: true, whatsnext: true }); - testDb.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('collab_polls_enabled', 'false')").run(); - expect(getCollabFeatures()).toEqual({ chat: true, notes: true, polls: false, whatsnext: true }); - }); }); diff --git a/server/tests/unit/nest/audit.service.test.ts b/server/tests/unit/nest/audit.service.test.ts index 874295656..16334572a 100644 --- a/server/tests/unit/nest/audit.service.test.ts +++ b/server/tests/unit/nest/audit.service.test.ts @@ -42,7 +42,7 @@ import { DatabaseService } from '../../../src/nest/database/database.service'; import { AuditService } from '../../../src/nest/audit/audit.service'; import { getClientIp } from '../../../src/nest/audit/client-ip'; import { logInfo, logDebug, logError } from '../../../src/nest/audit/audit-log.logger'; -import { writeAudit as bridgeWriteAudit, getClientIp as bridgeGetClientIp, logInfo as bridgeLogInfo } from '../../../src/nest/audit/audit.bridge'; +import { writeAudit as bridgeWriteAudit, getClientIp as bridgeGetClientIp } from '../../../src/nest/audit/audit.bridge'; const svc = new AuditService(new DatabaseService(testDb)); @@ -193,8 +193,4 @@ describe('audit.bridge delegation', () => { it('AUDIT-SVC-016: getClientIp is the same pure helper', () => { expect(bridgeGetClientIp).toBe(getClientIp); }); - - it('AUDIT-SVC-017: the log helpers re-export the logger module', () => { - expect(bridgeLogInfo).toBe(logInfo); - }); }); diff --git a/server/tests/unit/nest/auth.service.test.ts b/server/tests/unit/nest/auth.service.test.ts index 09c527007..05f428d5c 100644 --- a/server/tests/unit/nest/auth.service.test.ts +++ b/server/tests/unit/nest/auth.service.test.ts @@ -929,11 +929,6 @@ describe('auth quirk fixes', () => { // --------------------------------------------------------------------------- describe('auth.bridge delegation', () => { - it('AUTH-BR-001: isDemoUser returns false outside demo mode', () => { - const { user } = createUser(testDb); - expect(authBridge.isDemoUser(user.id)).toBe(false); - }); - it('AUTH-BR-002: verifyMcpToken resolves a freshly created token to its user', () => { const { user } = createUser(testDb); const created = tokens.createMcpToken(user.id, 'bridge-case'); @@ -943,32 +938,10 @@ describe('auth.bridge delegation', () => { expect(authBridge.verifyMcpToken('trek_no_such_token')).toBeNull(); }); - it('AUTH-BR-003: generateToken + verifyJwtToken round-trip through the pv gate', () => { + it('AUTH-BR-003: verifyJwtToken round-trips a service-minted token through the pv gate', () => { const { user } = createUser(testDb); - const token = authBridge.generateToken({ id: user.id }); + const token = svc.generateToken({ id: user.id }); expect(authBridge.verifyJwtToken(token)?.id).toBe(user.id); expect(authBridge.verifyJwtToken('not-a-jwt')).toBeNull(); }); - - it('AUTH-BR-004: resolveAuthToggles returns the container defaults', () => { - vi.stubEnv('OIDC_ONLY', ''); - expect(authBridge.resolveAuthToggles().password_login).toBe(true); - vi.unstubAllEnvs(); - }); - - it('AUTH-BR-005: createWsToken surfaces the 503 when the ephemeral store is down', () => { - const { user } = createUser(testDb); - // createEphemeralToken is mocked to return undefined in this suite. - expect(authBridge.createWsToken(user.id)).toEqual({ error: 'Service unavailable', status: 503 }); - }); - - it('AUTH-BR-006: stripUserForClient re-export strips secrets', () => { - const stripped = authBridge.stripUserForClient({ id: 1, password_hash: 'x' } as never); - expect(stripped).not.toHaveProperty('password_hash'); - }); - - it('AUTH-BR-007: avatarUrl re-export keeps the legacy shape', () => { - expect(authBridge.avatarUrl({ avatar: 'a.png' })).toBe('/uploads/avatars/a.png'); - expect(authBridge.avatarUrl({ avatar: null })).toBeNull(); - }); }); diff --git a/server/tests/unit/nest/permissions.service.test.ts b/server/tests/unit/nest/permissions.service.test.ts index f3eee800a..fd9c32d23 100644 --- a/server/tests/unit/nest/permissions.service.test.ts +++ b/server/tests/unit/nest/permissions.service.test.ts @@ -45,9 +45,6 @@ import { DatabaseService } from '../../../src/nest/database/database.service'; import { PermissionsService, PERMISSION_ACTIONS } from '../../../src/nest/permissions/permissions.service'; import { checkPermission as bridgeCheckPermission, - getPermissionLevel as bridgeGetPermissionLevel, - getAllPermissions as bridgeGetAllPermissions, - savePermissions as bridgeSavePermissions, invalidatePermissionsCache as bridgeInvalidatePermissionsCache, } from '../../../src/nest/permissions/permissions.bridge'; @@ -251,29 +248,17 @@ describe('permissions.bridge delegation', () => { expect(bridgeCheckPermission('trip_delete', 'user', 10, 20, true)).toBe(false); }); - it('PERM-SVC-018: savePermissions via the bridge writes the row and reports skips', () => { - const result = bridgeSavePermissions({ trip_edit: 'trip_member', bogus: 'trip_member' }); - expect(result.skipped).toEqual(['bogus']); - const row = testDb.prepare('SELECT value FROM app_settings WHERE key = ?').get('perm_trip_edit') as { value: string }; - expect(row.value).toBe('trip_member'); - }); - - it('PERM-SVC-019: getPermissionLevel/getAllPermissions delegate through the bridge', () => { - expect(bridgeGetPermissionLevel('trip_create')).toBe('everybody'); - const all = bridgeGetAllPermissions(); - expect(all.trip_delete).toBe('trip_owner'); - expect(Object.keys(all)).toHaveLength(PERMISSION_ACTIONS.length); - }); - it('PERM-SVC-020: the cache is module-scoped — shared by the DI and bridge instances', () => { - // Save through the DI instance; the bridge instance sees it immediately. + // Save through the DI instance; the bridge instance sees it immediately + // (checkPermission for a plain member flips with the stored level). svc.savePermissions({ trip_edit: 'trip_member' }); - expect(bridgeGetPermissionLevel('trip_edit')).toBe('trip_member'); + expect(bridgeCheckPermission('trip_edit', 'user', 10, 20, true)).toBe(true); // Raw SQL write, then invalidate through the BRIDGE — the DI instance // must serve the fresh value (backup-restore relies on exactly this). testDb.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)').run('perm_trip_edit', 'trip_owner'); expect(svc.getPermissionLevel('trip_edit')).toBe('trip_member'); // still cached bridgeInvalidatePermissionsCache(); expect(svc.getPermissionLevel('trip_edit')).toBe('trip_owner'); + expect(bridgeCheckPermission('trip_edit', 'user', 10, 20, true)).toBe(false); }); }); diff --git a/server/tests/websocket/connection.test.ts b/server/tests/websocket/connection.test.ts index f8aab7498..a58e6ff2b 100644 --- a/server/tests/websocket/connection.test.ts +++ b/server/tests/websocket/connection.test.ts @@ -55,7 +55,16 @@ import { resetTestDb, resetRateLimits } from '../helpers/test-db'; import { createUser, createTrip } from '../helpers/factories'; import { authCookie } from '../helpers/auth'; import { createEphemeralToken } from '../../src/nest/auth/ephemeral-tokens'; -import { createWsToken } from '../../src/nest/auth/auth.bridge'; +import { TokenService } from '../../src/nest/tokens/token.service'; +import { DatabaseService } from '../../src/nest/database/database.service'; +import { EphemeralTokenService } from '../../src/nest/auth/ephemeral-token.service'; + +// The gateway consumes ws-tokens through its injected TokenService; the +// ephemeral store is module-scoped on purpose, so a directly-constructed +// instance mints tokens the app under test accepts (TokenService is a leaf — +// its own unit suite constructs it the same way). +const tokenService = new TokenService(new DatabaseService(testDb), new EphemeralTokenService()); +const createWsToken = tokenService.createWsToken.bind(tokenService); let server: http.Server; let wsUrl: string;