UNPKG

file-server

Version:

Simple http file server that supports files and directories

698 lines (519 loc) 20 kB
const test = require('tape'); const proxyquire = require('proxyquire'); const path = require('path'); const pathToObjectUnderTest = '../'; const FileServer = require(pathToObjectUnderTest); const testDate = new Date(); const testFileName = './foo.txt'; const testMimeType = 'bar'; const testMaxAge = 123; const testKey = 'bar'; const testRootDirectory = './files'; const testRequest = { url: "/bar/foo.txt", headers: { 'accept-encoding': 'foo gzip bar', }, }; const testResponse = { setHeader: () => {}, removeHeader: () => {}, on: () => {}, }; const testError = { code: 404, message: `404: Not Found ${testFileName}`, }; function createErrorCallback(t, expectedError = testError, expectedResponse = testResponse) { return function errorCallback(error, request, response) { t.deepEqual(error, expectedError, 'got correct error'); t.equal(request, testRequest, 'got correct request'); t.equal(response, expectedResponse, 'got correct response'); }; } function getBaseMocks() { return { 'graceful-fs': { stat: (fileName, callback) => { callback(~fileName.indexOf('.gz'), { isFile: function() { return true; }, mtime: testDate, }); }, }, hashr: { hash: value => value }, 'stream-catcher': function() { this.read = () => {}; this.write = () => {}; }, chokidar: { watch: function() { return { on: () => {}, close: () => Promise.resolve(), }; }, }, }; } test('FileServer is a function', t => { t.plan(1); t.equal(typeof FileServer, 'function', 'FileServer is a function'); }); test('FileServer requires a callback', t => { t.plan(1); t.throws(() => { new FileServer(); }, 'FileServer throws if no callback'); }); test('FileServer constructs a cache', t => { t.plan(4); const expectedMax = 1024 * 1000; const lengthTest = { length: 'foo', }; const testCache = {}; const mocks = getBaseMocks(); mocks['stream-catcher'] = function(options) { t.equal(options.max, expectedMax, 'cache max set correctly'); t.equal(typeof options.length, 'function', 'length is a function'); t.equal(options.length(lengthTest), 'foo', 'length function gets length property'); return testCache; }; const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); t.equal(fileServer.cache, testCache, 'cache was set'); }); test('FileServer constructs a cache with custom size', t => { t.plan(1); const expectedMax = 123; const mocks = getBaseMocks(); mocks['stream-catcher'] = function(options) { t.equal(options.max, expectedMax, 'cache max set correctly'); }; const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); new MockFileServer(() => {}, expectedMax); }); test('FileServer sets and flips the error callback', t => { t.plan(3); const mocks = getBaseMocks(); const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer((error, request, response) => { t.equal(error, 3, 'error is correct'); t.equal(request, 1, 'request is correct'); t.equal(response, 2, 'response is correct'); }); fileServer.errorCallback(1, 2, 3); }); test('serveFile is a function', t => { t.plan(1); const mocks = getBaseMocks(); const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); t.equal(typeof fileServer.serveFile, 'function', 'serveFile is a function'); }); test('serveFile requires a fileName', t => { t.plan(1); const mocks = getBaseMocks(); const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); t.throws(() => { fileServer.serveFile(); }, 'serveFile throws if no fileName'); }); test('serveFile returns a function', t => { t.plan(1); const mocks = getBaseMocks(); const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); t.equal(typeof fileServer.serveFile('./foo'), 'function', 'serveFile returns a function'); }); test('serveFile returns full error on generic stats error', t => { t.plan(3); const mocks = getBaseMocks(); mocks['graceful-fs'].stat = (fileName, callback) => callback(testError); const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(createErrorCallback(t)); const serveFile = fileServer.serveFile(testFileName); serveFile(testRequest, testResponse); }); test('serveFile 404s on file stat ENOENT error', t => { t.plan(3); const mocks = getBaseMocks(); mocks['graceful-fs'].stat = (fileName, callback) => callback({ message: 'ENOENT' }); const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(createErrorCallback(t)); const serveFile = fileServer.serveFile(testFileName); serveFile(testRequest, testResponse); }); test('serveFile 404s on not isFile', t => { t.plan(3); const mocks = getBaseMocks(); mocks['graceful-fs'].stat = (fileName, callback) => { callback(~fileName.indexOf('.gz'), { isFile: function() { return false; }, }); }; const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(createErrorCallback(t)); const serveFile = fileServer.serveFile(testFileName); serveFile(testRequest, testResponse); }); test('serveFile sets headers and asks for file', t => { t.plan(10); const mocks = getBaseMocks(); const testResponse = { setHeader: (key, value) => { if (key === 'ETag') { t.equal(value, testFileName + testDate.getTime(), `got correct header value for ${key}`); return; } if (key === 'Cache-Control') { t.equal(value, `private, max-age=${testMaxAge}`, `got correct header value for ${key}`); return; } if (key === 'Content-Type') { t.equal(value, testMimeType, `got correct header value for ${key}`); return; } t.fail(`Set unexpected header key: ${key} value: ${value}`); }, on: (eventName, callback) => { t.equal(eventName, 'error', 'set error handeler'); callback(testError); }, }; mocks['stream-catcher'] = function() { this.write = function(fileName, response, createReadStream) { t.equal(fileName, testFileName, 'fileName is correct'); t.equal(response, testResponse, 'response is correct'); t.equal(typeof createReadStream, 'function', 'createReadStream is a function'); }; }; const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(createErrorCallback(t, undefined, testResponse)); const serveFile = fileServer.serveFile(testFileName, testMimeType, testMaxAge); serveFile(testRequest, testResponse); }); test('serveFile returns 304 if etag matches', t => { t.plan(4); const mocks = getBaseMocks(); const testRequest = { headers: { 'if-none-match': testFileName + testDate.getTime(), }, }; const testResponse = { setHeader: (key, value) => { if (key === 'ETag') { t.equal(value, testFileName + testDate.getTime(), `got correct header value for ${key}`); return; } if (key === 'Cache-Control') { t.equal(value, 'private, max-age=0', `got correct header value for ${key}`); return; } t.fail(`Set unexpected header key: ${key} value: ${value}`); }, writeHead: code => t.equal(code, 304, 'set 304 code correctly'), end: () => t.pass('end was called'), }; const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); const serveFile = fileServer.serveFile(testFileName); serveFile(testRequest, testResponse); }); test('serveFile passes create stream into cache', t => { t.plan(10); const mocks = getBaseMocks(); const testReadStream = { on: function(eventName, callback) { t.equal(eventName, 'error', 'set error handeler'); callback(testError); }, }; mocks['graceful-fs'] = { stat: function(fileName, callback) { callback(~fileName.indexOf('.gz'), { isFile: function() { return true; }, mtime: testDate, }); }, createReadStream: function(key) { t.equal(key, testKey, 'key is correct'); return testReadStream; }, }; mocks['stream-catcher'] = function() { this.write = function(fileName, response, createReadStream) { t.equal(fileName, testFileName, 'fileName is correct'); t.equal(response, testResponse, 'response is correct'); t.equal(typeof createReadStream, 'function', 'createReadStream is a function'); createReadStream(testKey); }; this.read = function(key, readStream) { t.equal(key, testKey, 'key is correct'); t.equal(readStream, testReadStream, 'readStream is correct'); }; }; const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(createErrorCallback(t)); const serveFile = fileServer.serveFile(testFileName); serveFile(testRequest, testResponse); }); test('serveFile watches files only once with chokidar', t => { t.plan(4); const mocks = getBaseMocks(); const expectedOptions = { persistent: true, ignoreInitial: true }; mocks.chokidar.watch = (fileName, options) => { t.equal(fileName, testFileName, 'got correct fileName'); t.deepEqual(options, expectedOptions, 'got correct options'); return { on: function(event, callback) { t.equal(event, 'change', 'got correct event'); callback(); }, }; }; mocks['stream-catcher'] = function() { this.del = fileName => t.equal(fileName, testFileName, 'deleted correct fileName'); }; const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); fileServer.serveFile(testFileName); fileServer.serveFile(testFileName); }); test('process on exit cleans up watches', t => { t.plan(1); const mocks = getBaseMocks(); const oldProcessOn = process.on; let MockFileServer; mocks.chokidar.watch = () => ({ on: (event, callback) => callback(), close: () => t.pass('closed watcher'), }); mocks['stream-catcher'] = function() { this.del = () => {}; }; process.on = function(event, callback) { setTimeout(() => { const fileServer = new MockFileServer(() => {}); fileServer.serveFile(testFileName); callback(); oldProcessOn.call(process, event, callback); }, 0); process.on = oldProcessOn; }; MockFileServer = proxyquire(pathToObjectUnderTest, mocks); }); test('serveFile checks for .gz file if gzip supported but uses original if not available', t => { t.plan(3); const mocks = getBaseMocks(); mocks['graceful-fs'] = { stat: (fileName, callback) => { callback(~fileName.indexOf('.gz'), { isFile: function() { return true; }, mtime: testDate, }); }, createReadStream: () => {}, }; mocks['stream-catcher'] = function() { this.write = function(fileName, response, createReadStream) { t.equal(fileName, testFileName, 'fileName is correct'); t.equal(response, testResponse, 'response is correct'); t.equal(typeof createReadStream, 'function', 'createReadStream is a function'); }; }; const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); const serveFile = fileServer.serveFile(testFileName); serveFile(testRequest, testResponse); }); test('serveFile uses .gz file if gzip supported and exists', t => { t.plan(3); const mocks = getBaseMocks(); mocks['graceful-fs'] = { stat: function(fileName, callback) { callback(!~fileName.indexOf('.gz'), { isFile: function() { return true; }, mtime: testDate, }); }, createReadStream: () => {}, }; mocks['stream-catcher'] = function() { this.write = function(fileName, response, createReadStream) { t.equal(fileName, `${testFileName}.gz`, 'fileName is correct'); t.equal(response, testResponse, 'response is correct'); t.equal(typeof createReadStream, 'function', 'createReadStream is a function'); }; }; const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); const serveFile = fileServer.serveFile(testFileName); serveFile(testRequest, testResponse); }); test('serveDirectory is a function', t => { t.plan(1); const mocks = getBaseMocks(); const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); t.equal(typeof fileServer.serveDirectory, 'function', 'serveDirectory is a function'); }); test('serveDirectory requires a rootDirectory', t => { t.plan(1); const mocks = getBaseMocks(); const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); t.throws(() => { fileServer.serveDirectory(); }, 'serveDirectory throws if no rootDirectory'); }); test('serveDirectory requires a mimeTypes object', t => { t.plan(1); const mocks = getBaseMocks(); const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); t.throws(() => { fileServer.serveDirectory('./files'); }, 'serveDirectory throws if no mimeTypes'); }); test('serveDirectory mimeTypes must start with a .', t => { t.plan(1); const mocks = getBaseMocks(); const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); t.throws(() => { fileServer.serveDirectory( './files', { '.foo': 'bar', meh: 'stuff', }, 123, ); }, 'serveDirectory throws if no mimeTypes'); }); test('serveDirectory returns a function', t => { t.plan(1); const mocks = getBaseMocks(); const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); t.equal(typeof fileServer.serveDirectory('./files', {}), 'function', 'serveDirectory returns a function'); }); test('serveDirectory 404s if mimetype mismatch', t => { t.plan(3); const testFile = './foo.bar'; const expectedError = { code: 404, message: `404: Not Found ${path.join(testRootDirectory, testFile)}` }; const mocks = getBaseMocks(); const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(createErrorCallback(t, expectedError)); const serveDirectory = fileServer.serveDirectory(testRootDirectory, { '.txt': 'text/plain', }); serveDirectory(testRequest, testResponse, testFile); }); test('serveDirectory 404s if try to navigate up a level', t => { t.plan(3); const testFile = '../../foo.txt'; const expectedError = { code: 404, message: `404: Not Found ${testFile}` }; const mocks = getBaseMocks(); const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(createErrorCallback(t, expectedError)); const serveDirectory = fileServer.serveDirectory(testRootDirectory, { '.txt': 'text/plain', }); serveDirectory(testRequest, testResponse, testFile); }); test('serveDirectory calls serveFile', t => { t.plan(5); const testFile = './bar/foo.txt'; const mocks = getBaseMocks(); const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); const serveDirectory = fileServer.serveDirectory( testRootDirectory, { '.txt': 'text/majigger', }, testMaxAge, ); fileServer.serveFile = function(fileName, mimeType, maxAge) { t.equal(fileName, path.join(testRootDirectory, testFile), 'fileName is correct'); t.equal(mimeType, 'text/majigger', 'mimeType is correct'); t.equal(maxAge, testMaxAge, 'maxAge is correct'); return function(request, response) { t.equal(request, testRequest, 'request is correct'); t.equal(response, testResponse, 'response is correct'); }; }; serveDirectory(testRequest, testResponse, testFile); }); test('serveDirectory calls serveFile with filename retrieved from url', t => { t.plan(5); const testFile = './bar/foo.txt'; const mocks = getBaseMocks(); const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); const serveDirectory = fileServer.serveDirectory( testRootDirectory, { '.txt': 'text/majigger', }, testMaxAge, ); fileServer.serveFile = function(fileName, mimeType, maxAge) { t.equal(fileName, path.join(testRootDirectory, testFile), 'fileName is correct'); t.equal(mimeType, 'text/majigger', 'mimeType is correct'); t.equal(maxAge, testMaxAge, 'maxAge is correct'); return function(request, response) { t.equal(request, testRequest, 'request is correct'); t.equal(response, testResponse, 'response is correct'); }; }; serveDirectory(testRequest, testResponse); }); test('close terminates all file watchers', t => { t.plan(3); let closedConnections = 0; const mocks = getBaseMocks(); mocks.chokidar.watch = () => { return { on: () => {}, close: () => { closedConnections++; Promise.resolve(); }, }; }; const MockFileServer = proxyquire(pathToObjectUnderTest, mocks); const fileServer = new MockFileServer(() => {}); // Observe 2 files via each method const serveDirectory = fileServer.serveDirectory( testRootDirectory, { '.txt': 'text/majigger', }, testMaxAge, ); const serveFile = fileServer.serveFile(testFileName); // Watchers start on first request serveDirectory(testRequest, testResponse); serveFile(testRequest, testResponse); t.equal(Object.keys(fileServer.watchers).length, 2); fileServer.close(() => { t.equal(closedConnections, 2); t.equal(Object.keys(fileServer.watchers).length, 0); }); });