refactor(trip-membership): domains ask TripAccessService who may see a trip

Thirty services, controllers, MCP tools and RPC handlers outside the trips
domain injected TripsRepository for one call, findAccessible. They now
inject TripAccessService from the trip-membership leaf, in the same
constructor slot under the same field name; it answers with the same
access row (or undefined) and binds the id exactly as given, so each
caller keeps its own coercion and its own 404, 403 or tool error.
This commit is contained in:
Maurice
2026-10-10 13:11:10 +02:00
parent 734cbc2a94
commit fe35e26313
70 changed files with 219 additions and 195 deletions
+2 -1
View File
@@ -21,6 +21,7 @@ import type { UsersRepository } from '../../../src/db/repositories/Users.reposit
import type { TripMembershipService } from '../../../src/nest/trip-membership/trip-membership.service';
import type { RpcRequest, RpcError } from '../../../src/nest/plugins/protocol/envelope';
import { makeDeps } from '../../helpers/rpc-host-deps';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
const req = (method: string, params: Record<string, unknown> = {}): RpcRequest => ({ k: 'req', id: 'x', method, params });
@@ -55,7 +56,7 @@ function build(opts: { addonOn?: boolean; canEdit?: boolean; missing?: boolean;
);
// The leaf membership read replaced the deleted trips.bridge for listMine.
const membership = { listAccessibleTripIds: vi.fn(() => [1, 2]) } as unknown as TripMembershipService;
const rpc = new CostsRpc(budget, db, realtime, guards, membership);
const rpc = new CostsRpc(budget, new TripAccessService(db), realtime, guards, membership);
const host = (...grants: string[]) =>
new PluginRpcHost('p', new Set(grants.length ? grants : ['db:read:costs', 'db:write:costs']), makeDeps(), createTestPluginRegistry([rpc]));
return { budget, realtime, host };
@@ -60,6 +60,7 @@ import { Days } from '../../../src/db/entities/Days.entity';
import { Places } from '../../../src/db/entities/Places.entity';
import { RoadtripVias } from '../../../src/db/entities/RoadtripVias.entity';
import { Users } from '../../../src/db/entities/Users.entity';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
let svc: AssignmentsService;
beforeAll(async () => {
@@ -67,7 +68,7 @@ beforeAll(async () => {
svc = new AssignmentsService(
// Plan 4 Task 2 — AssignmentsService's own canAccessTrip delegate is now
// TripsRepository.findAccessible, in the same constructor slot.
await createTestTripsRepo(testDb),
new TripAccessService(await createTestTripsRepo(testDb)),
new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)),
realtime,
new QueryHelpersService(await createTestTagsRepo(testDb), await createTestPlaceRatingsRepo(testDb), await createTestAssignmentParticipantsRepo(testDb)),
@@ -39,13 +39,14 @@ import { createTestUnitOfWork, createTestAppSettingsRepo, createTestDayNotesRepo
import { sharedTestOrm } from '../../helpers/test-uow';
import { findRow } from '../../helpers/factories/rows';
import { DayNotes } from '../../../src/db/entities/DayNotes.entity';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
let svc: DayNotesService;
beforeAll(async () => {
// Plan 4 Task 2 — DayNotesService's own canAccessTrip delegate is now
// TripsRepository.findAccessible, in the same constructor slot.
svc = new DayNotesService(
await createTestTripsRepo(testDb),
new TripAccessService(await createTestTripsRepo(testDb)),
new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)),
new RealtimeService(),
await createTestDayNotesRepo(testDb),
@@ -279,7 +280,7 @@ describe('DayNotesService.canEdit', () => {
it('DAYNOTE-SVC-090 asks for day_edit and flags a non-owner as shared', async () => {
const checkPermission = vi.fn(() => true);
const permissions = { checkPermission } as unknown as PermissionsService;
const withStub = new DayNotesService(await createTestTripsRepo(testDb), permissions, new RealtimeService(), await createTestDayNotesRepo(testDb), await createTestDaysRepo(testDb));
const withStub = new DayNotesService(new TripAccessService(await createTestTripsRepo(testDb)), permissions, new RealtimeService(), await createTestDayNotesRepo(testDb), await createTestDaysRepo(testDb));
const trip = { id: 1, user_id: 1 } as never;
expect(await withStub.canEdit(trip, { id: 1, role: 'user' } as never)).toBe(true);
@@ -62,6 +62,7 @@ import { ReservationEndpoints } from '../../../src/db/entities/ReservationEndpoi
import { RoadtripDayBoundaries } from '../../../src/db/entities/RoadtripDayBoundaries.entity';
import { Trips } from '../../../src/db/entities/Trips.entity';
import { legacyBoundIntegerText } from '../../../src/nest/common/row-id';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
let days: DaysService;
let journey: JourneyDomainService;
@@ -96,7 +97,7 @@ beforeAll(async () => {
await createTestJourneyPhotosRepo(testDb), await createTestJourneyEntryPhotosRepo(testDb), await createTestPlacesRepo(testDb),
);
assignments = new AssignmentsService(
await createTestTripsRepo(testDb), permissions, realtime, queryHelpers, journey,
new TripAccessService(await createTestTripsRepo(testDb)), permissions, realtime, queryHelpers, journey,
await createTestUnitOfWork(testDb),
await createTestDayAssignmentsRepo(testDb),
await createTestAssignmentParticipantsRepo(testDb),
@@ -108,7 +109,7 @@ beforeAll(async () => {
);
accommodations = new AccommodationsService(
permissions, realtime, assignments, await createTestUnitOfWork(testDb),
await createTestTripsRepo(testDb),
new TripAccessService(await createTestTripsRepo(testDb)),
await createTestDayAccommodationsRepo(testDb),
await createTestDayAssignmentsRepo(testDb),
await createTestPlacesRepo(testDb),
+2 -1
View File
@@ -79,6 +79,7 @@ import { EphemeralTokenService } from '../../../src/nest/auth-core/ephemeral-tok
import type { EntityManager } from '@mikro-orm/core';
import { Users } from '../../../src/db/entities/Users.entity';
import { UserSessions } from '../../../src/db/entities/UserSessions.entity';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
const storageDelete = vi.fn();
const storageStub = { delete: storageDelete } as unknown as import('../../../src/nest/storage/storage.service').StorageService;
@@ -114,7 +115,7 @@ beforeAll(async () => {
tripFilesRepo = await createTestTripFilesRepo(testDb);
fileLinksRepo = await createTestFileLinksRepo(testDb);
svc = new FilesService(
await createTestTripsRepo(testDb),
new TripAccessService(await createTestTripsRepo(testDb)),
permissionsStub,
new RealtimeService(),
new EphemeralTokenService(),
+17 -16
View File
@@ -43,6 +43,7 @@ import type { JourneyDomainService } from '../../../src/nest/journey/journey-dom
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
import type { AppSettingsRepository } from '../../../src/db/repositories/AppSettings.repository';
import type { User } from '../../../src/types';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
interface FakeSocket extends TrekWebSocket {
sent: string[];
@@ -88,7 +89,7 @@ const canAccessJourney = vi.fn((journeyId: number) => (journeyId === 4 ? null :
const journeys = { canAccessJourney } as unknown as JourneyDomainService;
async function connect(url: string) {
const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings);
const ws = socket();
await gw.handleConnection(ws, { url } as never);
return { gw, ws };
@@ -158,7 +159,7 @@ describe('RealtimeGateway handshake', () => {
});
it('WSGW-007b: survives an upgrade request with no url, rather than throwing at it', async () => {
const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings);
const ws = socket();
await expect(gw.handleConnection(ws, {} as never)).resolves.not.toThrow();
expect(ws.closedWith).toEqual([4001, 'Authentication required']);
@@ -198,7 +199,7 @@ describe('RealtimeGateway handshake', () => {
const throwingUsers = {
findForWsHandshake: () => { throw new Error('db exploded'); },
} as unknown as UsersRepository;
const gw = new RealtimeGateway(db, tokens, journeys, throwingUsers, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, throwingUsers, appSettings);
const ws = socket();
await gw.handleConnection(ws, { url: '/ws?token=x' } as never);
@@ -211,7 +212,7 @@ describe('RealtimeGateway handshake', () => {
const throwingUsers = {
findForWsHandshake: () => { throw 'db string'; }, // non-Error throw, pinning the ternary's String(err) fallback
} as unknown as UsersRepository;
const gw = new RealtimeGateway(db, tokens, journeys, throwingUsers, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, throwingUsers, appSettings);
const ws = socket();
await gw.handleConnection(ws, { url: '/ws?token=x' } as never);
@@ -248,7 +249,7 @@ describe('RealtimeGateway heartbeat', () => {
alive.isAlive = true;
const stale = socket();
stale.isAlive = false;
const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings);
gw.afterInit({ clients: new Set([alive, stale]) } as never);
vi.advanceTimersByTime(30_000);
@@ -269,7 +270,7 @@ describe('RealtimeGateway heartbeat', () => {
try {
const ws = socket();
ws.isAlive = true;
const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings);
gw.afterInit({ clients: new Set([ws]) } as never);
gw.onModuleDestroy();
@@ -378,7 +379,7 @@ describe('book rooms', () => {
let nextJourney = 100;
async function joined(journeyId: number) {
const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings);
const ws = socket();
registerSocket(ws, { id: 3, username: 'm' } as User);
const reply = await gw.handleBookJoin({ journeyId }, ws);
@@ -394,7 +395,7 @@ describe('book rooms', () => {
/* Same shape as the trip room's refusal, and for the same reason. */
it('WSGW-BOOK-002: refuses a journey the user cannot see, and adds nobody', async () => {
const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings);
const ws = socket();
registerSocket(ws, { id: 3, username: 'm' } as User);
@@ -445,7 +446,7 @@ describe('book pointers', () => {
async function pair() {
const journeyId = nextJourney++;
const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings);
const mine = socket();
const theirs = socket();
registerSocket(mine, { id: 3, username: 'm' } as User);
@@ -510,7 +511,7 @@ describe('book messages that are refused', () => {
let nextJourney = 300;
it('WSGW-BOOK-006: a join without a journey id is ignored', async () => {
const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings);
const ws = socket();
registerSocket(ws, { id: 3, username: 'm' } as User);
@@ -520,7 +521,7 @@ describe('book messages that are refused', () => {
/* An unauthenticated socket never got as far as the registry. */
it('WSGW-BOOK-007: a join from a socket with no user is ignored', async () => {
const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings);
const j = nextJourney++;
expect(await gw.handleBookJoin({ journeyId: j }, socket())).toBeUndefined();
@@ -528,7 +529,7 @@ describe('book messages that are refused', () => {
});
it('WSGW-BOOK-008: a journey id that is not a number is refused, not joined', async () => {
const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings);
const ws = socket();
registerSocket(ws, { id: 3, username: 'm' } as User);
@@ -537,7 +538,7 @@ describe('book messages that are refused', () => {
});
it('WSGW-BOOK-009: a leave without a journey id is ignored', () => {
const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings);
const ws = socket();
registerSocket(ws, { id: 3, username: 'm' } as User);
@@ -546,7 +547,7 @@ describe('book messages that are refused', () => {
/* Leaving a room nobody is in must not create one on the way out. */
it('WSGW-BOOK-010: leaving a book that was never joined does nothing', () => {
const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings);
const ws = socket();
registerSocket(ws, { id: 3, username: 'm' } as User);
const j = nextJourney++;
@@ -556,7 +557,7 @@ describe('book messages that are refused', () => {
});
it('WSGW-CUR-005: a pointer without a journey id reaches nobody', async () => {
const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings);
const mine = socket();
const theirs = socket();
registerSocket(mine, { id: 3, username: 'm' } as User);
@@ -571,7 +572,7 @@ describe('book messages that are refused', () => {
});
it('WSGW-CUR-006: a pointer from a socket with no user reaches nobody', async () => {
const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings);
const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings);
const theirs = socket();
registerSocket(theirs, { id: 4, username: 'o' } as User);
const j = nextJourney++;
@@ -7,6 +7,7 @@ import type { TripsRepository } from '../../../src/db/repositories/Trips.reposit
import type { PermissionsService } from '../../../src/nest/permissions/permissions.service';
import type { AddonsService } from '../../../src/nest/addons/addons.service';
import type { User } from '../../../src/types';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
const user = { id: 3, role: 'user' } as User;
const photo = (name = 'r.jpg') => ({ originalname: name, buffer: Buffer.from('x') } as Express.Multer.File);
@@ -17,7 +18,7 @@ function make(over: { readsImages?: boolean; trip?: unknown; addon?: boolean; al
const c = new ReceiptScanController(
{ startReceipt } as unknown as ImportJobsService,
{ readsImages: vi.fn(async () => over.readsImages ?? true) } as unknown as LlmParseService,
{ findAccessible: vi.fn(async () => ('trip' in over ? over.trip : { user_id: 5 })) } as unknown as TripsRepository,
new TripAccessService({ findAccessible: vi.fn(async () => ('trip' in over ? over.trip : { user_id: 5 })) } as unknown as TripsRepository),
{ checkPermission } as unknown as PermissionsService,
{ isAddonEnabled: vi.fn(async () => over.addon ?? true) } as unknown as AddonsService,
);
@@ -31,6 +31,7 @@ import { bookendAssignmentId, type RoadtripStop } from '@trek/shared/roadtrip';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { legacyBoundIntegerText } from '../../../src/nest/common/row-id';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
/** Every leg an hour and 60 km, whatever it joins. */
function hourlyRouter() {
@@ -61,7 +62,7 @@ function hourlyRouter() {
/** The repositories the service reads through, all on the worker's one test handle. */
async function planRepos() {
return [
await createTestTripsRepo(db),
new TripAccessService(await createTestTripsRepo(db)),
await createTestDaysRepo(db),
await createTestDayAssignmentsRepo(db),
await createTestDayAccommodationsRepo(db),
+3 -2
View File
@@ -37,6 +37,7 @@ import { TodoItems } from '../../../src/db/entities/TodoItems.entity';
import { TodoCategoryAssignees } from '../../../src/db/entities/TodoCategoryAssignees.entity';
import { Users } from '../../../src/db/entities/Users.entity';
import { createTestTodoItemsRepo, createTestTodoCategoryAssigneesRepo } from '../../helpers/todo-repos';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
let svc: TodoService;
let todoItemsRepoDirect: Awaited<ReturnType<typeof createTestTodoItemsRepo>>;
@@ -48,7 +49,7 @@ beforeAll(async () => {
todoItemsRepoDirect, await createTestTodoCategoryAssigneesRepo(testDb),
// Plan 4 Task 2 — TodoService's own canAccessTrip delegate is now
// TripsRepository.findAccessible, a new trailing constructor param.
await createTestTripsRepo(testDb),
new TripAccessService(await createTestTripsRepo(testDb)),
// Plan 4 Task 3 — DatabaseService.rosterUserIds inlined onto
// TripMembersRepository.rosterUserIds directly.
await createTestTripMembersRepo(testDb),
@@ -404,7 +405,7 @@ describe('TodoService.canEdit', () => {
const withStub = new TodoService(
permissions, new RealtimeService(), await createTestUnitOfWork(testDb),
await createTestTodoItemsRepo(testDb), await createTestTodoCategoryAssigneesRepo(testDb),
await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb),
new TripAccessService(await createTestTripsRepo(testDb)), await createTestTripMembersRepo(testDb),
);
const trip = { id: 1, user_id: 1 } as never;
@@ -36,6 +36,7 @@ import { createTestUnitOfWork, createTestAppSettingsRepo, createTestTripsRepo, c
import { sharedTestOrm } from '../../helpers/test-uow';
import { countRows, updateRows } from '../../helpers/factories/rows';
import { TripInviteTokens } from '../../../src/db/entities/TripInviteTokens.entity';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
let svc: TripInviteService;
beforeAll(async () => {
@@ -43,7 +44,7 @@ beforeAll(async () => {
svc = new TripInviteService(
// Plan 4 Task 2 — TripInviteService's own canAccessTrip delegate is now
// TripsRepository.findAccessible, in the same constructor slot.
await createTestTripsRepo(testDb),
new TripAccessService(await createTestTripsRepo(testDb)),
new PermissionsService(await createTestAppSettingsRepo(testDb), uow),
new TripMembershipService(await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb)),
uow,
@@ -108,6 +108,7 @@ import { createTestCollectionPlacesRepo } from '../../helpers/test-uow';
import { noGoogleQuota } from '../../helpers/google-quota';
import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos';
import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// Real sibling services over the same in-memory DB — the aggregation runs the
// actual SQL of every domain it fans out to, so a shape change downstream shows
@@ -200,7 +201,7 @@ const buildReadModel = async (tripsRepo: TripsRepository, roster: TripMembersSer
await createTestTodoItemsRepo(testDb), await createTestTodoCategoryAssigneesRepo(testDb),
// Plan 4 Task 2 — TodoService's own canAccessTrip delegate is now
// TripsRepository.findAccessible, a new trailing constructor param.
await createTestTripsRepo(testDb),
new TripAccessService(await createTestTripsRepo(testDb)),
// Plan 4 Task 3 — DatabaseService.rosterUserIds inlined onto
// TripMembersRepository.rosterUserIds directly.
await createTestTripMembersRepo(testDb),
@@ -208,7 +209,7 @@ const buildReadModel = async (tripsRepo: TripsRepository, roster: TripMembersSer
new FilesService(
// Plan 4 Task 2 — FilesService's own canAccessTrip delegate is now
// TripsRepository.findAccessible, in the same constructor slot.
await createTestTripsRepo(testDb),
new TripAccessService(await createTestTripsRepo(testDb)),
new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)),
new RealtimeService(),
new EphemeralTokenService(),
+3 -2
View File
@@ -153,6 +153,7 @@ import { TripMembers } from '../../../src/db/entities/TripMembers.entity';
import { Trips } from '../../../src/db/entities/Trips.entity';
import { Users } from '../../../src/db/entities/Users.entity';
import { legacyBoundIntegerText } from '../../../src/nest/common/row-id';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
const orm = () => sharedTestOrm(testDb);
@@ -322,13 +323,13 @@ beforeAll(async () => {
new TodoService(
new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new RealtimeService(), await createTestUnitOfWork(testDb),
await createTestTodoItemsRepo(testDb), await createTestTodoCategoryAssigneesRepo(testDb),
await createTestTripsRepo(testDb),
new TripAccessService(await createTestTripsRepo(testDb)),
await createTestTripMembersRepo(testDb),
),
new FilesService(
// Plan 4 Task 2 — FilesService's own canAccessTrip delegate is now
// TripsRepository.findAccessible, in the same constructor slot.
await createTestTripsRepo(testDb),
new TripAccessService(await createTestTripsRepo(testDb)),
new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)),
new RealtimeService(),
new EphemeralTokenService(),
@@ -26,6 +26,7 @@ import type { AddonsService } from '../../../src/nest/addons/addons.service';
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
import type { RpcRequest, RpcError } from '../../../src/nest/plugins/protocol/envelope';
import { makeDeps } from '../../helpers/rpc-host-deps';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
const req = (method: string, params: Record<string, unknown> = {}): RpcRequest => ({ k: 'req', id: 'x', method, params });
@@ -81,7 +82,7 @@ function build(addonOn: boolean) {
// SV8 — Plan 3i: DemoService.isDemoUserId, the shared demo-gate primitive.
{ isDemoUserId: async () => false } as never),
new CollectionsRpc(spyService(calls, 'collections'), guards),
new CostsRpc(spyService(calls, 'budget'), db, { broadcast: vi.fn() } as never, guards, spyService(calls, 'membership')),
new CostsRpc(spyService(calls, 'budget'), new TripAccessService(db), { broadcast: vi.fn() } as never, guards, spyService(calls, 'membership')),
]);
const granted = new Set([
'db:read:collab', 'db:write:collab', 'db:read:journal', 'db:write:journal',
@@ -22,6 +22,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { DayScheduleController } from '../../../src/nest/plugins/contributions/day-schedule.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { DaysRepository } from '../../../src/db/repositories/Days.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -32,7 +33,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) {
} as unknown as PluginHooks;
// CT1 (Plan 3j Task 5) — the day-id-set read is now DaysRepository.listIdsByTrip.
const days = { listIdsByTrip: vi.fn(async () => tripDays.value.map((d) => d.id)) } as unknown as DaysRepository;
return { c: new DayScheduleController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, days), runtime };
return { c: new DayScheduleController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository), days), runtime };
}
const item = (over: Record<string, unknown> = {}) => ({ id: 's1', dayId: 10, label: 'Charging', ...over });
@@ -22,6 +22,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { DayTintsController } from '../../../src/nest/plugins/contributions/day-tints.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { DaysRepository } from '../../../src/db/repositories/Days.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -32,7 +33,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) {
} as unknown as PluginHooks;
// CT2 (Plan 3j Task 5) — the day-id-set read is now DaysRepository.listIdsByTrip.
const days = { listIdsByTrip: vi.fn(async () => tripDays.value.map((d) => d.id)) } as unknown as DaysRepository;
return { c: new DayTintsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, days), runtime };
return { c: new DayTintsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository), days), runtime };
}
const tint = (over: Record<string, unknown> = {}) => ({ dayId: 10, tone: 'success', ...over });
@@ -17,6 +17,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { MapLayersController } from '../../../src/nest/plugins/contributions/map-layers.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { TripsRepository } from '../../../src/db/repositories/Trips.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -25,7 +26,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) {
providersOf: vi.fn(() => providers),
mapLayers: vi.fn(async (id: string) => invoke(id)),
} as unknown as PluginHooks;
return { c: new MapLayersController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime };
return { c: new MapLayersController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime };
}
const line = (n = 3, over: Record<string, unknown> = {}) => ({
type: 'polyline',
@@ -16,6 +16,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { MapMarkersController } from '../../../src/nest/plugins/contributions/map-markers.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { TripsRepository } from '../../../src/db/repositories/Trips.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -24,7 +25,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) {
providersOf: vi.fn(() => providers),
mapMarkers: vi.fn(async (id: string) => invoke(id)),
} as unknown as PluginHooks;
return { c: new MapMarkersController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime };
return { c: new MapMarkersController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime };
}
const mk = (over: Record<string, unknown> = {}) => ({ id: 'm1', lat: 48.85, lng: 2.35, ...over });
@@ -17,6 +17,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { PdfSectionsController } from '../../../src/nest/plugins/contributions/pdf-sections.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { TripsRepository } from '../../../src/db/repositories/Trips.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -25,7 +26,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) {
providersOf: vi.fn(() => providers),
pdfSections: vi.fn(async (id: string) => invoke(id)),
} as unknown as PluginHooks;
return { c: new PdfSectionsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime };
return { c: new PdfSectionsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime };
}
const sec = (over: Record<string, unknown> = {}) => ({ title: 'Weather', ...over });
@@ -15,6 +15,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { PlaceDetailsController } from '../../../src/nest/plugins/contributions/place-details.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { PlacesRepository } from '../../../src/db/repositories/Places.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -26,7 +27,7 @@ function controller(over: Partial<PluginHooks> = {}) {
} as unknown as PluginHooks;
// CT7 (Plan 3j Task 5) — the place's owning trip id is now Places.repository.ts#findTripId.
const places = { findTripId: vi.fn(async (placeId: number) => placeTrip(placeId)?.trip_id) } as unknown as PlacesRepository;
return { c: new PlaceDetailsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, places), runtime };
return { c: new PlaceDetailsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository), places), runtime };
}
describe('PlaceDetailsController', () => {
@@ -22,6 +22,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { PluginRoutesController } from '../../../src/nest/plugins/contributions/plugin-routes.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { PluginsRepository } from '../../../src/db/repositories/Plugins.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -36,7 +37,7 @@ function controller(invoke: () => unknown, providers = ['ev-plug']) {
const plugins = {
findCapabilities: vi.fn(async () => (capabilitiesRow.value === undefined ? null : capabilitiesRow.value)),
} as unknown as PluginsRepository;
return { c: new PluginRoutesController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, plugins), runtime };
return { c: new PluginRoutesController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository), plugins), runtime };
}
const wp = (n = 3) => Array.from({ length: n }, (_, i) => ({ lat: 48 + i * 0.1, lng: 2 + i * 0.1 }));
const goodRoute = (n = 3, over: Record<string, unknown> = {}) => ({
@@ -18,6 +18,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { TripCardContributionsController } from '../../../src/nest/plugins/contributions/trip-card-contributions.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { TripsRepository } from '../../../src/db/repositories/Trips.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -26,7 +27,7 @@ function controller(invoke: (id: string, args: unknown[]) => unknown, providers
providersOf: vi.fn(() => providers),
tripCards: vi.fn(async (id: string, _h: string, _fn: string, args: unknown[]) => invoke(id, args)),
} as unknown as PluginHooks;
return { c: new TripCardContributionsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime };
return { c: new TripCardContributionsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime };
}
const badge = (over: Record<string, unknown> = {}) => ({ tripId: 1, id: 'b1', label: 'Visa', ...over });
@@ -10,6 +10,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { TripWarningsController } from '../../../src/nest/plugins/contributions/trip-warnings.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { TripsRepository } from '../../../src/db/repositories/Trips.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -20,7 +21,7 @@ function controller(over: Partial<PluginHooks> = {}) {
id === 'p2' ? [{ level: 'error', message: 'Day 3 is overpacked', dayId: 3 }] : [{ level: 'warning', message: 'Museum closed Mon', placeId: 7 }]),
...over,
} as unknown as PluginHooks;
return { c: new TripWarningsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime };
return { c: new TripWarningsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime };
}
describe('TripWarningsController', () => {
@@ -17,6 +17,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { ViewContributionsController } from '../../../src/nest/plugins/contributions/view-contributions.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { TripsRepository } from '../../../src/db/repositories/Trips.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -25,7 +26,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) {
providersOf: vi.fn(() => providers),
tableContributions: vi.fn(async (id: string) => invoke(id)),
} as unknown as PluginHooks;
return { c: new ViewContributionsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime };
return { c: new ViewContributionsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime };
}
const col = (over: Record<string, unknown> = {}) => ({ kind: 'column', entityId: 1, id: 'c1', label: 'X', ...over });
const act = (over: Record<string, unknown> = {}) => ({ kind: 'action', entityId: 1, id: 'a1', label: 'Go', target: { kind: 'frame', sub: '/ui' }, ...over });