mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
177 lines
6.1 KiB
TypeScript
177 lines
6.1 KiB
TypeScript
/**
|
|
* Miscellaneous integration tests.
|
|
* Covers MISC-001, 002, 004, 007, 008, 013, 015.
|
|
*/
|
|
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
|
import request from 'supertest';
|
|
import type { Application } from 'express';
|
|
import type { INestApplication } from '@nestjs/common';
|
|
|
|
vi.mock('../../src/db/database', async () => {
|
|
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
|
return buildDbMock(createSnapshotTestDb());
|
|
});
|
|
vi.mock('../../src/config', () => ({
|
|
JWT_SECRET: 'test-jwt-secret-for-trek-testing-only',
|
|
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
|
|
updateJwtSecret: () => {},
|
|
SESSION_DURATION: '24h',
|
|
SESSION_DURATION_MS: 86400000,
|
|
SESSION_DURATION_SECONDS: 86400,
|
|
DEFAULT_LANGUAGE: 'en',
|
|
}));
|
|
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
|
|
|
import { db as testDb } from '../../src/db/database';
|
|
import { buildApp } from '../../src/bootstrap';
|
|
import { resetTestDb, resetRateLimits } from '../helpers/test-db';
|
|
import { createUser } from '../helpers/factories';
|
|
import { authCookie } from '../helpers/auth';
|
|
|
|
let nestApp: INestApplication;
|
|
let app: Application;
|
|
|
|
beforeAll(async () => {
|
|
nestApp = await buildApp();
|
|
app = nestApp.getHttpAdapter().getInstance();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
resetTestDb(testDb);
|
|
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);
|
|
});
|
|
});
|