Files
trip-plan/tests/api.test.js
T
grabowski 4cedab0cf6 Route ground legs along real roads via an OSRM directions proxy
- GET /api/directions proxies OSRM (OSRM_URL env, default public demo
  server) with validation, Leaflet-order geometry, 24h capped cache
- Map draws straight lines first, then upgrades ground legs to
  road-following polylines with routed km (marked road) as directions
  arrive; silent fallback to great-circle on failure; air legs unchanged
- README documents OSRM_URL
2026-07-20 10:54:04 +07:00

620 lines
25 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-test-'));
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 });
});
// Create an account (Mullvad-style) and return an agent with the session set,
// plus the user object {id, display_name} and the raw token.
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, token: res.body.token };
}
// ---------------------------------------------------------------------------
// Auth (account tokens)
// ---------------------------------------------------------------------------
test('account creation returns a grouped token and a generated display name', async () => {
const { user, token } = await createAccount();
assert.deepEqual(Object.keys(user).sort(), ['display_name', 'id']);
assert.match(token, /^[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{4}(-[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{4}){3}$/);
assert.match(user.display_name, /^[a-z]+-[a-z]+$/);
});
test('account / me / logout lifecycle', async () => {
const { agent, user } = await createAccount();
const me = await agent.get('/api/auth/me');
assert.equal(me.status, 200);
assert.deepEqual(me.body.user, user);
const out = await agent.post('/api/auth/logout');
assert.equal(out.status, 204);
const meAfter = await agent.get('/api/auth/me');
assert.equal(meAfter.status, 401);
assert.deepEqual(meAfter.body, { error: 'unauthorized' });
});
test('me requires auth', async () => {
const res = await request(app).get('/api/auth/me');
assert.equal(res.status, 401);
assert.equal(res.body.error, 'unauthorized');
});
test('login with token (normalized) succeeds; bad token 401', async () => {
const { user, token } = await createAccount();
// Fresh agent logs in with the token, dashes/lowercase/spaces tolerated.
const agent = request.agent(app);
const messy = ` ${token.toLowerCase().replace(/-/g, '')} `;
const ok = await agent.post('/api/auth/login').send({ token: messy });
assert.equal(ok.status, 200);
assert.deepEqual(ok.body.user, user);
const me = await agent.get('/api/auth/me');
assert.equal(me.body.user.id, user.id);
const bad = await request(app).post('/api/auth/login').send({ token: 'ZZZZ-ZZZZ-ZZZZ-ZZZZ' });
assert.equal(bad.status, 401);
const empty = await request(app).post('/api/auth/login').send({});
assert.equal(empty.status, 401);
});
test('patch display_name validates 1-40 chars after trim', async () => {
const { agent, user } = await createAccount();
const ok = await agent.patch('/api/auth/me').send({ display_name: ' Anna the Explorer ' });
assert.equal(ok.status, 200);
assert.equal(ok.body.user.display_name, 'Anna the Explorer');
assert.equal(ok.body.user.id, user.id);
const empty = await agent.patch('/api/auth/me').send({ display_name: ' ' });
assert.equal(empty.status, 400);
const tooLong = await agent.patch('/api/auth/me').send({ display_name: 'x'.repeat(41) });
assert.equal(tooLong.status, 400);
const noAuth = await request(app).patch('/api/auth/me').send({ display_name: 'nope' });
assert.equal(noAuth.status, 401);
});
// ---------------------------------------------------------------------------
// Trips: CRUD + validation
// ---------------------------------------------------------------------------
test('create trip and list it', async () => {
const { agent, user } = await createAccount();
const create = await agent
.post('/api/trips')
.send({ name: 'Thailand', start_date: '2026-08-01', end_date: '2026-08-10' });
assert.equal(create.status, 201);
assert.equal(create.body.trip.name, 'Thailand');
assert.equal(create.body.trip.owner_id, user.id);
// join_code is present on the created trip, in grouped display form.
assert.match(create.body.trip.join_code, /^[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{4}-[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{4}$/);
const list = await agent.get('/api/trips');
assert.equal(list.status, 200);
assert.equal(list.body.trips.length, 1);
assert.equal(list.body.trips[0].role, 'owner');
assert.equal(list.body.trips[0].member_count, 1);
assert.equal(list.body.trips[0].entry_count, 0);
});
test('trip validation: bad dates and oversized range', async () => {
const { agent } = await createAccount();
const emptyName = await agent.post('/api/trips').send({ name: '', start_date: '2026-08-01', end_date: '2026-08-02' });
assert.equal(emptyName.status, 400);
const badDate = await agent.post('/api/trips').send({ name: 'X', start_date: '2026-13-40', end_date: '2026-08-02' });
assert.equal(badDate.status, 400);
const reversed = await agent.post('/api/trips').send({ name: 'X', start_date: '2026-08-10', end_date: '2026-08-01' });
assert.equal(reversed.status, 400);
const tooLong = await agent.post('/api/trips').send({ name: 'X', start_date: '2026-01-01', end_date: '2027-06-01' });
assert.equal(tooLong.status, 400);
});
test('get / patch / delete trip', async () => {
const { agent } = await createAccount();
const created = (await agent.post('/api/trips').send({ name: 'Trip', start_date: '2026-08-01', end_date: '2026-08-05' })).body.trip;
const got = await agent.get(`/api/trips/${created.id}`);
assert.equal(got.status, 200);
assert.equal(got.body.trip.name, 'Trip');
assert.ok(got.body.trip.join_code);
assert.equal(got.body.members.length, 1);
assert.deepEqual(Object.keys(got.body.members[0]).sort(), ['display_name', 'id', 'role']);
assert.deepEqual(got.body.entries, []);
const patched = await agent.patch(`/api/trips/${created.id}`).send({ name: 'Renamed', end_date: '2026-08-08' });
assert.equal(patched.status, 200);
assert.equal(patched.body.trip.name, 'Renamed');
assert.equal(patched.body.trip.end_date, '2026-08-08');
const del = await agent.delete(`/api/trips/${created.id}`);
assert.equal(del.status, 204);
const gone = await agent.get(`/api/trips/${created.id}`);
assert.equal(gone.status, 404);
});
test('unknown trip returns 404', async () => {
const { agent } = await createAccount();
const res = await agent.get('/api/trips/9999');
assert.equal(res.status, 404);
assert.equal(res.body.error, 'not found');
});
test('PATCH /api/trips/:id/order reorders the dashboard list for the caller', async () => {
const { agent } = await createAccount();
const a = (await agent.post('/api/trips').send({ name: 'A', start_date: '2026-08-01', end_date: '2026-08-02' })).body.trip;
const b = (await agent.post('/api/trips').send({ name: 'B', start_date: '2026-08-01', end_date: '2026-08-02' })).body.trip;
const c = (await agent.post('/api/trips').send({ name: 'C', start_date: '2026-08-01', end_date: '2026-08-02' })).body.trip;
// Default order is newest first: C, B, A.
const initial = await agent.get('/api/trips');
assert.deepEqual(initial.body.trips.map((t) => t.id), [c.id, b.id, a.id]);
assert.ok(initial.body.trips.every((t) => t.sort_order === 0));
const setA = await agent.patch(`/api/trips/${a.id}/order`).send({ sort_order: 0 });
assert.equal(setA.status, 200);
assert.deepEqual(setA.body, { sort_order: 0 });
await agent.patch(`/api/trips/${b.id}/order`).send({ sort_order: 2 });
await agent.patch(`/api/trips/${c.id}/order`).send({ sort_order: 1 });
const reordered = await agent.get('/api/trips');
assert.deepEqual(reordered.body.trips.map((t) => t.id), [a.id, c.id, b.id]);
});
test('trip order is per-user', async () => {
const owner = await createAccount();
const guest = await createAccount();
const t1 = (await owner.agent.post('/api/trips').send({ name: 'T1', start_date: '2026-08-01', end_date: '2026-08-02' })).body.trip;
const t2 = (await owner.agent.post('/api/trips').send({ name: 'T2', start_date: '2026-08-01', end_date: '2026-08-02' })).body.trip;
await guest.agent.post('/api/trips/join').send({ code: t1.join_code });
await guest.agent.post('/api/trips/join').send({ code: t2.join_code });
// Owner reorders their own list; guest's list is unaffected.
const guestBefore = (await guest.agent.get('/api/trips')).body.trips.map((t) => t.id);
await owner.agent.patch(`/api/trips/${t1.id}/order`).send({ sort_order: 5 });
const guestAfter = (await guest.agent.get('/api/trips')).body.trips.map((t) => t.id);
assert.deepEqual(guestAfter, guestBefore);
const ownerList = (await owner.agent.get('/api/trips')).body.trips;
assert.equal(ownerList.find((t) => t.id === t1.id).sort_order, 5);
});
test('order endpoint validation: non-integer 400, non-member 404', async () => {
const owner = await createAccount();
const outsider = await createAccount();
const trip = (await owner.agent.post('/api/trips').send({ name: 'T', start_date: '2026-08-01', end_date: '2026-08-02' })).body.trip;
const badBody = await owner.agent.patch(`/api/trips/${trip.id}/order`).send({ sort_order: 'first' });
assert.equal(badBody.status, 400);
assert.equal(badBody.body.error, 'sort_order must be an integer');
const missing = await owner.agent.patch(`/api/trips/${trip.id}/order`).send({});
assert.equal(missing.status, 400);
const notMember = await outsider.agent.patch(`/api/trips/${trip.id}/order`).send({ sort_order: 1 });
assert.equal(notMember.status, 404);
});
// ---------------------------------------------------------------------------
// Membership via join code
// ---------------------------------------------------------------------------
test('join by code: non-member 404, join as editor, idempotent, unknown 404', async () => {
const owner = await createAccount();
const guest = await createAccount();
const trip = (await owner.agent.post('/api/trips').send({ name: 'Shared', start_date: '2026-08-01', end_date: '2026-08-03' })).body.trip;
const code = trip.join_code;
// Non-member cannot see the trip.
assert.equal((await guest.agent.get(`/api/trips/${trip.id}`)).status, 404);
// Unknown code -> 404.
assert.equal((await guest.agent.post('/api/trips/join').send({ code: 'ZZZZ-ZZZZ' })).status, 404);
// Join with the code (lowercase/spacing tolerated) -> 200, joined as editor.
const joined = await guest.agent.post('/api/trips/join').send({ code: ` ${code.toLowerCase()} ` });
assert.equal(joined.status, 200);
assert.equal(joined.body.trip.id, trip.id);
const view = await guest.agent.get(`/api/trips/${trip.id}`);
assert.equal(view.status, 200);
const guestMember = view.body.members.find((m) => m.id === guest.user.id);
assert.equal(guestMember.role, 'editor');
// Idempotent: joining again still 200, membership count unchanged.
const again = await guest.agent.post('/api/trips/join').send({ code });
assert.equal(again.status, 200);
const memberCount = (await owner.agent.get('/api/trips')).body.trips[0].member_count;
assert.equal(memberCount, 2);
});
test('join-code regeneration is owner-only and invalidates the old code', async () => {
const owner = await createAccount();
const guest = await createAccount();
const trip = (await owner.agent.post('/api/trips').send({ name: 'T', start_date: '2026-08-01', end_date: '2026-08-03' })).body.trip;
const oldCode = trip.join_code;
// Non-member cannot regenerate (they can't even see the trip) -> 404.
assert.equal((await guest.agent.post(`/api/trips/${trip.id}/join-code`)).status, 404);
const regen = await owner.agent.post(`/api/trips/${trip.id}/join-code`);
assert.equal(regen.status, 200);
assert.notEqual(regen.body.trip.join_code, oldCode);
// Old code no longer works; new one does.
assert.equal((await guest.agent.post('/api/trips/join').send({ code: oldCode })).status, 404);
assert.equal((await guest.agent.post('/api/trips/join').send({ code: regen.body.trip.join_code })).status, 200);
// A member who is not the owner cannot regenerate -> 403.
const nonOwnerRegen = await guest.agent.post(`/api/trips/${trip.id}/join-code`);
assert.equal(nonOwnerRegen.status, 403);
});
test('owner-only member removal; owner cannot remove self', async () => {
const owner = await createAccount();
const guest = await createAccount();
const trip = (await owner.agent.post('/api/trips').send({ name: 'T', start_date: '2026-08-01', end_date: '2026-08-03' })).body.trip;
await guest.agent.post('/api/trips/join').send({ code: trip.join_code });
// Non-owner cannot delete the trip.
assert.equal((await guest.agent.delete(`/api/trips/${trip.id}`)).status, 403);
// Owner removes the guest.
assert.equal((await owner.agent.delete(`/api/trips/${trip.id}/members/${guest.user.id}`)).status, 204);
assert.equal((await guest.agent.get(`/api/trips/${trip.id}`)).status, 404);
// Owner cannot remove self.
assert.equal((await owner.agent.delete(`/api/trips/${trip.id}/members/${owner.user.id}`)).status, 400);
});
// ---------------------------------------------------------------------------
// Entries: CRUD + validation
// ---------------------------------------------------------------------------
async function makeTrip(agent) {
return (await agent.post('/api/trips').send({ name: 'E', start_date: '2026-08-01', end_date: '2026-08-10' })).body.trip;
}
test('entry CRUD and full-row shape', async () => {
const { agent } = await createAccount();
const trip = await makeTrip(agent);
const create = await agent.post(`/api/trips/${trip.id}/entries`).send({
date: '2026-08-01',
type: 'flight',
title: 'BKK -> CNX',
start_time: '09:30',
location_name: 'Chiang Mai',
lat: 18.79,
lng: 98.98,
});
assert.equal(create.status, 201);
const entry = create.body.entry;
assert.deepEqual(
Object.keys(entry).sort(),
['auto_ref', 'date', 'end_date', 'details', 'end_time', 'id', 'lat', 'lng', 'location_name', 'paid_by', 'participants', 'price', 'rental', 'segments', 'sort_order', 'split_mode', 'start_time', 'title', 'transport_mode', 'trip_id', 'type'].sort()
);
assert.equal(entry.end_date, null);
assert.equal(entry.details, '');
assert.equal(entry.lat, 18.79);
assert.equal(entry.price, null);
assert.equal(entry.paid_by, null);
assert.equal(entry.split_mode, 'equal');
assert.deepEqual(entry.participants, []);
assert.equal(entry.segments, null);
assert.equal(entry.rental, null);
const patch = await agent.patch(`/api/entries/${entry.id}`).send({ title: 'BKK to CNX', details: 'window seat' });
assert.equal(patch.status, 200);
assert.equal(patch.body.entry.title, 'BKK to CNX');
assert.equal(patch.body.entry.details, 'window seat');
const list = await agent.get(`/api/trips/${trip.id}/entries`);
assert.equal(list.body.entries.length, 1);
const del = await agent.delete(`/api/entries/${entry.id}`);
assert.equal(del.status, 204);
const listAfter = await agent.get(`/api/trips/${trip.id}/entries`);
assert.equal(listAfter.body.entries.length, 0);
});
test('entry validation: type, title, lat/lng pairing and ranges', async () => {
const { agent } = await createAccount();
const trip = await makeTrip(agent);
const base = `/api/trips/${trip.id}/entries`;
const badType = await agent.post(base).send({ date: '2026-08-01', type: 'teleport', title: 'x' });
assert.equal(badType.status, 400);
const badDate = await agent.post(base).send({ date: '08/01/2026', type: 'note', title: 'x' });
assert.equal(badDate.status, 400);
const noTitle = await agent.post(base).send({ date: '2026-08-01', type: 'note', title: '' });
assert.equal(noTitle.status, 400);
const latOnly = await agent.post(base).send({ date: '2026-08-01', type: 'note', title: 'x', lat: 10 });
assert.equal(latOnly.status, 400);
const badLat = await agent.post(base).send({ date: '2026-08-01', type: 'note', title: 'x', lat: 99, lng: 10 });
assert.equal(badLat.status, 400);
const ok = await agent.post(base).send({ date: '2026-08-01', type: 'note', title: 'x', lat: 10, lng: 20 });
assert.equal(ok.status, 201);
});
test('legacy entry types (hotel, travel, immigration) are rejected', async () => {
const { agent } = await createAccount();
const trip = await makeTrip(agent);
const base = `/api/trips/${trip.id}/entries`;
for (const type of ['hotel', 'travel', 'immigration']) {
const res = await agent.post(base).send({ date: '2026-08-01', type, title: 'x' });
assert.equal(res.status, 400, `expected ${type} to be rejected`);
}
});
test('non-member cannot add or view entries', async () => {
const owner = await createAccount();
const guest = await createAccount();
const trip = await makeTrip(owner.agent);
const post = await guest.agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-01', type: 'note', title: 'x' });
assert.equal(post.status, 404);
const list = await guest.agent.get(`/api/trips/${trip.id}/entries`);
assert.equal(list.status, 404);
});
// ---------------------------------------------------------------------------
// Route + summary math
// ---------------------------------------------------------------------------
test('route computes legs, totalKm and summary counts', async () => {
const { agent } = await createAccount();
const trip = await makeTrip(agent); // 2026-08-01 .. 2026-08-10 => 10 days
// Bangkok
await agent.post(`/api/trips/${trip.id}/entries`).send({
date: '2026-08-01', type: 'flight', title: 'Depart', sort_order: 0,
location_name: 'Bangkok', lat: 13.7563, lng: 100.5018,
});
// Chiang Mai
await agent.post(`/api/trips/${trip.id}/entries`).send({
date: '2026-08-01', type: 'stay', title: 'Hotel CNX', sort_order: 1,
location_name: 'Chiang Mai', lat: 18.7883, lng: 98.9853,
});
// A second flight + stay + activities + a transport leg (no coords) to exercise counts
await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-02', type: 'flight', title: 'F2' });
await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-02', type: 'stay', title: 'H2' });
await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-02', type: 'transport', title: 'Drive' });
await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-03', type: 'activity', title: 'A1' });
await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-03', type: 'activity', title: 'A2' });
const res = await agent.get(`/api/trips/${trip.id}/route`);
assert.equal(res.status, 200);
// Two located stops -> one leg. BKK<->CNX great-circle ~ 585 km.
assert.equal(res.body.stops.length, 2);
assert.equal(res.body.legs.length, 1);
assert.ok(res.body.legs[0].km > 570 && res.body.legs[0].km < 600, `unexpected km ${res.body.legs[0].km}`);
assert.equal(res.body.totalKm, res.body.legs[0].km);
const s = res.body.summary;
assert.equal(s.days, 10);
assert.equal(s.nights, 9);
assert.equal(s.flights, 2);
assert.equal(s.transports, 1);
assert.equal(s.hotels, undefined);
assert.equal(s.travelLegs, undefined);
assert.equal(s.activities, 2);
assert.deepEqual(s.locations, ['Bangkok', 'Chiang Mai']);
});
test('route skips zero-distance legs but keeps the stop', async () => {
const { agent } = await createAccount();
const trip = await makeTrip(agent);
await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-01', type: 'activity', title: 'A', sort_order: 0, location_name: 'Same', lat: 10, lng: 10 });
await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-01', type: 'activity', title: 'B', sort_order: 1, location_name: 'Same', lat: 10, lng: 10 });
const res = await agent.get(`/api/trips/${trip.id}/route`);
assert.equal(res.body.stops.length, 2);
assert.equal(res.body.legs.length, 0);
assert.equal(res.body.totalKm, 0);
assert.deepEqual(res.body.summary.locations, ['Same']);
});
// ---------------------------------------------------------------------------
// Geocode (mocked upstream — never hits the real Nominatim)
// ---------------------------------------------------------------------------
test('geocode proxies and caches results (mocked fetch)', async () => {
const { agent } = await createAccount();
const original = global.fetch;
let calls = 0;
global.fetch = async () => {
calls += 1;
return {
ok: true,
json: async () => [
{ display_name: 'Chiang Mai, Thailand', lat: '18.7883', lon: '98.9853' },
],
};
};
try {
const first = await agent.get('/api/geocode?q=Chiang%20Mai');
assert.equal(first.status, 200);
assert.equal(first.body.results.length, 1);
assert.deepEqual(first.body.results[0], { name: 'Chiang Mai, Thailand', lat: 18.7883, lng: 98.9853 });
// Second identical query is served from cache -> fetch not called again.
const second = await agent.get('/api/geocode?q=Chiang%20Mai');
assert.equal(second.status, 200);
assert.equal(calls, 1);
} finally {
global.fetch = original;
}
});
test('geocode returns 502 on upstream failure (mocked fetch)', async () => {
const { agent } = await createAccount();
const original = global.fetch;
global.fetch = async () => {
throw new Error('network down');
};
try {
const res = await agent.get('/api/geocode?q=Nowhere');
assert.equal(res.status, 502);
assert.deepEqual(res.body, { error: 'geocoding unavailable' });
} finally {
global.fetch = original;
}
});
test('geocode requires auth and a query', async () => {
const noAuth = await request(app).get('/api/geocode?q=x');
assert.equal(noAuth.status, 401);
const { agent } = await createAccount();
const noQuery = await agent.get('/api/geocode');
assert.equal(noQuery.status, 400);
});
// ---------------------------------------------------------------------------
// Directions (mocked upstream — never hits the real OSRM)
// ---------------------------------------------------------------------------
test('directions proxies and caches results (mocked fetch)', async () => {
const { agent } = await createAccount();
const original = global.fetch;
let calls = 0;
global.fetch = async () => {
calls += 1;
return {
ok: true,
json: async () => ({
code: 'Ok',
routes: [
{
distance: 12345.6,
geometry: {
coordinates: [
[98.9853, 18.7883],
[99.0, 18.8],
],
},
},
],
}),
};
};
try {
const first = await agent.get('/api/directions?from=18.7883,98.9853&to=18.8,99.0');
assert.equal(first.status, 200);
assert.equal(first.body.km, 12.3);
assert.deepEqual(first.body.geometry, [
[18.7883, 98.9853],
[18.8, 99.0],
]);
// Second identical request is served from cache -> fetch not called again.
const second = await agent.get('/api/directions?from=18.7883,98.9853&to=18.8,99.0');
assert.equal(second.status, 200);
assert.equal(calls, 1);
} finally {
global.fetch = original;
}
});
test('directions validates from/to params', async () => {
const { agent } = await createAccount();
const missing = await agent.get('/api/directions?from=18.7883,98.9853');
assert.equal(missing.status, 400);
assert.deepEqual(missing.body, { error: 'from and to must be lat,lng' });
const malformed = await agent.get('/api/directions?from=abc&to=18.8,99.0');
assert.equal(malformed.status, 400);
const outOfRange = await agent.get('/api/directions?from=999,98.9853&to=18.8,99.0');
assert.equal(outOfRange.status, 400);
});
test('directions returns 502 on non-Ok OSRM code and on fetch failure', async () => {
const { agent } = await createAccount();
const original = global.fetch;
global.fetch = async () => ({
ok: true,
json: async () => ({ code: 'NoRoute', routes: [] }),
});
try {
const noRoute = await agent.get('/api/directions?from=18.7883,98.9853&to=18.8,99.0');
assert.equal(noRoute.status, 502);
assert.deepEqual(noRoute.body, { error: 'directions unavailable' });
} finally {
global.fetch = original;
}
global.fetch = async () => {
throw new Error('network down');
};
try {
const res = await agent.get('/api/directions?from=18.7883,98.9853&to=18.8,99.0');
assert.equal(res.status, 502);
assert.deepEqual(res.body, { error: 'directions unavailable' });
} finally {
global.fetch = original;
}
});
test('directions requires auth', async () => {
const noAuth = await request(app).get('/api/directions?from=18.7883,98.9853&to=18.8,99.0');
assert.equal(noAuth.status, 401);
});
// ---------------------------------------------------------------------------
// Unknown /api route -> JSON 404
// ---------------------------------------------------------------------------
test('unknown api path returns JSON 404', async () => {
const { agent } = await createAccount();
const res = await agent.get('/api/does-not-exist');
assert.equal(res.status, 404);
assert.deepEqual(res.body, { error: 'not found' });
});