#!/usr/bin/env node /** * Tests for markdown-novel-viewer * Run: node scripts/tests/server.test.cjs */ const fs = require('fs'); const path = require('path'); const http = require('http'); const { isPortAvailable, findAvailablePort, DEFAULT_PORT } = require('../lib/port-finder.cjs'); const { writePidFile, readPidFile, removePidFile, findRunningInstances } = require('../lib/process-mgr.cjs'); const { getMimeType, MIME_TYPES, isPathSafe, sanitizeErrorMessage } = require('../lib/http-server.cjs'); const { resolveImages, addHeadingIds, generateTOC, renderTOCHtml } = require('../lib/markdown-renderer.cjs'); const { detectPlan, parsePlanTable, getNavigationContext, generateNavSidebar } = require('../lib/plan-navigator.cjs'); // Test utilities let passed = 0; let failed = 0; function test(name, fn) { try { fn(); passed++; console.log(` ✓ ${name}`); } catch (err) { failed++; console.log(` ✗ ${name}`); console.log(` Error: ${err.message}`); } } function assertEqual(actual, expected, message) { if (actual !== expected) { throw new Error(`${message}: expected "${expected}", got "${actual}"`); } } function assertTrue(value, message) { if (!value) { throw new Error(`${message}: expected truthy value`); } } function assertFalse(value, message) { if (value) { throw new Error(`${message}: expected falsy value`); } } function assertIncludes(str, substr, message) { if (!str.includes(substr)) { throw new Error(`${message}: expected to include "${substr}"`); } } // Test suites console.log('\n--- Port Finder Tests ---'); test('DEFAULT_PORT is 3456', () => { assertEqual(DEFAULT_PORT, 3456, 'Default port'); }); test('isPortAvailable returns boolean', () => { // Sync test - function exists assertTrue(typeof isPortAvailable === 'function', 'Should be function'); }); test('findAvailablePort returns number', () => { // Sync test - actual async behavior tested in integration assertTrue(typeof findAvailablePort === 'function', 'Should be function'); }); console.log('\n--- Process Manager Tests ---'); test('writePidFile and readPidFile work correctly', () => { const testPort = 9876; const testPid = 12345; writePidFile(testPort, testPid); const readPid = readPidFile(testPort); assertEqual(readPid, testPid, 'PID should match'); removePidFile(testPort); const afterRemove = readPidFile(testPort); assertEqual(afterRemove, null, 'Should be null after remove'); }); test('findRunningInstances returns array', () => { const instances = findRunningInstances(); assertTrue(Array.isArray(instances), 'Should return array'); }); console.log('\n--- HTTP Server Tests ---'); test('getMimeType returns correct types', () => { assertEqual(getMimeType('test.html'), 'text/html', 'HTML type'); assertEqual(getMimeType('test.css'), 'text/css', 'CSS type'); assertEqual(getMimeType('test.js'), 'application/javascript', 'JS type'); assertEqual(getMimeType('test.png'), 'image/png', 'PNG type'); assertEqual(getMimeType('test.jpg'), 'image/jpeg', 'JPG type'); assertEqual(getMimeType('test.unknown'), 'application/octet-stream', 'Unknown type'); }); test('MIME_TYPES has common extensions', () => { assertTrue(MIME_TYPES['.html'], 'Has .html'); assertTrue(MIME_TYPES['.css'], 'Has .css'); assertTrue(MIME_TYPES['.js'], 'Has .js'); assertTrue(MIME_TYPES['.png'], 'Has .png'); assertTrue(MIME_TYPES['.md'], 'Has .md'); }); console.log('\n--- Security Tests ---'); test('isPathSafe blocks path traversal', () => { assertFalse(isPathSafe('/etc/../etc/passwd', ['/home']), 'Should block .. traversal'); assertFalse(isPathSafe('/path\0/file', ['/path']), 'Should block null bytes'); }); test('isPathSafe allows valid paths', () => { assertTrue(isPathSafe('/tmp/test.md', ['/tmp']), 'Should allow path in allowed dir'); }); test('sanitizeErrorMessage removes paths', () => { const sanitized = sanitizeErrorMessage('Error: /etc/passwd not found'); assertFalse(sanitized.includes('/etc/passwd'), 'Should not contain path'); assertIncludes(sanitized, '[path]', 'Should replace with placeholder'); }); console.log('\n--- Markdown Renderer Tests ---'); test('resolveImages converts relative paths', () => { const md = ''; const resolved = resolveImages(md, '/base/path'); assertIncludes(resolved, '/file/', 'Should include /file/ route'); // Path is URL-encoded; decode to verify base path is present assertIncludes(decodeURIComponent(resolved), '/base/path', 'Should include base path'); }); test('resolveImages preserves absolute URLs', () => { const md = ''; const resolved = resolveImages(md, '/base/path'); assertEqual(resolved, md, 'Should preserve absolute URL'); }); test('resolveImages handles reference-style definitions', () => { const md = '![Step 1 Initial]\n\n[Step 1 Initial]: ./screenshots/step1.png'; const resolved = resolveImages(md, '/base/path'); assertIncludes(resolved, '/file/', 'Should include /file/ route in ref definition'); // Path is URL-encoded; decode to verify resolved path assertIncludes(decodeURIComponent(resolved), '/base/path/screenshots/step1.png', 'Should resolve relative path'); }); test('resolveImages handles reference-style with titles', () => { const md = '[logo]: ./images/logo.png "Company Logo"'; const resolved = resolveImages(md, '/project'); // Path is URL-encoded; decode to verify assertIncludes(decodeURIComponent(resolved), '/project/images/logo.png', 'Should resolve path with title'); }); test('resolveImages handles inline images with titles', () => { const md = ''; const resolved = resolveImages(md, '/base'); // Path is URL-encoded; decode to verify assertIncludes(decodeURIComponent(resolved), '/base/image.png', 'Should resolve inline with title'); }); test('addHeadingIds adds id attributes', () => { const html = '