From efce09f3a231607821e1eb7aa492a42b4d73278b Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 8 Oct 2026 17:12:35 +0200 Subject: [PATCH] test(server): seed the memories access service spec through the factories Converts the raw prepare() fixtures and reads in the memories access service spec to the ORM factories and readers: shared trip photos, journey photos and contributors, album links and the trip memberships the access checks walk are written through insertRow. The legacy MA5 statement stays raw behind an allow marker as the oracle its parity cases compare with, and the database mock loses the raw canAccessTrip and isOwner nothing reads. --- .../unit/nest/memories-access.service.test.ts | 197 ++++++++---------- 1 file changed, 89 insertions(+), 108 deletions(-) diff --git a/server/tests/unit/nest/memories-access.service.test.ts b/server/tests/unit/nest/memories-access.service.test.ts index bb5b22b6d..674245bc4 100644 --- a/server/tests/unit/nest/memories-access.service.test.ts +++ b/server/tests/unit/nest/memories-access.service.test.ts @@ -17,14 +17,6 @@ vi.mock('../../../src/db/database', async () => { closeDb: () => {}, reinitialize: () => {}, getPlaceWithTags: () => null, - canAccessTrip: (tripId: any, userId: number) => - db.prepare(` - SELECT t.id FROM trips t - LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? - WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL) - `).get(userId, tripId, userId), - isOwner: (tripId: any, userId: number) => - !!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId), }; return mock; }); @@ -64,6 +56,9 @@ import { Journeys } from '../../../src/db/entities/Journeys.entity'; import { JourneyContributors } from '../../../src/db/entities/JourneyContributors.entity'; import { JourneyPhotos } from '../../../src/db/entities/JourneyPhotos.entity'; import { sharedTestOrm } from '../../helpers/test-uow'; +import type { TestOrm } from '../../helpers/test-orm'; +import { findRow, insertRow } from '../../helpers/factories/rows'; +import { TripMembers } from '../../../src/db/entities/TripMembers.entity'; // Plan 3c Task 0b / Plan 3e Task 6 / Plan 3g Task 4: `access` used to be // constructed at module load, before any `beforeAll` could resolve a real @@ -79,8 +74,29 @@ let access: MemoriesAccessService; const getAlbumIdFromLink = (...a: Parameters) => access.getAlbumIdFromLink(...a); import { SsrfBlockedError } from '../../../src/utils/ssrfGuard'; +let orm: TestOrm; + +/** An album link of the user on the trip, written straight in. */ +function insertAlbumLink(tripId: number, userId: number, albumId: string, albumName: string, passphrase?: string | null): Promise { + return insertRow(orm, TripAlbumLinks, { + trip: tripId, user: userId, provider: 'immich', album_id: albumId, album_name: albumName, + ...(passphrase !== undefined ? { passphrase } : {}), + }); +} + +/** A journey photo row pointing at a trek photo, the way the journey gallery holds it. */ +async function addToJourney(journeyId: number, photoId: number): Promise { + await insertRow(orm, JourneyPhotos, { journey: journeyId, photo: photoId, created_at: 0 }); +} + +/** Adds the user to the journey's contributors with the given role. */ +async function addContributor(journeyId: number, userId: number, role: 'editor' | 'viewer'): Promise { + await insertRow(orm, JourneyContributors, { journey: journeyId, user: userId, role, added_at: 0 }); +} + beforeAll(async () => { const t = await sharedTestOrm(testDb); + orm = t; // Plan 4 Task 4: `DatabaseService` is gone — `MemoriesAccessService` is // fully repository-backed, so the `DatabaseService.prototype.canAccessTrip` // spy this block used to route to the real predicate is dead; removed @@ -164,10 +180,7 @@ describe('getAlbumIdFromLink', () => { const trip = createTrip(testDb, user.id); // Insert with auto-increment id (INTEGER PRIMARY KEY) - 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-123', 'My Album'); - const linkId = ins.lastInsertRowid; + const linkId = await insertAlbumLink(trip.id, user.id, 'album-123', 'My Album'); const result = await getAlbumIdFromLink(String(trip.id), String(linkId), user.id); expect(result.success).toBe(true); @@ -320,19 +333,14 @@ describe('pipeAsset fetch options (#1611)', () => { // --------------------------------------------------------------------------- /** A trek_photos row plus the trip_photos link that shares it. */ -function shareInTrip(tripId: number, ownerId: number, assetId: string, provider = 'immich', shared = 1): number { - const photoId = Number(testDb.prepare( - 'INSERT INTO trek_photos (provider, asset_id, owner_id) VALUES (?, ?, ?)' - ).run(provider, assetId, ownerId).lastInsertRowid); - testDb.prepare('INSERT INTO trip_photos (trip_id, photo_id, user_id, shared) VALUES (?, ?, ?, ?)') - .run(tripId, photoId, ownerId, shared); +async function shareInTrip(tripId: number, ownerId: number, assetId: string, provider = 'immich', shared = 1): Promise { + const photoId = await insertRow(orm, TrekPhotos, { provider, asset_id: assetId, owner: ownerId }); + await insertRow(orm, TripPhotos, { trip: tripId, photo: photoId, user: ownerId, shared }); return photoId; } -function makeJourney(userId: number): number { - return Number(testDb.prepare( - "INSERT INTO journeys (user_id, title, status, created_at, updated_at) VALUES (?, 'J', 'draft', 0, 0)" - ).run(userId).lastInsertRowid); +function makeJourney(userId: number): Promise { + return insertRow(orm, Journeys, { user: userId, title: 'J', status: 'draft', created_at: 0, updated_at: 0 }); } describe('canAccessUserPhoto', () => { @@ -344,8 +352,8 @@ describe('canAccessUserPhoto', () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb, { username: 'member' }); const trip = createTrip(testDb, owner.id); - testDb.prepare('INSERT INTO trip_members (trip_id, user_id) VALUES (?, ?)').run(trip.id, member.id); - shareInTrip(trip.id, owner.id, 'asset-shared'); + await insertRow(orm, TripMembers, { trip: trip.id, user: member.id }); + await shareInTrip(trip.id, owner.id, 'asset-shared'); expect(await access.canAccessUserPhoto(member.id, owner.id, String(trip.id), 'asset-shared', 'immich')).toBe(true); }); @@ -354,8 +362,8 @@ describe('canAccessUserPhoto', () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb, { username: 'member' }); const trip = createTrip(testDb, owner.id); - testDb.prepare('INSERT INTO trip_members (trip_id, user_id) VALUES (?, ?)').run(trip.id, member.id); - shareInTrip(trip.id, owner.id, 'asset-private', 'immich', 0); + await insertRow(orm, TripMembers, { trip: trip.id, user: member.id }); + await shareInTrip(trip.id, owner.id, 'asset-private', 'immich', 0); expect(await access.canAccessUserPhoto(member.id, owner.id, String(trip.id), 'asset-private', 'immich')).toBe(false); }); @@ -364,7 +372,7 @@ describe('canAccessUserPhoto', () => { const { user: owner } = createUser(testDb); const { user: stranger } = createUser(testDb, { username: 'stranger' }); const trip = createTrip(testDb, owner.id); - shareInTrip(trip.id, owner.id, 'asset-shared'); + await shareInTrip(trip.id, owner.id, 'asset-shared'); expect(await access.canAccessUserPhoto(stranger.id, owner.id, String(trip.id), 'asset-shared', 'immich')).toBe(false); }); @@ -373,8 +381,8 @@ describe('canAccessUserPhoto', () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb, { username: 'member' }); const trip = createTrip(testDb, owner.id); - testDb.prepare('INSERT INTO trip_members (trip_id, user_id) VALUES (?, ?)').run(trip.id, member.id); - shareInTrip(trip.id, owner.id, 'same-id', 'immich'); + await insertRow(orm, TripMembers, { trip: trip.id, user: member.id }); + await shareInTrip(trip.id, owner.id, 'same-id', 'immich'); expect(await access.canAccessUserPhoto(member.id, owner.id, String(trip.id), 'same-id', 'synologyphotos')).toBe(false); }); @@ -382,12 +390,10 @@ describe('canAccessUserPhoto', () => { it('MEM-ACCESS-006: tripId "0" routes through journeys — a contributor passes', async () => { const { user: owner } = createUser(testDb); const { user: contributor } = createUser(testDb, { username: 'contrib' }); - const journeyId = makeJourney(owner.id); - const photoId = Number(testDb.prepare( - "INSERT INTO trek_photos (provider, asset_id, owner_id) VALUES ('immich', 'j-asset', ?)" - ).run(owner.id).lastInsertRowid); - testDb.prepare('INSERT INTO journey_photos (journey_id, photo_id, created_at) VALUES (?, ?, 0)').run(journeyId, photoId); - testDb.prepare("INSERT INTO journey_contributors (journey_id, user_id, role, added_at) VALUES (?, ?, 'editor', 0)").run(journeyId, contributor.id); + const journeyId = await makeJourney(owner.id); + const photoId = await insertRow(orm, TrekPhotos, { provider: 'immich', asset_id: 'j-asset', owner: owner.id }); + await addToJourney(journeyId, photoId); + await addContributor(journeyId, contributor.id, 'editor'); expect(await access.canAccessUserPhoto(contributor.id, owner.id, '0', 'j-asset', 'immich')).toBe(true); }); @@ -395,11 +401,9 @@ describe('canAccessUserPhoto', () => { it('MEM-ACCESS-006b: tripId "0" — the journey OWNER (not the photo\'s own owner_id) passes via MA2\'s owner branch, not the trivial requestingUserId===ownerUserId shortcut', async () => { const { user: journeyOwner } = createUser(testDb, { username: 'journey-owner' }); const { user: uploader } = createUser(testDb, { username: 'uploader-6b' }); - const journeyId = makeJourney(journeyOwner.id); - const photoId = Number(testDb.prepare( - "INSERT INTO trek_photos (provider, asset_id, owner_id) VALUES ('immich', 'j-asset-owner', ?)" - ).run(uploader.id).lastInsertRowid); - testDb.prepare('INSERT INTO journey_photos (journey_id, photo_id, created_at) VALUES (?, ?, 0)').run(journeyId, photoId); + const journeyId = await makeJourney(journeyOwner.id); + const photoId = await insertRow(orm, TrekPhotos, { provider: 'immich', asset_id: 'j-asset-owner', owner: uploader.id }); + await addToJourney(journeyId, photoId); // requestingUserId (journeyOwner) !== ownerUserId (uploader) — MA1's read is // scoped to tkp.owner_id=uploader, so the trivial owner shortcut at the top @@ -410,12 +414,10 @@ describe('canAccessUserPhoto', () => { it('MEM-ACCESS-006c: tripId "0" — a VIEWER-role contributor also passes (MA2 checks any role, not just editor)', async () => { const { user: owner } = createUser(testDb); const { user: viewer } = createUser(testDb, { username: 'viewer-6c' }); - const journeyId = makeJourney(owner.id); - const photoId = Number(testDb.prepare( - "INSERT INTO trek_photos (provider, asset_id, owner_id) VALUES ('immich', 'j-asset-viewer', ?)" - ).run(owner.id).lastInsertRowid); - testDb.prepare('INSERT INTO journey_photos (journey_id, photo_id, created_at) VALUES (?, ?, 0)').run(journeyId, photoId); - testDb.prepare("INSERT INTO journey_contributors (journey_id, user_id, role, added_at) VALUES (?, ?, 'viewer', 0)").run(journeyId, viewer.id); + const journeyId = await makeJourney(owner.id); + const photoId = await insertRow(orm, TrekPhotos, { provider: 'immich', asset_id: 'j-asset-viewer', owner: owner.id }); + await addToJourney(journeyId, photoId); + await addContributor(journeyId, viewer.id, 'viewer'); expect(await access.canAccessUserPhoto(viewer.id, owner.id, '0', 'j-asset-viewer', 'immich')).toBe(true); }); @@ -433,11 +435,9 @@ describe('canAccessUserPhoto', () => { const { user: owner } = createUser(testDb); const { user: realUploader } = createUser(testDb, { username: 'real-uploader-m5b' }); const { user: otherOwner } = createUser(testDb, { username: 'other-owner-m5b' }); - const journeyId = makeJourney(owner.id); - const photoId = Number(testDb.prepare( - "INSERT INTO trek_photos (provider, asset_id, owner_id) VALUES ('immich', 'm5b-shared-asset', ?)" - ).run(realUploader.id).lastInsertRowid); - testDb.prepare('INSERT INTO journey_photos (journey_id, photo_id, created_at) VALUES (?, ?, 0)').run(journeyId, photoId); + const journeyId = await makeJourney(owner.id); + const photoId = await insertRow(orm, TrekPhotos, { provider: 'immich', asset_id: 'm5b-shared-asset', owner: realUploader.id }); + await addToJourney(journeyId, photoId); // requestingUserId (owner) !== ownerUserId (otherOwner), so the trivial // shortcut does not fire; MA1 must refuse because otherOwner never @@ -448,11 +448,9 @@ describe('canAccessUserPhoto', () => { it('MEM-ACCESS-007: tripId "0" refuses someone with no journey link', async () => { const { user: owner } = createUser(testDb); const { user: stranger } = createUser(testDb, { username: 'stranger' }); - const journeyId = makeJourney(owner.id); - const photoId = Number(testDb.prepare( - "INSERT INTO trek_photos (provider, asset_id, owner_id) VALUES ('immich', 'j-asset-2', ?)" - ).run(owner.id).lastInsertRowid); - testDb.prepare('INSERT INTO journey_photos (journey_id, photo_id, created_at) VALUES (?, ?, 0)').run(journeyId, photoId); + const journeyId = await makeJourney(owner.id); + const photoId = await insertRow(orm, TrekPhotos, { provider: 'immich', asset_id: 'j-asset-2', owner: owner.id }); + await addToJourney(journeyId, photoId); expect(await access.canAccessUserPhoto(stranger.id, owner.id, '0', 'j-asset-2', 'immich')).toBe(false); }); @@ -471,9 +469,7 @@ describe('canAccessTrekPhoto', () => { it('MEM-ACCESS-011: the owner passes', async () => { const { user } = createUser(testDb); - const photoId = Number(testDb.prepare( - "INSERT INTO trek_photos (provider, asset_id, owner_id) VALUES ('immich', 'own', ?)" - ).run(user.id).lastInsertRowid); + const photoId = await insertRow(orm, TrekPhotos, { provider: 'immich', asset_id: 'own', owner: user.id }); expect(await access.canAccessTrekPhoto(user.id, photoId)).toBe(true); }); @@ -483,8 +479,8 @@ describe('canAccessTrekPhoto', () => { const { user: member } = createUser(testDb, { username: 'member' }); const { user: stranger } = createUser(testDb, { username: 'stranger' }); const trip = createTrip(testDb, owner.id); - testDb.prepare('INSERT INTO trip_members (trip_id, user_id) VALUES (?, ?)').run(trip.id, member.id); - const photoId = shareInTrip(trip.id, owner.id, 'trek-shared'); + await insertRow(orm, TripMembers, { trip: trip.id, user: member.id }); + const photoId = await shareInTrip(trip.id, owner.id, 'trek-shared'); expect(await access.canAccessTrekPhoto(member.id, photoId)).toBe(true); expect(await access.canAccessTrekPhoto(stranger.id, photoId)).toBe(false); @@ -494,7 +490,7 @@ describe('canAccessTrekPhoto', () => { const { user: owner } = createUser(testDb); const { user: uploader } = createUser(testDb, { username: 'uploader' }); const trip = createTrip(testDb, owner.id); - const photoId = shareInTrip(trip.id, uploader.id, 'owner-path'); + const photoId = await shareInTrip(trip.id, uploader.id, 'owner-path'); expect(await access.canAccessTrekPhoto(owner.id, photoId)).toBe(true); }); @@ -502,12 +498,10 @@ describe('canAccessTrekPhoto', () => { it('MEM-ACCESS-014: a journey contributor passes', async () => { const { user: owner } = createUser(testDb); const { user: contributor } = createUser(testDb, { username: 'contrib' }); - const journeyId = makeJourney(owner.id); - const photoId = Number(testDb.prepare( - "INSERT INTO trek_photos (provider, asset_id, owner_id) VALUES ('immich', 'j-trek', ?)" - ).run(owner.id).lastInsertRowid); - testDb.prepare('INSERT INTO journey_photos (journey_id, photo_id, created_at) VALUES (?, ?, 0)').run(journeyId, photoId); - testDb.prepare("INSERT INTO journey_contributors (journey_id, user_id, role, added_at) VALUES (?, ?, 'editor', 0)").run(journeyId, contributor.id); + const journeyId = await makeJourney(owner.id); + const photoId = await insertRow(orm, TrekPhotos, { provider: 'immich', asset_id: 'j-trek', owner: owner.id }); + await addToJourney(journeyId, photoId); + await addContributor(journeyId, contributor.id, 'editor'); expect(await access.canAccessTrekPhoto(contributor.id, photoId)).toBe(true); }); @@ -516,12 +510,10 @@ describe('canAccessTrekPhoto', () => { const { user: owner } = createUser(testDb); const { user: viewer } = createUser(testDb, { username: 'viewer-14b' }); const { user: stranger } = createUser(testDb, { username: 'stranger-14b' }); - const journeyId = makeJourney(owner.id); - const photoId = Number(testDb.prepare( - "INSERT INTO trek_photos (provider, asset_id, owner_id) VALUES ('immich', 'j-trek-viewer', ?)" - ).run(owner.id).lastInsertRowid); - testDb.prepare('INSERT INTO journey_photos (journey_id, photo_id, created_at) VALUES (?, ?, 0)').run(journeyId, photoId); - testDb.prepare("INSERT INTO journey_contributors (journey_id, user_id, role, added_at) VALUES (?, ?, 'viewer', 0)").run(journeyId, viewer.id); + const journeyId = await makeJourney(owner.id); + const photoId = await insertRow(orm, TrekPhotos, { provider: 'immich', asset_id: 'j-trek-viewer', owner: owner.id }); + await addToJourney(journeyId, photoId); + await addContributor(journeyId, viewer.id, 'viewer'); expect(await access.canAccessTrekPhoto(viewer.id, photoId)).toBe(true); expect(await access.canAccessTrekPhoto(stranger.id, photoId)).toBe(false); @@ -530,11 +522,9 @@ describe('canAccessTrekPhoto', () => { it('MEM-ACCESS-015: an ownerless local upload is reachable only through its journey', async () => { const { user: owner } = createUser(testDb); const { user: outsider } = createUser(testDb, { username: 'outsider' }); - const journeyId = makeJourney(owner.id); - const photoId = Number(testDb.prepare( - "INSERT INTO trek_photos (provider, file_path) VALUES ('local', 'journey/x.jpg')" - ).run().lastInsertRowid); - testDb.prepare('INSERT INTO journey_photos (journey_id, photo_id, created_at) VALUES (?, ?, 0)').run(journeyId, photoId); + const journeyId = await makeJourney(owner.id); + const photoId = await insertRow(orm, TrekPhotos, { provider: 'local', file_path: 'journey/x.jpg' }); + await addToJourney(journeyId, photoId); expect(await access.canAccessTrekPhoto(owner.id, photoId)).toBe(true); expect(await access.canAccessTrekPhoto(outsider.id, photoId)).toBe(false); @@ -542,9 +532,7 @@ describe('canAccessTrekPhoto', () => { it('MEM-ACCESS-016: an ownerless local upload in no journey is reachable by nobody', async () => { const { user } = createUser(testDb); - const photoId = Number(testDb.prepare( - "INSERT INTO trek_photos (provider, file_path) VALUES ('local', 'orphan.jpg')" - ).run().lastInsertRowid); + const photoId = await insertRow(orm, TrekPhotos, { provider: 'local', file_path: 'orphan.jpg' }); expect(await access.canAccessTrekPhoto(user.id, photoId)).toBe(false); }); @@ -562,6 +550,7 @@ describe('canAccessTrekPhoto', () => { /** The MA5 statement, run raw for comparison — never converted, kept only as the oracle. */ function legacyMA5(photoId: number, userId: number): boolean { + // test-sql-allow: the legacy statement is this parity test's oracle and has to run as written. return !!testDb.prepare(` SELECT 1 FROM trip_photos tp WHERE tp.photo_id = ? AND tp.shared = 1 AND EXISTS ( @@ -578,7 +567,7 @@ describe('canAccessTrekPhoto — MA5 parity (TripsRepository.findAccessible rewr 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'); + const photoId = await 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); @@ -589,8 +578,8 @@ describe('canAccessTrekPhoto — MA5 parity (TripsRepository.findAccessible rewr 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'); + await insertRow(orm, TripMembers, { trip: trip.id, user: member.id }); + const photoId = await 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); @@ -601,7 +590,7 @@ describe('canAccessTrekPhoto — MA5 parity (TripsRepository.findAccessible rewr 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'); + const photoId = await 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); @@ -611,7 +600,7 @@ describe('canAccessTrekPhoto — MA5 parity (TripsRepository.findAccessible rewr 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); + const photoId = await 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); @@ -647,12 +636,10 @@ describe('canAccessUserPhoto/canAccessTrekPhoto — MA1/MA2/MA6 mutation-proof c it('MEMACCESS-MA2-001: an editor who is not the journey owner still passes (breaks under an owner-AND-contributor mutation)', async () => { const { user: owner } = createUser(testDb); const { user: editor } = createUser(testDb, { username: 'ma2-editor' }); - const journeyId = makeJourney(owner.id); - const photoId = Number(testDb.prepare( - "INSERT INTO trek_photos (provider, asset_id, owner_id) VALUES ('immich', 'ma2-editor-asset', ?)" - ).run(owner.id).lastInsertRowid); - testDb.prepare('INSERT INTO journey_photos (journey_id, photo_id, created_at) VALUES (?, ?, 0)').run(journeyId, photoId); - testDb.prepare("INSERT INTO journey_contributors (journey_id, user_id, role, added_at) VALUES (?, ?, 'editor', 0)").run(journeyId, editor.id); + const journeyId = await makeJourney(owner.id); + const photoId = await insertRow(orm, TrekPhotos, { provider: 'immich', asset_id: 'ma2-editor-asset', owner: owner.id }); + await addToJourney(journeyId, photoId); + await addContributor(journeyId, editor.id, 'editor'); expect(await access.canAccessUserPhoto(editor.id, owner.id, '0', 'ma2-editor-asset', 'immich')).toBe(true); expect(await access.canAccessTrekPhoto(editor.id, photoId)).toBe(true); @@ -668,11 +655,9 @@ 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 linkId = await insertAlbumLink(trip.id, user.id, 'album-sync-1', 'Sync Album', null); - const result = await access.getAlbumLinkForSync(String(trip.id), String(ins.lastInsertRowid), user.id); + const result = await access.getAlbumLinkForSync(String(trip.id), String(linkId), user.id); expect(result.success).toBe(true); expect((result as { data: { albumId: string } }).data.albumId).toBe('album-sync-1'); }); @@ -681,12 +666,10 @@ describe('getAlbumLinkForSync (MA8)', () => { 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'); + await insertRow(orm, TripMembers, { trip: trip.id, user: other.id }); + const linkId = await insertAlbumLink(trip.id, owner.id, 'album-scoped', 'Scoped'); - const result = await access.getAlbumLinkForSync(String(trip.id), String(ins.lastInsertRowid), other.id); + const result = await access.getAlbumLinkForSync(String(trip.id), String(linkId), other.id); expect(result.success).toBe(false); }); @@ -703,14 +686,12 @@ 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'); + const linkId = await insertAlbumLink(trip.id, user.id, 'album-touch', 'Touch'); - await access.updateSyncTimeForAlbumLink(String(ins.lastInsertRowid)); + await access.updateSyncTimeForAlbumLink(String(linkId)); - 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(); + const row = await findRow(orm, TripAlbumLinks, { id: linkId }); + 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 () => {