Files
TREK/server/tests/integration/dayReorder.test.ts
T
Maurice 939452f9d8 refactor(server): consolidate trip access and fold the query helpers into the container (#1824)
* refactor(server): route every trip-access check through DatabaseService

Twelve Nest services carry the same verifyTripAccess/canEdit pair. Six already
asked DatabaseService.canAccessTrip; the other six went through
services/tripAccess, a nine-line re-export of the identical function. Same
query, same result, two ways to reach it.

All twelve now take the injected route, so services/tripAccess is gone.

The bigger duplication was in the types. Thirteen files declared their own
`Trip` for the same return value, in three different shapes:

  NonNullable<ReturnType<typeof verifyTripAccess>>   6x
  { user_id: number }                                4x
  TripAccess                                         3x

The narrow one is why assignments, days, places and accommodations all wrote
`as Trip | null | undefined` on the call — the cast existed to shrink
TripAccess down to a local type that was never meant to differ from it. All
thirteen now say TripAccess, and the four casts are gone with no other change.

middleware/tripAccess.ts goes too: it was already dead in production — nothing
under src/ imported requireTripAccess or requireTripOwner, only comments in the
services that had absorbed them. Its test went with it.

* refactor(server): fold the query helpers and trip joins into the container

Two more legacy files whose only reason for staying outside the container was
that they predated it.

services/queryHelpers.ts held five exports doing two different jobs. Three are
the batch loaders that keep the list endpoints off N+1 queries — they hit the
database and are now QueryHelpersService over the injected connection, imported
by assignments, days, places and share. The other two, formatAssignmentWithPlace
and ratingAggregate, touch no database at all; they sat next to the loaders only
because the loaders were free functions too. Those stay free functions, in
common/rowShape.ts.

The README note on the assignments service claimed the loaders had to stay in
services/ because the day, share and place services were unmigrated. All three
have since moved, so the reason is gone.

services/tripMembership.ts becomes TripMembershipService in its own module. It
could have folded into auth or trip-invite, but both call it — as do oidc and
the plugin RPC host — and putting it in either would have made AuthModule and
TripInviteModule import each other.

Neither module is @Global, matching PermissionsModule: e2e TestingModules
resolve them transitively through each consumer's explicit import.

Test seams changed shape accordingly. Two path mocks of services/tripMembership
became injected stubs; the trip-invite e2e overrides the provider in the
container instead. The oidc test keeps the real implementation, which is what
OIDC-SVC-045 was always asserting.

ratingAggregate moved out of the ungated services/ tree into the coverage-gated
one, so it gets the direct tests it never had — as does the tags/participants
pass-through, which two long-dead fixture variables in the moved test file had
been waiting for.

Also drops four stale comments that outlived their subject: the requireTripAccess
middleware they pointed at was deleted in the previous commit.
2026-08-07 16:00:12 +02:00

143 lines
6.6 KiB
TypeScript

/**
* Day reorder + insert integration tests (#589) — exercises the real
* DaysService against the real schema. Covers: position renumber, dates pinned
* to slots while content rides along by id, booking-date re-stamp, permutation
* validation, the accommodation-inversion guard, and insert (dated + dateless).
*/
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
const { testDb, dbMock } = vi.hoisted(() => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const Database = require('better-sqlite3');
const db = new Database(':memory:');
db.exec('PRAGMA journal_mode = WAL');
db.exec('PRAGMA foreign_keys = ON');
db.exec('PRAGMA busy_timeout = 5000');
return { testDb: db, dbMock: { db, closeDb: () => {}, reinitialize: () => {}, canAccessTrip: vi.fn() } };
});
vi.mock('../../src/db/database', () => dbMock);
vi.mock('../../src/config', () => ({
JWT_SECRET: 'test-jwt-secret-for-trek-testing-only',
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
updateJwtSecret: () => {},
DEFAULT_LANGUAGE: 'en',
}));
import { createTables } from '../../src/db/schema';
import { runMigrations } from '../../src/db/migrations';
import { resetTestDb } from '../helpers/test-db';
import { createUser, createTrip, createPlace, createDay, createDayAssignment, createReservation, createDayAccommodation } from '../helpers/factories';
import { DatabaseService } from '../../src/nest/database/database.service';
import { PermissionsService } from '../../src/nest/permissions/permissions.service';
import { DaysService, DayReorderError } from '../../src/nest/days/days.service';
import { RealtimeService } from '../../src/nest/realtime/realtime.service';
import { QueryHelpersService } from '../../src/nest/query-helpers/query-helpers.service';
const svc = new DaysService(new DatabaseService(testDb), new PermissionsService(new DatabaseService(testDb)), new RealtimeService(), new QueryHelpersService(new DatabaseService(testDb)));
const reorderDays = (tripId: number, orderedIds: number[]) => svc.reorder(tripId, orderedIds);
const insertDay = (tripId: number, position?: number) => svc.insert(tripId, position);
let userId: number;
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
});
beforeEach(() => {
resetTestDb(testDb);
userId = createUser(testDb).user.id;
});
afterAll(() => testDb.close());
const orderedDays = (tripId: number) =>
testDb.prepare('SELECT id, day_number, date FROM days WHERE trip_id = ? ORDER BY day_number').all(tripId) as
{ id: number; day_number: number; date: string | null }[];
describe('reorderDays', () => {
it('permutes positions, pins dates to slots, and content rides along by id', () => {
const trip = createTrip(testDb, userId, { start_date: '2026-03-01', end_date: '2026-03-03' });
const [d1, d2, d3] = orderedDays(trip.id);
const place = createPlace(testDb, trip.id);
createDayAssignment(testDb, d2.id, place.id); // place sits on day 2
// Move day 2 to the front: [d2, d1, d3]
reorderDays(trip.id, [d2.id, d1.id, d3.id]);
const after = orderedDays(trip.id);
expect(after.map(d => d.id)).toEqual([d2.id, d1.id, d3.id]);
// Dates stay pinned to their calendar slots
expect(after.map(d => d.date)).toEqual(['2026-03-01', '2026-03-02', '2026-03-03']);
// The place rides along with its day row (still attached to d2.id, now at slot 1)
const onD2 = testDb.prepare('SELECT * FROM day_assignments WHERE day_id = ?').all(d2.id);
expect(onD2).toHaveLength(1);
});
it('re-stamps a booking\'s date onto its day\'s new date, keeping the time', () => {
const trip = createTrip(testDb, userId, { start_date: '2026-03-01', end_date: '2026-03-03' });
const [d1, d2, d3] = orderedDays(trip.id);
const res = createReservation(testDb, trip.id, { day_id: d2.id, type: 'restaurant' });
testDb.prepare('UPDATE reservations SET reservation_time = ? WHERE id = ?').run('2026-03-02T19:00', res.id);
reorderDays(trip.id, [d2.id, d1.id, d3.id]); // d2 moves to the 2026-03-01 slot
const r = testDb.prepare('SELECT reservation_time FROM reservations WHERE id = ?').get(res.id) as { reservation_time: string };
expect(r.reservation_time).toBe('2026-03-01T19:00');
});
it('rejects an orderedIds list that is not a permutation of the trip days', () => {
const trip = createTrip(testDb, userId, { start_date: '2026-03-01', end_date: '2026-03-03' });
const [d1, d2] = orderedDays(trip.id);
expect(() => reorderDays(trip.id, [d1.id, d2.id])).toThrow(DayReorderError);
});
it('blocks a move that would make an accommodation end before it starts, and rolls back', () => {
const trip = createTrip(testDb, userId, { start_date: '2026-03-01', end_date: '2026-03-03' });
const [d1, d2, d3] = orderedDays(trip.id);
const place = createPlace(testDb, trip.id);
createDayAccommodation(testDb, trip.id, place.id, d1.id, d2.id); // stay spans day 1 -> day 2
// Put the start day (d1) after the end day (d2): [d2, d3, d1]
expect(() => reorderDays(trip.id, [d2.id, d3.id, d1.id])).toThrow(DayReorderError);
// Transaction rolled back: original order intact
expect(orderedDays(trip.id).map(d => d.id)).toEqual([d1.id, d2.id, d3.id]);
});
});
describe('insertDay', () => {
it('inserts an empty day at a position on a dateless trip and shifts the rest', () => {
const trip = createTrip(testDb, userId);
const d1 = createDay(testDb, trip.id);
const d2 = createDay(testDb, trip.id);
const d3 = createDay(testDb, trip.id);
const created = insertDay(trip.id, 1);
const after = orderedDays(trip.id);
expect(after).toHaveLength(4);
expect(after[0].id).toBe(created.id);
expect(after[0].date).toBeNull();
expect(after.slice(1).map(d => d.id)).toEqual([d1.id, d2.id, d3.id]);
});
it('inserts at the front of a dated trip: dates stay contiguous and the trip extends', () => {
const trip = createTrip(testDb, userId, { start_date: '2026-03-01', end_date: '2026-03-03' });
const [d1, d2, d3] = orderedDays(trip.id);
const created = insertDay(trip.id, 1);
const after = orderedDays(trip.id);
expect(after).toHaveLength(4);
expect(after[0].id).toBe(created.id);
expect(after.map(d => d.date)).toEqual(['2026-03-01', '2026-03-02', '2026-03-03', '2026-03-04']);
// Old content shifted down a slot
expect(after.slice(1).map(d => d.id)).toEqual([d1.id, d2.id, d3.id]);
// Trip range extended by one day
const t = testDb.prepare('SELECT end_date FROM trips WHERE id = ?').get(trip.id) as { end_date: string };
expect(t.end_date).toBe('2026-03-04');
});
});