mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
refactor(photos): trek photos, memories access and cache sweeps through repositories
This commit is contained in:
@@ -6,7 +6,9 @@ import { JourneyDomainService } from '../../src/nest/journey/journey-domain.serv
|
||||
import { PermissionsService } from '../../src/nest/permissions/permissions.service';
|
||||
import { QueryHelpersService } from '../../src/nest/query-helpers/query-helpers.service';
|
||||
import { RealtimeService } from '../../src/nest/realtime/realtime.service';
|
||||
import { TrekPhotosRepository } from '../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../src/db/entities/TripPhotos.entity';
|
||||
import {
|
||||
createTestUnitOfWork, createTestAppSettingsRepo, createTestTagsRepo, createTestPlaceRatingsRepo,
|
||||
createTestAssignmentParticipantsRepo, createTestDayAssignmentsRepo, createTestDaysRepo, createTestPlacesRepo,
|
||||
@@ -41,10 +43,11 @@ export async function makeAccommodationsService(conn: Database): Promise<Accommo
|
||||
export async function accommodationsOver(dbs: DatabaseService): Promise<AccommodationsService> {
|
||||
const permissions = new PermissionsService(await createTestAppSettingsRepo(dbs.connection), await createTestUnitOfWork(dbs.connection));
|
||||
const realtime = new RealtimeService();
|
||||
const t = await sharedTestOrm(dbs.connection);
|
||||
const assignments = new AssignmentsService(
|
||||
dbs, permissions, realtime,
|
||||
new QueryHelpersService(await createTestTagsRepo(dbs.connection), await createTestPlaceRatingsRepo(dbs.connection), await createTestAssignmentParticipantsRepo(dbs.connection)),
|
||||
new JourneyDomainService(dbs, realtime, new TrekPhotosRepository(dbs), await createTestUnitOfWork(dbs.connection)),
|
||||
new JourneyDomainService(dbs, realtime, new TrekPhotoRegistrationService(t.repo(TrekPhotos), t.repo(TripPhotos), dbs), await createTestUnitOfWork(dbs.connection)),
|
||||
await createTestUnitOfWork(dbs.connection),
|
||||
await createTestDayAssignmentsRepo(dbs.connection),
|
||||
await createTestAssignmentParticipantsRepo(dbs.connection),
|
||||
|
||||
@@ -62,7 +62,12 @@ import { QueryHelpersService } from '../../src/nest/query-helpers/query-helpers.
|
||||
import { JourneyMcp } from '../../src/nest/journey/journey.mcp';
|
||||
import { JourneyDomainService } from '../../src/nest/journey/journey-domain.service';
|
||||
import { JourneyShareService } from '../../src/nest/journey/journey-share.service';
|
||||
import { TrekPhotosRepository } from '../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../src/db/entities/TripPhotos.entity';
|
||||
import { TripAlbumLinks } from '../../src/db/entities/TripAlbumLinks.entity';
|
||||
import { Trips } from '../../src/db/entities/Trips.entity';
|
||||
import { TrekPhotoCacheMeta } from '../../src/db/entities/TrekPhotoCacheMeta.entity';
|
||||
import { UnsplashService } from '../../src/nest/unsplash/unsplash.service';
|
||||
import { UserCleanupService } from '../../src/nest/auth/user-cleanup.service';
|
||||
import { WebauthnConfigService } from '../../src/nest/auth/webauthn-config.service';
|
||||
@@ -202,7 +207,7 @@ export async function createMcpTestRegistry(): Promise<McpRegistry> {
|
||||
// and its on-disk set only work if all three readers see the same maps.
|
||||
const placePhotoCache = new PlacePhotoCacheService(dbService, makeStorageFixture('photos/google/').storage, await createTestGooglePlacePhotoMetaRepo(dbService.connection), await createTestPlacesRepo(dbService.connection));
|
||||
const mapsService = new MapsService(dbService, placePhotoCache, appSettings, usersRepo);
|
||||
const journeyDomain = new JourneyDomainService(dbService, realtimeService, new TrekPhotosRepository(dbService), await createTestUnitOfWork(dbService.connection));
|
||||
const journeyDomain = new JourneyDomainService(dbService, realtimeService, new TrekPhotoRegistrationService(mcpOrm.repo(TrekPhotos), mcpOrm.repo(TripPhotos), dbService), await createTestUnitOfWork(dbService.connection));
|
||||
// The last three were previously omitted, which left them `undefined` at
|
||||
// runtime — silently fine while nothing called them, a TypeError the moment
|
||||
// the journey skeleton hooks landed on the place write paths. tsconfig.tests.json
|
||||
@@ -280,10 +285,11 @@ export async function createMcpTestRegistry(): Promise<McpRegistry> {
|
||||
// (which asks them when and where it was taken). Built for real rather than
|
||||
// stubbed: an empty provider registry would make the backfill answer "unknown
|
||||
// provider" for every id and hide a wiring mistake behind a caught error.
|
||||
const immichService = new ImmichService(dbService, new AuditService(auditLogRepo, usersRepo), new MemoriesAccessService(dbService), generalStorage);
|
||||
const synologyService = new SynologyService(dbService, new MemoriesAccessService(dbService), notificationsStub());
|
||||
const trekPhotos = new TrekPhotosRepository(dbService);
|
||||
const captureBackfill = new PhotoCaptureBackfillService(new PhotoResolverService(trekPhotos, new ThumbnailService(addonsService, generalStorage, dbService), new TrekPhotoCacheService(dbService, generalStorage), new PhotoProviderRegistry([new ImmichPhotoProvider(immichService), new SynologyPhotoProvider(synologyService)]), generalStorage), trekPhotos, generalStorage);
|
||||
const memoriesAccess = new MemoriesAccessService(dbService, mcpOrm.repo(TripPhotos), mcpOrm.repo(TrekPhotos), mcpOrm.repo(TripAlbumLinks), mcpOrm.repo(Trips));
|
||||
const immichService = new ImmichService(dbService, new AuditService(auditLogRepo, usersRepo), memoriesAccess, generalStorage);
|
||||
const synologyService = new SynologyService(dbService, memoriesAccess, notificationsStub());
|
||||
const trekPhotos = new TrekPhotoRegistrationService(mcpOrm.repo(TrekPhotos), mcpOrm.repo(TripPhotos), dbService);
|
||||
const captureBackfill = new PhotoCaptureBackfillService(new PhotoResolverService(trekPhotos, new ThumbnailService(addonsService, generalStorage, mcpOrm.repo(TrekPhotos)), new TrekPhotoCacheService(mcpOrm.repo(TrekPhotoCacheMeta), generalStorage), new PhotoProviderRegistry([new ImmichPhotoProvider(immichService), new SynologyPhotoProvider(synologyService)]), generalStorage), trekPhotos, generalStorage);
|
||||
return createTestRegistry(
|
||||
[
|
||||
new TagsMcp(new TagsService(await createTestTagsRepo(dbService.connection)), authService),
|
||||
|
||||
@@ -69,7 +69,9 @@ import { EphemeralTokenService } from '../../src/nest/auth/ephemeral-token.servi
|
||||
import { UserCleanupService } from '../../src/nest/auth/user-cleanup.service';
|
||||
import { UnsplashService } from '../../src/nest/unsplash/unsplash.service';
|
||||
import { PlacePhotoCacheService } from '../../src/nest/place-photos/place-photo-cache.service';
|
||||
import { TrekPhotosRepository } from '../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../src/db/entities/TripPhotos.entity';
|
||||
import { RuntimeEnvService } from '../../src/nest/app-config/runtime-env.service';
|
||||
import { makeStorageFixture } from './storage-fixture';
|
||||
import {
|
||||
@@ -129,7 +131,7 @@ export async function createPluginRpcHostFactory(dbs: DatabaseService): Promise<
|
||||
);
|
||||
const photoCache = new PlacePhotoCacheService(dbs, makeStorageFixture('photos/google/').storage, await createTestGooglePlacePhotoMetaRepo(dbs.connection), await createTestPlacesRepo(dbs.connection));
|
||||
const unsplash = new UnsplashService(appSettings, usersRepo, new RuntimeEnvService(), generalStorage);
|
||||
const journey = new JourneyDomainService(dbs, realtime, new TrekPhotosRepository(dbs), await createTestUnitOfWork(dbs.connection));
|
||||
const journey = new JourneyDomainService(dbs, realtime, new TrekPhotoRegistrationService((await sharedTestOrm(dbs.connection)).repo(TrekPhotos), (await sharedTestOrm(dbs.connection)).repo(TripPhotos), dbs), await createTestUnitOfWork(dbs.connection));
|
||||
const collections = new CollectionsService(dbs, permissions, realtime, notificationsStub(), generalStorage, await createTestUnitOfWork(dbs.connection));
|
||||
const atlas = new AtlasService(dbs, await createTestUnitOfWork(dbs.connection));
|
||||
const dayNotes = new DayNotesService(dbs, permissions, realtime);
|
||||
|
||||
@@ -1170,15 +1170,27 @@ describe('Synology SSRF blocked error handling', () => {
|
||||
|
||||
// ── Passphrase persistence fixes ─────────────────────────────────────────────
|
||||
|
||||
import { TrekPhotosRepository } from '../../src/nest/photos/trek-photos.repository';
|
||||
import { DatabaseService } from '../../src/nest/database/database.service';
|
||||
import { db as trekDb } from '../../src/db/database';
|
||||
import { MikroORM } from '@mikro-orm/core';
|
||||
import { withRequestContext } from '../../src/nest/database/request-context';
|
||||
import { TrekPhotoRegistrationService } from '../../src/nest/photos/trek-photos.repository';
|
||||
|
||||
// Was photos.bridge, which existed for consumers outside the container and had
|
||||
// none left. The repository is what it delegated to.
|
||||
const trekPhotos = new TrekPhotosRepository(new DatabaseService(trekDb));
|
||||
const getOrCreateTrekPhoto = (...a: Parameters<TrekPhotosRepository['getOrCreate']>) => trekPhotos.getOrCreate(...a);
|
||||
const deleteTrekPhotoIfOrphan = (id: number) => trekPhotos.deleteIfOrphan(id);
|
||||
// none left. The repository is what it delegated to. Resolved off the real,
|
||||
// DI-wired container (`nestApp`, built in the file's own `beforeAll` above)
|
||||
// rather than hand-constructed — `TrekPhotoRegistrationService` now takes ORM
|
||||
// repositories, not a bare `DatabaseService` (Plan 3e Task 6). These helpers
|
||||
// call it directly, with no HTTP request around them (unlike the real
|
||||
// `syncSynologyAlbum` call chain this pins, which always runs inside one) —
|
||||
// `withRequestContext` supplies the same per-call EntityManager fork a real
|
||||
// request's `MikroOrmMiddleware` would.
|
||||
let trekPhotos: TrekPhotoRegistrationService;
|
||||
let orm: MikroORM;
|
||||
beforeAll(() => {
|
||||
trekPhotos = nestApp.get(TrekPhotoRegistrationService);
|
||||
orm = nestApp.get(MikroORM);
|
||||
});
|
||||
const getOrCreateTrekPhoto = (...a: Parameters<TrekPhotoRegistrationService['getOrCreate']>) => withRequestContext(orm, () => trekPhotos.getOrCreate(...a));
|
||||
const deleteTrekPhotoIfOrphan = (id: number) => withRequestContext(orm, () => trekPhotos.deleteIfOrphan(id));
|
||||
import { decrypt_api_key } from '../../src/nest/common/crypto/apiKeyCrypto';
|
||||
|
||||
describe('trek_photos passphrase healing (SYNO-090)', () => {
|
||||
|
||||
@@ -54,11 +54,13 @@ import { AssignmentsService } from '../../../src/nest/assignments/assignments.se
|
||||
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
|
||||
import { QueryHelpersService } from '../../../src/nest/query-helpers/query-helpers.service';
|
||||
import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service';
|
||||
import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import {
|
||||
createTestUnitOfWork, createTestAppSettingsRepo, createTestTagsRepo, createTestPlaceRatingsRepo,
|
||||
createTestAssignmentParticipantsRepo, createTestDayAssignmentsRepo, createTestDaysRepo, createTestPlacesRepo,
|
||||
createTestTripMembersRepo, createTestDatabaseService, createTestRoadtripViasRepo,
|
||||
createTestTripMembersRepo, createTestDatabaseService, createTestRoadtripViasRepo, sharedTestOrm,
|
||||
} from '../../helpers/test-uow';
|
||||
|
||||
let svc: AssignmentsService;
|
||||
@@ -77,7 +79,7 @@ beforeAll(async () => {
|
||||
new QueryHelpersService(await createTestTagsRepo(dbs.connection), await createTestPlaceRatingsRepo(dbs.connection), await createTestAssignmentParticipantsRepo(dbs.connection)),
|
||||
// Real collaborator rather than a stub: reconcile() runs after every mutation
|
||||
// and needs the same connection to see the rows these cases write.
|
||||
new JourneyDomainService(dbs, realtime, new TrekPhotosRepository(dbs), await createTestUnitOfWork(dbs.connection)),
|
||||
new JourneyDomainService(dbs, realtime, new TrekPhotoRegistrationService((await sharedTestOrm(dbs.connection)).repo(TrekPhotos), (await sharedTestOrm(dbs.connection)).repo(TripPhotos), dbs), await createTestUnitOfWork(dbs.connection)),
|
||||
await createTestUnitOfWork(dbs.connection),
|
||||
await createTestDayAssignmentsRepo(dbs.connection),
|
||||
await createTestAssignmentParticipantsRepo(dbs.connection),
|
||||
|
||||
@@ -41,11 +41,13 @@ import { resetTestDb } from '../../helpers/test-db';
|
||||
import { createUser, createJourney, addJourneyContributor } from '../../helpers/factories';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
|
||||
import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service';
|
||||
import { JourneyBookService } from '../../../src/nest/journey/journey-book.service';
|
||||
import { db as dbConn } from '../../../src/db/database';
|
||||
import { createTestUnitOfWork } from '../../helpers/test-uow';
|
||||
import { createTestUnitOfWork, sharedTestOrm } from '../../helpers/test-uow';
|
||||
|
||||
const dbs = new DatabaseService(dbConn);
|
||||
let domain: JourneyDomainService;
|
||||
@@ -65,7 +67,8 @@ beforeAll(async () => {
|
||||
createTables(testDb);
|
||||
runMigrations(testDb);
|
||||
const uow = await createTestUnitOfWork(testDb);
|
||||
domain = new JourneyDomainService(dbs, new RealtimeService(), new TrekPhotosRepository(dbs), uow);
|
||||
const t = await sharedTestOrm(testDb);
|
||||
domain = new JourneyDomainService(dbs, new RealtimeService(), new TrekPhotoRegistrationService(t.repo(TrekPhotos), t.repo(TripPhotos), dbs), uow);
|
||||
books = new JourneyBookService(dbs, domain);
|
||||
});
|
||||
|
||||
|
||||
@@ -55,7 +55,9 @@ import {
|
||||
} from '../../helpers/factories';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
|
||||
import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service';
|
||||
import { db as dbConn } from '../../../src/db/database';
|
||||
import { createTestUnitOfWork, sharedTestOrm } from '../../helpers/test-uow';
|
||||
@@ -66,8 +68,9 @@ let svc: JourneyDomainService;
|
||||
beforeAll(async () => {
|
||||
createTables(testDb);
|
||||
runMigrations(testDb);
|
||||
dbs = new DatabaseService(dbConn, (await sharedTestOrm(testDb)).em);
|
||||
svc = new JourneyDomainService(dbs, new RealtimeService(), new TrekPhotosRepository(dbs), await createTestUnitOfWork(testDb));
|
||||
const t = await sharedTestOrm(testDb);
|
||||
dbs = new DatabaseService(dbConn, t.em);
|
||||
svc = new JourneyDomainService(dbs, new RealtimeService(), new TrekPhotoRegistrationService(t.repo(TrekPhotos), t.repo(TripPhotos), dbs), await createTestUnitOfWork(testDb));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -36,7 +36,9 @@ import { resetTestDb } from '../../helpers/test-db';
|
||||
import { createUser, createJourney, createJourneyEntry, addJourneyContributor } from '../../helpers/factories';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
|
||||
import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service';
|
||||
import { JourneyShareService } from '../../../src/nest/journey/journey-share.service';
|
||||
import { SettingsService } from '../../../src/nest/settings/settings.service';
|
||||
@@ -60,7 +62,7 @@ beforeAll(async () => {
|
||||
t = await sharedTestOrm(testDb);
|
||||
svc = new JourneyShareService(
|
||||
dbs,
|
||||
new JourneyDomainService(dbs, new RealtimeService(), new TrekPhotosRepository(dbs), uow),
|
||||
new JourneyDomainService(dbs, new RealtimeService(), new TrekPhotoRegistrationService(t.repo(TrekPhotos), t.repo(TripPhotos), dbs), uow),
|
||||
new SettingsService(uow, await createTestAppSettingsRepo(testDb), await createTestSettingsRepo(testDb)),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -63,10 +63,12 @@ import {
|
||||
} from '../../helpers/factories';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
|
||||
import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service';
|
||||
import { db as dbConn } from '../../../src/db/database';
|
||||
import { createTestUnitOfWork } from '../../helpers/test-uow';
|
||||
import { createTestUnitOfWork, sharedTestOrm } from '../../helpers/test-uow';
|
||||
|
||||
const dbs = new DatabaseService(dbConn);
|
||||
let svc: JourneyDomainService;
|
||||
@@ -89,7 +91,8 @@ function placeEntry(
|
||||
beforeAll(async () => {
|
||||
createTables(testDb);
|
||||
runMigrations(testDb);
|
||||
svc = new JourneyDomainService(dbs, new RealtimeService(), new TrekPhotosRepository(dbs), await createTestUnitOfWork(testDb));
|
||||
const t = await sharedTestOrm(testDb);
|
||||
svc = new JourneyDomainService(dbs, new RealtimeService(), new TrekPhotoRegistrationService(t.repo(TrekPhotos), t.repo(TripPhotos), dbs), await createTestUnitOfWork(testDb));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -61,9 +61,18 @@ import { mapDbError, pipeAsset, type ServiceResult } from '../../../src/nest/mem
|
||||
import { MemoriesAccessService } from '../../../src/nest/memories/memories-access.service';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { Trips } from '../../../src/db/entities/Trips.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripAlbumLinks } from '../../../src/db/entities/TripAlbumLinks.entity';
|
||||
import { sharedTestOrm } from '../../helpers/test-uow';
|
||||
|
||||
const access = new MemoriesAccessService(new DatabaseService(testDb));
|
||||
// Plan 3c Task 0b / Plan 3e Task 6: `access` used to be constructed at module
|
||||
// load, before any `beforeAll` could resolve a real `EntityManager` — it now
|
||||
// also needs `TripPhotosRepository`/`TrekPhotosRepository`/
|
||||
// `TripAlbumLinksRepository`/`TripsRepository` (the ORM ones), which the
|
||||
// same `sharedTestOrm(testDb)` this file already used for the
|
||||
// `canAccessTrip` patch hands out. Built inside the async `beforeAll` below.
|
||||
let access: MemoriesAccessService;
|
||||
// A typed forwarder, not a `.bind` alias: a bound alias is typed `any`, which
|
||||
// hides a missing `await` from tsc and from all three lint rules.
|
||||
const getAlbumIdFromLink = (...a: Parameters<MemoriesAccessService['getAlbumIdFromLink']>) => access.getAlbumIdFromLink(...a);
|
||||
@@ -72,15 +81,15 @@ import { SsrfBlockedError } from '../../../src/utils/ssrfGuard';
|
||||
beforeAll(async () => {
|
||||
createTables(testDb);
|
||||
runMigrations(testDb);
|
||||
// Plan 3c Task 0b: `access` (and `DatabaseService.prototype.canAccessTrip`
|
||||
// it holds a reference to) is constructed at module load, before any
|
||||
// `beforeAll` can resolve a real `EntityManager` — patched onto the
|
||||
// PROTOTYPE instead, which the already-constructed instance's method
|
||||
// lookup still resolves through (`canAccessTrip` is not an own property).
|
||||
const em = (await sharedTestOrm(testDb)).em;
|
||||
const t = await sharedTestOrm(testDb);
|
||||
const dbs = new DatabaseService(testDb, t.em);
|
||||
// `DatabaseService.prototype.canAccessTrip` is patched on the PROTOTYPE
|
||||
// (not just this instance) so every OTHER hand-built `DatabaseService` in
|
||||
// this file's helpers resolves through the same real predicate.
|
||||
vi.spyOn(DatabaseService.prototype, 'canAccessTrip').mockImplementation(async (tripId, userId) =>
|
||||
em.getRepository(Trips).findAccessible(tripId, userId),
|
||||
t.em.getRepository(Trips).findAccessible(tripId, userId),
|
||||
);
|
||||
access = new MemoriesAccessService(dbs, t.repo(TripPhotos), t.repo(TrekPhotos), t.repo(TripAlbumLinks), t.repo(Trips));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -476,3 +485,130 @@ describe('canAccessTrekPhoto', () => {
|
||||
expect(await access.canAccessTrekPhoto(user.id, photoId)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MA5 — canAccessTrekPhoto's trip-membership predicate, rewritten (R4) onto
|
||||
// TripsRepository.findAccessible instead of a hand-translated second copy of
|
||||
// the legacy `EXISTS(...trip_members...UNION ALL...trips...)` text. This
|
||||
// parity suite proves the rewrite answers identically to the legacy text on
|
||||
// the same rows across owner/member/stranger, and is mutation-proof-worthy
|
||||
// per the plan's rule: MEMACCESS-MA5-003 is the one the report's mutation
|
||||
// proof (temporarily breaking the `findAccessible` call) must turn red.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** The MA5 statement, run raw for comparison — never converted, kept only as the oracle. */
|
||||
function legacyMA5(photoId: number, userId: number): boolean {
|
||||
return !!testDb.prepare(`
|
||||
SELECT 1 FROM trip_photos tp WHERE tp.photo_id = ? AND tp.shared = 1
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM trip_members tm WHERE tm.trip_id = tp.trip_id AND tm.user_id = ?
|
||||
UNION ALL
|
||||
SELECT 1 FROM trips t WHERE t.id = tp.trip_id AND t.user_id = ?
|
||||
)
|
||||
LIMIT 1
|
||||
`).get(photoId, userId, userId);
|
||||
}
|
||||
|
||||
describe('canAccessTrekPhoto — MA5 parity (TripsRepository.findAccessible rewrite vs. the legacy EXISTS/UNION ALL text)', () => {
|
||||
it('MEMACCESS-MA5-001: the trip owner (not the photo owner, no trip_members row) matches the legacy predicate — both true', async () => {
|
||||
const { user: photoOwner } = createUser(testDb);
|
||||
const { user: tripOwner } = createUser(testDb, { username: 'trip-owner' });
|
||||
const trip = createTrip(testDb, tripOwner.id);
|
||||
const photoId = shareInTrip(trip.id, photoOwner.id, 'ma5-owner-asset');
|
||||
|
||||
expect(await access.canAccessTrekPhoto(tripOwner.id, photoId)).toBe(legacyMA5(photoId, tripOwner.id));
|
||||
expect(await access.canAccessTrekPhoto(tripOwner.id, photoId)).toBe(true);
|
||||
});
|
||||
|
||||
it('MEMACCESS-MA5-002: a trip member (a trip_members row, not the trip owner) matches the legacy predicate — both true', async () => {
|
||||
const { user: photoOwner } = createUser(testDb);
|
||||
const { user: tripOwner } = createUser(testDb, { username: 'trip-owner-2' });
|
||||
const { user: member } = createUser(testDb, { username: 'member-2' });
|
||||
const trip = createTrip(testDb, tripOwner.id);
|
||||
testDb.prepare('INSERT INTO trip_members (trip_id, user_id) VALUES (?, ?)').run(trip.id, member.id);
|
||||
const photoId = shareInTrip(trip.id, photoOwner.id, 'ma5-member-asset');
|
||||
|
||||
expect(await access.canAccessTrekPhoto(member.id, photoId)).toBe(legacyMA5(photoId, member.id));
|
||||
expect(await access.canAccessTrekPhoto(member.id, photoId)).toBe(true);
|
||||
});
|
||||
|
||||
it('MEMACCESS-MA5-003: a stranger (neither owner nor member) matches the legacy predicate — both false. Mutation proof: breaking the findAccessible call (e.g. hard-coding it to return undefined) turns this red.', async () => {
|
||||
const { user: photoOwner } = createUser(testDb);
|
||||
const { user: tripOwner } = createUser(testDb, { username: 'trip-owner-3' });
|
||||
const { user: stranger } = createUser(testDb, { username: 'stranger-3' });
|
||||
const trip = createTrip(testDb, tripOwner.id);
|
||||
const photoId = shareInTrip(trip.id, photoOwner.id, 'ma5-stranger-asset');
|
||||
|
||||
expect(await access.canAccessTrekPhoto(stranger.id, photoId)).toBe(legacyMA5(photoId, stranger.id));
|
||||
expect(await access.canAccessTrekPhoto(stranger.id, photoId)).toBe(false);
|
||||
});
|
||||
|
||||
it('MEMACCESS-MA5-004: an unshared trip_photos row (shared=0) matches the legacy predicate — both false even for the trip owner', async () => {
|
||||
const { user: photoOwner } = createUser(testDb);
|
||||
const { user: tripOwner } = createUser(testDb, { username: 'trip-owner-4' });
|
||||
const trip = createTrip(testDb, tripOwner.id);
|
||||
const photoId = shareInTrip(trip.id, photoOwner.id, 'ma5-unshared-asset', 'immich', 0);
|
||||
|
||||
expect(await access.canAccessTrekPhoto(tripOwner.id, photoId)).toBe(legacyMA5(photoId, tripOwner.id));
|
||||
expect(await access.canAccessTrekPhoto(tripOwner.id, photoId)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Album link syncing — MA7/MA8 (getAlbumIdFromLink/getAlbumLinkForSync, one
|
||||
// shared `findScoped` repository method) and MA9 (updateSyncTimeForAlbumLink).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('getAlbumLinkForSync (MA8)', () => {
|
||||
it('MEMACCESS-MA8-001: returns the album id and decrypted passphrase when the link exists', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const ins = testDb.prepare(
|
||||
'INSERT INTO trip_album_links (trip_id, user_id, provider, album_id, album_name, passphrase) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
).run(trip.id, user.id, 'immich', 'album-sync-1', 'Sync Album', null);
|
||||
|
||||
const result = await access.getAlbumLinkForSync(String(trip.id), String(ins.lastInsertRowid), user.id);
|
||||
expect(result.success).toBe(true);
|
||||
expect((result as { data: { albumId: string } }).data.albumId).toBe('album-sync-1');
|
||||
});
|
||||
|
||||
it('MEMACCESS-MA8-002: is USER-scoped — a different user on the same trip gets "not found", not another user\'s link', async () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const { user: other } = createUser(testDb, { username: 'other-link-user' });
|
||||
const trip = createTrip(testDb, owner.id);
|
||||
testDb.prepare('INSERT INTO trip_members (trip_id, user_id) VALUES (?, ?)').run(trip.id, other.id);
|
||||
const ins = testDb.prepare(
|
||||
'INSERT INTO trip_album_links (trip_id, user_id, provider, album_id, album_name) VALUES (?, ?, ?, ?, ?)'
|
||||
).run(trip.id, owner.id, 'immich', 'album-scoped', 'Scoped');
|
||||
|
||||
const result = await access.getAlbumLinkForSync(String(trip.id), String(ins.lastInsertRowid), other.id);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('MEMACCESS-MA8-003: a non-canonical link id (rule 15) answers the same "not found" as a genuinely missing link', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
|
||||
const result = await access.getAlbumLinkForSync(String(trip.id), '1.0', user.id);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSyncTimeForAlbumLink (MA9)', () => {
|
||||
it('MEMACCESS-MA9-001: stamps last_synced_at on the given link', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const ins = testDb.prepare(
|
||||
'INSERT INTO trip_album_links (trip_id, user_id, provider, album_id, album_name) VALUES (?, ?, ?, ?, ?)'
|
||||
).run(trip.id, user.id, 'immich', 'album-touch', 'Touch');
|
||||
|
||||
await access.updateSyncTimeForAlbumLink(String(ins.lastInsertRowid));
|
||||
|
||||
const row = testDb.prepare('SELECT last_synced_at FROM trip_album_links WHERE id = ?').get(ins.lastInsertRowid) as { last_synced_at: string | null };
|
||||
expect(row.last_synced_at).not.toBeNull();
|
||||
});
|
||||
|
||||
it('MEMACCESS-MA9-002: a non-canonical id is a silent no-op, matching a legacy UPDATE that matched zero rows', async () => {
|
||||
await expect(access.updateSyncTimeForAlbumLink('not-a-number')).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ vi.mock('exifr', () => ({ default: { parse: vi.fn() } }));
|
||||
import exifr from 'exifr';
|
||||
import { PhotoCaptureBackfillService } from '../../../src/nest/memories/photo-capture-backfill.service';
|
||||
import type { PhotoResolverService } from '../../../src/nest/memories/photo-resolver.service';
|
||||
import type { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import type { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import type { StorageService } from '../../../src/nest/storage/storage.service';
|
||||
|
||||
// The storage layer's job here is only to hand the EXIF reader a real path;
|
||||
@@ -32,7 +32,7 @@ function build(rows: Row[], info: Record<number, unknown>) {
|
||||
const photos = {
|
||||
resolve: (id: number) => rows.find(r => r.id === id) ?? null,
|
||||
recordCaptureMetadata,
|
||||
} as unknown as TrekPhotosRepository;
|
||||
} as unknown as TrekPhotoRegistrationService;
|
||||
const resolver = { getPhotoInfo } as unknown as PhotoResolverService;
|
||||
return { svc: new PhotoCaptureBackfillService(resolver, photos, storageStub), recordCaptureMetadata, getPhotoInfo };
|
||||
}
|
||||
@@ -125,7 +125,7 @@ describe('PhotoCaptureBackfillService — local files', () => {
|
||||
const photos = {
|
||||
resolve: (id: number) => rows.find(r => r.id === id) ?? null,
|
||||
recordCaptureMetadata,
|
||||
} as unknown as TrekPhotosRepository;
|
||||
} as unknown as TrekPhotoRegistrationService;
|
||||
const resolver = { getPhotoInfo } as unknown as PhotoResolverService;
|
||||
return { svc: new PhotoCaptureBackfillService(resolver, photos, storageStub), recordCaptureMetadata, getPhotoInfo };
|
||||
}
|
||||
@@ -216,7 +216,7 @@ describe('PhotoCaptureBackfillService — local files', () => {
|
||||
const photos = {
|
||||
resolve: () => { throw new Error('db gone'); },
|
||||
recordCaptureMetadata,
|
||||
} as unknown as TrekPhotosRepository;
|
||||
} as unknown as TrekPhotoRegistrationService;
|
||||
const svc = new PhotoCaptureBackfillService({} as PhotoResolverService, photos, storageStub);
|
||||
|
||||
await expect(svc.run([7], 1)).resolves.toBeUndefined();
|
||||
|
||||
@@ -34,7 +34,10 @@ vi.mock('../../../src/nest/common/crypto/apiKeyCrypto', () => ({
|
||||
import { createTables } from '../../../src/db/schema';
|
||||
import { runMigrations } from '../../../src/db/migrations';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
|
||||
import { PhotoResolverService } from '../../../src/nest/memories/photo-resolver.service';
|
||||
import type { ImmichService } from '../../../src/nest/memories/immich.service';
|
||||
import type { SynologyService } from '../../../src/nest/memories/synology.service';
|
||||
@@ -68,7 +71,6 @@ const cache = {
|
||||
const storage = { exists: vi.fn(), sendToResponse: vi.fn() };
|
||||
|
||||
const dbs = new DatabaseService(testDb);
|
||||
const repo = new TrekPhotosRepository(dbs);
|
||||
// Real adapters over the stubbed services, and a real registry: the cases below
|
||||
// keep asserting on immich.streamImmichAsset/synology.streamSynologyAsset, so
|
||||
// they now also pin the adapters' argument mapping — which is where the two
|
||||
@@ -78,13 +80,8 @@ const providers = new PhotoProviderRegistry([
|
||||
new ImmichPhotoProvider(immich as unknown as ImmichService),
|
||||
new SynologyPhotoProvider(synology as unknown as SynologyService),
|
||||
]);
|
||||
const svc = new PhotoResolverService(
|
||||
repo,
|
||||
thumbnails as unknown as ThumbnailService,
|
||||
cache as unknown as TrekPhotoCacheService,
|
||||
providers,
|
||||
storage as unknown as import('../../../src/nest/storage/storage.service').StorageService,
|
||||
);
|
||||
let t: TestOrm;
|
||||
let svc: PhotoResolverService;
|
||||
|
||||
function makeRes() {
|
||||
return { status: vi.fn().mockReturnThis(), json: vi.fn(), set: vi.fn(), sendFile: vi.fn(), end: vi.fn() };
|
||||
@@ -99,9 +96,18 @@ function insertPhoto(cols: Record<string, unknown>): number {
|
||||
return Number(res.lastInsertRowid);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
createTables(testDb);
|
||||
runMigrations(testDb);
|
||||
t = await createTestOrm(testDb);
|
||||
const repo = new TrekPhotoRegistrationService(t.repo(TrekPhotos), t.repo(TripPhotos), dbs);
|
||||
svc = new PhotoResolverService(
|
||||
repo,
|
||||
thumbnails as unknown as ThumbnailService,
|
||||
cache as unknown as TrekPhotoCacheService,
|
||||
providers,
|
||||
storage as unknown as import('../../../src/nest/storage/storage.service').StorageService,
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -119,7 +125,10 @@ beforeEach(() => {
|
||||
storage.sendToResponse.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterAll(() => testDb.close());
|
||||
afterAll(async () => {
|
||||
await t.close();
|
||||
testDb.close();
|
||||
});
|
||||
|
||||
describe('streamPhoto — dispatch', () => {
|
||||
it('RESOLVE-001: 404s a photo id that does not exist', async () => {
|
||||
|
||||
@@ -52,7 +52,9 @@ import { UnsplashService } from '../../../src/nest/unsplash/unsplash.service';
|
||||
import { RuntimeEnvService } from '../../../src/nest/app-config/runtime-env.service';
|
||||
import type { PlacePhotoCacheService } from '../../../src/nest/place-photos/place-photo-cache.service';
|
||||
import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service';
|
||||
import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { makeStorageFixture } from '../../helpers/storage-fixture';
|
||||
import { createTestUnitOfWork, createTestAppSettingsRepo, createTestUsersRepo, createTestTagsRepo, createTestPlaceRatingsRepo, createTestAssignmentParticipantsRepo, createTestPlacesRepo, createTestTripMembersRepo, createTestDayAssignmentsRepo, createTestCategoriesRepo, createTestTripsRepo, sharedTestOrm } from '../../helpers/test-uow';
|
||||
|
||||
@@ -89,7 +91,7 @@ async function svc(searchNominatim: MapsService['searchNominatim']): Promise<Pla
|
||||
new QueryHelpersService(await createTestTagsRepo(dbs.connection), await createTestPlaceRatingsRepo(dbs.connection), await createTestAssignmentParticipantsRepo(dbs.connection)),
|
||||
new UnsplashService(await createTestAppSettingsRepo(dbs.connection), await createTestUsersRepo(dbs.connection), new RuntimeEnvService(), storageFx.storage),
|
||||
photoCacheStub,
|
||||
new JourneyDomainService(dbs, new RealtimeService(), new TrekPhotosRepository(dbs), await createTestUnitOfWork(dbs.connection)),
|
||||
new JourneyDomainService(dbs, new RealtimeService(), new TrekPhotoRegistrationService((await sharedTestOrm(dbs.connection)).repo(TrekPhotos), (await sharedTestOrm(dbs.connection)).repo(TripPhotos), dbs), await createTestUnitOfWork(dbs.connection)),
|
||||
storageFx.storage,
|
||||
await accommodationsOver(dbs), await createTestUnitOfWork(dbs.connection),
|
||||
await createTestPlacesRepo(dbs.connection),
|
||||
|
||||
@@ -75,7 +75,9 @@ import { PlacesService } from '../../../src/nest/places/places.service';
|
||||
import { MapsService } from '../../../src/nest/maps/maps.service';
|
||||
import { QueryHelpersService } from '../../../src/nest/query-helpers/query-helpers.service';
|
||||
import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service';
|
||||
import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { makeStorageFixture } from '../../helpers/storage-fixture';
|
||||
import { createTestUnitOfWork, createTestAppSettingsRepo, createTestUsersRepo, createTestTagsRepo, createTestPlaceRatingsRepo, createTestAssignmentParticipantsRepo, createTestPlacesRepo, createTestTripMembersRepo, createTestDayAssignmentsRepo, createTestCategoriesRepo, createTestTripsRepo, sharedTestOrm } from '../../helpers/test-uow';
|
||||
import type { AppSettingsRepository } from '../../../src/db/repositories/AppSettings.repository';
|
||||
@@ -131,7 +133,7 @@ async function makePlacesService(
|
||||
new QueryHelpersService(await createTestTagsRepo(dbs.connection), await createTestPlaceRatingsRepo(dbs.connection), await createTestAssignmentParticipantsRepo(dbs.connection)),
|
||||
new UnsplashService(await createTestAppSettingsRepo(dbs.connection), await createTestUsersRepo(dbs.connection), new RuntimeEnvService(), placesStorageFx.storage),
|
||||
photoCacheStub,
|
||||
new JourneyDomainService(dbs, new RealtimeService(), new TrekPhotosRepository(dbs), await createTestUnitOfWork(dbs.connection)),
|
||||
new JourneyDomainService(dbs, new RealtimeService(), new TrekPhotoRegistrationService((await sharedTestOrm(dbs.connection)).repo(TrekPhotos), (await sharedTestOrm(dbs.connection)).repo(TripPhotos), dbs), await createTestUnitOfWork(dbs.connection)),
|
||||
placesStorageFx.storage,
|
||||
await accommodationsOver(dbs), await createTestUnitOfWork(dbs.connection),
|
||||
await createTestPlacesRepo(dbs.connection),
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* picture or a broken tile: the addon gate, the "source is gone" bail-out, and
|
||||
* the mtime check that avoids regenerating an up-to-date thumbnail.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
||||
|
||||
const { isAddonEnabled } = vi.hoisted(() => ({ isAddonEnabled: vi.fn(() => true) }));
|
||||
|
||||
@@ -15,18 +15,22 @@ import path from 'node:path';
|
||||
import { Jimp } from 'jimp';
|
||||
import { ThumbnailService, journeyThumbName } from '../../../src/nest/memories/thumbnail.service';
|
||||
import type { AddonsService } from '../../../src/nest/addons/addons.service';
|
||||
import Database from 'better-sqlite3';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { createSnapshotTestDb } from '../../helpers/db-mock';
|
||||
import { resetTestDb } from '../../helpers/test-db';
|
||||
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { makeStorageFixture } from '../../helpers/storage-fixture';
|
||||
|
||||
// Category-addressed since slice 4: originals + thumbs are ('journey', <name>)
|
||||
// objects; the fixture's 'journey/' prefix reproduces the real layout, so the
|
||||
// on-disk paths below look exactly like the old uploads-root ones.
|
||||
const fx = makeStorageFixture('journey/');
|
||||
// Minimal real DB — the orphan sweep only SELECTs these two columns.
|
||||
const thumbsDb = new Database(':memory:');
|
||||
thumbsDb.exec('CREATE TABLE trek_photos (id INTEGER PRIMARY KEY AUTOINCREMENT, file_path TEXT, thumbnail_path TEXT)');
|
||||
const svc = new ThumbnailService({ isAddonEnabled } as unknown as AddonsService, fx.storage, new DatabaseService(thumbsDb as never));
|
||||
// Full migrated schema now (Plan 3e Task 6): sweepOrphanThumbs reads through
|
||||
// TrekPhotosRepository (the ORM one), which needs the real trek_photos table,
|
||||
// not the three-column hand-rolled stand-in this file used before.
|
||||
const thumbsDb = createSnapshotTestDb();
|
||||
let t: TestOrm;
|
||||
let svc: ThumbnailService;
|
||||
const root = fx.root;
|
||||
|
||||
/** A real 1200x900 JPEG — Jimp has to be able to decode it for the happy path. */
|
||||
@@ -37,12 +41,18 @@ async function writeSourceImage(rel: string): Promise<void> {
|
||||
await img.write(abs as `${string}.jpg`);
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
t = await createTestOrm(thumbsDb);
|
||||
svc = new ThumbnailService({ isAddonEnabled } as unknown as AddonsService, fx.storage, t.repo(TrekPhotos));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
isAddonEnabled.mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
await t.close();
|
||||
thumbsDb.close();
|
||||
fx.cleanup();
|
||||
});
|
||||
@@ -132,7 +142,7 @@ describe('sweepOrphanThumbs (spec fix #2)', () => {
|
||||
});
|
||||
|
||||
it('THUMB-SWEEP-001: deletes strays, spares thumbs derivable from live journey rows', async () => {
|
||||
thumbsDb.prepare('INSERT INTO trek_photos (file_path) VALUES (?)').run('journey/live.jpg');
|
||||
thumbsDb.prepare("INSERT INTO trek_photos (provider, file_path) VALUES ('local', ?)").run('journey/live.jpg');
|
||||
const liveName = journeyThumbName('journey/live.jpg'); // 'thumbs/<hash>.jpg'
|
||||
const livePath = writeThumb(path.basename(liveName));
|
||||
const strayPath = writeThumb('deadbeefdeadbeef.jpg');
|
||||
@@ -145,7 +155,7 @@ describe('sweepOrphanThumbs (spec fix #2)', () => {
|
||||
});
|
||||
|
||||
it('THUMB-SWEEP-002: spares a recorded thumbnail_path even without a matching file_path hash', async () => {
|
||||
thumbsDb.prepare('INSERT INTO trek_photos (file_path, thumbnail_path) VALUES (?, ?)')
|
||||
thumbsDb.prepare("INSERT INTO trek_photos (provider, file_path, thumbnail_path) VALUES ('local', ?, ?)")
|
||||
.run('elsewhere/x.jpg', 'journey/thumbs/recorded00000000.jpg');
|
||||
const recorded = writeThumb('recorded00000000.jpg');
|
||||
|
||||
|
||||
@@ -13,28 +13,19 @@
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
||||
|
||||
const { testDb, dbMock } = vi.hoisted(() => {
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database(':memory:');
|
||||
db.exec('PRAGMA journal_mode = WAL');
|
||||
return {
|
||||
testDb: db,
|
||||
dbMock: { db, closeDb: () => {}, reinitialize: () => {}, canAccessTrip: () => null, isOwner: () => false, getPlaceWithTags: () => null },
|
||||
};
|
||||
});
|
||||
vi.mock('../../../src/db/database', () => dbMock);
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { createTables } from '../../../src/db/schema';
|
||||
import { runMigrations } from '../../../src/db/migrations';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { createSnapshotTestDb } from '../../helpers/db-mock';
|
||||
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
|
||||
import { TrekPhotoCacheMeta } from '../../../src/db/entities/TrekPhotoCacheMeta.entity';
|
||||
import { TrekPhotoCacheService, CACHE_TTL } from '../../../src/nest/memories/trek-photo-cache.service';
|
||||
import { StorageNotFoundError } from '../../../src/nest/storage/storage.types';
|
||||
import { makeStorageFixture } from '../../helpers/storage-fixture';
|
||||
|
||||
const fx = makeStorageFixture('photos/trek/');
|
||||
const svc = new TrekPhotoCacheService(new DatabaseService(testDb), fx.storage);
|
||||
const testDb = createSnapshotTestDb();
|
||||
let t: TestOrm;
|
||||
let svc: TrekPhotoCacheService;
|
||||
const CACHE_DIR = path.join(fx.root, 'photos/trek');
|
||||
const binPath = (key: string) => path.join(CACHE_DIR, `${key}.bin`);
|
||||
let counter = 0;
|
||||
@@ -44,16 +35,17 @@ function freshKey(label: string): string {
|
||||
return svc.cacheKey('test', `${label}-${counter++}`, 'thumbnail', 1);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
createTables(testDb);
|
||||
runMigrations(testDb);
|
||||
beforeAll(async () => {
|
||||
t = await createTestOrm(testDb);
|
||||
svc = new TrekPhotoCacheService(t.repo(TrekPhotoCacheMeta), fx.storage);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
testDb.prepare('DELETE FROM trek_photo_cache_meta').run();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
await t.close();
|
||||
testDb.close();
|
||||
fx.cleanup();
|
||||
});
|
||||
@@ -169,7 +161,7 @@ describe('the stampede guard', () => {
|
||||
// Load-bearing: a per-instance map would hand any second instance a private
|
||||
// guard and the dedup would be silently gone. The sweep cron injects the
|
||||
// container singleton now, but the invariant stays pinned.
|
||||
const other = new TrekPhotoCacheService(new DatabaseService(testDb), fx.storage);
|
||||
const other = new TrekPhotoCacheService(t.repo(TrekPhotoCacheMeta), fx.storage);
|
||||
const promise = Promise.resolve<Buffer | null>(null);
|
||||
svc.setInFlight('shared-key', promise);
|
||||
expect(other.getInFlight('shared-key')).toBe(promise);
|
||||
|
||||
@@ -1,44 +1,36 @@
|
||||
/**
|
||||
* TrekPhotosRepository — capture metadata (#1614).
|
||||
* TrekPhotoRegistrationService (renamed from `TrekPhotosRepository`, Plan 3e
|
||||
* R13 — collided with the generated ORM `TrekPhotos.repository.ts`) — PH1-11,
|
||||
* now onto `TrekPhotosRepository`/`TripPhotosRepository` (the ORM ones).
|
||||
*
|
||||
* The three sources of a photo's "when and where" disagree in how much they know:
|
||||
* a provider search answers with coordinates, the same provider's album listing
|
||||
* does not, and a local file gives them up only once its EXIF has been read. These
|
||||
* pin the merge rule that keeps a later, emptier answer from erasing an earlier one.
|
||||
* The three sources of a photo's "when and where" disagree in how much they
|
||||
* know: a provider search answers with coordinates, the same provider's
|
||||
* album listing does not, and a local file gives them up only once its EXIF
|
||||
* has been read (#1614). These pin the merge rule that keeps a later,
|
||||
* emptier answer from erasing an earlier one, plus the rest of PH1-11.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from 'vitest';
|
||||
|
||||
const { testDb, dbMock } = vi.hoisted(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database(':memory:');
|
||||
db.exec('PRAGMA foreign_keys = ON');
|
||||
return { testDb: db, dbMock: { db, closeDb: () => {}, reinitialize: () => {}, getPlaceWithTags: () => null, canAccessTrip: () => undefined, isOwner: () => false } };
|
||||
});
|
||||
|
||||
vi.mock('../../../src/db/database', () => dbMock);
|
||||
vi.mock('../../../src/config', () => ({
|
||||
JWT_SECRET: 'test-secret',
|
||||
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
|
||||
updateJwtSecret: () => {},
|
||||
}));
|
||||
|
||||
import { createTables } from '../../../src/db/schema';
|
||||
import { runMigrations } from '../../../src/db/migrations';
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest';
|
||||
import { createSnapshotTestDb } from '../../helpers/db-mock';
|
||||
import { resetTestDb } from '../../helpers/test-db';
|
||||
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
|
||||
import { createUser, createTrip } from '../../helpers/factories';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { decrypt_api_key } from '../../../src/nest/common/crypto/apiKeyCrypto';
|
||||
|
||||
let repo: TrekPhotosRepository;
|
||||
const testDb = createSnapshotTestDb();
|
||||
let t: TestOrm;
|
||||
let repo: TrekPhotoRegistrationService;
|
||||
|
||||
beforeAll(() => {
|
||||
createTables(testDb);
|
||||
runMigrations(testDb);
|
||||
repo = new TrekPhotosRepository(new DatabaseService(testDb));
|
||||
beforeAll(async () => {
|
||||
t = await createTestOrm(testDb);
|
||||
repo = new TrekPhotoRegistrationService(t.repo(TrekPhotos), t.repo(TripPhotos), new DatabaseService(testDb, t.em));
|
||||
});
|
||||
|
||||
beforeEach(() => resetTestDb(testDb));
|
||||
afterAll(() => testDb.close());
|
||||
beforeEach(() => { resetTestDb(testDb); t.clear(); });
|
||||
afterAll(async () => { await t.close(); testDb.close(); });
|
||||
|
||||
// A fresh path per call: getOrCreateLocal is keyed on file_path and resetTestDb
|
||||
// leaves trek_photos alone, so a shared path would hand every test the same row.
|
||||
@@ -47,13 +39,17 @@ async function makePhoto(): Promise<number> {
|
||||
return await repo.getOrCreateLocal(`/uploads/journey/${++seq}.jpg`, null, null, null, 'image', null);
|
||||
}
|
||||
|
||||
function rawRow(id: number): unknown {
|
||||
return testDb.prepare('SELECT * FROM trek_photos WHERE id = ?').get(id);
|
||||
}
|
||||
|
||||
function read(id: number) {
|
||||
return testDb.prepare('SELECT taken_at, lat, lng FROM trek_photos WHERE id = ?').get(id) as {
|
||||
taken_at: string | null; lat: number | null; lng: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
describe('TrekPhotosRepository.recordCaptureMetadata', () => {
|
||||
describe('TrekPhotoRegistrationService.recordCaptureMetadata', () => {
|
||||
it('TREKPHOTO-001: stores the capture time and coordinates', async () => {
|
||||
const id = await makePhoto();
|
||||
await repo.recordCaptureMetadata(id, { takenAt: '2026-03-15T10:20:00Z', lat: 48.8584, lng: 2.2945 });
|
||||
@@ -93,3 +89,144 @@ describe('TrekPhotosRepository.recordCaptureMetadata', () => {
|
||||
expect(read(id)).toEqual({ taken_at: '2026-03-15T10:20:00Z', lat: 48.8584, lng: 2.2945 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('TrekPhotoRegistrationService.getOrCreate (PH1-3)', () => {
|
||||
it('TREKPHOTO-010: registers a new remote asset', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const id = await repo.getOrCreate('immich', 'asset-1', user.id, undefined, 'video');
|
||||
expect(rawRow(id)).toMatchObject({ provider: 'immich', asset_id: 'asset-1', owner_id: user.id, media_type: 'video' });
|
||||
});
|
||||
|
||||
it('TREKPHOTO-011: a repeat lookup returns the same id rather than inserting a duplicate row', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const first = await repo.getOrCreate('immich', 'asset-2', user.id);
|
||||
const second = await repo.getOrCreate('immich', 'asset-2', user.id);
|
||||
expect(second).toBe(first);
|
||||
expect(testDb.prepare('SELECT COUNT(*) as c FROM trek_photos WHERE asset_id = ?').get('asset-2')).toEqual({ c: 1 });
|
||||
});
|
||||
|
||||
it('TREKPHOTO-012 (PH2): a passphrase on an already-registered asset re-encrypts and replaces it', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const id = await repo.getOrCreate('synologyphotos', 'asset-3', user.id, 'first-pass');
|
||||
await repo.getOrCreate('synologyphotos', 'asset-3', user.id, 'second-pass');
|
||||
|
||||
const row = rawRow(id) as { passphrase: string };
|
||||
expect(decrypt_api_key(row.passphrase)).toBe('second-pass');
|
||||
});
|
||||
|
||||
it('TREKPHOTO-013: a passphrase-free repeat lookup leaves the stored passphrase untouched', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const id = await repo.getOrCreate('synologyphotos', 'asset-4', user.id, 'keep-me');
|
||||
await repo.getOrCreate('synologyphotos', 'asset-4', user.id);
|
||||
|
||||
const row = rawRow(id) as { passphrase: string };
|
||||
expect(decrypt_api_key(row.passphrase)).toBe('keep-me');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TrekPhotoRegistrationService.getOrCreateLocal (PH4-5)', () => {
|
||||
it('TREKPHOTO-020: registers a new local photo', async () => {
|
||||
const id = await repo.getOrCreateLocal('journey/a.jpg', 'journey/thumbs/a.jpg', 800, 600, 'image', null);
|
||||
expect(rawRow(id)).toMatchObject({
|
||||
provider: 'local', file_path: 'journey/a.jpg', thumbnail_path: 'journey/thumbs/a.jpg', width: 800, height: 600,
|
||||
});
|
||||
});
|
||||
|
||||
it('TREKPHOTO-021: a repeat lookup on the same path returns the same id', async () => {
|
||||
const first = await repo.getOrCreateLocal('journey/b.jpg');
|
||||
const second = await repo.getOrCreateLocal('journey/b.jpg', 'ignored-thumb.jpg', 1, 1);
|
||||
expect(second).toBe(first);
|
||||
// The second call's fields never reach the row — getOrCreateLocal only inserts once.
|
||||
expect(rawRow(first)).toMatchObject({ thumbnail_path: null, width: null, height: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe('TrekPhotoRegistrationService.resolve (PH6) — parity with the legacy SELECT *', () => {
|
||||
it('TREKPHOTO-030: a remote photo with capture metadata partially set matches the raw row byte-for-byte, full key set', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const id = await repo.getOrCreate('immich', 'asset-parity', user.id, 'a-passphrase', 'video');
|
||||
await repo.recordCaptureMetadata(id, { takenAt: '2026-05-01T00:00:00Z', lat: null, lng: null });
|
||||
|
||||
const legacy = testDb.prepare('SELECT * FROM trek_photos WHERE id = ?').get(id);
|
||||
const resolved = await repo.resolve(id);
|
||||
|
||||
expect(resolved).toEqual(legacy);
|
||||
});
|
||||
|
||||
it('TREKPHOTO-031: a local photo with a full coordinate pair matches the raw row byte-for-byte, full key set', async () => {
|
||||
const id = await repo.getOrCreateLocal('journey/parity.jpg', 'journey/thumbs/parity.jpg', 800, 600, 'image', null);
|
||||
await repo.recordCaptureMetadata(id, { takenAt: '2026-05-02T00:00:00Z', lat: 48.1, lng: 11.6 });
|
||||
|
||||
const legacy = testDb.prepare('SELECT * FROM trek_photos WHERE id = ?').get(id);
|
||||
const resolved = await repo.resolve(id);
|
||||
|
||||
expect(resolved).toEqual(legacy);
|
||||
});
|
||||
|
||||
it('TREKPHOTO-032: an unknown id resolves to null, not a thrown error', async () => {
|
||||
expect(await repo.resolve(999999)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TrekPhotoRegistrationService.setProvider (PH7)', () => {
|
||||
it('TREKPHOTO-040: retargets a local photo onto a provider once uploaded there', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const id = await repo.getOrCreateLocal('journey/retarget.jpg');
|
||||
await repo.setProvider(id, 'immich', 'new-asset', user.id);
|
||||
|
||||
expect(rawRow(id)).toMatchObject({ provider: 'immich', asset_id: 'new-asset', owner_id: user.id });
|
||||
});
|
||||
});
|
||||
|
||||
describe('TrekPhotoRegistrationService.recordLocalThumbnail (PH8)', () => {
|
||||
it('TREKPHOTO-050: stamps a generated thumbnail and its dimensions', async () => {
|
||||
const id = await repo.getOrCreateLocal('journey/thumb-src.jpg');
|
||||
await repo.recordLocalThumbnail(id, 'journey/thumbs/thumb-src.jpg', 800, 600);
|
||||
|
||||
expect(rawRow(id)).toMatchObject({ thumbnail_path: 'journey/thumbs/thumb-src.jpg', width: 800, height: 600 });
|
||||
});
|
||||
|
||||
it('TREKPHOTO-051: a re-generated thumbnail does not blank dimensions already known', async () => {
|
||||
const id = await repo.getOrCreateLocal('journey/thumb-keep.jpg', null, 1200, 900);
|
||||
await repo.recordLocalThumbnail(id, 'journey/thumbs/thumb-keep.jpg', 400, 300);
|
||||
|
||||
// COALESCE(width, ?) keeps the already-known 1200x900, not the new 400x300.
|
||||
expect(rawRow(id)).toMatchObject({ thumbnail_path: 'journey/thumbs/thumb-keep.jpg', width: 1200, height: 900 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('TrekPhotoRegistrationService.deleteIfOrphan (PH10-11)', () => {
|
||||
it('TREKPHOTO-060: an unreferenced remote photo is deleted', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const id = await repo.getOrCreate('immich', 'orphan-1', user.id);
|
||||
await repo.deleteIfOrphan(id);
|
||||
expect(rawRow(id)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('TREKPHOTO-061: a photo still referenced by trip_photos (3e-owned) is kept', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const id = await repo.getOrCreate('immich', 'referenced-1', user.id);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
testDb.prepare('INSERT INTO trip_photos (trip_id, user_id, photo_id, shared) VALUES (?, ?, ?, 1)').run(trip.id, user.id, id);
|
||||
|
||||
await repo.deleteIfOrphan(id);
|
||||
expect(rawRow(id)).toBeDefined();
|
||||
});
|
||||
|
||||
it('TREKPHOTO-062: a photo still referenced by journey_photos (Plan 3g, raw) is kept', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const id = await repo.getOrCreate('immich', 'referenced-2', user.id);
|
||||
testDb.prepare("INSERT INTO journeys (user_id, title, status, created_at, updated_at) VALUES (?, 'J', 'draft', 0, 0)").run(user.id);
|
||||
const journeyId = testDb.prepare('SELECT id FROM journeys WHERE user_id = ?').get(user.id) as { id: number };
|
||||
testDb.prepare('INSERT INTO journey_photos (journey_id, photo_id, created_at) VALUES (?, ?, 0)').run(journeyId.id, id);
|
||||
|
||||
await repo.deleteIfOrphan(id);
|
||||
expect(rawRow(id)).toBeDefined();
|
||||
});
|
||||
|
||||
it('TREKPHOTO-063: an unreferenced LOCAL photo is never reclaimed here — its bytes are ours', async () => {
|
||||
const id = await repo.getOrCreateLocal('journey/never-reclaimed.jpg');
|
||||
await repo.deleteIfOrphan(id);
|
||||
expect(rawRow(id)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,7 +77,9 @@ import { PlacePhotoCacheService } from '../../../src/nest/place-photos/place-pho
|
||||
import { RuntimeEnvService } from '../../../src/nest/app-config/runtime-env.service';
|
||||
import { makeStorageFixture } from '../../helpers/storage-fixture';
|
||||
import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service';
|
||||
import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import type { TripsRepository } from '../../../src/db/repositories/Trips.repository';
|
||||
import {
|
||||
createTestUnitOfWork, createTestAppSettingsRepo, createTestUsersRepo, sharedTestOrm,
|
||||
@@ -136,7 +138,7 @@ beforeAll(async () => {
|
||||
dbs(), new PermissionsService(await createTestAppSettingsRepo(dbs().connection), await createTestUnitOfWork(dbs().connection)), new RealtimeService(),
|
||||
new MapsService(dbs(), photoCache, await createTestAppSettingsRepo(dbs().connection), await createTestUsersRepo(dbs().connection)), new QueryHelpersService(await createTestTagsRepo(dbs().connection), await createTestPlaceRatingsRepo(dbs().connection), await createTestAssignmentParticipantsRepo(dbs().connection)),
|
||||
new UnsplashService(await createTestAppSettingsRepo(dbs().connection), await createTestUsersRepo(dbs().connection), new RuntimeEnvService(), makeStorageFixture('').storage), photoCache,
|
||||
new JourneyDomainService(dbs(), new RealtimeService(), new TrekPhotosRepository(dbs()), await createTestUnitOfWork(dbs().connection)),
|
||||
new JourneyDomainService(dbs(), new RealtimeService(), new TrekPhotoRegistrationService((await sharedTestOrm(testDb)).repo(TrekPhotos), (await sharedTestOrm(testDb)).repo(TripPhotos), dbs()), await createTestUnitOfWork(dbs().connection)),
|
||||
makeStorageFixture('').storage,
|
||||
await accommodationsOver(dbs()), await createTestUnitOfWork(dbs().connection),
|
||||
await createTestPlacesRepo(dbs().connection),
|
||||
|
||||
@@ -80,7 +80,9 @@ import { MapsService } from '../../../src/nest/maps/maps.service';
|
||||
import { UnsplashService } from '../../../src/nest/unsplash/unsplash.service';
|
||||
import { PlacePhotoCacheService } from '../../../src/nest/place-photos/place-photo-cache.service';
|
||||
import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service';
|
||||
import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { RuntimeEnvService } from '../../../src/nest/app-config/runtime-env.service';
|
||||
import { makeStorageFixture } from '../../helpers/storage-fixture';
|
||||
import { QueryHelpersService } from '../../../src/nest/query-helpers/query-helpers.service';
|
||||
@@ -173,7 +175,7 @@ beforeAll(async () => {
|
||||
new QueryHelpersService(await createTestTagsRepo(dbs().connection), await createTestPlaceRatingsRepo(dbs().connection), await createTestAssignmentParticipantsRepo(dbs().connection)),
|
||||
new UnsplashService(await createTestAppSettingsRepo(dbs().connection), await createTestUsersRepo(dbs().connection), new RuntimeEnvService(), coversFx.storage),
|
||||
photoCache,
|
||||
new JourneyDomainService(dbs(), new RealtimeService(), new TrekPhotosRepository(dbs()), await createTestUnitOfWork(dbs().connection)),
|
||||
new JourneyDomainService(dbs(), new RealtimeService(), new TrekPhotoRegistrationService(dbsEm!.getRepository(TrekPhotos), dbsEm!.getRepository(TripPhotos), dbs()), await createTestUnitOfWork(dbs().connection)),
|
||||
makeStorageFixture('').storage,
|
||||
await accommodationsOver(dbs()), await createTestUnitOfWork(dbs().connection),
|
||||
await createTestPlacesRepo(dbs().connection),
|
||||
|
||||
@@ -46,7 +46,11 @@ import { ADDON_IDS } from '../../../src/addons';
|
||||
import { createTestAddonsService } from '../../helpers/test-addons';
|
||||
import { UnifiedMemoriesService } from '../../../src/nest/memories/unified-memories.service';
|
||||
import { MemoriesAccessService } from '../../../src/nest/memories/memories-access.service';
|
||||
import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { TripAlbumLinks } from '../../../src/db/entities/TripAlbumLinks.entity';
|
||||
import { Trips } from '../../../src/db/entities/Trips.entity';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import type { ImmichService } from '../../../src/nest/memories/immich.service';
|
||||
import type { SynologyService } from '../../../src/nest/memories/synology.service';
|
||||
@@ -79,14 +83,15 @@ beforeAll(async () => {
|
||||
// only one of the four primitives this suite's code paths reach) is spied
|
||||
// directly on this instance, routed to a real `DatabaseService` built
|
||||
// with one.
|
||||
const real = new DatabaseService(testDb, (await sharedTestOrm(testDb)).em);
|
||||
const t = await sharedTestOrm(testDb);
|
||||
const real = new DatabaseService(testDb, t.em);
|
||||
vi.spyOn(dbs, 'canAccessTrip').mockImplementation((...a) => real.canAccessTrip(...a));
|
||||
svc = new UnifiedMemoriesService(
|
||||
dbs,
|
||||
new TrekPhotosRepository(dbs),
|
||||
new TrekPhotoRegistrationService(t.repo(TrekPhotos), t.repo(TripPhotos), dbs),
|
||||
{} as ImmichService,
|
||||
{} as SynologyService,
|
||||
new MemoriesAccessService(dbs),
|
||||
new MemoriesAccessService(dbs, t.repo(TripPhotos), t.repo(TrekPhotos), t.repo(TripAlbumLinks), t.repo(Trips)),
|
||||
notificationsStub(),
|
||||
await createTestAddonsService(testDb, dbs),
|
||||
await createTestUnitOfWork(testDb),
|
||||
|
||||
@@ -50,7 +50,9 @@ import { QueryHelpersService } from '../../../src/nest/query-helpers/query-helpe
|
||||
import { UnsplashService } from '../../../src/nest/unsplash/unsplash.service';
|
||||
import { PlacePhotoCacheService } from '../../../src/nest/place-photos/place-photo-cache.service';
|
||||
import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service';
|
||||
import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { RuntimeEnvService } from '../../../src/nest/app-config/runtime-env.service';
|
||||
import { notificationsStub } from '../../helpers/notifications';
|
||||
import { makeStorageFixture } from '../../helpers/storage-fixture';
|
||||
@@ -92,7 +94,8 @@ beforeAll(async () => {
|
||||
// `beforeAll` can resolve a real `EntityManager` — the four
|
||||
// repository-backed methods are spied directly on this instance instead,
|
||||
// routed to a real `DatabaseService` built with one.
|
||||
const real = new DatabaseService(testDb, (await sharedTestOrm(testDb)).em);
|
||||
const t = await sharedTestOrm(testDb);
|
||||
const real = new DatabaseService(testDb, t.em);
|
||||
vi.spyOn(dbs, 'canAccessTrip').mockImplementation((...a) => real.canAccessTrip(...a));
|
||||
vi.spyOn(dbs, 'isOwner').mockImplementation((...a) => real.isOwner(...a));
|
||||
vi.spyOn(dbs, 'rosterUserIds').mockImplementation((...a) => real.rosterUserIds(...a));
|
||||
@@ -106,7 +109,7 @@ beforeAll(async () => {
|
||||
new QueryHelpersService(await createTestTagsRepo(dbs.connection), await createTestPlaceRatingsRepo(dbs.connection), await createTestAssignmentParticipantsRepo(dbs.connection)),
|
||||
new UnsplashService(await createTestAppSettingsRepo(dbs.connection), await createTestUsersRepo(dbs.connection), runtimeEnv, makeStorageFixture('').storage),
|
||||
photoCache,
|
||||
new JourneyDomainService(dbs, realtime, new TrekPhotosRepository(dbs), await createTestUnitOfWork(dbs.connection)),
|
||||
new JourneyDomainService(dbs, realtime, new TrekPhotoRegistrationService(t.repo(TrekPhotos), t.repo(TripPhotos), dbs), await createTestUnitOfWork(dbs.connection)),
|
||||
makeStorageFixture('').storage,
|
||||
await accommodationsOver(dbs), await createTestUnitOfWork(dbs.connection),
|
||||
await createTestPlacesRepo(dbs.connection),
|
||||
|
||||
@@ -23,27 +23,34 @@ vi.mock('../../../src/config', () => ({
|
||||
import { createTables } from '../../../src/db/schema';
|
||||
import { runMigrations } from '../../../src/db/migrations';
|
||||
import { createUser } from '../../helpers/factories';
|
||||
import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photos.repository';
|
||||
import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { db as trekDb } from '../../../src/db/database';
|
||||
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
|
||||
|
||||
// Was photos.bridge, deleted with the other three that had no consumer outside
|
||||
// the container. These call the repository directly now.
|
||||
const trekPhotos = new TrekPhotosRepository(new DatabaseService(trekDb));
|
||||
const getOrCreateTrekPhoto = (...a: Parameters<TrekPhotosRepository['getOrCreate']>) => trekPhotos.getOrCreate(...a);
|
||||
const getOrCreateLocalTrekPhoto = (...a: Parameters<TrekPhotosRepository['getOrCreateLocal']>) => trekPhotos.getOrCreateLocal(...a);
|
||||
let t: TestOrm;
|
||||
let trekPhotos: TrekPhotoRegistrationService;
|
||||
const getOrCreateTrekPhoto = (...a: Parameters<TrekPhotoRegistrationService['getOrCreate']>) => trekPhotos.getOrCreate(...a);
|
||||
const getOrCreateLocalTrekPhoto = (...a: Parameters<TrekPhotoRegistrationService['getOrCreateLocal']>) => trekPhotos.getOrCreateLocal(...a);
|
||||
const resolveTrekPhoto = (id: number) => trekPhotos.resolve(id);
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
createTables(testDb);
|
||||
runMigrations(testDb);
|
||||
t = await createTestOrm(testDb);
|
||||
trekPhotos = new TrekPhotoRegistrationService(t.repo(TrekPhotos), t.repo(TripPhotos), new DatabaseService(trekDb, t.em));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
testDb.prepare('DELETE FROM trek_photos').run();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
await t.close();
|
||||
testDb.close();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user