diff --git a/server/tests/unit/nest/atlas.service.test.ts b/server/tests/unit/nest/atlas.service.test.ts index f74cc5c88..f2446d2eb 100644 --- a/server/tests/unit/nest/atlas.service.test.ts +++ b/server/tests/unit/nest/atlas.service.test.ts @@ -19,14 +19,6 @@ vi.mock('../../../src/db/database', async () => { closeDb: () => {}, reinitialize: () => {}, getPlaceWithTags: () => null, - canAccessTrip: (tripId: any, userId: number) => - db.prepare(` - SELECT t.id, t.user_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; }); @@ -49,8 +41,17 @@ import { createTestPlaceRegionsRepo, } from '../../helpers/atlas-repos'; import type { PlaceRegionsRepository } from '../../../src/db/repositories/PlaceRegions.repository'; +import type { PlaceRow } from '../../../src/db/repositories/Places.repository'; import { PlaceRegions } from '../../../src/db/entities/PlaceRegions.entity'; import { createTestOrm, type TestOrm } from '../../helpers/test-orm'; +import { countRows, findRow, insertRow, updateRows, upsertRow } from '../../helpers/factories/rows'; +import { makePlace } from '../../helpers/factories/places'; +import { addReservationTraveler, makeReservationEndpoint } from '../../helpers/factories/reservations'; +import { BucketList } from '../../../src/db/entities/BucketList.entity'; +import { Reservations } from '../../../src/db/entities/Reservations.entity'; +import { TripMembers } from '../../../src/db/entities/TripMembers.entity'; +import { VisitedCountries } from '../../../src/db/entities/VisitedCountries.entity'; +import { VisitedRegions } from '../../../src/db/entities/VisitedRegions.entity'; // Direct construction over the shared test connection — no TestingModule (repo // convention for DI-native service unit tests). @@ -58,8 +59,7 @@ let atlas: AtlasService; // Kept so the #2527 race tests can step in between a lookup and its cache write. let placeRegionsRepo: PlaceRegionsRepository; -function insertReservationEndpoint( - db: any, +async function insertReservationEndpoint( reservationId: number, role: 'from' | 'to' | 'stop', sequence: number, @@ -69,20 +69,25 @@ function insertReservationEndpoint( localDate: string | null = null, localTime: string | null = null ) { - db.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, lat, lng, code, local_date, local_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)' - ).run(reservationId, role, sequence, `Endpoint ${sequence}`, lat, lng, code, localDate, localTime); + await makeReservationEndpoint(orm, reservationId, { + role, sequence, name: `Endpoint ${sequence}`, lat, lng, code, local_date: localDate, local_time: localTime, + }); } -function insertPlace(db: any, tripId: number, name: string, address: string | null = null) { - const cat = db.prepare('SELECT id FROM categories LIMIT 1').get() as { id: number } | undefined; - const result = db.prepare( - 'INSERT INTO places (trip_id, name, address, category_id) VALUES (?, ?, ?, ?)' - ).run(tripId, name, address, cat?.id ?? null); - return db.prepare('SELECT * FROM places WHERE id = ?').get(result.lastInsertRowid); +/** A place with only an address, filed under the first category, the way the geocoder meets it. */ +function insertPlace(tripId: number, name: string, address: string | null = null) { + return makePlace(orm, tripId, { name, address, lat: null, lng: null }); } +/** Caches the place's region, as a finished background lookup leaves it. */ +async function stampRegion(placeId: number, countryCode: string, regionCode: string, regionName: string): Promise { + await upsertRow(orm, PlaceRegions, { place: placeId, country_code: countryCode, region_code: regionCode, region_name: regionName }); +} + +let orm: TestOrm; + beforeAll(async () => { + orm = await sharedTestOrm(testDb); atlas = new AtlasService( await createTestBucketListRepo(testDb), await createTestHiddenCountriesRepo(testDb), @@ -119,7 +124,7 @@ describe('getStats', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Mystery Trip' }); // Place with no address and no coordinates → can't resolve country - insertPlace(testDb, trip.id, 'Unknown Place', null); + await insertPlace(trip.id, 'Unknown Place', null); const stats = await atlas.stats(user.id); @@ -137,9 +142,9 @@ describe('getStats', () => { // 3 places in France, 1 in Germany → France should win for (let i = 0; i < 3; i++) { - insertPlace(testDb, trip.id, `Paris Place ${i}`, `Street ${i}, Paris, France`); + await insertPlace(trip.id, `Paris Place ${i}`, `Street ${i}, Paris, France`); } - insertPlace(testDb, trip.id, 'Berlin Place', 'Some Street, Berlin, Germany'); + await insertPlace(trip.id, 'Berlin Place', 'Some Street, Berlin, Germany'); const stats = await atlas.stats(user.id); @@ -152,8 +157,8 @@ describe('getStats', () => { it('ATLAS-UNIT-003: returns manually marked countries when user has no trips', async () => { const { user } = createUser(testDb); - testDb.prepare('INSERT INTO visited_countries (user_id, country_code) VALUES (?, ?)').run(user.id, 'JP'); - testDb.prepare('INSERT INTO visited_countries (user_id, country_code) VALUES (?, ?)').run(user.id, 'AU'); + await insertRow(orm, VisitedCountries, { user: user.id, country_code: 'JP' }); + await insertRow(orm, VisitedCountries, { user: user.id, country_code: 'AU' }); const stats = await atlas.stats(user.id); @@ -167,7 +172,7 @@ describe('getStats', () => { const { user } = createUser(testDb); // Past dates — see ATLAS-UNIT-002; a dateless trip is an 'idea' and never mostVisited. const trip = createTrip(testDb, user.id, { title: 'Italy Trip', start_date: '2023-05-01', end_date: '2023-05-10' }); - insertPlace(testDb, trip.id, 'Colosseum', 'Piazza del Colosseo, Rome, Italy'); + await insertPlace(trip.id, 'Colosseum', 'Piazza del Colosseo, Rome, Italy'); const stats = await atlas.stats(user.id); @@ -181,8 +186,8 @@ describe('getStats', () => { const trip = createTrip(testDb, user.id, { title: 'Tokyo Layover Trip' }); const reservation = createReservation(testDb, trip.id, { type: 'flight' }); // Tokyo: 35.6762°N, 139.6503°E — inside the JP bounding box, no place row. - insertReservationEndpoint(testDb, reservation.id, 'from', 0, 35.6762, 139.6503); - insertReservationEndpoint(testDb, reservation.id, 'to', 1, 51.4700, -0.4543); + await insertReservationEndpoint(reservation.id, 'from', 0, 35.6762, 139.6503); + await insertReservationEndpoint(reservation.id, 'to', 1, 51.4700, -0.4543); const stats = await atlas.stats(user.id); @@ -197,9 +202,9 @@ describe('getStats', () => { const reservation = createReservation(testDb, trip.id, { type: 'flight' }); // Departs Belgium, connects through Tokyo (role: stop — never leaves the airport), // lands in Australia. Only BE/AU were actually reached. - insertReservationEndpoint(testDb, reservation.id, 'from', 0, 50.9014, 4.4844); - insertReservationEndpoint(testDb, reservation.id, 'stop', 1, 35.6762, 139.6503); - insertReservationEndpoint(testDb, reservation.id, 'to', 2, -33.8688, 151.2093); + await insertReservationEndpoint(reservation.id, 'from', 0, 50.9014, 4.4844); + await insertReservationEndpoint(reservation.id, 'stop', 1, 35.6762, 139.6503); + await insertReservationEndpoint(reservation.id, 'to', 2, -33.8688, 151.2093); const stats = await atlas.stats(user.id); @@ -215,8 +220,8 @@ describe('getStats', () => { const reservation = createReservation(testDb, trip.id, { type: 'flight' }); // Brussels -> Malaga airport (36.6749, -4.4991). The destination sits inside both // the ES and DZ bounding boxes; without the ES entry it geocoded to Algeria. - insertReservationEndpoint(testDb, reservation.id, 'from', 0, 50.9014, 4.4844); - insertReservationEndpoint(testDb, reservation.id, 'to', 1, 36.6749, -4.4991); + await insertReservationEndpoint(reservation.id, 'from', 0, 50.9014, 4.4844); + await insertReservationEndpoint(reservation.id, 'to', 1, 36.6749, -4.4991); const stats = await atlas.stats(user.id); @@ -242,17 +247,17 @@ describe('getStats layover pairs across two bookings', () => { type Clock = { date: string | null; time: string | null }; - function inboundToHelsinki(tripId: number, arrival: Clock) { + async function inboundToHelsinki(tripId: number, arrival: Clock) { const res = createReservation(testDb, tripId, { type: 'flight', title: 'BRU-HEL' }); - insertReservationEndpoint(testDb, res.id, 'from', 0, BRU.lat, BRU.lng, BRU.code, arrival.date, '07:00'); - insertReservationEndpoint(testDb, res.id, 'to', 1, HEL.lat, HEL.lng, HEL.code, arrival.date, arrival.time); + await insertReservationEndpoint(res.id, 'from', 0, BRU.lat, BRU.lng, BRU.code, arrival.date, '07:00'); + await insertReservationEndpoint(res.id, 'to', 1, HEL.lat, HEL.lng, HEL.code, arrival.date, arrival.time); return res; } - function onwardFromHelsinki(tripId: number, departure: Clock, type = 'flight') { + async function onwardFromHelsinki(tripId: number, departure: Clock, type = 'flight') { const res = createReservation(testDb, tripId, { type, title: 'HEL-JFK' }); - insertReservationEndpoint(testDb, res.id, 'from', 0, HEL.lat, HEL.lng, HEL.code, departure.date, departure.time); - insertReservationEndpoint(testDb, res.id, 'to', 1, JFK.lat, JFK.lng, JFK.code, departure.date, '15:00'); + await insertReservationEndpoint(res.id, 'from', 0, HEL.lat, HEL.lng, HEL.code, departure.date, departure.time); + await insertReservationEndpoint(res.id, 'to', 1, JFK.lat, JFK.lng, JFK.code, departure.date, '15:00'); return res; } @@ -262,8 +267,8 @@ describe('getStats layover pairs across two bookings', () => { it('ATLAS-UNIT-044 (#1535): a hub arrived at and left again the same day is not visited', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'New York via Helsinki' }); - inboundToHelsinki(trip.id, { date: '2026-08-01', time: '09:30' }); - onwardFromHelsinki(trip.id, { date: '2026-08-01', time: '11:00' }); + await inboundToHelsinki(trip.id, { date: '2026-08-01', time: '09:30' }); + await onwardFromHelsinki(trip.id, { date: '2026-08-01', time: '11:00' }); const codes = await codesFor(user.id); expect(codes).toContain('BE'); @@ -274,8 +279,8 @@ describe('getStats layover pairs across two bookings', () => { it('ATLAS-UNIT-045 (#1535): a hub the traveler left two days later stays visited', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Helsinki stopover' }); - inboundToHelsinki(trip.id, { date: '2026-08-01', time: '09:30' }); - onwardFromHelsinki(trip.id, { date: '2026-08-03', time: '11:00' }); + await inboundToHelsinki(trip.id, { date: '2026-08-01', time: '09:30' }); + await onwardFromHelsinki(trip.id, { date: '2026-08-03', time: '11:00' }); expect(await codesFor(user.id)).toContain('FI'); }); @@ -285,11 +290,10 @@ describe('getStats layover pairs across two bookings', () => { // endpoints without local parts, so only reservation_time carries the day. const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'New York via Helsinki' }); - const inbound = inboundToHelsinki(trip.id, { date: null, time: null }); - const onward = onwardFromHelsinki(trip.id, { date: null, time: null }); - const setTime = testDb.prepare('UPDATE reservations SET reservation_time = ? WHERE id = ?'); - setTime.run('2026-08-01', inbound.id); - setTime.run('2026-08-01', onward.id); + const inbound = await inboundToHelsinki(trip.id, { date: null, time: null }); + const onward = await onwardFromHelsinki(trip.id, { date: null, time: null }); + await updateRows(orm, Reservations, { id: inbound.id }, { reservation_time: '2026-08-01' }); + await updateRows(orm, Reservations, { id: onward.id }, { reservation_time: '2026-08-01' }); const codes = await codesFor(user.id); expect(codes).toContain('BE'); @@ -300,11 +304,10 @@ describe('getStats layover pairs across two bookings', () => { it('ATLAS-UNIT-047 (#1535): clockless bookings two days apart keep the hub visited', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Helsinki stopover' }); - const inbound = inboundToHelsinki(trip.id, { date: null, time: null }); - const onward = onwardFromHelsinki(trip.id, { date: null, time: null }); - const setTime = testDb.prepare('UPDATE reservations SET reservation_time = ? WHERE id = ?'); - setTime.run('2026-08-01', inbound.id); - setTime.run('2026-08-03', onward.id); + const inbound = await inboundToHelsinki(trip.id, { date: null, time: null }); + const onward = await onwardFromHelsinki(trip.id, { date: null, time: null }); + await updateRows(orm, Reservations, { id: inbound.id }, { reservation_time: '2026-08-01' }); + await updateRows(orm, Reservations, { id: onward.id }, { reservation_time: '2026-08-03' }); expect(await codesFor(user.id)).toContain('FI'); }); @@ -312,9 +315,9 @@ describe('getStats layover pairs across two bookings', () => { it('ATLAS-UNIT-048 (#1535): a place in the layover country keeps it visited', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'New York via Helsinki' }); - inboundToHelsinki(trip.id, { date: '2026-08-01', time: '09:30' }); - onwardFromHelsinki(trip.id, { date: '2026-08-01', time: '11:00' }); - insertPlace(testDb, trip.id, 'Kamppi Chapel', 'Simonkatu 7, Helsinki, Finland'); + await inboundToHelsinki(trip.id, { date: '2026-08-01', time: '09:30' }); + await onwardFromHelsinki(trip.id, { date: '2026-08-01', time: '11:00' }); + await insertPlace(trip.id, 'Kamppi Chapel', 'Simonkatu 7, Helsinki, Finland'); expect(await codesFor(user.id)).toContain('FI'); }); @@ -322,8 +325,8 @@ describe('getStats layover pairs across two bookings', () => { it('ATLAS-UNIT-049 (#1366 guard): a car rental picked up where the flight landed keeps the country', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Driving Finland' }); - inboundToHelsinki(trip.id, { date: '2026-08-01', time: '09:30' }); - onwardFromHelsinki(trip.id, { date: '2026-08-01', time: '11:00' }, 'car_rental'); + await inboundToHelsinki(trip.id, { date: '2026-08-01', time: '09:30' }); + await onwardFromHelsinki(trip.id, { date: '2026-08-01', time: '11:00' }, 'car_rental'); expect(await codesFor(user.id)).toContain('FI'); }); @@ -331,9 +334,9 @@ describe('getStats layover pairs across two bookings', () => { it('ATLAS-UNIT-050 (#1535): a cancelled onward flight is no connection', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Helsinki, stuck' }); - inboundToHelsinki(trip.id, { date: '2026-08-01', time: '09:30' }); - const onward = onwardFromHelsinki(trip.id, { date: '2026-08-01', time: '11:00' }); - testDb.prepare('UPDATE reservations SET status = ? WHERE id = ?').run('cancelled', onward.id); + await inboundToHelsinki(trip.id, { date: '2026-08-01', time: '09:30' }); + const onward = await onwardFromHelsinki(trip.id, { date: '2026-08-01', time: '11:00' }); + await updateRows(orm, Reservations, { id: onward.id }, { status: 'cancelled' }); expect(await codesFor(user.id)).toContain('FI'); }); @@ -344,8 +347,8 @@ describe('getStats layover pairs across two bookings', () => { const { user } = createUser(testDb); const arriving = createTrip(testDb, user.id, { title: 'Back from Brussels' }); const leaving = createTrip(testDb, user.id, { title: 'Off to New York' }); - inboundToHelsinki(arriving.id, { date: '2026-08-01', time: '09:30' }); - onwardFromHelsinki(leaving.id, { date: '2026-08-01', time: '11:00' }); + await inboundToHelsinki(arriving.id, { date: '2026-08-01', time: '09:30' }); + await onwardFromHelsinki(leaving.id, { date: '2026-08-01', time: '11:00' }); expect(await codesFor(user.id)).toContain('FI'); }); @@ -353,10 +356,10 @@ describe('getStats layover pairs across two bookings', () => { it('ATLAS-UNIT-052 (#1535): a same-day out-and-back is a day in the country, not a transfer', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'A day in Helsinki' }); - inboundToHelsinki(trip.id, { date: '2026-08-01', time: '09:30' }); + await inboundToHelsinki(trip.id, { date: '2026-08-01', time: '09:30' }); const back = createReservation(testDb, trip.id, { type: 'flight', title: 'HEL-BRU' }); - insertReservationEndpoint(testDb, back.id, 'from', 0, HEL.lat, HEL.lng, HEL.code, '2026-08-01', '18:00'); - insertReservationEndpoint(testDb, back.id, 'to', 1, BRU.lat, BRU.lng, BRU.code, '2026-08-01', '20:30'); + await insertReservationEndpoint(back.id, 'from', 0, HEL.lat, HEL.lng, HEL.code, '2026-08-01', '18:00'); + await insertReservationEndpoint(back.id, 'to', 1, BRU.lat, BRU.lng, BRU.code, '2026-08-01', '20:30'); expect(await codesFor(user.id)).toContain('FI'); }); @@ -367,10 +370,10 @@ describe('getStats layover pairs across two bookings', () => { // onwards and drops Finland, though the traveler spent the day in Helsinki. const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'A day in Helsinki' }); - inboundToHelsinki(trip.id, { date: '2026-08-01', time: '09:30' }); + await inboundToHelsinki(trip.id, { date: '2026-08-01', time: '09:30' }); const back = createReservation(testDb, trip.id, { type: 'flight', title: 'HEL-CRL' }); - insertReservationEndpoint(testDb, back.id, 'from', 0, HEL.lat, HEL.lng, HEL.code, '2026-08-01', '18:00'); - insertReservationEndpoint(testDb, back.id, 'to', 1, 50.4592, 4.4538, 'CRL', '2026-08-01', '20:30'); + await insertReservationEndpoint(back.id, 'from', 0, HEL.lat, HEL.lng, HEL.code, '2026-08-01', '18:00'); + await insertReservationEndpoint(back.id, 'to', 1, 50.4592, 4.4538, 'CRL', '2026-08-01', '20:30'); const codes = await codesFor(user.id); expect(codes).toContain('FI'); @@ -553,8 +556,8 @@ describe('unmarkCountryVisited — tombstones', () => { // Brussels -> Tokyo. JP is derived from the endpoint; it has no visited_countries // row, so the DELETE in unmarkCountryVisited used to affect nothing and getStats // re-derived JP on the very next call. - insertReservationEndpoint(testDb, reservation.id, 'from', 0, 50.9014, 4.4844); - insertReservationEndpoint(testDb, reservation.id, 'to', 1, 35.6762, 139.6503); + await insertReservationEndpoint(reservation.id, 'from', 0, 50.9014, 4.4844); + await insertReservationEndpoint(reservation.id, 'to', 1, 35.6762, 139.6503); const before = await atlas.stats(user.id); expect(before.countries.map((c: { code: string }) => c.code)).toContain('JP'); @@ -571,8 +574,8 @@ describe('unmarkCountryVisited — tombstones', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Layover Trip' }); const reservation = createReservation(testDb, trip.id, { type: 'flight' }); - insertReservationEndpoint(testDb, reservation.id, 'from', 0, 50.9014, 4.4844); - insertReservationEndpoint(testDb, reservation.id, 'to', 1, 35.6762, 139.6503); + await insertReservationEndpoint(reservation.id, 'from', 0, 50.9014, 4.4844); + await insertReservationEndpoint(reservation.id, 'to', 1, 35.6762, 139.6503); await atlas.unmarkCountry(user.id, 'JP'); expect((await atlas.stats(user.id)).countries.map((c: { code: string }) => c.code)).not.toContain('JP'); @@ -587,13 +590,13 @@ describe('unmarkCountryVisited — tombstones', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Japan Trip' }); const reservation = createReservation(testDb, trip.id, { type: 'flight' }); - insertReservationEndpoint(testDb, reservation.id, 'from', 0, 50.9014, 4.4844); - insertReservationEndpoint(testDb, reservation.id, 'to', 1, 35.6762, 139.6503); + await insertReservationEndpoint(reservation.id, 'from', 0, 50.9014, 4.4844); + await insertReservationEndpoint(reservation.id, 'to', 1, 35.6762, 139.6503); await atlas.unmarkCountry(user.id, 'JP'); expect((await atlas.stats(user.id)).countries.map((c: { code: string }) => c.code)).not.toContain('JP'); - insertPlace(testDb, trip.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); + await insertPlace(trip.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); const after = await atlas.stats(user.id); const jp = after.countries.find((c: { code: string }) => c.code === 'JP'); @@ -751,12 +754,8 @@ describe('getCountryGeo', () => { // ── Helpers for new tests ──────────────────────────────────────────────────── -function insertPlaceWithCoords(db: any, tripId: number, name: string, lat: number, lng: number, address: string | null = null) { - const cat = db.prepare('SELECT id FROM categories LIMIT 1').get() as { id: number } | undefined; - const result = db.prepare( - 'INSERT INTO places (trip_id, name, address, lat, lng, category_id) VALUES (?, ?, ?, ?, ?, ?)' - ).run(tripId, name, address, lat, lng, cat?.id ?? null); - return db.prepare('SELECT * FROM places WHERE id = ?').get(result.lastInsertRowid); +function insertPlaceWithCoords(tripId: number, name: string, lat: number, lng: number, address: string | null = null) { + return makePlace(orm, tripId, { name, address, lat, lng }); } // ── getStats — extended ────────────────────────────────────────────────────── @@ -784,7 +783,7 @@ describe('getStats — extended', () => { it('ATLAS-UNIT-007: manually marked country is merged when user has trips but no resolvable places for that country', async () => { const { user } = createUser(testDb); createTrip(testDb, user.id, { title: 'Japan Trip', start_date: '2024-01-01', end_date: '2024-01-10' }); - testDb.prepare('INSERT INTO visited_countries (user_id, country_code) VALUES (?, ?)').run(user.id, 'JP'); + await insertRow(orm, VisitedCountries, { user: user.id, country_code: 'JP' }); const stats = await atlas.stats(user.id); @@ -797,7 +796,7 @@ describe('getStats — extended', () => { it('ATLAS-UNIT-008: lastTrip is resolved with a country code when its places have an address', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Past France Trip', start_date: '2023-05-01', end_date: '2023-05-10' }); - insertPlace(testDb, trip.id, 'Eiffel Tower', 'Champ de Mars, Paris, France'); + await insertPlace(trip.id, 'Eiffel Tower', 'Champ de Mars, Paris, France'); const stats = await atlas.stats(user.id); @@ -859,7 +858,7 @@ describe('getStats — extended', () => { // region-level fix (ATLAS-UNIT-024) at the country level. const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'San Francisco Trip' }); - insertPlaceWithCoords(testDb, trip.id, 'Hotel Pickwick', 37.7830549, -122.4066689, '85 5th St, San Francisco, CA'); + await insertPlaceWithCoords(trip.id, 'Hotel Pickwick', 37.7830549, -122.4066689, '85 5th St, San Francisco, CA'); const stats = await atlas.stats(user.id); @@ -873,7 +872,7 @@ describe('getStats — extended', () => { // place_regions cache), so this exercises the fix independently of ATLAS-UNIT-027. const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Past NY Trip', start_date: '2023-05-01', end_date: '2023-05-10' }); - insertPlaceWithCoords(testDb, trip.id, 'Imperial Court Hotel', 40.7848394, -73.981643, '307 W 79th Street, New York, NY'); + await insertPlaceWithCoords(trip.id, 'Imperial Court Hotel', 40.7848394, -73.981643, '307 W 79th Street, New York, NY'); const stats = await atlas.stats(user.id); @@ -898,8 +897,8 @@ describe('getCountryPlaces', () => { it('ATLAS-UNIT-014: returns matching places when place address resolves to the requested country', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'France Trip' }); - insertPlace(testDb, trip.id, 'Louvre', '75001 Paris, France'); - insertPlace(testDb, trip.id, 'Berlin Wall', 'Bernauer Str., Berlin, Germany'); + await insertPlace(trip.id, 'Louvre', '75001 Paris, France'); + await insertPlace(trip.id, 'Berlin Wall', 'Bernauer Str., Berlin, Germany'); const result = await atlas.countryPlaces(user.id, 'FR'); @@ -911,7 +910,7 @@ describe('getCountryPlaces', () => { it('ATLAS-UNIT-015: manually_marked is true when country is in visited_countries', async () => { const { user } = createUser(testDb); - testDb.prepare('INSERT INTO visited_countries (user_id, country_code) VALUES (?, ?)').run(user.id, 'JP'); + await insertRow(orm, VisitedCountries, { user: user.id, country_code: 'JP' }); createTrip(testDb, user.id, { title: 'Japan' }); const result = await atlas.countryPlaces(user.id, 'JP'); @@ -923,7 +922,7 @@ describe('getCountryPlaces', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Coord Trip' }); // Paris coordinates (48.85°N, 2.35°E) — falls inside FR bounding box - insertPlaceWithCoords(testDb, trip.id, 'Secret Paris Spot', 48.85, 2.35); + await insertPlaceWithCoords(trip.id, 'Secret Paris Spot', 48.85, 2.35); const result = await atlas.countryPlaces(user.id, 'FR'); @@ -945,8 +944,8 @@ describe('getVisitedRegions', () => { it('ATLAS-UNIT-018: returns manually marked regions even when user has no places with coordinates', async () => { const { user } = createUser(testDb); - testDb.prepare('INSERT INTO visited_countries (user_id, country_code) VALUES (?, ?)').run(user.id, 'DE'); - testDb.prepare('INSERT INTO visited_regions (user_id, region_code, region_name, country_code) VALUES (?, ?, ?, ?)').run(user.id, 'DE-BY', 'Bayern', 'DE'); + await insertRow(orm, VisitedCountries, { user: user.id, country_code: 'DE' }); + await insertRow(orm, VisitedRegions, { user: user.id, region_code: 'DE-BY', region_name: 'Bayern', country_code: 'DE' }); const result = await atlas.visitedRegions(user.id); @@ -972,7 +971,7 @@ describe('getVisitedRegions', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris Trip' }); - insertPlaceWithCoords(testDb, trip.id, 'Paris Hotel', 48.85, 2.35); + await insertPlaceWithCoords(trip.id, 'Paris Hotel', 48.85, 2.35); // First call triggers the background geocoding fire-and-forget await atlas.visitedRegions(user.id); @@ -989,12 +988,10 @@ describe('getVisitedRegions', () => { it('ATLAS-UNIT-020: places already cached in place_regions are not re-geocoded', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Cached Trip' }); - const place = insertPlaceWithCoords(testDb, trip.id, 'Cached Place', 48.85, 2.35); + const place = await insertPlaceWithCoords(trip.id, 'Cached Place', 48.85, 2.35); // Pre-populate the place_regions cache so the fetch path is never reached - testDb.prepare( - 'INSERT OR REPLACE INTO place_regions (place_id, country_code, region_code, region_name) VALUES (?, ?, ?, ?)' - ).run(place.id, 'FR', 'FR-75', 'Île-de-France'); + await stampRegion(place.id, 'FR', 'FR-75', 'Île-de-France'); const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) }); vi.stubGlobal('fetch', mockFetch); @@ -1018,7 +1015,7 @@ describe('getVisitedRegions', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Manchester Trip' }); - insertPlaceWithCoords(testDb, trip.id, 'Old Trafford', 53.4631, -2.2913); + await insertPlaceWithCoords(trip.id, 'Old Trafford', 53.4631, -2.2913); await atlas.visitedRegions(user.id); // The background geocode is fire-and-forget; give its microtasks a turn to settle @@ -1044,7 +1041,7 @@ describe('getVisitedRegions', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Barcelona Trip' }); - insertPlaceWithCoords(testDb, trip.id, 'Sagrada Familia', 41.4036, 2.1744); + await insertPlaceWithCoords(trip.id, 'Sagrada Familia', 41.4036, 2.1744); await atlas.visitedRegions(user.id); // The background geocode is fire-and-forget; give its microtasks a turn to settle @@ -1068,8 +1065,8 @@ describe('getVisitedRegions', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Luxembourg Trip' }); - insertPlaceWithCoords( - testDb, trip.id, 'Airbnb - Welcome Home', 49.8502458, 6.3576404, + await insertPlaceWithCoords( + trip.id, 'Airbnb - Welcome Home', 49.8502458, 6.3576404, '4 Gruusswiss, Bollendorf-Pont, Distrikt Gréiwemaacher 6555, Luxembourg' ); @@ -1095,8 +1092,8 @@ describe('getVisitedRegions', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'San Francisco Trip' }); - insertPlaceWithCoords( - testDb, trip.id, 'Hotel Pickwick', 37.7830549, -122.4066689, + await insertPlaceWithCoords( + trip.id, 'Hotel Pickwick', 37.7830549, -122.4066689, '85 5th St, San Francisco, CA' ); @@ -1126,7 +1123,7 @@ describe('getVisitedRegions', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Middle of the ocean' }); - insertPlaceWithCoords(testDb, trip.id, 'Buoy', 10, -40); + await insertPlaceWithCoords(trip.id, 'Buoy', 10, -40); await atlas.visitedRegions(user.id); await vi.runAllTimersAsync(); @@ -1163,7 +1160,7 @@ describe('getVisitedRegions', () => { // regionCache is an in-memory Map keyed by rounded lat/lng and persists across tests in // this file, so reusing the same point would silently hit that cached result instead of // exercising this test's fetch/gate path. - insertPlaceWithCoords(testDb, trip.id, 'Weather buoy', 20, -35, '123 Nowhere Rd, JP'); + await insertPlaceWithCoords(trip.id, 'Weather buoy', 20, -35, '123 Nowhere Rd, JP'); await atlas.visitedRegions(user.id); await vi.runAllTimersAsync(); @@ -1179,17 +1176,14 @@ describe('getVisitedRegions', () => { // ── A place that moves (#2527) ─────────────────────────────────────────────── describe('a place that moves keeps no stale region', () => { - function cachedCountryOf(placeId: number): string | undefined { - const row = testDb.prepare('SELECT country_code FROM place_regions WHERE place_id = ?').get(placeId) as - | { country_code: string } - | undefined; - return row?.country_code; + async function cachedCountryOf(placeId: number): Promise { + return (await findRow(orm, PlaceRegions, { place: placeId }))?.country_code; } // The lookup runs in the background; the first one in a worker also loads the admin-1 bundle. async function settledCountryOf(placeId: number): Promise { for (let i = 0; i < 150; i++) { - const code = cachedCountryOf(placeId); + const code = await cachedCountryOf(placeId); if (code) return code; await new Promise((resolve) => setTimeout(resolve, 20)); } @@ -1199,7 +1193,7 @@ describe('a place that moves keeps no stale region', () => { it('ATLAS-SVC-2527a: a lookup still running when the place moves does not write the old country back', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Lyon, then Munich' }); - const place = insertPlaceWithCoords(testDb, trip.id, 'Hotel', 45.764, 4.8357, 'Lyon, France'); + const place = await insertPlaceWithCoords(trip.id, 'Hotel', 45.764, 4.8357, 'Lyon, France'); // The Atlas load starts resolving Lyon, and the place is corrected before it answers. // The repository reads are async here, so the move is placed deterministically @@ -1207,6 +1201,7 @@ describe('a place that moves keeps no stale region', () => { const write = placeRegionsRepo.upsertRegionWhileUnmoved.bind(placeRegionsRepo); const step = vi.spyOn(placeRegionsRepo, 'upsertRegionWhileUnmoved').mockImplementationOnce(async (at, info) => { testDb + // test-sql-allow: the move lands inside the repository call this test intercepts, on its own connection, synchronously. .prepare('UPDATE places SET lat = ?, lng = ?, address = ? WHERE id = ?') .run(48.1374, 11.5755, 'Marienplatz, Munich, Germany', place.id); return write(at, info); @@ -1215,7 +1210,7 @@ describe('a place that moves keeps no stale region', () => { await vi.waitFor(() => expect(step).toHaveBeenCalled(), { timeout: 3000 }); step.mockRestore(); await new Promise((resolve) => setTimeout(resolve, 300)); - expect(cachedCountryOf(place.id)).toBeUndefined(); + expect(await cachedCountryOf(place.id)).toBeUndefined(); // The next load resolves where the place is now. expect((await atlas.stats(user.id)).countries.map((c) => c.code)).toEqual(['DE']); @@ -1226,10 +1221,11 @@ describe('a place that moves keeps no stale region', () => { it('ATLAS-SVC-2527b: a place deleted while its lookup runs leaves nothing behind', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Cancelled' }); - const place = insertPlaceWithCoords(testDb, trip.id, 'Hotel', 43.2965, 5.3698, 'Marseille, France'); + const place = await insertPlaceWithCoords(trip.id, 'Hotel', 43.2965, 5.3698, 'Marseille, France'); const write = placeRegionsRepo.upsertRegionWhileUnmoved.bind(placeRegionsRepo); const step = vi.spyOn(placeRegionsRepo, 'upsertRegionWhileUnmoved').mockImplementationOnce(async (at, info) => { + // test-sql-allow: the delete lands inside the repository call this test intercepts, on its own connection, synchronously. testDb.prepare('DELETE FROM places WHERE id = ?').run(place.id); return write(at, info); }); @@ -1238,7 +1234,7 @@ describe('a place that moves keeps no stale region', () => { step.mockRestore(); await new Promise((resolve) => setTimeout(resolve, 300)); - expect(testDb.prepare('SELECT COUNT(*) AS n FROM place_regions').get()).toEqual({ n: 0 }); + expect(await countRows(orm, PlaceRegions)).toBe(0); }); }); @@ -1269,7 +1265,7 @@ describe('the background lookup runs inside its own request context', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Manchester Trip' }); // The bundled GB polygons answer without Nominatim (ATLAS-UNIT-021), so no fetch is involved. - const place = insertPlaceWithCoords(testDb, trip.id, 'Old Trafford', 53.4631, -2.2913); + const place = await insertPlaceWithCoords(trip.id, 'Old Trafford', 53.4631, -2.2913); // Only the repository the loop writes through is strict; the rest of the service is not exercised here. const bare = new AtlasService( await createTestBucketListRepo(testDb), @@ -1286,10 +1282,10 @@ describe('the background lookup runs inside its own request context', () => { ); // Private on purpose; bracket notation reaches it without an `any`. - bare['cacheRegionsInBackground']([place]); + bare['cacheRegionsInBackground']([place as PlaceRow]); await vi.waitFor( - () => expect(testDb.prepare('SELECT country_code FROM place_regions WHERE place_id = ?').get(place.id)).toEqual({ country_code: 'GB' }), + async () => expect((await findRow(orm, PlaceRegions, { place: place.id }))?.country_code).toBe('GB'), { timeout: 5000 }, ); }); @@ -1315,7 +1311,7 @@ describe('unmarkRegionVisited — tombstones + country cascade', () => { // placeCount until explicitly re-marked. const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'SF Trip' }); - insertPlaceWithCoords(testDb, trip.id, 'Golden Gate Park', 37.7694, -122.4862); + await insertPlaceWithCoords(trip.id, 'Golden Gate Park', 37.7694, -122.4862); await primeRegionCache(user.id); const before = await atlas.visitedRegions(user.id); @@ -1330,7 +1326,7 @@ describe('unmarkRegionVisited — tombstones + country cascade', () => { it('ATLAS-SVC-025: re-marking a hidden region brings it back', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'SF Trip' }); - insertPlaceWithCoords(testDb, trip.id, 'Golden Gate Park', 37.7694, -122.4862); + await insertPlaceWithCoords(trip.id, 'Golden Gate Park', 37.7694, -122.4862); await primeRegionCache(user.id); await atlas.unmarkRegion(user.id, 'US-CA'); @@ -1362,8 +1358,8 @@ describe('unmarkRegionVisited — tombstones + country cascade', () => { it('ATLAS-SVC-027: hiding one of a country\'s several regions does NOT cascade-hide the country', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'NY road trip' }); - insertPlaceWithCoords(testDb, trip.id, 'Boston hotel', 42.3588336, -71.0578303); // MA - insertPlaceWithCoords(testDb, trip.id, 'Philly hotel', 39.9527237, -75.1635262); // PA + await insertPlaceWithCoords(trip.id, 'Boston hotel', 42.3588336, -71.0578303); // MA + await insertPlaceWithCoords(trip.id, 'Philly hotel', 39.9527237, -75.1635262); // PA await primeRegionCache(user.id); await atlas.unmarkRegion(user.id, 'US-MA'); @@ -1407,7 +1403,7 @@ describe('atlas quirk fixes', () => { // The legacy early return hardcoded manually_marked: false, so a user who // marked a country visited before creating any trip saw it as unmarked. const { user } = createUser(testDb); - testDb.prepare('INSERT INTO visited_countries (user_id, country_code) VALUES (?, ?)').run(user.id, 'JP'); + await insertRow(orm, VisitedCountries, { user: user.id, country_code: 'JP' }); const result = await atlas.countryPlaces(user.id, 'JP'); @@ -1465,8 +1461,8 @@ describe('atlas quirk fixes', () => { expect(await atlas.updateBucketItem(other.id, item.id, { name: 'Stolen' })).toBeNull(); expect(await atlas.deleteBucketItem(other.id, item.id)).toBe(false); - const row = testDb.prepare('SELECT name FROM bucket_list WHERE id = ?').get(item.id) as { name: string }; - expect(row.name).toBe('Mine'); + const row = await findRow(orm, BucketList, { id: item.id }); + expect(row?.name).toBe('Mine'); }); it('ATLAS-SVC-035: unmarkRegion is atomic — a failure mid-flow rolls the region delete back', async () => { @@ -1482,10 +1478,8 @@ describe('atlas quirk fixes', () => { testDb.exec('ALTER TABLE hidden_regions_gone RENAME TO hidden_regions'); } - const row = testDb - .prepare('SELECT 1 FROM visited_regions WHERE user_id = ? AND region_code = ?') - .get(user.id, 'JP-13'); - expect(row).toBeDefined(); + const row = await findRow(orm, VisitedRegions, { user: user.id, region_code: 'JP-13' }); + expect(row).not.toBeNull(); }); it('ATLAS-SVC-036: markCountry is atomic — a failure on the tombstone lift rolls the insert back', async () => { @@ -1498,10 +1492,8 @@ describe('atlas quirk fixes', () => { testDb.exec('ALTER TABLE hidden_countries_gone RENAME TO hidden_countries'); } - const row = testDb - .prepare('SELECT 1 FROM visited_countries WHERE user_id = ? AND country_code = ?') - .get(user.id, 'DE'); - expect(row).toBeUndefined(); + const row = await findRow(orm, VisitedCountries, { user: user.id, country_code: 'DE' }); + expect(row).toBeNull(); }); }); @@ -1513,15 +1505,14 @@ describe('atlas quirk fixes', () => { // which is exactly what the report asks for. describe('bucket-list duplicates (#1898)', () => { - const countRows = (userId: number): number => - (testDb.prepare('SELECT COUNT(*) AS n FROM bucket_list WHERE user_id = ?').get(userId) as { n: number }).n; + const bucketCount = (userId: number): Promise => countRows(orm, BucketList, { user: userId }); it('ATLAS-SVC-037: adding the same wish twice is refused and writes no second row', async () => { const { user } = createUser(testDb); await atlas.createBucketItem(user.id, { name: 'Japan', country_code: 'JP' }); await expect(atlas.createBucketItem(user.id, { name: 'Japan', country_code: 'JP' })).rejects.toThrow(BucketItemExistsError); - expect(countRows(user.id)).toBe(1); + expect(await bucketCount(user.id)).toBe(1); }); it('ATLAS-SVC-038: the same place with another target date is a separate entry', async () => { @@ -1531,7 +1522,7 @@ describe('bucket-list duplicates (#1898)', () => { await atlas.createBucketItem(user.id, { name: 'Japan', country_code: 'JP', target_date: '2027-05' }); await atlas.createBucketItem(user.id, { name: 'Japan', country_code: 'JP', target_date: '2028-09' }); - expect(countRows(user.id)).toBe(3); + expect(await bucketCount(user.id)).toBe(3); // ...but each of those dates only once. await expect(atlas.createBucketItem(user.id, { name: 'Japan', country_code: 'JP', target_date: '2027-05' })).rejects.toThrow( BucketItemExistsError, @@ -1543,7 +1534,7 @@ describe('bucket-list duplicates (#1898)', () => { await atlas.createBucketItem(user.id, { name: 'Kyoto' }); await expect(atlas.createBucketItem(user.id, { name: ' kyoto ' })).rejects.toThrow(BucketItemExistsError); - expect(countRows(user.id)).toBe(1); + expect(await bucketCount(user.id)).toBe(1); }); it('ATLAS-SVC-040: an empty string and NULL are the same "not set"', async () => { @@ -1552,8 +1543,8 @@ describe('bucket-list duplicates (#1898)', () => { // The forms send '' where the map dialogs send null — both must land on the // same identity, and the row itself is stored normalised. - const row = testDb.prepare('SELECT country_code, target_date FROM bucket_list WHERE user_id = ?').get(user.id); - expect(row).toEqual({ country_code: null, target_date: null }); + const row = await findRow(orm, BucketList, { user: user.id }); + expect({ country_code: row?.country_code, target_date: row?.target_date }).toEqual({ country_code: null, target_date: null }); await expect(atlas.createBucketItem(user.id, { name: 'Lisbon', country_code: null, target_date: null })).rejects.toThrow( BucketItemExistsError, ); @@ -1568,8 +1559,8 @@ describe('bucket-list duplicates (#1898)', () => { await atlas.createBucketItem(user.id, { name: 'Altstadt', country_code: 'DE', lat: 50.94, lng: 6.96 }); await atlas.createBucketItem(other.id, { name: 'Altstadt', country_code: 'DE', lat: 48.13, lng: 11.57 }); - expect(countRows(user.id)).toBe(3); - expect(countRows(other.id)).toBe(1); + expect(await bucketCount(user.id)).toBe(3); + expect(await bucketCount(other.id)).toBe(1); }); it('ATLAS-SVC-042: a coordinate-less wish does not collide with the same name pinned to a place', async () => { @@ -1578,7 +1569,7 @@ describe('bucket-list duplicates (#1898)', () => { await atlas.createBucketItem(user.id, { name: 'Kyoto', country_code: 'JP', lat: 35.01, lng: 135.76 }); - expect(countRows(user.id)).toBe(2); + expect(await bucketCount(user.id)).toBe(2); }); it('ATLAS-SVC-043: an update may not move a row onto another wish', async () => { @@ -1590,10 +1581,8 @@ describe('bucket-list duplicates (#1898)', () => { }; await expect(atlas.updateBucketItem(user.id, second.id, { target_date: '2027-05' })).rejects.toThrow(BucketItemExistsError); - const row = testDb.prepare('SELECT target_date FROM bucket_list WHERE id = ?').get(second.id) as { - target_date: string; - }; - expect(row.target_date).toBe('2028-09'); + const row = await findRow(orm, BucketList, { id: second.id }); + expect(row?.target_date).toBe('2028-09'); }); it('ATLAS-SVC-044: an update of a row against itself is not a collision', async () => { @@ -1612,13 +1601,12 @@ describe('bucket-list duplicates (#1898)', () => { const { user } = createUser(testDb); // Rows written before the guard existed are left alone on purpose — no // migration deletes user data for this. - const insert = testDb.prepare('INSERT INTO bucket_list (user_id, name, country_code) VALUES (?, ?, ?)'); - insert.run(user.id, 'Japan', 'JP'); - const legacy = insert.run(user.id, 'Japan', 'JP'); + await insertRow(orm, BucketList, { user: user.id, name: 'Japan', country_code: 'JP' }); + const legacy = await insertRow(orm, BucketList, { user: user.id, name: 'Japan', country_code: 'JP' }); expect(await atlas.bucketList(user.id)).toHaveLength(2); - expect(await atlas.deleteBucketItem(user.id, Number(legacy.lastInsertRowid))).toBe(true); - expect(countRows(user.id)).toBe(1); + expect(await atlas.deleteBucketItem(user.id, legacy)).toBe(true); + expect(await bucketCount(user.id)).toBe(1); }); }); @@ -1643,7 +1631,7 @@ describe('getStats — visited vs planned vs idea (#1048)', () => { it('ATLAS-UNIT-029: a country from a trip that already happened is visited', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Rome, last month', start_date: PAST_START, end_date: PAST_END }); - insertPlace(testDb, trip.id, 'Colosseum', 'Piazza del Colosseo, Rome, Italy'); + await insertPlace(trip.id, 'Colosseum', 'Piazza del Colosseo, Rome, Italy'); const stats = await atlas.stats(user.id); @@ -1656,7 +1644,7 @@ describe('getStats — visited vs planned vs idea (#1048)', () => { it('ATLAS-UNIT-030: a country from a future trip is planned — listed, but not counted as visited', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Japan, next month', start_date: FUTURE_START, end_date: FUTURE_END }); - insertPlace(testDb, trip.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); + await insertPlace(trip.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); const stats = await atlas.stats(user.id); @@ -1675,7 +1663,7 @@ describe('getStats — visited vs planned vs idea (#1048)', () => { start_date: isoOffsetDays(-2), end_date: isoOffsetDays(5), }); - insertPlace(testDb, trip.id, 'Brandenburger Tor', 'Pariser Platz, Berlin, Germany'); + await insertPlace(trip.id, 'Brandenburger Tor', 'Pariser Platz, Berlin, Germany'); const stats = await atlas.stats(user.id); @@ -1687,7 +1675,7 @@ describe('getStats — visited vs planned vs idea (#1048)', () => { const { user } = createUser(testDb); const today = isoOffsetDays(0); const trip = createTrip(testDb, user.id, { title: 'Flying out today', start_date: today, end_date: isoOffsetDays(6) }); - insertPlace(testDb, trip.id, 'Louvre', '75001 Paris, France'); + await insertPlace(trip.id, 'Louvre', '75001 Paris, France'); const stats = await atlas.stats(user.id); @@ -1698,7 +1686,7 @@ describe('getStats — visited vs planned vs idea (#1048)', () => { it('ATLAS-UNIT-033: a trip with no dates at all is an idea, not a plan', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Someday: Japan' }); - insertPlace(testDb, trip.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); + await insertPlace(trip.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); const stats = await atlas.stats(user.id); @@ -1712,8 +1700,8 @@ describe('getStats — visited vs planned vs idea (#1048)', () => { const { user } = createUser(testDb); const past = createTrip(testDb, user.id, { title: 'Munich 2023', start_date: PAST_START, end_date: PAST_END }); const future = createTrip(testDb, user.id, { title: 'Munich again', start_date: FUTURE_START, end_date: FUTURE_END }); - insertPlace(testDb, past.id, 'Marienplatz', 'Marienplatz, Munich, Germany'); - insertPlace(testDb, future.id, 'Englischer Garten', 'Englischer Garten, Munich, Germany'); + await insertPlace(past.id, 'Marienplatz', 'Marienplatz, Munich, Germany'); + await insertPlace(future.id, 'Englischer Garten', 'Englischer Garten, Munich, Germany'); const stats = await atlas.stats(user.id); @@ -1728,8 +1716,8 @@ describe('getStats — visited vs planned vs idea (#1048)', () => { // downgrade it back to a plan. const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Japan, next month', start_date: FUTURE_START, end_date: FUTURE_END }); - insertPlace(testDb, trip.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); - testDb.prepare('INSERT INTO visited_countries (user_id, country_code) VALUES (?, ?)').run(user.id, 'JP'); + await insertPlace(trip.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); + await insertRow(orm, VisitedCountries, { user: user.id, country_code: 'JP' }); const stats = await atlas.stats(user.id); @@ -1744,8 +1732,8 @@ describe('getStats — visited vs planned vs idea (#1048)', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Tokyo, next month', start_date: FUTURE_START, end_date: FUTURE_END }); const reservation = createReservation(testDb, trip.id, { type: 'flight' }); - insertReservationEndpoint(testDb, reservation.id, 'from', 0, 50.9014, 4.4844); // Brussels - insertReservationEndpoint(testDb, reservation.id, 'to', 1, 35.6762, 139.6503); // Tokyo + await insertReservationEndpoint(reservation.id, 'from', 0, 50.9014, 4.4844); // Brussels + await insertReservationEndpoint(reservation.id, 'to', 1, 35.6762, 139.6503); // Tokyo const before = await atlas.stats(user.id); expect(before.countries.find((c: any) => c.code === 'JP')).toMatchObject({ status: 'planned' }); @@ -1762,8 +1750,8 @@ describe('getStats — visited vs planned vs idea (#1048)', () => { const { user } = createUser(testDb); const past = createTrip(testDb, user.id, { title: 'Paris 2023', start_date: PAST_START, end_date: PAST_END }); const future = createTrip(testDb, user.id, { title: 'Tokyo soon', start_date: FUTURE_START, end_date: FUTURE_END }); - insertPlace(testDb, past.id, 'Louvre', '75001 Paris, France'); - insertPlace(testDb, future.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); + await insertPlace(past.id, 'Louvre', '75001 Paris, France'); + await insertPlace(future.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); const stats = await atlas.stats(user.id); @@ -1775,8 +1763,8 @@ describe('getStats — visited vs planned vs idea (#1048)', () => { const { user } = createUser(testDb); const past = createTrip(testDb, user.id, { title: 'Rome 2023', start_date: PAST_START, end_date: PAST_END }); const future = createTrip(testDb, user.id, { title: 'Japan soon', start_date: FUTURE_START, end_date: FUTURE_END }); - insertPlace(testDb, past.id, 'Colosseum', 'Piazza del Colosseo, Rome, Italy'); - for (let i = 0; i < 3; i++) insertPlace(testDb, future.id, `Tokyo Place ${i}`, `Street ${i}, Tokyo, Japan`); + await insertPlace(past.id, 'Colosseum', 'Piazza del Colosseo, Rome, Italy'); + for (let i = 0; i < 3; i++) await insertPlace(future.id, `Tokyo Place ${i}`, `Street ${i}, Tokyo, Japan`); const stats = await atlas.stats(user.id); @@ -1792,8 +1780,8 @@ describe('getStats — visited vs planned vs idea (#1048)', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Tokyo, next month', start_date: FUTURE_START, end_date: FUTURE_END }); const reservation = createReservation(testDb, trip.id, { type: 'flight' }); - insertReservationEndpoint(testDb, reservation.id, 'from', 0, 50.9014, 4.4844); // Brussels - insertReservationEndpoint(testDb, reservation.id, 'to', 1, 35.6762, 139.6503); // Tokyo + await insertReservationEndpoint(reservation.id, 'from', 0, 50.9014, 4.4844); // Brussels + await insertReservationEndpoint(reservation.id, 'to', 1, 35.6762, 139.6503); // Tokyo const stats = await atlas.stats(user.id); @@ -1808,7 +1796,7 @@ describe('getStats first and last visit (#1535)', () => { it('ATLAS-UNIT-054: one trip runs from its start to its end, not from its start to its start', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris', start_date: PAST_START, end_date: PAST_END }); - insertPlace(testDb, trip.id, 'Louvre', '75001 Paris, France'); + await insertPlace(trip.id, 'Louvre', '75001 Paris, France'); const stats = await atlas.stats(user.id); @@ -1819,8 +1807,8 @@ describe('getStats first and last visit (#1535)', () => { const { user } = createUser(testDb); const past = createTrip(testDb, user.id, { title: 'Munich 2023', start_date: PAST_START, end_date: PAST_END }); const future = createTrip(testDb, user.id, { title: 'Munich again', start_date: FUTURE_START, end_date: FUTURE_END }); - insertPlace(testDb, past.id, 'Marienplatz', 'Marienplatz, Munich, Germany'); - insertPlace(testDb, future.id, 'Englischer Garten', 'Englischer Garten, Munich, Germany'); + await insertPlace(past.id, 'Marienplatz', 'Marienplatz, Munich, Germany'); + await insertPlace(future.id, 'Englischer Garten', 'Englischer Garten, Munich, Germany'); const stats = await atlas.stats(user.id); @@ -1834,7 +1822,7 @@ describe('getStats first and last visit (#1535)', () => { it('ATLAS-UNIT-056: a trip still under way ends today', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Rome now', start_date: isoOffsetDays(-2), end_date: isoOffsetDays(5) }); - insertPlace(testDb, trip.id, 'Colosseum', 'Piazza del Colosseo, Rome, Italy'); + await insertPlace(trip.id, 'Colosseum', 'Piazza del Colosseo, Rome, Italy'); const stats = await atlas.stats(user.id); @@ -1847,7 +1835,7 @@ describe('getStats first and last visit (#1535)', () => { it('ATLAS-UNIT-057: a planned country keeps the dates of its planned trip', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Tokyo soon', start_date: FUTURE_START, end_date: FUTURE_END }); - insertPlace(testDb, trip.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); + await insertPlace(trip.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); const stats = await atlas.stats(user.id); @@ -1861,8 +1849,8 @@ describe('getStats first and last visit (#1535)', () => { it('ATLAS-UNIT-058: a country marked by hand takes no dates from a trip that has not happened yet', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Japan, next month', start_date: FUTURE_START, end_date: FUTURE_END }); - insertPlace(testDb, trip.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); - testDb.prepare('INSERT INTO visited_countries (user_id, country_code) VALUES (?, ?)').run(user.id, 'JP'); + await insertPlace(trip.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); + await insertRow(orm, VisitedCountries, { user: user.id, country_code: 'JP' }); const stats = await atlas.stats(user.id); @@ -1877,10 +1865,10 @@ describe('getStats first and last visit (#1535)', () => { const { user } = createUser(testDb); const flown = createTrip(testDb, user.id, { title: 'Tokyo 2023', start_date: PAST_START, end_date: PAST_END }); const reservation = createReservation(testDb, flown.id, { type: 'flight' }); - insertReservationEndpoint(testDb, reservation.id, 'from', 0, 50.9014, 4.4844); // Brussels - insertReservationEndpoint(testDb, reservation.id, 'to', 1, 35.6762, 139.6503); // Tokyo + await insertReservationEndpoint(reservation.id, 'from', 0, 50.9014, 4.4844); // Brussels + await insertReservationEndpoint(reservation.id, 'to', 1, 35.6762, 139.6503); // Tokyo const planned = createTrip(testDb, user.id, { title: 'Japan again', start_date: FUTURE_START, end_date: FUTURE_END }); - insertPlace(testDb, planned.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); + await insertPlace(planned.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); const stats = await atlas.stats(user.id); @@ -1896,10 +1884,10 @@ describe('getStats first and last visit (#1535)', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Tokyo', start_date: PAST_START, end_date: PAST_END }); const reservation = createReservation(testDb, trip.id, { type: 'flight' }); - insertReservationEndpoint(testDb, reservation.id, 'from', 0, 50.9014, 4.4844); // Brussels - insertReservationEndpoint(testDb, reservation.id, 'to', 1, 35.6762, 139.6503); // Tokyo + await insertReservationEndpoint(reservation.id, 'from', 0, 50.9014, 4.4844); // Brussels + await insertReservationEndpoint(reservation.id, 'to', 1, 35.6762, 139.6503); // Tokyo const someday = createTrip(testDb, user.id, { title: 'Someday: Paris' }); - insertPlace(testDb, someday.id, 'Louvre', '75001 Paris, France'); + await insertPlace(someday.id, 'Louvre', '75001 Paris, France'); const stats = await atlas.stats(user.id); @@ -1913,8 +1901,8 @@ describe('getCountryPlaces — status (#1048)', () => { const { user } = createUser(testDb); const past = createTrip(testDb, user.id, { title: 'Paris 2023', start_date: PAST_START, end_date: PAST_END }); const future = createTrip(testDb, user.id, { title: 'Tokyo soon', start_date: FUTURE_START, end_date: FUTURE_END }); - insertPlace(testDb, past.id, 'Louvre', '75001 Paris, France'); - insertPlace(testDb, future.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); + await insertPlace(past.id, 'Louvre', '75001 Paris, France'); + await insertPlace(future.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); expect((await atlas.countryPlaces(user.id, 'FR')).status).toBe('visited'); expect((await atlas.countryPlaces(user.id, 'JP')).status).toBe('planned'); @@ -1925,7 +1913,7 @@ describe('getCountryPlaces — status (#1048)', () => { it('ATLAS-UNIT-041: a manual mark makes the sheet visited even for a future-only country', async () => { const { user } = createUser(testDb); const future = createTrip(testDb, user.id, { title: 'Tokyo soon', start_date: FUTURE_START, end_date: FUTURE_END }); - insertPlace(testDb, future.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); + await insertPlace(future.id, 'Senso-ji', 'Asakusa, Tokyo, Japan'); await atlas.markCountry(user.id, 'JP'); const result = await atlas.countryPlaces(user.id, 'JP'); @@ -1940,14 +1928,10 @@ describe('getVisitedRegions — status (#1048)', () => { it('ATLAS-UNIT-042: a region inherits its trip status; a manually marked one is visited', async () => { const { user } = createUser(testDb); const future = createTrip(testDb, user.id, { title: 'Paris soon', start_date: FUTURE_START, end_date: FUTURE_END }); - const place = insertPlaceWithCoords(testDb, future.id, 'Paris Hotel', 48.85, 2.35); + const place = await insertPlaceWithCoords(future.id, 'Paris Hotel', 48.85, 2.35); // Pre-seed the region cache so nothing geocodes in the background (see ATLAS-UNIT-020). - testDb - .prepare('INSERT OR REPLACE INTO place_regions (place_id, country_code, region_code, region_name) VALUES (?, ?, ?, ?)') - .run(place.id, 'FR', 'FR-75', 'Île-de-France'); - testDb - .prepare('INSERT INTO visited_regions (user_id, region_code, region_name, country_code) VALUES (?, ?, ?, ?)') - .run(user.id, 'DE-BY', 'Bayern', 'DE'); + await stampRegion(place.id, 'FR', 'FR-75', 'Île-de-France'); + await insertRow(orm, VisitedRegions, { user: user.id, region_code: 'DE-BY', region_name: 'Bayern', country_code: 'DE' }); const result = await atlas.visitedRegions(user.id); @@ -1959,10 +1943,8 @@ describe('getVisitedRegions — status (#1048)', () => { it('ATLAS-UNIT-043: marking a planned region by hand upgrades it to visited', async () => { const { user } = createUser(testDb); const future = createTrip(testDb, user.id, { title: 'Paris soon', start_date: FUTURE_START, end_date: FUTURE_END }); - const place = insertPlaceWithCoords(testDb, future.id, 'Paris Hotel', 48.85, 2.35); - testDb - .prepare('INSERT OR REPLACE INTO place_regions (place_id, country_code, region_code, region_name) VALUES (?, ?, ?, ?)') - .run(place.id, 'FR', 'FR-75', 'Île-de-France'); + const place = await insertPlaceWithCoords(future.id, 'Paris Hotel', 48.85, 2.35); + await stampRegion(place.id, 'FR', 'FR-75', 'Île-de-France'); expect((await atlas.visitedRegions(user.id)).regions['FR'][0].status).toBe('planned'); @@ -1996,10 +1978,8 @@ describe('lastTrip', () => { const recent = createTrip(testDb, user.id, { title: 'Recent', start_date: PAST_START, end_date: PAST_END }); createTrip(testDb, user.id, { title: 'Booked', start_date: FUTURE_START, end_date: FUTURE_END }); - const place = insertPlaceWithCoords(testDb, recent.id, 'Paris Hotel', 48.85, 2.35); - testDb - .prepare('INSERT OR REPLACE INTO place_regions (place_id, country_code, region_code, region_name) VALUES (?, ?, ?, ?)') - .run(place.id, 'fr', 'FR-75', 'Ile-de-France'); + const place = await insertPlaceWithCoords(recent.id, 'Paris Hotel', 48.85, 2.35); + await stampRegion(place.id, 'fr', 'FR-75', 'Ile-de-France'); const last = await atlas.lastTrip(user.id); expect(last).toMatchObject({ title: 'Recent', start_date: PAST_START, end_date: PAST_END }); @@ -2010,10 +1990,9 @@ describe('lastTrip', () => { it('ATLAS-LAST-004: orders a multi-country trip by how many places sit in each', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Interrail', start_date: PAST_START, end_date: PAST_END }); - const stamp = testDb.prepare('INSERT OR REPLACE INTO place_regions (place_id, country_code, region_code, region_name) VALUES (?, ?, ?, ?)'); - stamp.run(insertPlaceWithCoords(testDb, trip.id, 'Wien', 48.2, 16.37).id, 'AT', 'AT-9', 'Wien'); - stamp.run(insertPlaceWithCoords(testDb, trip.id, 'Praha', 50.08, 14.44).id, 'CZ', 'CZ-10', 'Praha'); - stamp.run(insertPlaceWithCoords(testDb, trip.id, 'Brno', 49.19, 16.61).id, 'CZ', 'CZ-64', 'Brno'); + await stampRegion((await insertPlaceWithCoords(trip.id, 'Wien', 48.2, 16.37)).id, 'AT', 'AT-9', 'Wien'); + await stampRegion((await insertPlaceWithCoords(trip.id, 'Praha', 50.08, 14.44)).id, 'CZ', 'CZ-10', 'Praha'); + await stampRegion((await insertPlaceWithCoords(trip.id, 'Brno', 49.19, 16.61)).id, 'CZ', 'CZ-64', 'Brno'); // Two Czech stops beat one Austrian, so CZ leads and becomes `country`. expect((await atlas.lastTrip(user.id))!.countries).toEqual(['CZ', 'AT']); @@ -2022,7 +2001,7 @@ describe('lastTrip', () => { it('ATLAS-LAST-005: a trip whose places were never geocoded reports no countries, not a wrong one', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Roadtrip', start_date: PAST_START, end_date: PAST_END }); - insertPlace(testDb, trip.id, 'That diner off the highway'); + await insertPlace(trip.id, 'That diner off the highway'); expect((await atlas.lastTrip(user.id))!.countries).toEqual([]); }); @@ -2030,7 +2009,7 @@ describe('lastTrip', () => { const { user: owner } = createUser(testDb); const { user: member } = createUser(testDb); const trip = createTrip(testDb, owner.id, { title: 'Shared', start_date: PAST_START, end_date: PAST_END }); - testDb.prepare('INSERT INTO trip_members (trip_id, user_id) VALUES (?, ?)').run(trip.id, member.id); + await insertRow(orm, TripMembers, { trip: trip.id, user: member.id }); expect((await atlas.lastTrip(member.id))?.title).toBe('Shared'); }); @@ -2052,35 +2031,32 @@ describe('lastTrip', () => { // in this file exercises with a populated reservation_travelers table. describe('personal figures on a shared trip in stats() (#1966, AT6)', () => { const endpoint = (reservationId: number, role: 'from' | 'to', sequence: number, lat: number, lng: number) => - testDb.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, lat, lng) VALUES (?, ?, ?, ?, ?, ?)' - ).run(reservationId, role, sequence, `Endpoint ${sequence}`, lat, lng); + makeReservationEndpoint(orm, reservationId, { role, sequence, name: `Endpoint ${sequence}`, lat, lng, code: null }); - const assignTo = (reservationId: number, userId: number) => - testDb.prepare('INSERT INTO reservation_travelers (reservation_id, user_id) VALUES (?, ?)').run(reservationId, userId); + const assignTo = (reservationId: number, userId: number) => addReservationTraveler(orm, reservationId, userId); /** A shared trip: Alice flies Brussels→Tokyo, Bob flies Brussels→Sydney. */ - function sharedTrip() { + async function sharedTrip() { const { user: alice } = createUser(testDb); const { user: bob } = createUser(testDb); const trip = createTrip(testDb, alice.id, { title: 'Shared trip' }); - testDb.prepare('INSERT INTO trip_members (trip_id, user_id) VALUES (?, ?)').run(trip.id, bob.id); + await insertRow(orm, TripMembers, { trip: trip.id, user: bob.id }); const toTokyo = createReservation(testDb, trip.id, { type: 'flight', title: 'BRU-NRT' }); - endpoint(toTokyo.id, 'from', 0, 50.9014, 4.4844); // Brussels - endpoint(toTokyo.id, 'to', 1, 35.6762, 139.6503); // Tokyo + await endpoint(toTokyo.id, 'from', 0, 50.9014, 4.4844); // Brussels + await endpoint(toTokyo.id, 'to', 1, 35.6762, 139.6503); // Tokyo const toSydney = createReservation(testDb, trip.id, { type: 'flight', title: 'BRU-SYD' }); - endpoint(toSydney.id, 'from', 0, 50.9014, 4.4844); // Brussels - endpoint(toSydney.id, 'to', 1, -33.8688, 151.2093); // Sydney + await endpoint(toSydney.id, 'from', 0, 50.9014, 4.4844); // Brussels + await endpoint(toSydney.id, 'to', 1, -33.8688, 151.2093); // Sydney return { alice, bob, toTokyo, toSydney }; } it('ATLAS-TRAVOWNS-001: an assigned traveler sees only their own flight leg, not their trip-mate\'s', async () => { - const { alice, bob, toTokyo, toSydney } = sharedTrip(); - assignTo(toTokyo.id, alice.id); - assignTo(toSydney.id, bob.id); + const { alice, bob, toTokyo, toSydney } = await sharedTrip(); + await assignTo(toTokyo.id, alice.id); + await assignTo(toSydney.id, bob.id); const aliceCodes = (await atlas.stats(alice.id)).countries.map((c: { code: string }) => c.code); const bobCodes = (await atlas.stats(bob.id)).countries.map((c: { code: string }) => c.code); @@ -2095,7 +2071,7 @@ describe('personal figures on a shared trip in stats() (#1966, AT6)', () => { }); it('ATLAS-TRAVOWNS-002: a booking with nobody named on it still counts for every trip member (pre-4.0 backward compat)', async () => { - const { alice, bob } = sharedTrip(); + const { alice, bob } = await sharedTrip(); // Neither flight is assigned to anyone. const aliceCodes = (await atlas.stats(alice.id)).countries.map((c: { code: string }) => c.code); @@ -2123,8 +2099,7 @@ describe('nextTrip', () => { const { user } = createUser(testDb); createTrip(testDb, user.id, { title: 'Later', start_date: isoOffsetDays(90), end_date: isoOffsetDays(95) }); const soon = createTrip(testDb, user.id, { title: 'Soon', start_date: FUTURE_START, end_date: FUTURE_END }); - const stamp = testDb.prepare('INSERT OR REPLACE INTO place_regions (place_id, country_code, region_code, region_name) VALUES (?, ?, ?, ?)'); - stamp.run(insertPlaceWithCoords(testDb, soon.id, 'Lisboa', 38.72, -9.14).id, 'pt', 'PT-11', 'Lisboa'); + await stampRegion((await insertPlaceWithCoords(soon.id, 'Lisboa', 38.72, -9.14)).id, 'pt', 'PT-11', 'Lisboa'); expect(await atlas.nextTrip(user.id)).toEqual({ title: 'Soon', @@ -2159,8 +2134,8 @@ describe('nextTrip', () => { const { user: member } = createUser(testDb); const first = createTrip(testDb, owner.id, { title: 'First', start_date: FUTURE_START, end_date: FUTURE_END }); const second = createTrip(testDb, owner.id, { title: 'Second', start_date: FUTURE_START, end_date: FUTURE_END }); - testDb.prepare('INSERT INTO trip_members (trip_id, user_id) VALUES (?, ?)').run(first.id, member.id); - testDb.prepare('INSERT INTO trip_members (trip_id, user_id) VALUES (?, ?)').run(second.id, member.id); + await insertRow(orm, TripMembers, { trip: first.id, user: member.id }); + await insertRow(orm, TripMembers, { trip: second.id, user: member.id }); expect((await atlas.nextTrip(member.id))?.title).toBe('First'); }); });