Files
TREK/server/tests/integration/misc.test.ts
T
Maurice 31859651d7 feat(help): rewrite the help center for v5 with a Tours section, new pictures in every language, and fix the tour planner bugs it surfaced
Every help screen checked against the v5 redesign. Tours is documented for
the first time, as a screen of its own with twelve screens under it, and
notifications get help of their own. All pictures are taken again on a
realistic demo trip, with short loops where a still cannot show the motion.
The English is plain and every language is written naturally rather than
word for word.

The switcher shows two levels at most, a screen's overview lists its
sub-screens as cards above its guides, each wiki page is linked once, and
the help sidebar is wider.

Fixes found along the way: Save to Collection keeps a tour's route, a track
dropped on the places column opens the Tours import, tour Share on desktop,
the unsaved tour draft lives on the account, a route click no longer adds
an end point, Trip places leaves the trip's tours out, Escape closes a
stage's day menu, confirms before destructive deletes, DWD warnings sharing
one id kept apart, a booking's date moves with it, and smaller fixes across
notifications, bookings, the day plan, stays, Atlas, Collections, Vacay and
the admin screens.
2026-10-22 22:29:35 +02:00

210 lines
7.7 KiB
TypeScript

/**
* Miscellaneous integration tests.
* Covers MISC-001, 002, 004, 007, 008, 009 to 014.
*/
import { buildApp } from '../../src/bootstrap';
import { db as testDb } from '../../src/db/database';
import { authCookie } from '../helpers/auth';
import { createUser } from '../helpers/factories';
import { resetTestDb, resetRateLimits } from '../helpers/test-db';
import type { INestApplication } from '@nestjs/common';
import { TOUR_WRITE_MAX_BYTES } from '@trek/shared';
import type { Application } from 'express';
import request from 'supertest';
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
vi.mock('../../src/db/database', async () => {
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
return buildDbMock(createSnapshotTestDb());
});
let nestApp: INestApplication;
let app: Application;
beforeAll(async () => {
nestApp = await buildApp();
app = nestApp.getHttpAdapter().getInstance();
});
beforeEach(async () => {
resetTestDb(testDb);
await resetRateLimits(nestApp);
});
afterAll(async () => {
await nestApp.close();
testDb.close();
});
describe('Health check', () => {
it('MISC-001 — GET /api/health returns 200 with status ok', async () => {
const res = await request(app).get('/api/health');
expect(res.status).toBe(200);
expect(res.body.status).toBe('ok');
});
});
describe('Addons list', () => {
it('MISC-002 — GET /api/addons returns enabled addons', async () => {
const { user } = createUser(testDb);
const res = await request(app).get('/api/addons').set('Cookie', authCookie(user.id));
expect(res.status).toBe(200);
expect(Array.isArray(res.body.addons)).toBe(true);
// Should only return enabled addons
const enabled = (res.body.addons as any[]).filter((a: any) => !a.enabled);
expect(enabled.length).toBe(0);
});
});
describe('Photo endpoint auth', () => {
it('MISC-007 — GET /uploads/files without auth is blocked (401)', async () => {
// /uploads/files is blocked without auth; /uploads/avatars and /uploads/covers are public static
const res = await request(app).get('/uploads/files/nonexistent.txt');
expect(res.status).toBe(401);
});
});
describe('Force HTTPS redirect', () => {
it('MISC-004 — FORCE_HTTPS redirect sends 301 for HTTP requests on non-health paths', async () => {
// applyGlobalMiddleware reads FORCE_HTTPS when buildApp() composes the app, so
// we need a fresh Nest instance built with the flag set.
process.env.FORCE_HTTPS = 'true';
let httpsApp: INestApplication | undefined;
try {
httpsApp = await buildApp();
const res = await request(httpsApp.getHttpAdapter().getInstance())
.get('/api/addons')
.set('X-Forwarded-Proto', 'http');
expect(res.status).toBe(301);
} finally {
if (httpsApp) await httpsApp.close();
delete process.env.FORCE_HTTPS;
}
});
it('MISC-008 — FORCE_HTTPS does not redirect /api/health (probes must reach it over HTTP)', async () => {
process.env.FORCE_HTTPS = 'true';
let httpsApp: INestApplication | undefined;
try {
httpsApp = await buildApp();
const res = await request(httpsApp.getHttpAdapter().getInstance())
.get('/api/health')
.set('X-Forwarded-Proto', 'http');
expect(res.status).toBe(200);
expect(res.body.status).toBe('ok');
} finally {
if (httpsApp) await httpsApp.close();
delete process.env.FORCE_HTTPS;
}
});
it('MISC-004 — no redirect when FORCE_HTTPS is not set', async () => {
delete process.env.FORCE_HTTPS;
const res = await request(app).get('/api/health').set('X-Forwarded-Proto', 'http');
expect(res.status).toBe(200);
});
});
describe('Request body ceiling', () => {
// The Express shell set '100kb' and stopped when the Nest instance took over
// parsing, which left the limit implicit. This pins it, so it cannot drift
// back to a framework default without somebody noticing.
it('MISC-009 — a body over 100kb is refused with 413', async () => {
const res = await request(app)
.post('/api/auth/login')
.set('Content-Type', 'application/json')
.send({ email: 'a@b.c', password: 'x'.repeat(200 * 1024) });
expect(res.status).toBe(413);
});
it('MISC-010 — a body under it still reaches the handler', async () => {
const res = await request(app)
.post('/api/auth/login')
.set('Content-Type', 'application/json')
.send({ email: 'a@b.c', password: 'x'.repeat(1024) });
expect(res.status).not.toBe(413);
});
// A list file may be a megabyte (#2198), and the three routes that carry one
// whole are measured against that instead (#2301): the import into a new
// list, the GPX reader, and the import into a list that already exists.
// Only those three.
it('MISC-011: a list file over 100kb reaches both imports and the GPX reader', async () => {
for (const route of [
'/api/addons/collections/import',
'/api/addons/collections/gpx/read',
'/api/addons/collections/7/import',
]) {
const res = await request(app)
.post(route)
.set('Content-Type', 'application/json')
.send({ gpx: 'x'.repeat(300 * 1024) });
expect(res.status, route).not.toBe(413);
}
});
it('MISC-012: a list file past twice the file limit is still refused, and no other route got the larger ceiling', async () => {
const huge = await request(app)
.post('/api/addons/collections/gpx/read')
.set('Content-Type', 'application/json')
.send({ gpx: 'x'.repeat(2 * 1024 * 1024 + 1) });
expect(huge.status).toBe(413);
const elsewhere = await request(app)
.post('/api/addons/collections')
.set('Content-Type', 'application/json')
.send({ name: 'x'.repeat(200 * 1024) });
expect(elsewhere.status).toBe(413);
});
// A tour is sent whole on create and edit, its line and every fixed leg's
// line besides: a ride along a horse route passes 100kb (HOR-01). Only those
// two routes take the larger ceiling, and only up to TOUR_WRITE_MAX_BYTES.
it('MISC-013: a tour create or edit over 100kb reaches the router', async () => {
const created = await request(app)
.post('/api/trips/5/tours')
.set('Content-Type', 'application/json')
.send({ name: 'x'.repeat(300 * 1024) });
expect(created.status).not.toBe(413);
const edited = await request(app)
.put('/api/trips/5/tours/9')
.set('Content-Type', 'application/json')
.send({ name: 'x'.repeat(300 * 1024) });
expect(edited.status).not.toBe(413);
});
it('MISC-014: a tour past its own ceiling is refused, and the other tour routes keep 100kb', async () => {
const huge = await request(app)
.post('/api/trips/5/tours')
.set('Content-Type', 'application/json')
.send({ name: 'x'.repeat(TOUR_WRITE_MAX_BYTES + 1) });
expect(huge.status).toBe(413);
const copy = await request(app)
.post('/api/trips/5/tours/9/copy')
.set('Content-Type', 'application/json')
.send({ name: 'x'.repeat(200 * 1024) });
expect(copy.status).toBe(413);
});
// The planner's unsaved draft carries the same points and fixed legs as the
// tour it becomes, so its write takes the tour ceiling as well.
it('MISC-015: a tour draft over 100kb reaches the router, one past the tour ceiling is refused', async () => {
const kept = await request(app)
.put('/api/trips/5/tour-draft')
.set('Content-Type', 'application/json')
.send({ payload: 'x'.repeat(300 * 1024) });
expect(kept.status).not.toBe(413);
const huge = await request(app)
.put('/api/trips/5/tour-draft')
.set('Content-Type', 'application/json')
.send({ payload: 'x'.repeat(TOUR_WRITE_MAX_BYTES + 1) });
expect(huge.status).toBe(413);
});
});