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
						
					
					
				
			
		
		
	
	
							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)
 | |
|       })
 | |
|     }
 | |
|   })
 | |
| })
 |