import { readAppFile } from '../test-support/i18n.js'; import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import path from 'node:path'; import test from 'node:test'; const root = path.resolve(import.meta.dirname, '..'); const ignoredDirectories = new Set([ '.git', 'coverage', 'data', 'node_modules', 'storage' ]); async function read(relativePath) { return readAppFile(relativePath); } async function markdownFiles(directory = path.join(root, 'docs')) { const result = []; const entries = await fs.readdir(directory, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { if (ignoredDirectories.has(entry.name)) continue; result.push(...await markdownFiles(path.join(directory, entry.name))); continue; } if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.md')) continue; result.push(path.join(directory, entry.name)); } return result; } function routeDeclaration(source, method, endpoint) { const marker = `api.${method}('${endpoint}'`; const start = source.indexOf(marker); assert.notEqual(start, -1, `API route missing: ${method.toUpperCase()} ${endpoint}`); const end = source.indexOf('\n', start); return source.slice(start, end === -1 ? source.length : end); } function assertMiddleware(declaration, middleware) { assert.ok( declaration.includes(middleware), `Expected ${middleware} in route declaration: ${declaration}` ); } test('complete REST API documentation is kept outside the README', async () => { const files = await markdownFiles(); const readmePath = path.join(root, 'docs', 'README.md'); assert.ok(files.includes(readmePath), 'docs/README.md is missing'); const readme = await fs.readFile(readmePath, 'utf8'); const outsideReadme = files.filter(file => file !== readmePath); const candidates = []; for (const file of outsideReadme) { const content = await fs.readFile(file, 'utf8'); const endpointMentions = content.match(/\/api\/[a-z0-9_:/?=&.-]+/gi) || []; if (endpointMentions.length >= 5) candidates.push({ file, content, endpointMentions }); } assert.ok(candidates.length > 0, 'No standalone REST API documentation was found.'); const apiDocument = candidates.sort((a, b) => b.endpointMentions.length - a.endpointMentions.length)[0]; assert.match(apiDocument.content, /\/api\/health/); assert.match(apiDocument.content, /\/api\/routes/); assert.match(apiDocument.content, /pois/); assert.match(apiDocument.content, /pictures/); assert.match(apiDocument.content, /audio/); const readmeEndpointMentions = readme.match(/\/api\/[a-z0-9_:/?=&.-]+/gi) || []; assert.ok( apiDocument.endpointMentions.length > readmeEndpointMentions.length, `The standalone API documentation (${path.relative(root, apiDocument.file)}) must remain more complete than README.md.` ); }); test('Streckenpunkt media endpoints keep upload validation and release-write guards', async () => { const api = await read('src/routes/api.js'); const picturePost = routeDeclaration(api, 'post', '/routes/:routeId/pois/:poiId/pictures'); assertMiddleware(picturePost, "requireWritableRoute('routeId')"); assertMiddleware(picturePost, "upload.single('picture')"); assertMiddleware(picturePost, "inspectUpload('picture', 'picture'"); const picturePut = routeDeclaration(api, 'put', '/routes/:routeId/pois/:poiId/pictures/:pictureId'); assertMiddleware(picturePut, "requireWritableRoute('routeId')"); assertMiddleware(picturePut, "upload.single('picture')"); assertMiddleware(picturePut, "inspectUpload('picture', 'picture'"); const audioPost = routeDeclaration(api, 'post', '/routes/:routeId/pois/:poiId/audio'); assertMiddleware(audioPost, "requireWritableRoute('routeId')"); assertMiddleware(audioPost, "upload.single('audio')"); assertMiddleware(audioPost, "inspectUpload('audio', 'audio'"); const audioPut = routeDeclaration(api, 'put', '/routes/:routeId/pois/:poiId/audio'); assertMiddleware(audioPut, "requireWritableRoute('routeId')"); assertMiddleware(audioPut, "upload.single('audio')"); assertMiddleware(audioPut, "inspectUpload('audio', 'audio'"); const pictureDelete = routeDeclaration(api, 'delete', '/routes/:routeId/pois/:poiId/pictures/:pictureId'); assertMiddleware(pictureDelete, "requireRouteDeleteAllowed('routeId')"); const audioDelete = routeDeclaration(api, 'delete', '/routes/:routeId/pois/:poiId/audio'); assertMiddleware(audioDelete, "requireRouteDeleteAllowed('routeId')"); assert.doesNotMatch(api, /\/pois\/:poiId\/images/); });