698 lines
19 KiB
JavaScript
698 lines
19 KiB
JavaScript
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);
|
|
});
|
|
|
|
}); |