'use strict' /* eslint node/no-deprecated-api: "off" */ const fs = require('fs') const path = require('path') const t = require('tap') const simple = require('simple-get') const Fastify = require('fastify') const fastifyStatic = require('..') const helper = { arrange: function (t, options, f) { return helper.arrangeModule(t, options, fastifyStatic, f) }, arrangeModule: function (t, options, mock, f) { const fastify = Fastify() fastify.register(mock, options) t.teardown(fastify.close.bind(fastify)) fastify.listen(0, err => { t.error(err) fastify.server.unref() f('http://localhost:' + fastify.server.address().port) }) return f } } try { fs.mkdirSync(path.join(__dirname, 'static/shallow/empty')) } catch (error) {} t.test('dir list wrong options', t => { t.plan(3) const cases = [ { options: { root: path.join(__dirname, '/static'), prefix: '/public', list: { format: 'no-json,no-html' } }, error: new TypeError('The `list.format` option must be json or html') }, { options: { root: path.join(__dirname, '/static'), list: { format: 'html' // no render function } }, error: new TypeError('The `list.render` option must be a function and is required with html format') }, { options: { root: path.join(__dirname, '/static'), list: { names: 'not-an-array' } }, error: new TypeError('The `list.names` option must be an array') } ] for (const case_ of cases) { const fastify = Fastify() fastify.register(fastifyStatic, case_.options) fastify.listen(0, err => { t.equal(err.message, case_.error.message) fastify.server.unref() }) } }) t.test('dir list default options', t => { t.plan(2) const options = { root: path.join(__dirname, '/static'), prefix: '/public', list: true } const route = '/public/shallow' const content = { dirs: ['empty'], files: ['sample.jpg'] } helper.arrange(t, options, (url) => { t.test(route, t => { t.plan(3) simple.concat({ method: 'GET', url: url + route }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(body.toString(), JSON.stringify(content)) }) }) }) }) t.test('dir list, custom options', t => { t.plan(2) const options = { root: path.join(__dirname, '/static'), prefix: '/public', index: false, list: true } const route = '/public/' const content = { dirs: ['deep', 'shallow'], files: ['.example', 'a .md', 'foo.html', 'foobar.html', 'index.css', 'index.html'] } helper.arrange(t, options, (url) => { t.test(route, t => { t.plan(3) simple.concat({ method: 'GET', url: url + route }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(body.toString(), JSON.stringify(content)) }) }) }) }) t.test('dir list html format', t => { t.plan(6) // render html in 2 ways: one with handlebars and one with template string const Handlebars = require('handlebars') const source = ` ` const handlebarTemplate = Handlebars.compile(source) const templates = [ { render: (dirs, files) => { return handlebarTemplate({ dirs, files }) }, output: ` ` }, { render: (dirs, files) => { return ` ` }, output: ` ` } ] for (const template of templates) { const options = { root: path.join(__dirname, '/static'), prefix: '/public', index: false, list: { format: 'html', names: ['index', 'index.htm'], render: template.render } } const routes = ['/public/index.htm', '/public/index'] // check all routes by names helper.arrange(t, options, (url) => { for (const route of routes) { t.test(route, t => { t.plan(3) simple.concat({ method: 'GET', url: url + route }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(body.toString(), template.output) }) }) } }) } }) t.test('dir list href nested structure', t => { t.plan(6) const options = { root: path.join(__dirname, '/static'), prefix: '/public', index: false, list: { format: 'html', names: ['index', 'index.htm'], render (dirs, files) { return dirs[0].href } } } const routes = [ { path: '/public/', response: '/public/deep' }, { path: '/public/index', response: '/public/deep' }, { path: '/public/deep/', response: '/public/deep/path' }, { path: '/public/deep/index.htm', response: '/public/deep/path' }, { path: '/public/deep/path/', response: '/public/deep/path/for' } ] helper.arrange(t, options, (url) => { for (const route of routes) { t.test(route.path, t => { t.plan(5) simple.concat({ method: 'GET', url: url + route.path }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(body.toString(), route.response) simple.concat({ method: 'GET', url: url + body.toString() }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) }) }) }) } }) }) t.test('dir list html format - stats', t => { t.plan(7) const options1 = { root: path.join(__dirname, '/static'), prefix: '/public', index: false, list: { format: 'html', render (dirs, files) { t.ok(dirs.length > 0) t.ok(files.length > 0) t.ok(dirs.every(every)) t.ok(files.every(every)) function every (value) { return value.stats && value.stats.atime && !value.extendedInfo } } } } const route = '/public/' helper.arrange(t, options1, (url) => { simple.concat({ method: 'GET', url: url + route }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) }) }) }) t.test('dir list html format - extended info', t => { t.plan(4) const route = '/public/' const options = { root: path.join(__dirname, '/static'), prefix: '/public', index: false, list: { format: 'html', extendedFolderInfo: true, render (dirs, files) { t.test('dirs', t => { t.plan(dirs.length * 7) for (const value of dirs) { t.ok(value.extendedInfo) t.equal(typeof value.extendedInfo.fileCount, 'number') t.equal(typeof value.extendedInfo.totalFileCount, 'number') t.equal(typeof value.extendedInfo.folderCount, 'number') t.equal(typeof value.extendedInfo.totalFolderCount, 'number') t.equal(typeof value.extendedInfo.totalSize, 'number') t.equal(typeof value.extendedInfo.lastModified, 'number') } }) } } } helper.arrange(t, options, (url) => { simple.concat({ method: 'GET', url: url + route }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) }) }) }) t.test('dir list json format', t => { t.plan(2) const options = { root: path.join(__dirname, '/static'), prefix: '/public', prefixAvoidTrailingSlash: true, list: { format: 'json', names: ['index', 'index.json', '/'] } } // const routes = ['/public/shallow', 'public/shallow/', '/public/shallow/index.json', '/public/shallow/index'] const routes = ['/public/shallow/'] const content = { dirs: ['empty'], files: ['sample.jpg'] } helper.arrange(t, options, (url) => { for (const route of routes) { t.test(route, t => { t.plan(3) simple.concat({ method: 'GET', url: url + route }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(body.toString(), JSON.stringify(content)) }) }) } }) }) t.test('dir list json format - extended info', t => { t.plan(2) const options = { root: path.join(__dirname, '/static'), prefix: '/public', prefixAvoidTrailingSlash: true, list: { format: 'json', names: ['index', 'index.json', '/'], extendedFolderInfo: true, jsonFormat: 'extended' } } const routes = ['/public/shallow/'] helper.arrange(t, options, (url) => { for (const route of routes) { t.test(route, t => { t.plan(5) simple.concat({ method: 'GET', url: url + route }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) const bodyObject = JSON.parse(body.toString()) t.equal(bodyObject.dirs[0].name, 'empty') t.equal(typeof bodyObject.dirs[0].stats.atime, 'string') t.equal(typeof bodyObject.dirs[0].extendedInfo.totalSize, 'number') }) }) } }) }) t.test('dir list - url parameter format', t => { t.plan(13) const options = { root: path.join(__dirname, '/static'), prefix: '/public', index: false, list: { format: 'html', render (dirs, files) { return 'html' } } } const route = '/public/' helper.arrange(t, options, (url) => { simple.concat({ method: 'GET', url: url + route }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(body.toString(), 'html') t.ok(response.headers['content-type'].includes('text/html')) }) simple.concat({ method: 'GET', url: url + route + '?format=html' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(body.toString(), 'html') t.ok(response.headers['content-type'].includes('text/html')) }) simple.concat({ method: 'GET', url: url + route + '?format=json' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.ok(body.toString()) t.ok(response.headers['content-type'].includes('application/json')) }) }) }) t.test('dir list on empty dir', t => { t.plan(2) const options = { root: path.join(__dirname, '/static'), prefix: '/public', list: true } const route = '/public/shallow/empty' const content = { dirs: [], files: [] } helper.arrange(t, options, (url) => { t.test(route, t => { t.plan(3) simple.concat({ method: 'GET', url: url + route }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(body.toString(), JSON.stringify(content)) }) }) }) }) t.test('dir list serve index.html on index option', t => { t.plan(2) const options = { root: path.join(__dirname, '/static'), prefix: '/public', index: false, list: { format: 'html', names: ['index', 'index.html'], render: () => 'dir list index' } } helper.arrange(t, options, (url) => { t.test('serve index.html from fs', t => { t.plan(6) let route = '/public/index.html' simple.concat({ method: 'GET', url: url + route }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(body.toString(), '\n \n the body\n \n\n') }) route = '/public/index' simple.concat({ method: 'GET', url: url + route }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(body.toString(), 'dir list index') }) }) }) }) t.test('serve a non existent dir and get error', t => { t.plan(2) const options = { root: '/none', prefix: '/public', list: true } const route = '/public/' helper.arrange(t, options, (url) => { t.test(route, t => { t.plan(2) simple.concat({ method: 'GET', url: url + route }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 404) }) }) }) }) t.test('serve a non existent dir and get error', t => { t.plan(2) const options = { root: path.join(__dirname, '/static'), prefix: '/public', list: { names: ['index'] } } const route = '/public/none/index' helper.arrange(t, options, (url) => { t.test(route, t => { t.plan(2) simple.concat({ method: 'GET', url: url + route }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 404) }) }) }) }) t.test('dir list error', t => { t.plan(7) const options = { root: path.join(__dirname, '/static'), prefix: '/public', prefixAvoidTrailingSlash: true, index: false, list: { format: 'html', names: ['index', 'index.htm'], render: () => '' } } const errorMessage = 'mocking send' const dirList = require('../lib/dirList') dirList.send = async () => { throw new Error(errorMessage) } const mock = t.mock('..', { '../lib/dirList.js': dirList }) const routes = ['/public/', '/public/index.htm'] helper.arrangeModule(t, options, mock, (url) => { for (const route of routes) { simple.concat({ method: 'GET', url: url + route }, (err, response, body) => { t.error(err) t.equal(JSON.parse(body.toString()).message, errorMessage) t.equal(response.statusCode, 500) }) } }) })