Files
grabowski 154f56a0a0 Add multi-day entries and area stay blocks
Entries gain an optional inclusive end_date (backfilled via migration): flights and travel longer than 24h show a continuation marker on following days, and a new stay entry type marks a time frame in one area (e.g. 3 days Venice) rendered as continuous bands across the calendar weeks. Stays feed the map route as stops; the route summary gains stays count and a chronological areas list with day spans. 49 API tests.
2026-07-19 00:30:14 +07:00

131 lines
5.0 KiB
JavaScript

import { test, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import request from 'supertest';
import { createApp } from '../src/server/app.js';
let tmpDir;
let app;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tripplan-stays-'));
app = createApp({ dbPath: path.join(tmpDir, 'test.db'), sessionSecret: 'test-secret' });
});
afterEach(() => {
try {
if (app.locals.db) app.locals.db.close();
} catch {
/* ignore */
}
fs.rmSync(tmpDir, { recursive: true, force: true });
});
async function createAccount() {
const agent = request.agent(app);
const res = await agent.post('/api/auth/account').send({});
assert.equal(res.status, 201);
return { agent, user: res.body.user };
}
async function makeTrip(agent) {
return (await agent.post('/api/trips').send({ name: 'S', start_date: '2026-08-01', end_date: '2026-08-31' })).body.trip;
}
// ---------------------------------------------------------------------------
// end_date validation
// ---------------------------------------------------------------------------
test('end_date: accepted (>= date), rejected (< date), cleared via null', async () => {
const { agent } = await createAccount();
const trip = await makeTrip(agent);
const base = `/api/trips/${trip.id}/entries`;
const ok = await agent.post(base).send({ date: '2026-08-01', end_date: '2026-08-03', type: 'stay', title: 'Venice' });
assert.equal(ok.status, 201);
assert.equal(ok.body.entry.end_date, '2026-08-03');
const bad = await agent.post(base).send({ date: '2026-08-05', end_date: '2026-08-02', type: 'stay', title: 'X' });
assert.equal(bad.status, 400);
const cleared = await agent.patch(`/api/entries/${ok.body.entry.id}`).send({ end_date: null });
assert.equal(cleared.status, 200);
assert.equal(cleared.body.entry.end_date, null);
});
test('end_date: PATCHing date past a stored end_date is rejected', async () => {
const { agent } = await createAccount();
const trip = await makeTrip(agent);
const entry = (await agent.post(`/api/trips/${trip.id}/entries`).send({
date: '2026-08-01', end_date: '2026-08-03', type: 'stay', title: 'Venice',
})).body.entry;
// Moving date to 08-10 leaves stored end_date (08-03) < date -> 400.
const conflict = await agent.patch(`/api/entries/${entry.id}`).send({ date: '2026-08-10' });
assert.equal(conflict.status, 400);
// Moving date and end_date together is fine.
const ok = await agent.patch(`/api/entries/${entry.id}`).send({ date: '2026-08-10', end_date: '2026-08-12' });
assert.equal(ok.status, 200);
assert.equal(ok.body.entry.date, '2026-08-10');
assert.equal(ok.body.entry.end_date, '2026-08-12');
});
test('a >24h flight round-trips end_date', async () => {
const { agent } = await createAccount();
const trip = await makeTrip(agent);
const res = await agent.post(`/api/trips/${trip.id}/entries`).send({
date: '2026-08-01', end_date: '2026-08-02', type: 'flight', title: 'SYD-LHR',
start_time: '21:00', end_time: '05:30',
});
assert.equal(res.status, 201);
assert.equal(res.body.entry.date, '2026-08-01');
assert.equal(res.body.entry.end_date, '2026-08-02');
});
// ---------------------------------------------------------------------------
// Stay type + areas summary
// ---------------------------------------------------------------------------
test('stay entries accepted; summary reports stays count and areas in order', async () => {
const { agent } = await createAccount();
const trip = await makeTrip(agent);
// Venice 3 days (name from location_name), then Berlin 7 days (name from title fallback).
await agent.post(`/api/trips/${trip.id}/entries`).send({
date: '2026-08-01', end_date: '2026-08-03', type: 'stay', title: 'Venice stay',
location_name: 'Venice', sort_order: 0,
});
await agent.post(`/api/trips/${trip.id}/entries`).send({
date: '2026-08-04', end_date: '2026-08-10', type: 'stay', title: 'Berlin', sort_order: 1,
});
// A single-day stay (no end_date) -> days 1.
await agent.post(`/api/trips/${trip.id}/entries`).send({
date: '2026-08-11', type: 'stay', title: 'Layover', location_name: 'Doha',
});
const res = await agent.get(`/api/trips/${trip.id}/route`);
assert.equal(res.status, 200);
assert.equal(res.body.summary.stays, 3);
assert.deepEqual(res.body.summary.areas, [
{ name: 'Venice', days: 3 },
{ name: 'Berlin', days: 7 },
{ name: 'Doha', days: 1 },
]);
});
test('stay with location feeds the route as one stop', async () => {
const { agent } = await createAccount();
const trip = await makeTrip(agent);
await agent.post(`/api/trips/${trip.id}/entries`).send({
date: '2026-08-01', end_date: '2026-08-05', type: 'stay', title: 'Venice',
location_name: 'Venice', lat: 45.44, lng: 12.32,
});
const res = await agent.get(`/api/trips/${trip.id}/route`);
assert.equal(res.body.stops.length, 1);
assert.equal(res.body.stops[0].lat, 45.44);
assert.equal(res.body.summary.stays, 1);
});