fix(server): plan 3h close-out — new-value-wins COALESCE restores stay/reservation propagation; doc-sync state filter truthiness; request-context and parity ratchets for share, doc-sync and the integrations

This commit is contained in:
jubnl
2026-09-24 02:39:30 +02:00
parent 915ab84faf
commit f337f116d9
14 changed files with 379 additions and 103 deletions
@@ -1155,3 +1155,65 @@ describe('PlacesRepository.setSource (DWS7)', () => {
expect(row.updated_at).toBe(before.updated_at);
});
});
// ---------------------------------------------------------------------------
// Plan 3h Task 7 review, M1 coverage — Plan 3h Task 4's `maps.service.ts`
// photo-fetch (MAP9), untested at the repository level (the service test
// stubs this method).
// ---------------------------------------------------------------------------
describe('PlacesRepository.setImageUrlIfUnset (MAP9)', () => {
it('MAP9-001: fills a NULL image_url, matching the legacy $or guard', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const place = createPlace(testDb, trip.id, { name: 'No photo yet' });
testDb.prepare("UPDATE places SET google_place_id = 'ChIJ_shared' WHERE id = ?").run(place.id);
const n = await places.setImageUrlIfUnset('ChIJ_shared', '/uploads/photo-cache/new.jpg');
expect(n).toBe(1);
const row = testDb.prepare('SELECT image_url FROM places WHERE id = ?').get(place.id) as { image_url: string | null };
expect(row.image_url).toBe('/uploads/photo-cache/new.jpg');
});
it('MAP9-002: fills an EMPTY-STRING image_url too (the second half of the $or)', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const place = createPlace(testDb, trip.id, { name: 'Blank photo' });
testDb.prepare("UPDATE places SET google_place_id = 'ChIJ_blank', image_url = '' WHERE id = ?").run(place.id);
const n = await places.setImageUrlIfUnset('ChIJ_blank', '/uploads/photo-cache/filled.jpg');
expect(n).toBe(1);
const row = testDb.prepare('SELECT image_url FROM places WHERE id = ?').get(place.id) as { image_url: string | null };
expect(row.image_url).toBe('/uploads/photo-cache/filled.jpg');
});
it('MAP9-003: an already-set image_url is left untouched — never clobbers a custom image', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const place = createPlace(testDb, trip.id, { name: 'Custom photo' });
testDb.prepare("UPDATE places SET google_place_id = 'ChIJ_custom', image_url = '/uploads/custom.jpg' WHERE id = ?").run(place.id);
const n = await places.setImageUrlIfUnset('ChIJ_custom', '/uploads/photo-cache/should-not-land.jpg');
expect(n).toBe(0);
const row = testDb.prepare('SELECT image_url FROM places WHERE id = ?').get(place.id) as { image_url: string | null };
expect(row.image_url).toBe('/uploads/custom.jpg');
});
it('MAP9-004: unscoped by trip — every empty-image row sharing the google_place_id across trips is filled in one statement', async () => {
const { user } = createUser(testDb);
const tripA = createTrip(testDb, user.id);
const tripB = createTrip(testDb, user.id);
const placeA = createPlace(testDb, tripA.id, { name: 'A' });
const placeB = createPlace(testDb, tripB.id, { name: 'B' });
testDb.prepare("UPDATE places SET google_place_id = 'ChIJ_shared2' WHERE id IN (?, ?)").run(placeA.id, placeB.id);
const n = await places.setImageUrlIfUnset('ChIJ_shared2', '/uploads/photo-cache/both.jpg');
expect(n).toBe(2);
expect((testDb.prepare('SELECT image_url FROM places WHERE id = ?').get(placeA.id) as { image_url: string }).image_url).toBe('/uploads/photo-cache/both.jpg');
expect((testDb.prepare('SELECT image_url FROM places WHERE id = ?').get(placeB.id) as { image_url: string }).image_url).toBe('/uploads/photo-cache/both.jpg');
});
});
@@ -505,3 +505,64 @@ describe('ReservationsRepository — Task 2 reads (listForTrip / findWithJoins /
expect(typed.map((r) => r.id).sort((a, b) => a - b)).toEqual([dinner.id, hotel.id].sort((a, b) => a - b));
});
});
// ---------------------------------------------------------------------------
// Plan 3h Task 7 review, M1 coverage — the AirTrail leaf's writes/reads,
// untested at the repository level (the airtrail-link/sync service tests
// stub this repository).
// ---------------------------------------------------------------------------
describe('ReservationsRepository — AirTrail leaf (ATL1/ATL2/ATL4, airports)', () => {
it('ATL1 linkAirtrailSingleFlight — stamps every AirTrail column and switches sync on, matching the legacy statement', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const res = createReservation(testDb, trip.id, { title: 'Flight', type: 'flight' });
await reservationsRepo.linkAirtrailSingleFlight(res.id, 'AT-42', user.id, 'hash-1', '2026-09-24T10:00:00Z');
const row = testDb.prepare(
"SELECT external_source, external_id, external_owner_user_id, sync_enabled, external_hash, external_synced_at FROM reservations WHERE id = ?",
).get(res.id);
expect(row).toEqual({
external_source: 'airtrail', external_id: 'AT-42', external_owner_user_id: user.id,
sync_enabled: 1, external_hash: 'hash-1', external_synced_at: '2026-09-24T10:00:00Z',
});
});
it('ATL2 setAirtrailSyncDisabled — turns sync_enabled off, matching the legacy statement, and leaves every other column alone', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const res = createReservation(testDb, trip.id, { title: 'Flight', type: 'flight' });
await reservationsRepo.linkAirtrailSingleFlight(res.id, 'AT-42', user.id, 'hash-1', '2026-09-24T10:00:00Z');
await reservationsRepo.setAirtrailSyncDisabled(res.id);
const row = testDb.prepare('SELECT sync_enabled, external_id FROM reservations WHERE id = ?').get(res.id);
expect(row).toEqual({ sync_enabled: 0, external_id: 'AT-42' });
});
it('ATL4 findAirtrailLinked — matches the legacy statement, scoped to external_source = \'airtrail\' (a plain flight misses)', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const linked = createReservation(testDb, trip.id, { title: 'Flight', type: 'flight' });
await reservationsRepo.linkAirtrailSingleFlight(linked.id, 'AT-7', user.id, 'hash-2', '2026-09-24T11:00:00Z');
const plain = createReservation(testDb, trip.id, { title: 'Dinner', type: 'restaurant' });
const legacy = testDb.prepare(
"SELECT id, trip_id, external_id, external_owner_user_id, sync_enabled FROM reservations WHERE id = ? AND external_source = 'airtrail'",
).get(linked.id);
expect(await reservationsRepo.findAirtrailLinked(linked.id)).toEqual(legacy);
expect(await reservationsRepo.findAirtrailLinked(plain.id)).toBeUndefined();
});
it('markNeedsReview (airports) — sets needs_review, matching the legacy statement', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const res = createReservation(testDb, trip.id, { title: 'Flight', type: 'flight' });
expect((testDb.prepare('SELECT needs_review FROM reservations WHERE id = ?').get(res.id) as { needs_review: number }).needs_review).toBe(0);
await reservationsRepo.markNeedsReview(res.id);
expect((testDb.prepare('SELECT needs_review FROM reservations WHERE id = ?').get(res.id) as { needs_review: number }).needs_review).toBe(1);
});
});