mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 15:06:32 +00:00
test(server): plan 4 — full-key parity for Reservations share/public-api reads (3d carry)
Task 8b-2 item 1 (partial): the 8 share.service.ts/public-api.service.ts reads on ReservationsRepository flagged by the 3d Task 7 review as having no repository-level toEqual(<legacy raw>) coverage (listEndpointsForShare, listDayPositionsForShare, listPublicForShare, listPublicAccommodationsForShare, listScheduledForPublicApi, listUnscheduledForPublicApi, listAccommodationsForPublicApi, listUnplannedPlacesForPublicApi). 12 new cases against the legacy statement run raw on the same connection.
This commit is contained in:
@@ -11,7 +11,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createSnapshotTestDb } from '../../../helpers/db-mock';
|
||||
import { resetTestDb } from '../../../helpers/test-db';
|
||||
import {
|
||||
createDay, createDayAccommodation, createPlace, createReservation, createTrip, createUser, addTripMember,
|
||||
createDay, createDayAccommodation, createDayAssignment, createPlace, createReservation, createTrip, createUser, addTripMember,
|
||||
} from '../../../helpers/factories';
|
||||
import {
|
||||
createTestReservationsRepo, createTestTripsRepo, createTestDaysRepo, createTestDayNotesRepo,
|
||||
@@ -566,3 +566,209 @@ describe('ReservationsRepository — AirTrail leaf (ATL1/ATL2/ATL4, airports)',
|
||||
expect((testDb.prepare('SELECT needs_review FROM reservations WHERE id = ?').get(res.id) as { needs_review: number }).needs_review).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Plan 4 Task 8b-2, item 1 (3d Task 7 review's "share/public-api ×8 reads",
|
||||
// `phase3d-reports/task-7-review.md`'s "Parity tests: read models with no
|
||||
// full-key `toEqual(<legacy raw>)`" list): `share.service.ts`'s 4 reads
|
||||
// (SH-family: listEndpointsForShare, listDayPositionsForShare,
|
||||
// listPublicForShare, listPublicAccommodationsForShare) and
|
||||
// `public-api.service.ts`'s 4 reads (listScheduledForPublicApi,
|
||||
// listUnscheduledForPublicApi, listAccommodationsForPublicApi,
|
||||
// listUnplannedPlacesForPublicApi) — service-level coverage only until now.
|
||||
// ONE seeded world per describe block, `toEqual(<legacy raw>)` per method.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const insertReservationEndpoint = (
|
||||
reservationId: number,
|
||||
role: string,
|
||||
sequence: number,
|
||||
overrides: Partial<{ name: string; code: string | null; lat: number; lng: number; timezone: string | null; local_time: string | null; local_date: string | null }> = {},
|
||||
) => {
|
||||
testDb.prepare(`
|
||||
INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
||||
reservationId, role, sequence,
|
||||
overrides.name ?? `${role} endpoint`, overrides.code ?? null, overrides.lat ?? 48.0, overrides.lng ?? 11.0,
|
||||
overrides.timezone ?? null, overrides.local_time ?? null, overrides.local_date ?? null,
|
||||
);
|
||||
};
|
||||
|
||||
const insertDayPosition = (reservationId: number, dayId: number, position: number) => {
|
||||
testDb.prepare('INSERT INTO reservation_day_positions (reservation_id, day_id, position) VALUES (?, ?, ?)')
|
||||
.run(reservationId, dayId, position);
|
||||
};
|
||||
|
||||
describe('ReservationsRepository — share.service.ts reads (SH-family)', () => {
|
||||
it('listEndpointsForShare — matches the legacy JOIN, ordered by reservation then sequence, scoped by trip', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const other = createTrip(testDb, user.id);
|
||||
const flight = createReservation(testDb, trip.id, { title: 'Flight', type: 'flight' });
|
||||
insertReservationEndpoint(flight.id, 'from', 1, { name: 'JFK', code: 'JFK', lat: 40.6, lng: -73.7, timezone: 'America/New_York', local_time: '10:00', local_date: '2026-09-01' });
|
||||
insertReservationEndpoint(flight.id, 'to', 0, { name: 'MUC', code: null, lat: 48.35, lng: 11.78, timezone: null, local_time: null, local_date: null });
|
||||
const foreign = createReservation(testDb, other.id, { title: 'Foreign', type: 'flight' });
|
||||
insertReservationEndpoint(foreign.id, 'from', 0, { name: 'LHR' });
|
||||
|
||||
const legacy = testDb.prepare(`
|
||||
SELECT e.reservation_id, e.role, e.sequence, e.name, e.code, e.lat, e.lng, e.timezone, e.local_date, e.local_time
|
||||
FROM reservation_endpoints e JOIN reservations r ON r.id = e.reservation_id
|
||||
WHERE r.trip_id = ? ORDER BY e.reservation_id ASC, e.sequence ASC`).all(trip.id);
|
||||
|
||||
const typed = await reservationsRepo.listEndpointsForShare(trip.id);
|
||||
expect(typed).toEqual(legacy);
|
||||
expect(typed.map((e) => e.sequence)).toEqual([0, 1]);
|
||||
});
|
||||
|
||||
it('listDayPositionsForShare — matches the legacy JOIN, scoped by trip', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2026-09-01', end_date: '2026-09-03' });
|
||||
const other = createTrip(testDb, user.id);
|
||||
const days = testDb.prepare('SELECT id FROM days WHERE trip_id = ? ORDER BY day_number ASC').all(trip.id) as { id: number }[];
|
||||
const res = createReservation(testDb, trip.id, { title: 'Multi-day rental', type: 'car_rental' });
|
||||
insertDayPosition(res.id, days[0].id, 0);
|
||||
insertDayPosition(res.id, days[1].id, 1);
|
||||
const foreignRes = createReservation(testDb, other.id, { title: 'Foreign', type: 'car_rental' });
|
||||
testDb.prepare('INSERT INTO days (trip_id, day_number, date) VALUES (?, 1, ?)').run(other.id, '2026-01-01');
|
||||
const foreignDay = testDb.prepare('SELECT id FROM days WHERE trip_id = ?').get(other.id) as { id: number };
|
||||
insertDayPosition(foreignRes.id, foreignDay.id, 0);
|
||||
|
||||
const legacy = testDb.prepare(`
|
||||
SELECT rdp.reservation_id, rdp.day_id, rdp.position
|
||||
FROM reservation_day_positions rdp JOIN reservations r ON r.id = rdp.reservation_id
|
||||
WHERE r.trip_id = ?`).all(trip.id);
|
||||
|
||||
const typed = await reservationsRepo.listDayPositionsForShare(trip.id);
|
||||
expect(typed).toEqual(legacy);
|
||||
expect(typed.length).toBe(2);
|
||||
});
|
||||
|
||||
it('listPublicForShare — matches the legacy statement, live bookings only, staged excluded', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2026-09-01', end_date: '2026-09-03' });
|
||||
const days = testDb.prepare('SELECT id FROM days WHERE trip_id = ? ORDER BY day_number ASC').all(trip.id) as { id: number }[];
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const live = createReservation(testDb, trip.id, { title: 'Live', type: 'flight', day_id: days[0].id });
|
||||
testDb.prepare(`UPDATE reservations SET end_day_id = ?, place_id = ?, accommodation_id = NULL, status = 'confirmed',
|
||||
location = ?, reservation_time = ?, reservation_end_time = ?, notes = ?, url = ?, metadata = ? WHERE id = ?`)
|
||||
.run(days[1].id, place.id, 'Gate 4', '2026-09-01T10:00', '2026-09-01T12:00', 'note', 'https://example.com', '{"k":"v"}', live.id);
|
||||
const staged = createReservation(testDb, trip.id, { title: 'Staged', type: 'flight' });
|
||||
testDb.prepare("UPDATE reservations SET ingest_state = 'staged' WHERE id = ?").run(staged.id);
|
||||
|
||||
const legacy = testDb.prepare(`
|
||||
SELECT id, trip_id, day_id, end_day_id, place_id, accommodation_id, title, type, status, location,
|
||||
reservation_time, reservation_end_time, notes, url, metadata, created_at
|
||||
FROM reservations WHERE trip_id = ? AND ${publicReservationSql('reservations')}
|
||||
ORDER BY reservation_time ASC`).all(trip.id);
|
||||
|
||||
const typed = await reservationsRepo.listPublicForShare(trip.id);
|
||||
expect(typed).toEqual(legacy);
|
||||
expect(typed.map((r) => r.id)).toEqual([live.id]);
|
||||
});
|
||||
|
||||
it('listPublicAccommodationsForShare — matches the legacy statement, only stays with a public booking (or none) are included', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2026-09-01', end_date: '2026-09-05' });
|
||||
const days = testDb.prepare('SELECT id FROM days WHERE trip_id = ? ORDER BY day_number ASC').all(trip.id) as { id: number }[];
|
||||
const place = createPlace(testDb, trip.id, { name: 'Grand Hotel' });
|
||||
const noBooking = createDayAccommodation(testDb, trip.id, place.id, days[0].id, days[1].id, { check_in: '15:00', check_out: '11:00' });
|
||||
const liveLinked = createDayAccommodation(testDb, trip.id, place.id, days[2].id, days[3].id);
|
||||
const liveBooking = createReservation(testDb, trip.id, { title: 'Live', type: 'hotel' });
|
||||
testDb.prepare('UPDATE reservations SET accommodation_id = ? WHERE id = ?').run(String(liveLinked.id), liveBooking.id);
|
||||
const stagedOnly = createDayAccommodation(testDb, trip.id, place.id, days[3].id, days[4].id);
|
||||
const stagedBooking = createReservation(testDb, trip.id, { title: 'Staged', type: 'hotel' });
|
||||
testDb.prepare("UPDATE reservations SET accommodation_id = ?, ingest_state = 'staged' WHERE id = ?").run(String(stagedOnly.id), stagedBooking.id);
|
||||
|
||||
const legacy = testDb.prepare(`
|
||||
SELECT a.id, a.trip_id, a.place_id, a.start_day_id, a.end_day_id, a.check_in, a.check_in_end, a.check_out, a.notes,
|
||||
p.name as place_name, p.address as place_address, p.lat as place_lat, p.lng as place_lng
|
||||
FROM day_accommodations a JOIN places p ON p.id = a.place_id
|
||||
WHERE a.trip_id = ? AND ${publicStaySql('a')}`).all(trip.id);
|
||||
|
||||
const typed = await reservationsRepo.listPublicAccommodationsForShare(trip.id);
|
||||
expect(typed).toEqual(legacy);
|
||||
expect(typed.map((s) => s.id).sort((a, b) => a - b)).toEqual([noBooking.id, liveLinked.id].sort((a, b) => a - b));
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReservationsRepository — public-api.service.ts reads', () => {
|
||||
it('listScheduledForPublicApi — matches the legacy statement, scoped by trip, ordered by day then time', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2026-09-01', end_date: '2026-09-03' });
|
||||
const days = testDb.prepare('SELECT id FROM days WHERE trip_id = ? ORDER BY day_number ASC').all(trip.id) as { id: number }[];
|
||||
const scheduled = createReservation(testDb, trip.id, { title: 'Dinner', type: 'restaurant', day_id: days[0].id });
|
||||
testDb.prepare("UPDATE reservations SET reservation_time = ?, location = ?, notes = ? WHERE id = ?").run('2026-09-01T19:00', 'Downtown', 'window seat', scheduled.id);
|
||||
createReservation(testDb, trip.id, { title: 'Unscheduled', type: 'restaurant' });
|
||||
|
||||
const legacy = testDb.prepare(`
|
||||
SELECT day_id, type, title, location, reservation_time, reservation_end_time, status, notes
|
||||
FROM reservations WHERE trip_id = ? AND day_id IS NOT NULL
|
||||
ORDER BY day_id ASC, reservation_time ASC`).all(trip.id);
|
||||
|
||||
const typed = await reservationsRepo.listScheduledForPublicApi(trip.id);
|
||||
expect(typed).toEqual(legacy);
|
||||
expect(typed.length).toBe(1);
|
||||
});
|
||||
|
||||
it('listUnscheduledForPublicApi — matches the legacy statement, day_id IS NULL only', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2026-09-01', end_date: '2026-09-02' });
|
||||
const days = testDb.prepare('SELECT id FROM days WHERE trip_id = ? ORDER BY day_number ASC').all(trip.id) as { id: number }[];
|
||||
const unscheduled = createReservation(testDb, trip.id, { title: 'Unscheduled', type: 'restaurant' });
|
||||
testDb.prepare("UPDATE reservations SET reservation_time = NULL, location = ?, notes = ? WHERE id = ?").run(null, null, unscheduled.id);
|
||||
createReservation(testDb, trip.id, { title: 'Scheduled', type: 'restaurant', day_id: days[0].id });
|
||||
|
||||
const legacy = testDb.prepare(`
|
||||
SELECT type, title, location, reservation_time, reservation_end_time, status, notes
|
||||
FROM reservations WHERE trip_id = ? AND day_id IS NULL
|
||||
ORDER BY reservation_time ASC, id ASC`).all(trip.id);
|
||||
|
||||
const typed = await reservationsRepo.listUnscheduledForPublicApi(trip.id);
|
||||
expect(typed).toEqual(legacy);
|
||||
expect(typed.map((r) => r.title)).toEqual(['Unscheduled']);
|
||||
});
|
||||
|
||||
it('listAccommodationsForPublicApi — matches the legacy LEFT JOIN statement, ordered by start date', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2026-09-01', end_date: '2026-09-05' });
|
||||
const days = testDb.prepare('SELECT id FROM days WHERE trip_id = ? ORDER BY day_number ASC').all(trip.id) as { id: number }[];
|
||||
const place = createPlace(testDb, trip.id, { name: 'Grand Hotel' });
|
||||
createDayAccommodation(testDb, trip.id, place.id, days[2].id, days[3].id, { check_in: '15:00', check_out: '11:00' });
|
||||
createDayAccommodation(testDb, trip.id, place.id, days[0].id, days[1].id);
|
||||
|
||||
const legacy = testDb.prepare(`
|
||||
SELECT p.name, p.address, p.lat, p.lng, ds.date as start_date, de.date as end_date, a.check_in, a.check_out, a.notes
|
||||
FROM day_accommodations a
|
||||
LEFT JOIN places p ON p.id = a.place_id
|
||||
LEFT JOIN days ds ON ds.id = a.start_day_id
|
||||
LEFT JOIN days de ON de.id = a.end_day_id
|
||||
WHERE a.trip_id = ? ORDER BY ds.date ASC`).all(trip.id) as { start_date: string | null }[];
|
||||
|
||||
const typed = await reservationsRepo.listAccommodationsForPublicApi(trip.id);
|
||||
expect(typed).toEqual(legacy);
|
||||
expect(typed.map((a) => a.start_date)).toEqual([legacy[0].start_date, legacy[1].start_date]);
|
||||
});
|
||||
|
||||
it('listUnplannedPlacesForPublicApi — matches the legacy NOT EXISTS statement, excluding assigned and staying places', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2026-09-01', end_date: '2026-09-02' });
|
||||
const days = testDb.prepare('SELECT id FROM days WHERE trip_id = ? ORDER BY day_number ASC').all(trip.id) as { id: number }[];
|
||||
const unplanned = createPlace(testDb, trip.id, { name: 'Unplanned Cafe' });
|
||||
const assigned = createPlace(testDb, trip.id, { name: 'Assigned Museum' });
|
||||
createDayAssignment(testDb, days[0].id, assigned.id);
|
||||
const staying = createPlace(testDb, trip.id, { name: 'Hotel' });
|
||||
createDayAccommodation(testDb, trip.id, staying.id, days[0].id, days[1].id);
|
||||
|
||||
const legacy = testDb.prepare(`
|
||||
SELECT p.name, p.address, p.lat, p.lng, p.place_time, p.end_time, p.duration_minutes, p.notes, p.transport_mode, c.name as category
|
||||
FROM places p LEFT JOIN categories c ON c.id = p.category_id
|
||||
WHERE p.trip_id = ?
|
||||
AND NOT EXISTS (SELECT 1 FROM day_assignments da WHERE da.place_id = p.id)
|
||||
AND NOT EXISTS (SELECT 1 FROM day_accommodations a WHERE a.place_id = p.id)
|
||||
ORDER BY p.created_at ASC, p.id ASC`).all(trip.id);
|
||||
|
||||
const typed = await reservationsRepo.listUnplannedPlacesForPublicApi(trip.id);
|
||||
expect(typed).toEqual(legacy);
|
||||
expect(typed.map((p) => p.name)).toEqual([unplanned.name]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user