diff --git a/server/tests/unit/db/repositories/AuditLog.repository.test.ts b/server/tests/unit/db/repositories/AuditLog.repository.test.ts index 47355e755..1f4ec8b4a 100644 --- a/server/tests/unit/db/repositories/AuditLog.repository.test.ts +++ b/server/tests/unit/db/repositories/AuditLog.repository.test.ts @@ -7,6 +7,8 @@ import { AuditLog } from '../../../../src/db/entities/AuditLog.entity'; import { AppSettings } from '../../../../src/db/entities/AppSettings.entity'; import type { AuditLogRepository } from '../../../../src/db/repositories/AuditLog.repository'; import { DB_TIMESTAMP_RE } from '../../../../src/db/types'; +import { Users } from '../../../../src/db/entities/Users.entity'; +import { countRows, deleteRows, findRow } from '../../../helpers/factories/rows'; const testDb = createSnapshotTestDb(); let t: TestOrm; @@ -23,6 +25,7 @@ describe('AuditLogRepository', () => { it('AUDITREPO-001: insert writes the legacy column set, matching the SELECT * row exactly', async () => { const { user } = createUser(testDb); await auditLog.insertEntry({ user_id: user.id, action: 'trip.create', resource: 'trip', details: '{"title":"Rome"}', ip: '1.2.3.4' }); + // test-sql-allow: the stored SELECT * row is the oracle for the legacy column set the insert must write. const row = testDb.prepare('SELECT * FROM audit_log').get() as { id: number; created_at: string }; expect(row).toStrictEqual({ id: expect.any(Number), @@ -38,6 +41,7 @@ describe('AuditLogRepository', () => { it('AUDITREPO-002: created_at is left to the column default, as text, in the YYYY-MM-DD HH:MM:SS shape', async () => { const { user } = createUser(testDb); await auditLog.insertEntry({ user_id: user.id, action: 'user.login', resource: null, details: null, ip: null }); + // test-sql-allow: typeof() reports the storage class SQLite holds, which no entity maps. const stored = testDb.prepare('SELECT created_at, typeof(created_at) AS kind FROM audit_log').get() as { created_at: string; kind: string }; expect(stored.kind).toBe('text'); expect(stored.created_at).toMatch(DB_TIMESTAMP_RE); @@ -45,15 +49,15 @@ describe('AuditLogRepository', () => { it('AUDITREPO-003: a null user writes a null user_id twin, not a foreign-key value', async () => { await auditLog.insertEntry({ user_id: null, action: 'user.login', resource: null, details: null, ip: null }); - const row = testDb.prepare('SELECT user_id FROM audit_log').get() as { user_id: number | null }; - expect(row.user_id).toBeNull(); + const row = await findRow(t, AuditLog, {}); + expect(row?.user_id).toBeNull(); }); it('AUDITREPO-004: null resource/details/ip are stored as NULL, not the string "null"', async () => { const { user } = createUser(testDb); await auditLog.insertEntry({ user_id: user.id, action: 'user.login', resource: null, details: null, ip: null }); - const row = testDb.prepare('SELECT resource, details, ip FROM audit_log').get(); - expect(row).toEqual({ resource: null, details: null, ip: null }); + const row = await findRow(t, AuditLog, {}); + expect(row).toMatchObject({ resource: null, details: null, ip: null }); }); it('AUDITREPO-005: resolves without returning a value — no id or row is read back', async () => { @@ -69,11 +73,11 @@ describe('AuditLogRepository', () => { t.em.create(AppSettings, { key: 'pending-during-audit-insert', value: 'should-not-be-written' }); await auditLog.insertEntry({ user_id: user.id, action: 'trip.create', resource: 'trip', details: null, ip: null }); - const pendingCount = (testDb.prepare('SELECT COUNT(*) AS n FROM app_settings WHERE key = ?').get('pending-during-audit-insert') as { n: number }).n; + const pendingCount = await countRows(t, AppSettings, { key: 'pending-during-audit-insert' }); expect(pendingCount).toBe(0); - const auditRow = testDb.prepare('SELECT action FROM audit_log').get() as { action: string }; - expect(auditRow.action).toBe('trip.create'); + const auditRow = await findRow(t, AuditLog, {}); + expect(auditRow?.action).toBe('trip.create'); }); // ── Plan 3i Task 0 — listPage/count (AD22/AD23) ──────────────────────────── @@ -98,8 +102,9 @@ describe('AuditLogRepository', () => { // bob is deleted after the fact; `audit_log.user_id REFERENCES users(id) // ON DELETE SET NULL` fires — this row's own user_id becomes NULL, the // same shape a NULL-user_id row has, reached a different way. - testDb.prepare('DELETE FROM users WHERE id = ?').run(bob.id); + await deleteRows(t, Users, { id: bob.id }); + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. const legacy = testDb.prepare(LEGACY_PAGE_SQL).all(10, 0); const result = await auditLog.listPage(10, 0); expect(result).toEqual(legacy); @@ -118,6 +123,7 @@ describe('AuditLogRepository', () => { for (let i = 0; i < 5; i++) { await auditLog.insertEntry({ user_id: user.id, action: `action.${i}`, resource: null, details: null, ip: null }); } + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. const legacyPage = testDb.prepare(LEGACY_PAGE_SQL).all(2, 1); const result = await auditLog.listPage(2, 1); expect(result).toEqual(legacyPage); @@ -129,6 +135,7 @@ describe('AuditLogRepository', () => { await auditLog.insertEntry({ user_id: user.id, action: 'a', resource: null, details: null, ip: null }); await auditLog.insertEntry({ user_id: null, action: 'b', resource: null, details: null, ip: null }); + // test-sql-allow: the raw COUNT is the legacy oracle this parity test holds the repository to. const legacyCount = (testDb.prepare('SELECT COUNT(*) as c FROM audit_log').get() as { c: number }).c; const result = await auditLog.count(); expect(result).toBe(legacyCount); diff --git a/server/tests/unit/db/repositories/ShareTokens.repository.test.ts b/server/tests/unit/db/repositories/ShareTokens.repository.test.ts index b71130551..9d41711da 100644 --- a/server/tests/unit/db/repositories/ShareTokens.repository.test.ts +++ b/server/tests/unit/db/repositories/ShareTokens.repository.test.ts @@ -12,6 +12,8 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { createSnapshotTestDb } from '../../../helpers/db-mock'; import { resetTestDb } from '../../../helpers/test-db'; import { createTestOrm, type TestOrm } from '../../../helpers/test-orm'; +import { findRow } from '../../../helpers/factories/rows'; +import { makeShareToken } from '../../../helpers/factories/trips'; import { createTrip, createUser } from '../../../helpers/factories'; import { ShareTokens } from '../../../../src/db/entities/ShareTokens.entity'; import type { ShareTokensRepository } from '../../../../src/db/repositories/ShareTokens.repository'; @@ -27,12 +29,19 @@ beforeAll(async () => { beforeEach(() => { resetTestDb(testDb); t.clear(); }); afterAll(async () => { await t.close(); testDb.close(); }); +/** The SELECT * row as the database holds it: the oracle the repository's full rows are compared with. */ function rawShareToken(id: number): unknown { + // test-sql-allow: the raw row is the oracle the repository's full-row reads are held against. return testDb.prepare('SELECT * FROM share_tokens WHERE id = ?').get(id); } -/** A raw `share_tokens` insert — no factory exists for this table yet. */ -function insertShareToken( +/** The stored row, read through the ORM, for the cases that check what a write left behind. */ +function storedShareToken(id: number) { + return findRow(t, ShareTokens, { id }); +} + +/** A `share_tokens` row with the legacy defaults this suite was written against. */ +async function insertShareToken( tripId: number, createdBy: number, overrides: Partial<{ @@ -44,23 +53,17 @@ function insertShareToken( share_collab: number | null; expires_at: string | null; }> = {}, -): number { - const result = testDb.prepare( - `INSERT INTO share_tokens - (trip_id, token, created_by, share_map, share_bookings, share_packing, share_budget, share_collab, expires_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - ).run( - tripId, - overrides.token ?? `token-${tripId}-${crypto.randomUUID().slice(0, 8)}`, - createdBy, - overrides.share_map === undefined ? 1 : overrides.share_map, - overrides.share_bookings === undefined ? 1 : overrides.share_bookings, - overrides.share_packing === undefined ? 0 : overrides.share_packing, - overrides.share_budget === undefined ? 0 : overrides.share_budget, - overrides.share_collab === undefined ? 0 : overrides.share_collab, - overrides.expires_at === undefined ? null : overrides.expires_at, - ); - return Number(result.lastInsertRowid); +): Promise { + const row = await makeShareToken(t, tripId, createdBy, { + token: overrides.token ?? `token-${tripId}-${crypto.randomUUID().slice(0, 8)}`, + share_map: overrides.share_map === undefined ? 1 : overrides.share_map, + share_bookings: overrides.share_bookings === undefined ? 1 : overrides.share_bookings, + share_packing: overrides.share_packing === undefined ? 0 : overrides.share_packing, + share_budget: overrides.share_budget === undefined ? 0 : overrides.share_budget, + share_collab: overrides.share_collab === undefined ? 0 : overrides.share_collab, + expires_at: overrides.expires_at === undefined ? null : overrides.expires_at, + }); + return row.id; } describe('ShareTokensRepository', () => { @@ -68,7 +71,7 @@ describe('ShareTokensRepository', () => { it('SHTOKREPO-001: a share_tokens row with every nullable column NULL matches SELECT * run raw', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); - const id = insertShareToken(trip.id, user.id, { + const id = await insertShareToken(trip.id, user.id, { share_map: null, share_bookings: null, share_packing: null, share_budget: null, share_collab: null, expires_at: null, }); @@ -80,7 +83,7 @@ describe('ShareTokensRepository', () => { it('SHTOKREPO-002: a share_tokens row with every nullable column SET matches SELECT * run raw', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); - const id = insertShareToken(trip.id, user.id, { + const id = await insertShareToken(trip.id, user.id, { share_map: 1, share_bookings: 0, share_packing: 1, share_budget: 0, share_collab: 1, expires_at: '2099-01-01 00:00:00', }); @@ -100,8 +103,9 @@ describe('ShareTokensRepository', () => { it('SHTOKREPO-004: matches SELECT token FROM share_tokens WHERE trip_id = ? run raw', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); - insertShareToken(trip.id, user.id, { token: 'find-me-by-trip' }); + await insertShareToken(trip.id, user.id, { token: 'find-me-by-trip' }); + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. const legacy = testDb.prepare('SELECT token FROM share_tokens WHERE trip_id = ?').get(trip.id); expect(await repo.findTokenByTrip(trip.id)).toEqual(legacy); @@ -118,9 +122,10 @@ describe('ShareTokensRepository', () => { it('SHTOKREPO-006: a token with expires_at NULL (never expires) matches the full row run raw', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); - const id = insertShareToken(trip.id, user.id, { token: 'never-expires', expires_at: null }); + const id = await insertShareToken(trip.id, user.id, { token: 'never-expires', expires_at: null }); const legacy = testDb + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. .prepare("SELECT * FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))") .get('never-expires'); @@ -132,9 +137,10 @@ describe('ShareTokensRepository', () => { it('SHTOKREPO-007: a token with a future expires_at still matches', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); - insertShareToken(trip.id, user.id, { token: 'future-expiry', expires_at: '2099-01-01 00:00:00' }); + await insertShareToken(trip.id, user.id, { token: 'future-expiry', expires_at: '2099-01-01 00:00:00' }); const legacy = testDb + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. .prepare("SELECT * FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))") .get('future-expiry'); @@ -144,9 +150,10 @@ describe('ShareTokensRepository', () => { it('SHTOKREPO-008: an EXPIRED token (past expires_at) matches neither the repository read nor the legacy predicate — both undefined', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); - insertShareToken(trip.id, user.id, { token: 'past-expiry', expires_at: '2020-01-01 00:00:00' }); + await insertShareToken(trip.id, user.id, { token: 'past-expiry', expires_at: '2020-01-01 00:00:00' }); const legacy = testDb + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. .prepare("SELECT * FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))") .get('past-expiry'); @@ -164,13 +171,15 @@ describe('ShareTokensRepository', () => { const { user } = createUser(testDb); const tripA = createTrip(testDb, user.id); const tripB = createTrip(testDb, user.id); - insertShareToken(tripA.id, user.id, { token: 'map-set', share_map: 1 }); - insertShareToken(tripB.id, user.id, { token: 'map-null', share_map: null }); + await insertShareToken(tripA.id, user.id, { token: 'map-set', share_map: 1 }); + await insertShareToken(tripB.id, user.id, { token: 'map-null', share_map: null }); const legacySet = testDb + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. .prepare("SELECT trip_id, share_map, share_hide_images FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))") .get('map-set'); const legacyNull = testDb + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. .prepare("SELECT trip_id, share_map, share_hide_images FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))") .get('map-null'); @@ -181,7 +190,7 @@ describe('ShareTokensRepository', () => { it('SHTOKREPO-011: an expired token is undefined, same as the legacy predicate', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); - insertShareToken(trip.id, user.id, { token: 'expired-map', expires_at: '2020-01-01 00:00:00' }); + await insertShareToken(trip.id, user.id, { token: 'expired-map', expires_at: '2020-01-01 00:00:00' }); expect(await repo.findTripAndShareMapByToken('expired-map')).toBeUndefined(); }); }); @@ -190,9 +199,10 @@ describe('ShareTokensRepository', () => { it('SHTOKREPO-012: matches the narrow legacy projection', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); - insertShareToken(trip.id, user.id, { token: 'trip-id-only' }); + await insertShareToken(trip.id, user.id, { token: 'trip-id-only' }); const legacy = testDb + // test-sql-allow: the raw statement is the legacy oracle this parity test holds the repository to. .prepare("SELECT trip_id FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))") .get('trip-id-only'); @@ -202,7 +212,7 @@ describe('ShareTokensRepository', () => { it('SHTOKREPO-013: an expired token is undefined', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); - insertShareToken(trip.id, user.id, { token: 'trip-id-expired', expires_at: '2020-01-01 00:00:00' }); + await insertShareToken(trip.id, user.id, { token: 'trip-id-expired', expires_at: '2020-01-01 00:00:00' }); expect(await repo.findTripIdByToken('trip-id-expired')).toBeUndefined(); }); }); @@ -218,7 +228,7 @@ describe('ShareTokensRepository', () => { share_travel_only: 1, share_hide_images: 0, expires_at: '2099-06-01 00:00:00', }); - const raw = testDb.prepare('SELECT * FROM share_tokens WHERE token = ?').get('inserted-token') as Record; + const raw = await findRow(t, ShareTokens, { token: 'inserted-token' }); expect(raw).toMatchObject({ trip_id: trip.id, token: 'inserted-token', created_by: user.id, share_map: 1, share_bookings: 0, share_packing: 1, share_budget: 0, share_collab: 1, @@ -230,33 +240,33 @@ describe('ShareTokensRepository', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); const other = createTrip(testDb, user.id); - const id = insertShareToken(trip.id, user.id, { token: 'to-update' }); - const otherId = insertShareToken(other.id, user.id, { token: 'untouched' }); + const id = await insertShareToken(trip.id, user.id, { token: 'to-update' }); + const otherId = await insertShareToken(other.id, user.id, { token: 'untouched' }); await repo.updateFlagsByTrip(trip.id, { share_map: 0, share_bookings: 0, share_packing: 0, share_budget: 0, share_collab: 0, share_travel_only: 1, share_hide_images: 1, expires_at: '2099-12-31 00:00:00', }); - expect(rawShareToken(id)).toMatchObject({ + expect(await storedShareToken(id)).toMatchObject({ share_map: 0, share_bookings: 0, share_packing: 0, share_budget: 0, share_collab: 0, share_travel_only: 1, share_hide_images: 1, expires_at: '2099-12-31 00:00:00', }); // The other trip's row is untouched — a different predicate, not a global update. - expect(rawShareToken(otherId)).toMatchObject({ share_map: 1, share_bookings: 1, share_travel_only: 0, share_hide_images: 0 }); + expect(await storedShareToken(otherId)).toMatchObject({ share_map: 1, share_bookings: 1, share_travel_only: 0, share_hide_images: 0 }); }); it('SHTOKREPO-016: deleteByTrip removes exactly that trip\'s row, leaving another trip\'s row alone', async () => { const { user } = createUser(testDb); const tripA = createTrip(testDb, user.id); const tripB = createTrip(testDb, user.id); - insertShareToken(tripA.id, user.id, { token: 'delete-me' }); - const keepId = insertShareToken(tripB.id, user.id, { token: 'keep-me' }); + await insertShareToken(tripA.id, user.id, { token: 'delete-me' }); + const keepId = await insertShareToken(tripB.id, user.id, { token: 'keep-me' }); await repo.deleteByTrip(tripA.id); expect(await repo.findRawByTrip(tripA.id)).toBeUndefined(); - expect(rawShareToken(keepId)).toBeDefined(); + expect(await storedShareToken(keepId)).not.toBeNull(); }); it('SHTOKREPO-017: deleteByCreator removes every link that user created, across every trip, leaving another creator\'s links alone (DISTINCT from deleteByTrip)', async () => { @@ -265,15 +275,15 @@ describe('ShareTokensRepository', () => { const tripA = createTrip(testDb, creator.id); const tripB = createTrip(testDb, creator.id); const tripC = createTrip(testDb, otherCreator.id); - insertShareToken(tripA.id, creator.id, { token: 'creator-a' }); - insertShareToken(tripB.id, creator.id, { token: 'creator-b' }); - const otherId = insertShareToken(tripC.id, otherCreator.id, { token: 'other-creator' }); + await insertShareToken(tripA.id, creator.id, { token: 'creator-a' }); + await insertShareToken(tripB.id, creator.id, { token: 'creator-b' }); + const otherId = await insertShareToken(tripC.id, otherCreator.id, { token: 'other-creator' }); await repo.deleteByCreator(creator.id); expect(await repo.findRawByTrip(tripA.id)).toBeUndefined(); expect(await repo.findRawByTrip(tripB.id)).toBeUndefined(); - expect(rawShareToken(otherId)).toBeDefined(); + expect(await storedShareToken(otherId)).not.toBeNull(); }); }); }); diff --git a/server/tests/unit/nest/trip-read-model.service.test.ts b/server/tests/unit/nest/trip-read-model.service.test.ts index 9d6176271..83f691e9e 100644 --- a/server/tests/unit/nest/trip-read-model.service.test.ts +++ b/server/tests/unit/nest/trip-read-model.service.test.ts @@ -21,14 +21,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; }); @@ -76,6 +68,10 @@ import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.s import { TrekPhotoRegistrationService } from '../../../src/nest/photos/trek-photo-registration.service'; import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity'; import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity'; +import { BudgetItems } from '../../../src/db/entities/BudgetItems.entity'; +import { PackingItems } from '../../../src/db/entities/PackingItems.entity'; +import { Trips } from '../../../src/db/entities/Trips.entity'; +import { insertRow, updateRows } from '../../helpers/factories/rows'; import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; import { createTestUnitOfWork, createTestAppSettingsRepo, createTestUsersRepo, sharedTestOrm, @@ -245,13 +241,13 @@ afterAll(() => { // ── Helpers ─────────────────────────────────────────────────────────────────── -const addBudgetItem = (tripId: number, name: string, totalPrice: number) => - testDb.prepare("INSERT INTO budget_items (trip_id, category, name, total_price) VALUES (?, 'food', ?, ?)") - .run(tripId, name, totalPrice); +const orm = () => sharedTestOrm(testDb); -const addPackingItem = (tripId: number, name: string, checked: number) => - testDb.prepare('INSERT INTO packing_items (trip_id, name, checked) VALUES (?, ?, ?)') - .run(tripId, name, checked); +const addBudgetItem = async (tripId: number, name: string, totalPrice: number) => + insertRow(await orm(), BudgetItems, { trip: tripId, category: 'food', name, total_price: totalPrice }); + +const addPackingItem = async (tripId: number, name: string, checked: number) => + insertRow(await orm(), PackingItems, { trip: tripId, name, checked }); // R8 (Plan 3c program brief item 8): the SQL-text-keyed Proxy fault injection // this file used to build (`ownerlessDbs`, keyed on the literal @@ -297,7 +293,7 @@ describe('getTripSummary — feed_token never reaches the wire (TR-B)', () => { it('TRIP-READ-002b: withoutFeedToken strips feed_token even though findRaw hands it back intact', async () => { const { user: owner } = createUser(testDb); const trip = createTrip(testDb, owner.id); - testDb.prepare('UPDATE trips SET feed_token = ? WHERE id = ?').run('secret-anon-feed-credential', trip.id); + await updateRows(await orm(), Trips, { id: trip.id }, { feed_token: 'secret-anon-feed-credential' }); // TripsRepository.findRaw is a plain `SELECT *` — it hands feed_token back // intact (proven directly, bypassing the service, so this test would fail @@ -317,13 +313,13 @@ describe('getTripSummary shaping', () => { it('TRIP-READ-003: folds a falsy total_price into the budget total instead of poisoning it', async () => { const { user: owner } = createUser(testDb); const trip = createTrip(testDb, owner.id); - addBudgetItem(trip.id, 'Dinner', 40); + await addBudgetItem(trip.id, 'Dinner', 40); // total_price is NOT NULL DEFAULT 0, so 0 is the reachable falsy price: a free // entry someone logged to keep it on the list. The `|| 0` in the reduce is what // keeps any such value out of the sum — drop it and a price that arrives // non-numeric turns the whole trip total into NaN, which the MCP summary and // the offline clients both render as an empty budget. - addBudgetItem(trip.id, 'Free walking tour', 0); + await addBudgetItem(trip.id, 'Free walking tour', 0); const summary = (await svc.getTripSummary(trip.id, owner.id))!; expect(summary.budget.item_count).toBe(2); @@ -334,11 +330,12 @@ describe('getTripSummary shaping', () => { it('TRIP-READ-008: totals a foreign-currency bill in the trip currency, at its booked rate (#2525)', async () => { const { user: owner } = createUser(testDb); const trip = createTrip(testDb, owner.id); - addBudgetItem(trip.id, 'Dinner', 100); + await addBudgetItem(trip.id, 'Dinner', 100); // 801.76 USD booked when a euro bought 1.17 dollars: 685.26 EUR of trip money. The // summary used to add the 801.76 to the euros and report 901.76 EUR. - testDb.prepare("INSERT INTO budget_items (trip_id, category, name, total_price, currency, exchange_rate) VALUES (?, 'accommodation', 'Aparthotel Silver', 801.76, 'USD', 1.17)") - .run(trip.id); + await insertRow(await orm(), BudgetItems, { + trip: trip.id, category: 'accommodation', name: 'Aparthotel Silver', total_price: 801.76, currency: 'USD', exchange_rate: 1.17, + }); const summary = (await svc.getTripSummary(trip.id, owner.id))!; expect(summary.budget.total).toBe(785.26); @@ -351,11 +348,12 @@ describe('getTripSummary shaping', () => { it('TRIP-READ-009: totals a trip saved without a currency in euros, the default the rest of the app reads it in (#2525)', async () => { const { user: owner } = createUser(testDb); const trip = createTrip(testDb, owner.id); - testDb.prepare('UPDATE trips SET currency = NULL WHERE id = ?').run(trip.id); - addBudgetItem(trip.id, 'Dinner', 100); + await updateRows(await orm(), Trips, { id: trip.id }, { currency: null }); + await addBudgetItem(trip.id, 'Dinner', 100); // 117.33 USD booked at 1.1733 dollars to the euro is 100 EUR of trip money. - testDb.prepare("INSERT INTO budget_items (trip_id, category, name, total_price, currency, exchange_rate) VALUES (?, 'transport', 'Taxi', 117.33, 'USD', 1.1733)") - .run(trip.id); + await insertRow(await orm(), BudgetItems, { + trip: trip.id, category: 'transport', name: 'Taxi', total_price: 117.33, currency: 'USD', exchange_rate: 1.1733, + }); const summary = (await svc.getTripSummary(trip.id, owner.id))!; expect(summary.budget.total).toBe(200); @@ -366,9 +364,9 @@ describe('getTripSummary shaping', () => { it('TRIP-READ-004: counts only checked packing items, not the whole list', async () => { const { user: owner } = createUser(testDb); const trip = createTrip(testDb, owner.id); - addPackingItem(trip.id, 'Socks', 1); - addPackingItem(trip.id, 'Charger', 1); - addPackingItem(trip.id, 'Passport', 0); + await addPackingItem(trip.id, 'Socks', 1); + await addPackingItem(trip.id, 'Charger', 1); + await addPackingItem(trip.id, 'Passport', 0); // total and checked come from the same array; if the filter is ever widened the // packing progress the summary reports jumps to 100% while items are still open. @@ -423,9 +421,8 @@ describe('private packing items stay viewer-scoped (#858)', () => { const { user: viewer } = createUser(testDb); const trip = createTrip(testDb, owner.id, { start_date: '2025-06-01', end_date: '2025-06-02' }); addTripMember(testDb, trip.id, viewer.id); - testDb.prepare("INSERT INTO packing_items (trip_id, name, is_private, owner_id) VALUES (?, 'Ring', 1, ?)") - .run(trip.id, owner.id); - testDb.prepare("INSERT INTO packing_items (trip_id, name) VALUES (?, 'Tent')").run(trip.id); + await insertRow(await orm(), PackingItems, { trip: trip.id, name: 'Ring', is_private: 1, owner: owner.id }); + await insertRow(await orm(), PackingItems, { trip: trip.id, name: 'Tent' }); // Both read paths pass the viewer into packing.listItems; that argument is the // ONLY thing filtering the list. If either call site loses it, listItems falls