diff --git a/server/src/db/repositories/MaintenanceRepository.ts b/server/src/db/repositories/MaintenanceRepository.ts index 3f2877024..d7f529c1b 100644 --- a/server/src/db/repositories/MaintenanceRepository.ts +++ b/server/src/db/repositories/MaintenanceRepository.ts @@ -15,15 +15,13 @@ import type { EntityManager } from '@mikro-orm/core'; * all — so `connection.execute()` (the last tier in the program's query-API * order) is the correct tool, not a shortcut around it. * - * Constructed directly (`new MaintenanceRepository(em)`), never through Nest - * DI: its only two callers — `backup.impl.ts#createBackup` and - * `demo/demo-reset.ts` (`resetDemoUser`/`saveBaseline`) — are plain function - * modules, not Nest providers, and obtain the `EntityManager` via - * `RequestContext.getEntityManager()` (the same static accessor MikroORM's - * own per-request middleware and `CronRegistrarService`'s `wrappedTick` - * populate) rather than constructor injection. No module registers this - * class as a provider — nothing today asks Nest's DI container to resolve - * it. + * Provided through Nest DI by `MaintenanceModule` (`nest/database/`), a + * factory over the context-resolving global `EntityManager`; the readiness + * probe and `UserCleanupService` inject it. `backup.impl.ts` and + * `demo/demo-reset.ts` still build one over the request's EntityManager until + * they move behind an injected provider of their own. The class stays free of + * Nest decorators like every other file under `db/`, so a test can still + * build one over its own `EntityManager`. */ export class MaintenanceRepository { constructor(private readonly em: EntityManager) {} diff --git a/server/src/nest/auth/auth.module.ts b/server/src/nest/auth/auth.module.ts index 2a8b3bfe1..73ad3564d 100644 --- a/server/src/nest/auth/auth.module.ts +++ b/server/src/nest/auth/auth.module.ts @@ -34,6 +34,7 @@ import { UserCleanupService } from './user-cleanup.service'; import { WebauthnConfigService } from './webauthn-config.service'; import { AppConfigModule } from '../app-config/app-config.module'; import { BudgetModule } from '../budget/budget.module'; +import { MaintenanceModule } from '../database/maintenance.module'; import { AuditModule } from '../audit/audit.module'; import { MailerModule } from '../notifications/mailer/mailer.module'; import { PermissionsModule } from '../permissions/permissions.module'; @@ -84,6 +85,8 @@ import { AllowedFileTypesModule } from '../files/allowed-file-types.module'; StorageModule, AllowedFileTypesModule, EphemeralTokenModule, RateLimitModule, AuditModule, PermissionsModule, TripMembershipModule, MailerModule, AppConfigModule, TokensModule, BudgetModule, + // MaintenanceRepository: UserCleanupService's UC1 plugin-table erasure. + MaintenanceModule, // AppSettings/Users: AuthService/UserProfileService each pass their own // AppSettingsRepository/UsersRepository to instance-api-keys.ts's // resolveApiKey/readInstanceApiKey/writeInstanceApiKey now (Plan 3a Task diff --git a/server/src/nest/auth/user-cleanup.service.ts b/server/src/nest/auth/user-cleanup.service.ts index fdcb2bb68..48aa522ae 100644 --- a/server/src/nest/auth/user-cleanup.service.ts +++ b/server/src/nest/auth/user-cleanup.service.ts @@ -1,7 +1,6 @@ import fs from 'node:fs'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { EntityManager } from '@mikro-orm/core'; import { MaintenanceRepository } from '../../db/repositories/MaintenanceRepository'; // Injected since BudgetModule dropped its AuthModule import (BudgetMcp's demo // guard reads RuntimeEnvService + the users table now), which un-closed the @@ -48,11 +47,11 @@ import { enqueueHookUserDataErasures } from '../plugins/user-erasure-enqueue'; @Injectable() export class UserCleanupService { constructor( - // Plan 4 Task 4: `DatabaseService` dropped — UC1 (below) now goes - // through `MaintenanceRepository`, the rule-4-permitted raw-connection - // home, constructed from this directly-injected `EntityManager` + // Plan 4 Task 4: `DatabaseService` dropped. UC1 (below) goes through + // `MaintenanceRepository`, the rule-4-permitted raw-connection home, + // injected over the context-resolving EntityManager // (`uow.transactional` below always leaves a request context active). - private readonly em: EntityManager, + private readonly maintenance: MaintenanceRepository, private readonly budget: BudgetService, private readonly uow: UnitOfWork, @InjectRepository(Users) private readonly usersRepo: UsersRepository, @@ -92,8 +91,9 @@ export class UserCleanupService { * deleted `DatabaseService` onto `MaintenanceRepository.deletePluginUserData` * (rule 4's `connection.execute()` escape hatch), not onto a repository * this domain would otherwise own. This method always runs inside - * `deleteUserCompletely`'s `uow.transactional(...)` — `this.em` is safe to - * pass as-is (MikroORM resolves the active transactional fork through + * `deleteUserCompletely`'s `uow.transactional(...)`. The injected + * repository's EntityManager is safe as-is (MikroORM resolves the active + * transactional fork through * `EntityManager#getContext()`/`#getTransactionContext()` regardless of * which EM instance you call them on, via its own `TransactionContext` * async-local storage); `MaintenanceRepository.deletePluginUserData` is the @@ -116,7 +116,7 @@ export class UserCleanupService { * here, now via the same repository. */ async erasePluginUserData(userId: number): Promise { - await new MaintenanceRepository(this.em).deletePluginUserData(userId); // UC1 — Plan 4 Task 4 + await this.maintenance.deletePluginUserData(userId); // UC1, Plan 4 Task 4 try { const rows = await this.pluginsRepo.listIdsAndPermissions(); // UC2 — Plan 4 Task 8a const installed = new Set(rows.map((r) => r.id)); diff --git a/server/src/nest/database/maintenance.module.ts b/server/src/nest/database/maintenance.module.ts new file mode 100644 index 000000000..b15981d58 --- /dev/null +++ b/server/src/nest/database/maintenance.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { EntityManager } from '@mikro-orm/core'; +import { MaintenanceRepository } from '../../db/repositories/MaintenanceRepository'; + +/** + * Provides `MaintenanceRepository`, the home of the few whole-database + * statements no entity can express (the readiness ping, the WAL checkpoint, + * `VACUUM INTO`, the plugin-table erasure). + * + * A factory over the context-resolving global `EntityManager`, the same one + * Nest injects into every service, so the repository resolves the request or + * transaction fork that is active when a method runs. The class itself stays + * free of Nest, like every other file under `db/`. + */ +@Module({ + providers: [ + { + provide: MaintenanceRepository, + useFactory: (em: EntityManager) => new MaintenanceRepository(em), + inject: [EntityManager], + }, + ], + exports: [MaintenanceRepository], +}) +export class MaintenanceModule {} diff --git a/server/src/nest/health/features.controller.ts b/server/src/nest/health/features.controller.ts index a7eb06fa5..76ced46c1 100644 --- a/server/src/nest/health/features.controller.ts +++ b/server/src/nest/health/features.controller.ts @@ -1,5 +1,4 @@ import { Controller, Get, Res } from '@nestjs/common'; -import { EntityManager } from '@mikro-orm/core'; import type { Response } from 'express'; import { MaintenanceRepository } from '../../db/repositories/MaintenanceRepository'; import { KitineraryExtractorService } from '../booking-import/kitinerary-extractor.service'; @@ -15,7 +14,7 @@ export class FeaturesController { constructor( private readonly extractor: KitineraryExtractorService, private readonly addons: AddonsService, - private readonly em: EntityManager, + private readonly maintenance: MaintenanceRepository, ) {} /** The container/uptime probe. The forced-HTTPS redirect and HSTS exempt this @@ -37,7 +36,7 @@ export class FeaturesController { async ready(@Res() res: Response): Promise { res.setHeader('Cache-Control', 'no-store, must-revalidate'); try { - await new MaintenanceRepository(this.em).ping(); + await this.maintenance.ping(); res.json({ status: 'ready' }); } catch { res.status(503).json({ status: 'unavailable' }); diff --git a/server/src/nest/health/health.module.ts b/server/src/nest/health/health.module.ts index e362b8d4a..113f28b1b 100644 --- a/server/src/nest/health/health.module.ts +++ b/server/src/nest/health/health.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { FeaturesController } from './features.controller'; import { KitineraryExtractorModule } from '../booking-import/kitinerary-extractor.module'; import { AddonsModule } from '../addons/addons.module'; +import { MaintenanceModule } from '../database/maintenance.module'; /** Server capability reporting. `GET /api/health/features` tells the client which * optional server-side features are usable, so it can hide the affordances it @@ -9,7 +10,7 @@ import { AddonsModule } from '../addons/addons.module'; * redirect and HSTS exempt it by path inside globalMiddleware, so it answers * plain-HTTP probes no matter where the route itself is registered. */ @Module({ - imports: [KitineraryExtractorModule, AddonsModule], + imports: [KitineraryExtractorModule, AddonsModule, MaintenanceModule], controllers: [FeaturesController], }) export class HealthModule {} diff --git a/server/tests/helpers/mcp-test-controllers.ts b/server/tests/helpers/mcp-test-controllers.ts index a1cef31a0..d0be121e3 100644 --- a/server/tests/helpers/mcp-test-controllers.ts +++ b/server/tests/helpers/mcp-test-controllers.ts @@ -188,6 +188,7 @@ import { noGoogleQuota } from './google-quota'; import { createTestPushSubscriptionsRepo } from './notifications-repos'; import { createTestToursRepo } from './tours-repos'; import { createTestBudgetSettlementsRepo } from './budget-repos'; +import { MaintenanceRepository } from '../../src/db/repositories/MaintenanceRepository'; /** * Hand-wired counterpart of the boot-time discovery in McpRegistryService, @@ -232,7 +233,7 @@ export async function createMcpTestRegistry(): Promise { permissionsService, new TripMembershipService(await createTestTripsRepo(db), await createTestTripMembersRepo(db)), new WebauthnConfigService(appSettings), - new UserCleanupService(mcpOrm.em, budgetService, await createTestUnitOfWork(db), usersRepo, await createTestTripMembersRepo(db), await createTestBudgetItemsRepo(db), await createTestBudgetSettlementsRepo(db), await createTestJourneyShareTokensRepo(db), await createTestJourneysRepo(db), await createTestJourneyEntriesRepo(db), await createTestJourneyContributorsRepo(db), await createTestShareTokensRepo(db), await createTestPluginsRepo(db), await createTestPluginUserErasureQueueRepo(db)), + new UserCleanupService(new MaintenanceRepository(mcpOrm.em), budgetService, await createTestUnitOfWork(db), usersRepo, await createTestTripMembersRepo(db), await createTestBudgetItemsRepo(db), await createTestBudgetSettlementsRepo(db), await createTestJourneyShareTokensRepo(db), await createTestJourneysRepo(db), await createTestJourneyEntriesRepo(db), await createTestJourneyContributorsRepo(db), await createTestShareTokensRepo(db), await createTestPluginsRepo(db), await createTestPluginUserErasureQueueRepo(db)), new MailerService(usersRepo, settingsRepo, appSettings), new EphemeralTokenService(), new AllowedFileTypesService(appSettings), await createTestUnitOfWork(db), @@ -340,7 +341,7 @@ export async function createMcpTestRegistry(): Promise { daysService, accommodationsService, assignmentsService, await createTestUnitOfWork(db), await createTestDaysRepo(db), await createTestDayAccommodationsRepo(db), await createTestRoadtripDayBoundariesRepo(db), await createTestTripsRepo(db), ); - const membersService = new TripMembersService(budgetService, new UserCleanupService(mcpOrm.em, budgetService, await createTestUnitOfWork(db), usersRepo, await createTestTripMembersRepo(db), await createTestBudgetItemsRepo(db), await createTestBudgetSettlementsRepo(db), await createTestJourneyShareTokensRepo(db), await createTestJourneysRepo(db), await createTestJourneyEntriesRepo(db), await createTestJourneyContributorsRepo(db), await createTestShareTokensRepo(db), await createTestPluginsRepo(db), await createTestPluginUserErasureQueueRepo(db)), permissionsService, realtimeService, notificationsStub(), await createTestUnitOfWork(db), await createTestTripsRepo(db), await createTestTripMembersRepo(db), usersRepo); + const membersService = new TripMembersService(budgetService, new UserCleanupService(new MaintenanceRepository(mcpOrm.em), budgetService, await createTestUnitOfWork(db), usersRepo, await createTestTripMembersRepo(db), await createTestBudgetItemsRepo(db), await createTestBudgetSettlementsRepo(db), await createTestJourneyShareTokensRepo(db), await createTestJourneysRepo(db), await createTestJourneyEntriesRepo(db), await createTestJourneyContributorsRepo(db), await createTestShareTokensRepo(db), await createTestPluginsRepo(db), await createTestPluginUserErasureQueueRepo(db)), permissionsService, realtimeService, notificationsStub(), await createTestUnitOfWork(db), await createTestTripsRepo(db), await createTestTripMembersRepo(db), usersRepo); const tripsService = new TripsService( reservationsService, daysService, diff --git a/server/tests/helpers/plugin-host.ts b/server/tests/helpers/plugin-host.ts index 6150479d3..18aabec60 100644 --- a/server/tests/helpers/plugin-host.ts +++ b/server/tests/helpers/plugin-host.ts @@ -142,6 +142,7 @@ import { createTestJourneyShareTokensRepo } from './journey-share-repos'; import { noGoogleQuota } from './google-quota'; import { createTestToursRepo } from './tours-repos'; import { createTestBudgetSettlementsRepo } from './budget-repos'; +import { MaintenanceRepository } from '../../src/db/repositories/MaintenanceRepository'; /** * Hand-wired counterpart of the PluginsModule DI graph for no-Nest tests @@ -302,7 +303,7 @@ export async function createPluginRpcHostFactory(db: Database.Database): Promise const trips = new TripsService(reservations, days, permissions, budget, vacay, realtime, unsplash, generalStorage, await createTestUnitOfWork(db), (await sharedTestOrm(db)).em, new SettingsService(await createTestUnitOfWork(db), appSettings, await createTestSettingsRepo(db))); // Plan 4 Task 4: UserCleanupService's UC1 now goes through a directly-injected // EntityManager (MaintenanceRepository.deletePluginUserData), not DatabaseService. - const members = new TripMembersService(budget, new UserCleanupService((await sharedTestOrm(db)).em, budget, await createTestUnitOfWork(db), usersRepo, await createTestTripMembersRepo(db), await createTestBudgetItemsRepo(db), await createTestBudgetSettlementsRepo(db), await createTestJourneyShareTokensRepo(db), await createTestJourneysRepo(db), await createTestJourneyEntriesRepo(db), await createTestJourneyContributorsRepo(db), await createTestShareTokensRepo(db), await createTestPluginsRepo(db), await createTestPluginUserErasureQueueRepo(db)), permissions, realtime, notificationsStub(), await createTestUnitOfWork(db), await createTestTripsRepo(db), await createTestTripMembersRepo(db), usersRepo); + const members = new TripMembersService(budget, new UserCleanupService(new MaintenanceRepository((await sharedTestOrm(db)).em), budget, await createTestUnitOfWork(db), usersRepo, await createTestTripMembersRepo(db), await createTestBudgetItemsRepo(db), await createTestBudgetSettlementsRepo(db), await createTestJourneyShareTokensRepo(db), await createTestJourneysRepo(db), await createTestJourneyEntriesRepo(db), await createTestJourneyContributorsRepo(db), await createTestShareTokensRepo(db), await createTestPluginsRepo(db), await createTestPluginUserErasureQueueRepo(db)), permissions, realtime, notificationsStub(), await createTestUnitOfWork(db), await createTestTripsRepo(db), await createTestTripMembersRepo(db), usersRepo); // Plan 3j Task 1 — PluginGuards' own role lookup (PG3/PG4) now goes // through UsersRepository.getRole; `usersRepo` above is the same // sharedTestOrm-backed repository every other service in this file uses. diff --git a/server/tests/unit/nest/admin.service.test.ts b/server/tests/unit/nest/admin.service.test.ts index d3a099918..d207d5359 100644 --- a/server/tests/unit/nest/admin.service.test.ts +++ b/server/tests/unit/nest/admin.service.test.ts @@ -111,6 +111,7 @@ import { createTestJourneysRepo, createTestJourneyEntriesRepo, createTestJourney import { createTestJourneyShareTokensRepo } from '../../helpers/journey-share-repos'; import { createTestPushSubscriptionsRepo } from '../../helpers/notifications-repos'; import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos'; +import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository'; const realtime = new RealtimeService(); @@ -130,7 +131,7 @@ let auditLogRepo: AuditLogRepository; beforeAll(async () => { webauthn = new WebauthnConfigService(await createTestAppSettingsRepo(testDb)); permissions = new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)); - userCleanup = new UserCleanupService((await sharedTestOrm(testDb)).em, new BudgetService(permissions, new ExchangeRatesService(), realtime, await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)); + userCleanup = new UserCleanupService(new MaintenanceRepository((await sharedTestOrm(testDb)).em), new BudgetService(permissions, new ExchangeRatesService(), realtime, await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)); auth = new AuthService( permissions, new TripMembershipService(await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb)), webauthn, userCleanup, new MailerService(await createTestUsersRepo(testDb), await createTestSettingsRepo(testDb), await createTestAppSettingsRepo(testDb)), new EphemeralTokenService(), new AllowedFileTypesService(await createTestAppSettingsRepo(testDb)), await createTestUnitOfWork(testDb), await createTestAppSettingsRepo(testDb), await createTestUsersRepo(testDb), await createTestInviteTokensRepo(testDb), await createTestMcpTokensRepo(testDb), diff --git a/server/tests/unit/nest/admin.version-notification.test.ts b/server/tests/unit/nest/admin.version-notification.test.ts index f24637003..54a7bee75 100644 --- a/server/tests/unit/nest/admin.version-notification.test.ts +++ b/server/tests/unit/nest/admin.version-notification.test.ts @@ -87,6 +87,7 @@ import { createTestJourneysRepo, createTestJourneyEntriesRepo, createTestJourney import { createTestJourneyShareTokensRepo } from '../../helpers/journey-share-repos'; import { createTestPushSubscriptionsRepo } from '../../helpers/notifications-repos'; import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos'; +import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository'; const realtime = new RealtimeService(); @@ -104,7 +105,7 @@ let svc: AdminService; beforeAll(async () => { webauthn = new WebauthnConfigService(await createTestAppSettingsRepo(testDb)); permissions = new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)); - userCleanup = new UserCleanupService((await sharedTestOrm(testDb)).em, new BudgetService(permissions, new ExchangeRatesService(), realtime, await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)); + userCleanup = new UserCleanupService(new MaintenanceRepository((await sharedTestOrm(testDb)).em), new BudgetService(permissions, new ExchangeRatesService(), realtime, await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)); auth = new AuthService( permissions, new TripMembershipService(await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb)), webauthn, userCleanup, new MailerService(await createTestUsersRepo(testDb), await createTestSettingsRepo(testDb), await createTestAppSettingsRepo(testDb)), new EphemeralTokenService(), new AllowedFileTypesService(await createTestAppSettingsRepo(testDb)), await createTestUnitOfWork(testDb), await createTestAppSettingsRepo(testDb), await createTestUsersRepo(testDb), await createTestInviteTokensRepo(testDb), await createTestMcpTokensRepo(testDb), diff --git a/server/tests/unit/nest/auth.service.test.ts b/server/tests/unit/nest/auth.service.test.ts index ee59ada4d..3394d62e6 100644 --- a/server/tests/unit/nest/auth.service.test.ts +++ b/server/tests/unit/nest/auth.service.test.ts @@ -100,6 +100,7 @@ import { createTestBudgetItemsRepo } from '../../helpers/files-repos'; import { createTestJourneysRepo, createTestJourneyEntriesRepo, createTestJourneyContributorsRepo } from '../../helpers/journey-repos'; import { createTestJourneyShareTokensRepo } from '../../helpers/journey-share-repos'; import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos'; +import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository'; // MailerService is injected since the notifications fold — a stub instead of a // module mock. sendPasswordResetEmail is the only thing auth reaches for. @@ -120,7 +121,7 @@ beforeAll(async () => { new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), membershipStub, new WebauthnConfigService(await createTestAppSettingsRepo(testDb)), - new UserCleanupService((await sharedTestOrm(testDb)).em, new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), + new UserCleanupService(new MaintenanceRepository((await sharedTestOrm(testDb)).em), new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), mailerStub, new EphemeralTokenService(), new AllowedFileTypesService(await createTestAppSettingsRepo(testDb)), await createTestUnitOfWork(testDb), diff --git a/server/tests/unit/nest/budget.service.db.test.ts b/server/tests/unit/nest/budget.service.db.test.ts index aa5fc7054..b9cdb2d5c 100644 --- a/server/tests/unit/nest/budget.service.db.test.ts +++ b/server/tests/unit/nest/budget.service.db.test.ts @@ -78,6 +78,7 @@ import { createTestBudgetItemsRepo } from '../../helpers/files-repos'; import { createTestJourneysRepo, createTestJourneyEntriesRepo, createTestJourneyContributorsRepo } from '../../helpers/journey-repos'; import { createTestJourneyShareTokensRepo } from '../../helpers/journey-share-repos'; import { BudgetItemMembers } from '../../../src/db/entities/BudgetItemMembers.entity'; +import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository'; // Guest fixtures come from TripMembersService since the trip split (they were on // TripsService before, and on the deleted services/tripService before that); @@ -114,7 +115,7 @@ beforeAll(async () => { ); membersSvc = new TripMembersService( budget, - new UserCleanupService(dbsEm!, budget, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), + new UserCleanupService(new MaintenanceRepository(dbsEm!), budget, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new RealtimeService(), notificationsStub(), diff --git a/server/tests/unit/nest/features.controller.test.ts b/server/tests/unit/nest/features.controller.test.ts index 8c513713d..2e33d44cd 100644 --- a/server/tests/unit/nest/features.controller.test.ts +++ b/server/tests/unit/nest/features.controller.test.ts @@ -6,6 +6,8 @@ import { KitineraryExtractorService } from '../../../src/nest/booking-import/kit import { BookingImportModule } from '../../../src/nest/booking-import/booking-import.module'; import { expectRegisteredController, expectRegisteredProvider } from '../../helpers/module-providers'; import { ADDON_IDS } from '../../../src/addons'; +import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository'; +import { MaintenanceModule } from '../../../src/nest/database/maintenance.module'; type Extractor = Pick; type Addons = { isAddonEnabled: (id: string) => boolean }; @@ -62,7 +64,7 @@ describe('FeaturesController (GET /api/health/features)', () => { expectRegisteredProvider(KitineraryExtractorModule, KitineraryExtractorService); const healthImports = Reflect.getMetadata('imports', HealthModule) as unknown[]; expect(Array.isArray(healthImports)).toBe(true); - expect(healthImports).toEqual(expect.arrayContaining([KitineraryExtractorModule])); + expect(healthImports).toEqual(expect.arrayContaining([KitineraryExtractorModule, MaintenanceModule])); expect(healthImports).not.toContain(BookingImportModule); }); @@ -95,7 +97,7 @@ describe('FeaturesController (GET /api/health/features)', () => { const execute = vi.fn().mockResolvedValue([]); const em = { getConnection: () => ({ execute }), getContext: () => em }; const extractor = { isAvailable: vi.fn(() => true) }; - const controller = new FeaturesController(extractor as Extractor as KitineraryExtractorService, {} as never, em as never); + const controller = new FeaturesController(extractor as Extractor as KitineraryExtractorService, {} as never, new MaintenanceRepository(em as never)); const ok = response(); await controller.ready(ok as never); diff --git a/server/tests/unit/nest/oidc.service.test.ts b/server/tests/unit/nest/oidc.service.test.ts index 23966cee5..ab5481d57 100644 --- a/server/tests/unit/nest/oidc.service.test.ts +++ b/server/tests/unit/nest/oidc.service.test.ts @@ -121,6 +121,7 @@ import { createTestJourneysRepo, createTestJourneyEntriesRepo, createTestJourney import { createTestJourneyShareTokensRepo } from '../../helpers/journey-share-repos'; import { createTestPushSubscriptionsRepo } from '../../helpers/notifications-repos'; import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos'; +import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository'; // MailerService is injected since the notifications fold — a stub instead of a // module mock. sendPasswordResetEmail is the only thing auth reaches for. @@ -140,7 +141,7 @@ beforeAll(async () => { new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), membership, new WebauthnConfigService(await createTestAppSettingsRepo(testDb)), - new UserCleanupService((await sharedTestOrm(testDb)).em, new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), + new UserCleanupService(new MaintenanceRepository((await sharedTestOrm(testDb)).em), new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), mailerStub, new EphemeralTokenService(), new AllowedFileTypesService(await createTestAppSettingsRepo(testDb)), await createTestUnitOfWork(testDb), diff --git a/server/tests/unit/nest/passkey.service.test.ts b/server/tests/unit/nest/passkey.service.test.ts index 0773e22a8..ac3610d12 100644 --- a/server/tests/unit/nest/passkey.service.test.ts +++ b/server/tests/unit/nest/passkey.service.test.ts @@ -106,6 +106,7 @@ import { createTestJourneysRepo, createTestJourneyEntriesRepo, createTestJourney import { createTestJourneyShareTokensRepo } from '../../helpers/journey-share-repos'; import { createTestPushSubscriptionsRepo } from '../../helpers/notifications-repos'; import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos'; +import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository'; // MailerService is injected since the notifications fold — a stub instead of a // module mock. sendPasswordResetEmail is the only thing auth reaches for. @@ -134,7 +135,7 @@ beforeAll(async () => { new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new TripMembershipService(await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb)), new WebauthnConfigService(await createTestAppSettingsRepo(testDb)), - new UserCleanupService((await sharedTestOrm(testDb)).em, new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), + new UserCleanupService(new MaintenanceRepository((await sharedTestOrm(testDb)).em), new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), mailerStub, new EphemeralTokenService(), new AllowedFileTypesService(await createTestAppSettingsRepo(testDb)), await createTestUnitOfWork(testDb), diff --git a/server/tests/unit/nest/trip-members.service.test.ts b/server/tests/unit/nest/trip-members.service.test.ts index c53877b74..5ae44fa41 100644 --- a/server/tests/unit/nest/trip-members.service.test.ts +++ b/server/tests/unit/nest/trip-members.service.test.ts @@ -71,6 +71,7 @@ import type { TripsRepository } from '../../../src/db/repositories/Trips.reposit import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; import type { TripMembersRepository } from '../../../src/db/repositories/TripMembers.repository'; import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos'; +import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository'; // Plan 3c Task 0b: `dbsEm` is resolved once, at the top of the `beforeAll` // below — `canAccessTrip`/`isOwner`/`rosterUserIds`/`getPlaceWithTags` @@ -97,7 +98,7 @@ beforeAll(async () => { budgetSvc = new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))); roster = new TripMembersService( budgetSvc, - new UserCleanupService(dbsEm!, budgetSvc, await createTestUnitOfWork(testDb), usersRepo, await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), + new UserCleanupService(new MaintenanceRepository(dbsEm!), budgetSvc, await createTestUnitOfWork(testDb), usersRepo, await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new RealtimeService(), notificationsStub(notifySend), await createTestUnitOfWork(testDb), tripsRepo, tripMembersRepo, usersRepo, diff --git a/server/tests/unit/nest/trip-read-model.service.test.ts b/server/tests/unit/nest/trip-read-model.service.test.ts index 4d5b60cbf..9d6176271 100644 --- a/server/tests/unit/nest/trip-read-model.service.test.ts +++ b/server/tests/unit/nest/trip-read-model.service.test.ts @@ -111,6 +111,7 @@ import { createTestShareTokensRepo, createTestPluginsRepo, createTestPluginUserE 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'; // 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 @@ -176,7 +177,7 @@ beforeAll(async () => { await createTestBudgetItemsRepo(testDb), await createTestCollectionPlacesRepo(testDb), ); - membersSvc = new TripMembersService(budgetSvc, new UserCleanupService((await sharedTestOrm(testDb)).em, budgetSvc, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new RealtimeService(), notificationsStub(), await createTestUnitOfWork(testDb), await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb), await createTestUsersRepo(testDb)); + membersSvc = new TripMembersService(budgetSvc, new UserCleanupService(new MaintenanceRepository((await sharedTestOrm(testDb)).em), budgetSvc, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new RealtimeService(), notificationsStub(), await createTestUnitOfWork(testDb), await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb), await createTestUsersRepo(testDb)); }); const buildReadModel = async (tripsRepo: TripsRepository, roster: TripMembersService = membersSvc) => diff --git a/server/tests/unit/nest/trips.service.test.ts b/server/tests/unit/nest/trips.service.test.ts index b78f336cf..30856cd3b 100644 --- a/server/tests/unit/nest/trips.service.test.ts +++ b/server/tests/unit/nest/trips.service.test.ts @@ -131,6 +131,7 @@ import { createTestCollectionPlacesRepo } from '../../helpers/test-uow'; import { SettingsService } from '../../../src/nest/settings/settings.service'; import { noGoogleQuota } from '../../helpers/google-quota'; import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos'; +import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository'; // Real sibling services over the same in-memory DB — updateTrip's date-shift // resyncs and the summary/bundle aggregation run their actual SQL. @@ -238,7 +239,7 @@ beforeAll(async () => { (await sharedTestOrm(testDb)).em, new SettingsService(await createTestUnitOfWork(testDb), await createTestAppSettingsRepo(testDb), await createTestSettingsRepo(testDb)), ); - membersSvc = new TripMembersService(budgetSvc, new UserCleanupService(dbsEm!, budgetSvc, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new RealtimeService(), notificationsStub(), await createTestUnitOfWork(testDb), await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb), await createTestUsersRepo(testDb)); + membersSvc = new TripMembersService(budgetSvc, new UserCleanupService(new MaintenanceRepository(dbsEm!), budgetSvc, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new RealtimeService(), notificationsStub(), await createTestUnitOfWork(testDb), await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb), await createTestUsersRepo(testDb)); readModelSvc = new TripReadModelService( await createTestTripsRepo(testDb), membersSvc, daysSvc, accommodationsSvc, budgetSvc, new PackingService( diff --git a/server/tests/unit/nest/user-cleanup.service.test.ts b/server/tests/unit/nest/user-cleanup.service.test.ts index 7efe05595..1f471e722 100644 --- a/server/tests/unit/nest/user-cleanup.service.test.ts +++ b/server/tests/unit/nest/user-cleanup.service.test.ts @@ -58,18 +58,19 @@ import { createTestJourneysRepo, createTestJourneyEntriesRepo, createTestJourney import { createTestJourneyShareTokensRepo } from '../../helpers/journey-share-repos'; import { createTestShareTokensRepo, createTestPluginsRepo, createTestPluginUserErasureQueueRepo } from '../../helpers/share-repos'; import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos'; +import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository'; let em: EntityManager; let budget: BudgetService; let svc: UserCleanupService; beforeAll(async () => { - // Plan 4 Task 4: UserCleanupService's own DatabaseService param is gone — - // UC1 goes through MaintenanceRepository, built from a directly-injected - // EntityManager instead. + // Plan 4 Task 4: UserCleanupService's own DatabaseService param is gone. + // UC1 goes through MaintenanceRepository, which Nest injects (built here + // over the test ORM's EntityManager, as MaintenanceModule's factory does). em = (await sharedTestOrm(testDb)).em; budget = new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))); svc = new UserCleanupService( - em, budget, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), + new MaintenanceRepository(em), budget, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), // Plan 4 Task 1 constructor-ripple: UC4's repository. await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), @@ -181,7 +182,7 @@ describe('erasePluginUserData', () => { // journey repositories (UC7-10) still never reach their tables from this // call — stubs are enough for those. const slimSvc = new UserCleanupService( - (await sharedTestOrm(slim)).em, budget, await createTestUnitOfWork(slim), await createTestUsersRepo(slim), + new MaintenanceRepository((await sharedTestOrm(slim)).em), budget, await createTestUnitOfWork(slim), await createTestUsersRepo(slim), {} as unknown as TripMembersRepository, {} as unknown as BudgetItemsRepository, {} as unknown as BudgetSettlementsRepository, {} as unknown as JourneyShareTokensRepository, {} as unknown as JourneysRepository, {} as unknown as JourneyEntriesRepository, {} as unknown as JourneyContributorsRepository, @@ -342,7 +343,7 @@ describe('deleteUserCompletely', () => { const deleteByIdSpy = vi.spyOn(usersRepo, 'deleteById').mockRejectedValue(new Error('boom')); try { await expect(new UserCleanupService( - em, budget, await createTestUnitOfWork(testDb), usersRepo, + new MaintenanceRepository(em), budget, await createTestUnitOfWork(testDb), usersRepo, await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb),