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.
131 lines
5.0 KiB
JavaScript
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);
|
|
});
|