fix(synology): correct multi-album passphrase assignment and stale trek_photos

- ProviderPicker now tracks per-asset album passphrase in a Map; on confirm,
  assets are grouped by passphrase and submitted as separate batches so each
  asset receives its own album's passphrase instead of the last-selected one
- getOrCreateTrekPhoto unconditionally overwrites the stored passphrase when
  a fresh one is supplied, allowing re-adds to heal a stuck bad passphrase
- deleteTrekPhotoIfOrphan purges the trek_photos row for provider assets when
  no trip_photos or journey_photos reference it anymore; wired into
  removeTripPhoto, removeAlbumLink, and deletePhoto so remove + re-add is a
  clean slate
- Three new integration tests: SYNO-090 (passphrase overwrite), SYNO-091
  (orphan cleanup), SYNO-092 (remove + re-add restores correct passphrase)
This commit is contained in:
jubnl
2026-04-17 19:48:12 +02:00
parent c0aa252f9a
commit 8cd5aa0d23
5 changed files with 124 additions and 16 deletions
@@ -1169,3 +1169,74 @@ describe('Synology SSRF blocked error handling', () => {
expect(res.body.albums.length).toBeGreaterThan(0);
});
});
// ── Passphrase persistence fixes ─────────────────────────────────────────────
import { getOrCreateTrekPhoto, deleteTrekPhotoIfOrphan } from '../../src/services/memories/photoResolverService';
import { decrypt_api_key } from '../../src/services/apiKeyCrypto';
describe('trek_photos passphrase healing (SYNO-090)', () => {
it('SYNO-090 — getOrCreateTrekPhoto overwrites an existing bad passphrase when a new one is supplied', () => {
const { user } = createUser(testDb);
const wrongPass = 'wrong-passphrase';
const correctPass = 'correct-passphrase';
const id1 = getOrCreateTrekPhoto('synologyphotos', 'asset-heal-test', user.id, wrongPass);
const row1 = testDb.prepare('SELECT passphrase FROM trek_photos WHERE id = ?').get(id1) as { passphrase: string };
expect(decrypt_api_key(row1.passphrase)).toBe(wrongPass);
const id2 = getOrCreateTrekPhoto('synologyphotos', 'asset-heal-test', user.id, correctPass);
expect(id2).toBe(id1);
const row2 = testDb.prepare('SELECT passphrase FROM trek_photos WHERE id = ?').get(id2) as { passphrase: string };
expect(decrypt_api_key(row2.passphrase)).toBe(correctPass);
});
});
describe('trek_photos orphan cleanup (SYNO-091)', () => {
it('SYNO-091 — deleteTrekPhotoIfOrphan removes the trek_photos row when no trip_photos or journey_photos reference it', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
testDb.prepare("UPDATE photo_providers SET enabled = 1 WHERE id = 'synologyphotos'").run();
const trekPhotoId = getOrCreateTrekPhoto('synologyphotos', 'asset-orphan-test', user.id, 'pass-A');
testDb.prepare(
'INSERT OR IGNORE INTO trip_photos (trip_id, user_id, photo_id, shared) VALUES (?, ?, ?, 1)'
).run(trip.id, user.id, trekPhotoId);
// Still referenced — must not be deleted.
deleteTrekPhotoIfOrphan(trekPhotoId);
expect(testDb.prepare('SELECT id FROM trek_photos WHERE id = ?').get(trekPhotoId)).toBeDefined();
// Remove the reference, then orphan-cleanup should delete the trek_photos row.
testDb.prepare('DELETE FROM trip_photos WHERE photo_id = ?').run(trekPhotoId);
deleteTrekPhotoIfOrphan(trekPhotoId);
expect(testDb.prepare('SELECT id FROM trek_photos WHERE id = ?').get(trekPhotoId)).toBeUndefined();
});
it('SYNO-092 — re-adding a previously removed Synology photo stores the new passphrase correctly', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
testDb.prepare("UPDATE photo_providers SET enabled = 1 WHERE id = 'synologyphotos'").run();
const firstPass = 'first-passphrase';
const secondPass = 'second-passphrase';
// Add with wrong passphrase, then remove (simulating the bug scenario).
const id1 = getOrCreateTrekPhoto('synologyphotos', 'asset-readd-test', user.id, firstPass);
testDb.prepare(
'INSERT OR IGNORE INTO trip_photos (trip_id, user_id, photo_id, shared) VALUES (?, ?, ?, 1)'
).run(trip.id, user.id, id1);
testDb.prepare('DELETE FROM trip_photos WHERE photo_id = ?').run(id1);
deleteTrekPhotoIfOrphan(id1);
// trek_photos row should be gone.
expect(testDb.prepare('SELECT id FROM trek_photos WHERE id = ?').get(id1)).toBeUndefined();
// Re-add with the correct passphrase.
const id2 = getOrCreateTrekPhoto('synologyphotos', 'asset-readd-test', user.id, secondPass);
const row = testDb.prepare('SELECT passphrase FROM trek_photos WHERE id = ?').get(id2) as { passphrase: string };
expect(decrypt_api_key(row.passphrase)).toBe(secondPass);
});
});