You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

645 lines
15 KiB

'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 = `
<html><body>
<ul>
{{#dirs}}
<li><a href="{{href}}">{{name}}</a></li>
{{/dirs}}
</ul>
<ul>
{{#files}}
<li><a href="{{href}}" target="_blank">{{name}}</a></li>
{{/files}}
</ul>
</body></html>
`
const handlebarTemplate = Handlebars.compile(source)
const templates = [
{
render: (dirs, files) => {
return handlebarTemplate({ dirs, files })
},
output: `
<html><body>
<ul>
<li><a href="/public/deep">deep</a></li>
<li><a href="/public/shallow">shallow</a></li>
</ul>
<ul>
<li><a href="/public/.example" target="_blank">.example</a></li>
<li><a href="/public/a .md" target="_blank">a .md</a></li>
<li><a href="/public/foo.html" target="_blank">foo.html</a></li>
<li><a href="/public/foobar.html" target="_blank">foobar.html</a></li>
<li><a href="/public/index.css" target="_blank">index.css</a></li>
<li><a href="/public/index.html" target="_blank">index.html</a></li>
</ul>
</body></html>
`
},
{
render: (dirs, files) => {
return `
<html><body>
<ul>
${dirs.map(dir => `<li><a href="${dir.href}">${dir.name}</a></li>`).join('\n ')}
</ul>
<ul>
${files.map(file => `<li><a href="${file.href}" target="_blank">${file.name}</a></li>`).join('\n ')}
</ul>
</body></html>
`
},
output: `
<html><body>
<ul>
<li><a href="/public/deep">deep</a></li>
<li><a href="/public/shallow">shallow</a></li>
</ul>
<ul>
<li><a href="/public/.example" target="_blank">.example</a></li>
<li><a href="/public/a .md" target="_blank">a .md</a></li>
<li><a href="/public/foo.html" target="_blank">foo.html</a></li>
<li><a href="/public/foobar.html" target="_blank">foobar.html</a></li>
<li><a href="/public/index.css" target="_blank">index.css</a></li>
<li><a href="/public/index.html" target="_blank">index.html</a></li>
</ul>
</body></html>
`
}
]
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(), '<html>\n <body>\n the body\n </body>\n</html>\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)
})
}
})
})