mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
refactor(server): shrink the seam bridges to their live exports
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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<AuditService['writeAudit']>[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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<OauthService['createOAuthC
|
||||
return oauth.createOAuthClient(...args);
|
||||
}
|
||||
|
||||
export function createAuthCode(params: Parameters<OauthService['createAuthCode']>[0]) {
|
||||
return oauth.createAuthCode(params);
|
||||
}
|
||||
|
||||
export function consumeAuthCode(code: string) {
|
||||
return oauth.consumeAuthCode(code);
|
||||
}
|
||||
|
||||
@@ -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<string, PermissionLevel> {
|
||||
return permissions.getAllPermissions();
|
||||
}
|
||||
|
||||
export function savePermissions(settings: Record<string, string>): { skipped: string[] } {
|
||||
return permissions.savePermissions(settings);
|
||||
}
|
||||
|
||||
export function checkPermission(
|
||||
actionKey: string,
|
||||
userRole: string,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user