File size: 6,022 Bytes
1f5ea39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
 * Day reorder + insert integration tests (#589) — exercises the real
 * dayService 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 { reorderDays, insertDay, DayReorderError } from '../../src/services/dayService';

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');
  });
});