UNPKG

orator

Version:

Unopinionated API http server abstraction - REST or IPC

1,357 lines (1,236 loc) 45.4 kB
/** * Unit tests for Orator Static File Serving * * @license MIT * * @author Steven Velozo <steven@velozo.com> */ const libOrator = require('../source/Orator.js'); const Chai = require("chai"); const Expect = Chai.expect; const Assert = Chai.assert; const libFable = require('fable'); const libPath = require('path'); const libHTTP = require('http'); const defaultFableSettings = ( { Product:'Orator-StaticServingTests', ProductVersion: '0.0.0', APIServerPort: 0 }); const _StaticContentPath = libPath.normalize(__dirname + '/static_content/'); // Port counter for test HTTP servers to avoid collisions let _NextTestPort = 19100; function getNextTestPort() { return _NextTestPort++; } /** * Create a lightweight HTTP server that routes GET requests through Orator's IPC router. * This allows testing static file serving (which requires a real HTTP response stream) * without needing the orator-serviceserver-restify dependency. * * @param {Object} pOrator - The Orator instance with routes already registered. * @param {number} pPort - The port to listen on. * @param {Function} fCallback - Called with (server, port) when the server is ready. */ function createTestHTTPServer(pOrator, pPort, fCallback) { let tmpServer = libHTTP.createServer( (pRequest, pResponse) => { let tmpHandler = pOrator.serviceServer.router.find(pRequest.method, pRequest.url); if (tmpHandler) { pRequest.params = tmpHandler.params || {}; pRequest.searchParams = tmpHandler.searchParams || {}; tmpHandler.handler(pRequest, pResponse, null); } else { pResponse.writeHead(404); pResponse.end('Not found'); } }); tmpServer.listen(pPort, () => { return fCallback(tmpServer, tmpServer.address().port); }); } /** * Make an HTTP GET request and collect the response. * * @param {number} pPort - Port to connect to. * @param {string} pPath - URL path to request. * @param {Function} fCallback - Called with (error, statusCode, headers, body). * @param {Object} [pHeaders] - Optional extra headers for the request. */ function makeRequest(pPort, pPath, fCallback, pHeaders) { let tmpOptions = ( { hostname: 'localhost', port: pPort, path: pPath, method: 'GET', headers: Object.assign({}, pHeaders || {}) }); let tmpRequest = libHTTP.request(tmpOptions, (pResponse) => { let tmpData = ''; pResponse.on('data', (pChunk) => { tmpData += pChunk; }); pResponse.on('end', () => { return fCallback(null, pResponse.statusCode, pResponse.headers, tmpData); }); }); tmpRequest.on('error', (pError) => { return fCallback(pError); }); tmpRequest.end(); } suite ( 'Orator', () => { suite ( 'Static File Serving - Parameter Validation', () => { test ( 'addStaticRoute should return false when no file path is provided', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { let tmpResult = tmpOrator.addStaticRoute(); Expect(tmpResult).to.equal(false); tmpOrator.log.info('addStaticRoute correctly rejected missing file path'); return fDone(); }); } ); test ( 'addStaticRoute should reject non-string file paths', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { Expect(tmpOrator.addStaticRoute(42)).to.equal(false); Expect(tmpOrator.addStaticRoute(null)).to.equal(false); Expect(tmpOrator.addStaticRoute({})).to.equal(false); Expect(tmpOrator.addStaticRoute(true)).to.equal(false); Expect(tmpOrator.addStaticRoute(undefined)).to.equal(false); Expect(tmpOrator.addStaticRoute([])).to.equal(false); tmpOrator.log.info('addStaticRoute rejected all non-string file paths'); return fDone(); }); } ); test ( 'addStaticRoute should return true with a valid file path', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { let tmpResult = tmpOrator.addStaticRoute(_StaticContentPath); Expect(tmpResult).to.equal(true); tmpOrator.log.info(`addStaticRoute mapped [${_StaticContentPath}] successfully`); return fDone(); }); } ); test ( 'addStaticRoute should accept all optional parameters', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { let tmpResult = tmpOrator.addStaticRoute(_StaticContentPath, 'about.html', '/content/*', '/content/', {maxAge: '1d'}); Expect(tmpResult).to.equal(true); tmpOrator.log.info('addStaticRoute accepted all optional parameters'); return fDone(); }); } ); test ( 'addStaticRoute should use default route /* when no route is specified', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); let tmpResult = tmpOrator.addStaticRoute(_StaticContentPath); Expect(tmpResult).to.equal(true); // Verify the wildcard route is registered by checking the router let tmpHandler = tmpOrator.serviceServer.router.find('GET', '/anything'); Expect(tmpHandler).to.not.equal(null); Expect(tmpHandler.handler).to.be.a('function'); tmpOrator.log.info('Default wildcard route /* was registered'); return fDone(); } ); test ( 'addStaticRoute should register a GET route on the service server', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); let tmpResult = tmpOrator.addStaticRoute(_StaticContentPath, 'index.html', '/static/*', '/static/'); Expect(tmpResult).to.equal(true); // The route should match paths under /static/ let tmpHandler = tmpOrator.serviceServer.router.find('GET', '/static/test.html'); Expect(tmpHandler).to.not.equal(null); Expect(tmpHandler.handler).to.be.a('function'); tmpOrator.log.info('Custom route /static/* was registered'); return fDone(); } ); } ); suite ( 'Static File Serving - MIME Type Detection', () => { test ( 'setMimeHeader should set Content-Type for HTML files', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { // Trigger auto-registration of OratorStaticServer tmpOrator.addStaticRoute(_StaticContentPath); let tmpStaticServer = tmpFable.OratorStaticServer; let tmpCapturedHeaders = {}; let tmpMockResponse = { setHeader: function(pName, pValue) { tmpCapturedHeaders[pName] = pValue; } }; tmpStaticServer.setMimeHeader('index.html', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('text/html'); tmpStaticServer.setMimeHeader('/path/to/page.html', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('text/html'); tmpOrator.log.info('HTML MIME type correctly detected'); return fDone(); }); } ); test ( 'setMimeHeader should set Content-Type for CSS files', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { tmpOrator.addStaticRoute(_StaticContentPath); let tmpStaticServer = tmpFable.OratorStaticServer; let tmpCapturedHeaders = {}; let tmpMockResponse = { setHeader: function(pName, pValue) { tmpCapturedHeaders[pName] = pValue; } }; tmpStaticServer.setMimeHeader('style.css', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('text/css'); tmpOrator.log.info('CSS MIME type correctly detected'); return fDone(); }); } ); test ( 'setMimeHeader should set Content-Type for JSON files', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { tmpOrator.addStaticRoute(_StaticContentPath); let tmpStaticServer = tmpFable.OratorStaticServer; let tmpCapturedHeaders = {}; let tmpMockResponse = { setHeader: function(pName, pValue) { tmpCapturedHeaders[pName] = pValue; } }; tmpStaticServer.setMimeHeader('data.json', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('application/json'); tmpOrator.log.info('JSON MIME type correctly detected'); return fDone(); }); } ); test ( 'setMimeHeader should set Content-Type for JavaScript files', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { tmpOrator.addStaticRoute(_StaticContentPath); let tmpStaticServer = tmpFable.OratorStaticServer; let tmpCapturedHeaders = {}; let tmpMockResponse = { setHeader: function(pName, pValue) { tmpCapturedHeaders[pName] = pValue; } }; tmpStaticServer.setMimeHeader('app.js', tmpMockResponse); // mime v4 returns the IANA-canonical 'text/javascript'; earlier versions returned 'application/javascript'. Expect(tmpCapturedHeaders['Content-Type']).to.be.oneOf(['text/javascript', 'application/javascript']); tmpOrator.log.info('JavaScript MIME type correctly detected'); return fDone(); }); } ); test ( 'setMimeHeader should set Content-Type for image files', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { tmpOrator.addStaticRoute(_StaticContentPath); let tmpStaticServer = tmpFable.OratorStaticServer; let tmpCapturedHeaders = {}; let tmpMockResponse = { setHeader: function(pName, pValue) { tmpCapturedHeaders[pName] = pValue; } }; tmpStaticServer.setMimeHeader('photo.png', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('image/png'); tmpStaticServer.setMimeHeader('logo.jpg', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('image/jpeg'); tmpStaticServer.setMimeHeader('icon.gif', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('image/gif'); tmpStaticServer.setMimeHeader('vector.svg', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('image/svg+xml'); tmpOrator.log.info('Image MIME types correctly detected'); return fDone(); }); } ); test ( 'setMimeHeader should set Content-Type for common web font and document types', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { tmpOrator.addStaticRoute(_StaticContentPath); let tmpStaticServer = tmpFable.OratorStaticServer; let tmpCapturedHeaders = {}; let tmpMockResponse = { setHeader: function(pName, pValue) { tmpCapturedHeaders[pName] = pValue; } }; tmpStaticServer.setMimeHeader('document.pdf', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('application/pdf'); tmpStaticServer.setMimeHeader('archive.zip', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('application/zip'); tmpStaticServer.setMimeHeader('data.xml', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('application/xml'); tmpStaticServer.setMimeHeader('readme.txt', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('text/plain'); tmpOrator.log.info('Document and archive MIME types correctly detected'); return fDone(); }); } ); test ( 'setMimeHeader should fall back to application/octet-stream for unknown extensions', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { tmpOrator.addStaticRoute(_StaticContentPath); let tmpStaticServer = tmpFable.OratorStaticServer; let tmpCapturedHeaders = {}; let tmpMockResponse = { setHeader: function(pName, pValue) { tmpCapturedHeaders[pName] = pValue; } }; tmpStaticServer.setMimeHeader('mystery.xyz123', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('application/octet-stream'); tmpStaticServer.setMimeHeader('noextension', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('application/octet-stream'); tmpOrator.log.info('Unknown MIME types fall back to application/octet-stream'); return fDone(); }); } ); test ( 'setMimeHeader should handle file paths with directories', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { tmpOrator.addStaticRoute(_StaticContentPath); let tmpStaticServer = tmpFable.OratorStaticServer; let tmpCapturedHeaders = {}; let tmpMockResponse = { setHeader: function(pName, pValue) { tmpCapturedHeaders[pName] = pValue; } }; tmpStaticServer.setMimeHeader('/assets/css/main.css', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('text/css'); tmpStaticServer.setMimeHeader('/deep/nested/path/to/image.png', tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('image/png'); tmpOrator.log.info('MIME type detected correctly from paths with directories'); return fDone(); }); } ); } ); suite ( 'Static File Serving - Serving HTML Files', () => { test ( 'should serve index.html with correct content and Content-Type', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/index.html', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); Expect(pHeaders['content-type']).to.contain('text/html'); Expect(pBody).to.contain('Test Index'); Expect(pBody).to.contain('Welcome to the test server'); tmpOrator.log.info(`Served index.html: status=${pStatusCode} content-type=${pHeaders['content-type']}`); pServer.close(); return fDone(); }); }); } ); test ( 'should serve about.html with correct content', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/about.html', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); Expect(pHeaders['content-type']).to.contain('text/html'); Expect(pBody).to.contain('About'); Expect(pBody).to.contain('About page content'); tmpOrator.log.info(`Served about.html: status=${pStatusCode}`); pServer.close(); return fDone(); }); }); } ); test ( 'should serve default file (index.html) when requesting the root path', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); // The Content-Type should be text/html even when requesting '/' (no extension) Expect(pHeaders['content-type']).to.contain('text/html'); Expect(pBody).to.contain('Test Index'); tmpOrator.log.info(`Root path served default index.html: status=${pStatusCode} content-type=${pHeaders['content-type']}`); pServer.close(); return fDone(); }); }); } ); test ( 'should serve a custom default file when specified', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); // Set about.html as the default file tmpOrator.addStaticRoute(_StaticContentPath, 'about.html'); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); // Even with a custom default file, Content-Type should be text/html Expect(pHeaders['content-type']).to.contain('text/html'); Expect(pBody).to.contain('About page content'); tmpOrator.log.info(`Custom default file about.html served: status=${pStatusCode} content-type=${pHeaders['content-type']}`); pServer.close(); return fDone(); }); }); } ); } ); suite ( 'Static File Serving - Directory MIME Type Resolution', () => { test ( 'should set text/html Content-Type for directory paths with html default file', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath, 'index.html', '/site/*', '/site/'); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/site/', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); // Directory path should resolve MIME from default file, not the bare '/' Expect(pHeaders['content-type']).to.contain('text/html'); Expect(pBody).to.contain('Test Index'); tmpOrator.log.info(`Directory path MIME resolved to text/html from default file`); pServer.close(); return fDone(); }); }); } ); test ( 'should set correct Content-Type for extensionless paths based on default file', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { // Trigger auto-registration of OratorStaticServer tmpOrator.addStaticRoute(_StaticContentPath); let tmpStaticServer = tmpFable.OratorStaticServer; // Test the MIME detection logic directly with a mock response let tmpCapturedHeaders = {}; let tmpMockResponse = { setHeader: function(pName, pValue) { tmpCapturedHeaders[pName] = pValue; } }; // A URL like '/' has no extension, so should fall back to default // This simulates what happens in addStaticRoute after the fix let tmpUrl = '/'; let tmpMimeTarget = tmpUrl; if (tmpMimeTarget.endsWith('/') || tmpMimeTarget.indexOf('.') < 0) { tmpMimeTarget = 'index.html'; } tmpStaticServer.setMimeHeader(tmpMimeTarget, tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('text/html'); // A URL like '/somepath' (no extension, no slash) should also fall back tmpUrl = '/somepath'; tmpMimeTarget = tmpUrl; if (tmpMimeTarget.endsWith('/') || tmpMimeTarget.indexOf('.') < 0) { tmpMimeTarget = 'index.html'; } tmpStaticServer.setMimeHeader(tmpMimeTarget, tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('text/html'); // A URL with an extension should use its own extension tmpUrl = '/style.css'; tmpMimeTarget = tmpUrl; if (tmpMimeTarget.endsWith('/') || tmpMimeTarget.indexOf('.') < 0) { tmpMimeTarget = 'index.html'; } tmpStaticServer.setMimeHeader(tmpMimeTarget, tmpMockResponse); Expect(tmpCapturedHeaders['Content-Type']).to.equal('text/css'); tmpOrator.log.info('Directory and extensionless MIME detection verified'); return fDone(); }); } ); } ); suite ( 'Static File Serving - FilePersistence Auto-Instantiation', () => { test ( 'addStaticRoute should auto-instantiate FilePersistence if not present', (fDone) => { let tmpFable = new libFable(defaultFableSettings); // Verify FilePersistence is NOT yet on fable before Orator construction Expect(tmpFable.FilePersistence).to.equal(undefined); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { // FilePersistence is auto-instantiated by OratorStaticServer.addStaticRoute tmpOrator.addStaticRoute(_StaticContentPath); Expect(tmpFable.FilePersistence).to.be.an('object'); Expect(tmpFable.FilePersistence.libFS).to.be.an('object'); Expect(tmpFable.FilePersistence.libFS.existsSync).to.be.a('function'); tmpOrator.log.info('FilePersistence auto-instantiated by addStaticRoute'); return fDone(); }); } ); test ( 'addStaticRoute should not re-instantiate FilePersistence if already present', (fDone) => { let tmpFable = new libFable(defaultFableSettings); // Pre-instantiate FilePersistence let tmpOriginal = tmpFable.serviceManager.instantiateServiceProvider('FilePersistence'); Expect(tmpFable.FilePersistence).to.equal(tmpOriginal); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { tmpOrator.addStaticRoute(_StaticContentPath); // Should still be the same instance, not a new one Expect(tmpFable.FilePersistence).to.equal(tmpOriginal); tmpOrator.log.info('FilePersistence preserved when already present'); return fDone(); }); } ); } ); suite ( 'Static File Serving - CSS and JSON Files', () => { test ( 'should serve CSS files with correct Content-Type', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/style.css', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); Expect(pHeaders['content-type']).to.contain('text/css'); Expect(pBody).to.contain('font-family'); Expect(pBody).to.contain('sans-serif'); tmpOrator.log.info(`Served style.css: content-type=${pHeaders['content-type']}`); pServer.close(); return fDone(); }); }); } ); test ( 'should serve JSON files with correct Content-Type and parseable content', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/data.json', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); Expect(pHeaders['content-type']).to.contain('application/json'); let tmpParsed = JSON.parse(pBody); Expect(tmpParsed).to.have.a.property('TestKey'); Expect(tmpParsed.TestKey).to.equal('TestValue'); Expect(tmpParsed.Numbers).to.be.an('array'); Expect(tmpParsed.Numbers).to.have.lengthOf(3); tmpOrator.log.info(`Served data.json: parsed successfully with TestKey=${tmpParsed.TestKey}`); pServer.close(); return fDone(); }); }); } ); } ); suite ( 'Static File Serving - Route Stripping', () => { test ( 'should strip route prefix from URL before serving files', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); // Serve static content at /assets/* and strip /assets/ from the path tmpOrator.addStaticRoute(_StaticContentPath, 'index.html', '/assets/*', '/assets/'); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/assets/style.css', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); Expect(pHeaders['content-type']).to.contain('text/css'); Expect(pBody).to.contain('font-family'); tmpOrator.log.info(`Route stripping: /assets/style.css served correctly with content-type=${pHeaders['content-type']}`); pServer.close(); return fDone(); }); }); } ); test ( 'should serve the default file at the stripped route root', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath, 'index.html', '/docs/*', '/docs/'); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/docs/', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); Expect(pBody).to.contain('Test Index'); tmpOrator.log.info(`Route root /docs/ served default index.html`); pServer.close(); return fDone(); }); }); } ); test ( 'should serve JSON through a stripped route', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath, 'index.html', '/api/static/*', '/api/static/'); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/api/static/data.json', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); Expect(pHeaders['content-type']).to.contain('application/json'); let tmpParsed = JSON.parse(pBody); Expect(tmpParsed.TestKey).to.equal('TestValue'); tmpOrator.log.info(`Served data.json through stripped route /api/static/data.json`); pServer.close(); return fDone(); }); }); } ); } ); suite ( 'Static File Serving - Query String Handling', () => { test ( 'should strip query strings from URLs before serving files', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/style.css?v=1.0.0&bust=true', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); Expect(pHeaders['content-type']).to.contain('text/css'); Expect(pBody).to.contain('font-family'); tmpOrator.log.info('Query string was stripped and file served correctly'); pServer.close(); return fDone(); }); }); } ); } ); suite ( 'Static File Serving - Missing Files', () => { test ( 'should return 404 for a file that does not exist', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/nonexistent.html', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(404); tmpOrator.log.info(`Missing file correctly returned 404`); pServer.close(); return fDone(); }); }); } ); test ( 'should return 404 for a path traversal attempt', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath, 'index.html', '/safe/*', '/safe/'); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/safe/../../../etc/passwd', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); // serve-static should prevent path traversal Expect(pStatusCode).to.be.oneOf([400, 403, 404]); tmpOrator.log.info(`Path traversal attempt correctly blocked with status ${pStatusCode}`); pServer.close(); return fDone(); }); }); } ); } ); suite ( 'Static File Serving - Response Headers', () => { test ( 'should include standard caching headers in responses', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/index.html', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); // serve-static sets these headers by default Expect(pHeaders).to.have.a.property('etag'); Expect(pHeaders).to.have.a.property('last-modified'); Expect(pHeaders).to.have.a.property('content-length'); Expect(pHeaders).to.have.a.property('accept-ranges'); tmpOrator.log.info(`Standard headers present: etag=${pHeaders['etag']} last-modified=${pHeaders['last-modified']}`); pServer.close(); return fDone(); }); }); } ); test ( 'should pass custom serve-static params through to the library', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); // Pass maxAge as a custom parameter to serve-static tmpOrator.addStaticRoute(_StaticContentPath, 'index.html', '/*', '/', {maxAge: 86400000}); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/index.html', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); // maxAge should be reflected in the Cache-Control header Expect(pHeaders['cache-control']).to.contain('max-age=86400'); tmpOrator.log.info(`Custom params applied: cache-control=${pHeaders['cache-control']}`); pServer.close(); return fDone(); }); }); } ); } ); suite ( 'Static File Serving - Subdomain Magic Subfolder Routing', () => { test ( 'should serve from a subfolder when the hostname matches an existing subfolder', (fDone) => { let tmpFable = new libFable(defaultFableSettings); // FilePersistence is now auto-instantiated by OratorStaticServer.addStaticRoute tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { // Request with host 'subsite.example.com' -- the 'subsite' prefix matches the subfolder makeRequest(pActualPort, '/', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); // Should serve from static_content/subsite/index.html Expect(pBody).to.contain('Subsite'); Expect(pBody).to.contain('Subsite index page'); tmpOrator.log.info(`Subdomain magic routing: host=subsite.example.com served subsite content`); pServer.close(); return fDone(); }, { 'Host': 'subsite.example.com' }); }); } ); test ( 'should serve from the root folder when the hostname does not match any subfolder', (fDone) => { let tmpFable = new libFable(defaultFableSettings); // FilePersistence is now auto-instantiated by OratorStaticServer.addStaticRoute tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { // Request with host 'nonexistent.example.com' -- no matching subfolder makeRequest(pActualPort, '/', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); // Should serve from static_content/index.html (root) Expect(pBody).to.contain('Test Index'); tmpOrator.log.info(`Non-matching subdomain served root content correctly`); pServer.close(); return fDone(); }, { 'Host': 'nonexistent.example.com' }); }); } ); test ( 'should serve from the root folder when hostname has only one segment (no dots)', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { // 'localhost' has no dots, so no subdomain magic should apply makeRequest(pActualPort, '/', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); Expect(pBody).to.contain('Test Index'); tmpOrator.log.info(`Single-segment hostname served root content`); pServer.close(); return fDone(); }, { 'Host': 'localhost' }); }); } ); } ); suite ( 'Static File Serving - Subsite Direct Access', () => { test ( 'should serve files from a subdirectory path directly', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); tmpOrator.addStaticRoute(_StaticContentPath); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { makeRequest(pActualPort, '/subsite/index.html', (pError, pStatusCode, pHeaders, pBody) => { Expect(pError).to.equal(null); Expect(pStatusCode).to.equal(200); Expect(pHeaders['content-type']).to.contain('text/html'); Expect(pBody).to.contain('Subsite'); tmpOrator.log.info(`Subsite direct path access: /subsite/index.html served correctly`); pServer.close(); return fDone(); }); }); } ); } ); suite ( 'Static File Serving - Multiple Static Routes', () => { test ( 'should be able to register multiple static routes on different paths', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.startService(); // Register the subsite as a separate static route let tmpSubsitePath = libPath.normalize(__dirname + '/static_content/subsite/'); let tmpResult1 = tmpOrator.addStaticRoute(_StaticContentPath, 'index.html', '/main/*', '/main/'); let tmpResult2 = tmpOrator.addStaticRoute(tmpSubsitePath, 'index.html', '/sub/*', '/sub/'); Expect(tmpResult1).to.equal(true); Expect(tmpResult2).to.equal(true); let tmpPort = getNextTestPort(); createTestHTTPServer(tmpOrator, tmpPort, (pServer, pActualPort) => { tmpFable.Utility.waterfall([ (fStageComplete) => { makeRequest(pActualPort, '/main/data.json', (pError, pStatusCode, pHeaders, pBody) => { Expect(pStatusCode).to.equal(200); let tmpParsed = JSON.parse(pBody); Expect(tmpParsed.TestKey).to.equal('TestValue'); tmpOrator.log.info('Route /main/ served data.json from main content'); return fStageComplete(); }); }, (fStageComplete) => { makeRequest(pActualPort, '/sub/', (pError, pStatusCode, pHeaders, pBody) => { Expect(pStatusCode).to.equal(200); Expect(pBody).to.contain('Subsite'); tmpOrator.log.info('Route /sub/ served index.html from subsite content'); return fStageComplete(); }); } ], (pError) => { pServer.close(); return fDone(); }); }); } ); } ); suite ( 'Static File Serving - oldLibMime Compatibility Flag', () => { test ( 'should correctly detect the mime library version', (fDone) => { let tmpFable = new libFable(defaultFableSettings); tmpFable.serviceManager.addServiceType('Orator', libOrator); let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {}); tmpOrator.initialize( () => { // Trigger auto-registration of OratorStaticServer tmpOrator.addStaticRoute(_StaticContentPath); let tmpStaticServer = tmpFable.OratorStaticServer; // The oldLibMime flag should be a boolean Expect(tmpStaticServer.oldLibMime).to.be.a('boolean'); tmpOrator.log.info(`oldLibMime flag is ${tmpStaticServer.oldLibMime}`); return fDone(); }); } ); } ); } );