Use service-core
This commit is contained in:
parent
9a0fca4a22
commit
666c4bee89
20 changed files with 436 additions and 393 deletions
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"NODE_ENV": "development",
|
|
||||||
"server": {
|
|
||||||
"port": 4020,
|
|
||||||
"host": "0.0.0.0"
|
|
||||||
},
|
|
||||||
"bunyan": {
|
|
||||||
"name": "storage-upload",
|
|
||||||
"streams": [{
|
|
||||||
"stream": "process.stdout",
|
|
||||||
"level": "debug"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"jwt": {
|
|
||||||
"secret": "this-is-my-secret",
|
|
||||||
"options": {
|
|
||||||
"expiresIn": 604800
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fileSize": 524288000
|
|
||||||
}
|
|
|
@ -1,67 +1,61 @@
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
import Nconf from 'nconf-lite'
|
import Nconf from 'nconf-lite'
|
||||||
|
import { Util } from 'service-core'
|
||||||
|
|
||||||
const nconf = new Nconf()
|
const nconf = new Nconf()
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
||||||
let pckg = JSON.parse(fs.readFileSync(path.resolve(path.join(__dirname, '../package.json'))))
|
|
||||||
|
|
||||||
|
|
||||||
// Helper method for global usage.
|
// Helper method for global usage.
|
||||||
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
|
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
|
||||||
|
|
||||||
// Config follow the following priority check order:
|
// Config follow the following priority check order:
|
||||||
// 1. package.json
|
// 1. Enviroment variables
|
||||||
// 2. Enviroment variables
|
// 2. package.json
|
||||||
// 3. config/config.json
|
// 3. config/config.json
|
||||||
// 4. config/config.default.json
|
// 4. config/config.default.json
|
||||||
|
|
||||||
|
// Load enviroment variables as first priority
|
||||||
pckg = {
|
nconf.env({
|
||||||
name: pckg.name,
|
separator: '__',
|
||||||
version: pckg.version,
|
whitelist: [
|
||||||
description: pckg.description,
|
'NODE_ENV',
|
||||||
author: pckg.author,
|
'server__port',
|
||||||
license: pckg.license,
|
'server__host',
|
||||||
homepage: pckg.homepage,
|
'bunyan__name',
|
||||||
}
|
'frontend__url',
|
||||||
|
'fileSize',
|
||||||
|
'name',
|
||||||
|
'NODE_VERSION',
|
||||||
|
],
|
||||||
|
parseValues: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
// Load overrides as first priority
|
// Load empty overrides that can be overwritten later
|
||||||
nconf.overrides(pckg)
|
nconf.overrides({})
|
||||||
|
|
||||||
|
let util = new Util(import.meta.url)
|
||||||
|
let pckg = JSON.parse(fs.readFileSync(util.getPathFromRoot(`../package.json`)))
|
||||||
|
|
||||||
// Load enviroment variables as second priority
|
nconf.defaults({
|
||||||
nconf.env()
|
"name": pckg.name,
|
||||||
|
"version": pckg.version,
|
||||||
|
"NODE_ENV": "development",
|
||||||
// Load any overrides from the appropriate config file
|
"server": {
|
||||||
let configFile = '../config/config.json'
|
"port": 4040,
|
||||||
|
"host": "0.0.0.0"
|
||||||
/* istanbul ignore else */
|
},
|
||||||
if (nconf.get('NODE_ENV') === 'test') {
|
"bunyan": {
|
||||||
configFile = '../config/config.test.json'
|
"name": "storage-upload",
|
||||||
}
|
"streams": [{
|
||||||
|
"stream": "process.stdout",
|
||||||
/* istanbul ignore if */
|
"level": "debug"
|
||||||
if (nconf.get('NODE_ENV') === 'production') {
|
}
|
||||||
configFile = '../config/config.production.json'
|
]
|
||||||
}
|
},
|
||||||
|
"sites": {
|
||||||
nconf.file('main', path.resolve(path.join(__dirname, configFile)))
|
},
|
||||||
|
"uploadFolder": "./public",
|
||||||
// Load defaults
|
"fileSize": 524288000
|
||||||
nconf.file('default', path.resolve(path.join(__dirname, '../api/config.default.json')))
|
})
|
||||||
|
|
||||||
|
|
||||||
// Final sanity checks
|
|
||||||
/* istanbul ignore if */
|
|
||||||
if (typeof global.it === 'function' & !nconf.inTest()) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('Critical: potentially running test on production enviroment. Shutting down.')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default nconf
|
export default nconf
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
|
|
||||||
export class HttpError extends Error {
|
|
||||||
constructor(message, status = 500) {
|
|
||||||
super(message)
|
|
||||||
this.status = status
|
|
||||||
}
|
|
||||||
}
|
|
22
api/log.mjs
22
api/log.mjs
|
@ -1,22 +0,0 @@
|
||||||
import bunyan from 'bunyan-lite'
|
|
||||||
import config from './config.mjs'
|
|
||||||
import * as defaults from './defaults.mjs'
|
|
||||||
|
|
||||||
|
|
||||||
// Clone the settings as we will be touching
|
|
||||||
// on them slightly.
|
|
||||||
let settings = defaults.default(config.get('bunyan'))
|
|
||||||
|
|
||||||
// Replace any instance of 'process.stdout' with the
|
|
||||||
// actual reference to the process.stdout.
|
|
||||||
for (let i = 0; i < settings.streams.length; i++) {
|
|
||||||
/* istanbul ignore else */
|
|
||||||
if (settings.streams[i].stream === 'process.stdout') {
|
|
||||||
settings.streams[i].stream = process.stdout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create our logger.
|
|
||||||
const log = bunyan.createLogger(settings)
|
|
||||||
|
|
||||||
export default log
|
|
|
@ -1,5 +1,5 @@
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { HttpError } from '../error.mjs'
|
import { HttpError } from 'flaska'
|
||||||
import formidable from 'formidable'
|
import formidable from 'formidable'
|
||||||
import config from '../config.mjs'
|
import config from '../config.mjs'
|
||||||
|
|
||||||
|
@ -36,12 +36,12 @@ export function uploadFile(ctx, siteName, noprefix = false) {
|
||||||
let prefix = ''
|
let prefix = ''
|
||||||
|
|
||||||
var form = new formidable.IncomingForm()
|
var form = new formidable.IncomingForm()
|
||||||
form.uploadDir = `./public/${siteName}`
|
form.uploadDir = `${config.get('uploadFolder')}/${siteName}`
|
||||||
form.maxFileSize = config.get('fileSize')
|
form.maxFileSize = config.get('fileSize')
|
||||||
|
|
||||||
form.parse(ctx.req, function(err, fields, files) {
|
form.parse(ctx.req, function(err, fields, files) {
|
||||||
if (err) return rej(err)
|
if (err) return rej(err)
|
||||||
if (!files || !files.file) return rej(new HttpError('File in body was missing', 422))
|
if (!files || !files.file) return rej(new HttpError(422, 'File in body was missing'))
|
||||||
let file = files.file
|
let file = files.file
|
||||||
|
|
||||||
Object.keys(fields).forEach(function(key) {
|
Object.keys(fields).forEach(function(key) {
|
||||||
|
@ -51,13 +51,13 @@ export function uploadFile(ctx, siteName, noprefix = false) {
|
||||||
})
|
})
|
||||||
ctx.req.body = fields
|
ctx.req.body = fields
|
||||||
|
|
||||||
if (!noprefix || fs.existsSync(`./public/${siteName}/${prefix}${file.name}`)) {
|
if (!noprefix || fs.existsSync(`${config.get('uploadFolder')}/${siteName}/${prefix}${file.name}`)) {
|
||||||
prefix = getPrefix()
|
prefix = getPrefix()
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.rename(files.file.path, `./public/${siteName}/${prefix}${file.name}`, function(err) {
|
fs.rename(files.file.path, `${config.get('uploadFolder')}/${siteName}/${prefix}${file.name}`, function(err) {
|
||||||
if (err) return rej(err)
|
if (err) return rej(err)
|
||||||
file.path = `./public/${siteName}/${prefix}${file.name}`
|
file.path = `${config.get('uploadFolder')}/${siteName}/${prefix}${file.name}`
|
||||||
file.filename = `${prefix}${file.name}`
|
file.filename = `${prefix}${file.name}`
|
||||||
|
|
||||||
return res(file)
|
return res(file)
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import sharp from 'sharp'
|
import sharp from 'sharp'
|
||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import { HttpError } from '../error.mjs'
|
import config from '../config.mjs'
|
||||||
|
import { HttpError } from 'flaska'
|
||||||
import * as security from './security.mjs'
|
import * as security from './security.mjs'
|
||||||
import * as formidable from './formidable.mjs'
|
import * as formidable from './formidable.mjs'
|
||||||
|
|
||||||
|
@ -17,13 +18,27 @@ export default class MediaRoutes {
|
||||||
this.collator = new Intl.Collator('is-IS', { numeric: false, sensitivity: 'accent' })
|
this.collator = new Intl.Collator('is-IS', { numeric: false, sensitivity: 'accent' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
register(server) {
|
||||||
|
this.init().then(function() {}, function(err) {
|
||||||
|
server.core.log.error(err, 'Error initing media')
|
||||||
|
})
|
||||||
|
|
||||||
|
server.flaska.get('/media', [server.queryHandler()], this.listFiles.bind(this))
|
||||||
|
server.flaska.get('/media/:site', this.listPublicFiles.bind(this))
|
||||||
|
server.flaska.post('/media', [server.queryHandler()], this.upload.bind(this))
|
||||||
|
server.flaska.post('/media/noprefix', [server.queryHandler()], this.uploadNoPrefix.bind(this))
|
||||||
|
server.flaska.post('/media/resize', [server.queryHandler()], this.resize.bind(this))
|
||||||
|
server.flaska.post('/media/resize/:filename', [server.queryHandler(), server.jsonHandler()], this.resizeExisting.bind(this))
|
||||||
|
server.flaska.delete('/media/:filename', [server.queryHandler()], this.remove.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
return fs.readdir('./public').then(folders => {
|
return fs.readdir(config.get('uploadFolder')).then(folders => {
|
||||||
return Promise.all(folders.map(folder => {
|
return Promise.all(folders.map(folder => {
|
||||||
return fs.readdir('./public/' + folder)
|
return fs.readdir(config.get('uploadFolder') + '/' + folder)
|
||||||
.then(files => {
|
.then(files => {
|
||||||
return Promise.all(files.map(file => {
|
return Promise.all(files.map(file => {
|
||||||
return fs.stat(`./public/${folder}/${file}`)
|
return fs.stat(`${config.get('uploadFolder')}/${folder}/${file}`)
|
||||||
.then(function(stat) {
|
.then(function(stat) {
|
||||||
return { filename: file, size: stat.size }
|
return { filename: file, size: stat.size }
|
||||||
})
|
})
|
||||||
|
@ -94,7 +109,7 @@ export default class MediaRoutes {
|
||||||
|
|
||||||
ctx.log.info(`Uploaded ${result.filename}`)
|
ctx.log.info(`Uploaded ${result.filename}`)
|
||||||
|
|
||||||
let stat = await this.fs.stat(`./public/${ctx.state.site}/${result.filename}`)
|
let stat = await this.fs.stat(`${config.get('uploadFolder')}/${ctx.state.site}/${result.filename}`)
|
||||||
this.filesCacheAdd(ctx.state.site, result.filename, stat.size)
|
this.filesCacheAdd(ctx.state.site, result.filename, stat.size)
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
|
@ -124,7 +139,7 @@ export default class MediaRoutes {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
let item = ctx.req.body[key]
|
let item = ctx.req.body[key]
|
||||||
let sharp = this.sharp(`./public/${ctx.state.site}/${sourceFile}`)
|
let sharp = this.sharp(`${config.get('uploadFolder')}/${ctx.state.site}/${sourceFile}`)
|
||||||
.rotate()
|
.rotate()
|
||||||
|
|
||||||
for (let operation of allowedOperations) {
|
for (let operation of allowedOperations) {
|
||||||
|
@ -147,9 +162,9 @@ export default class MediaRoutes {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await sharp.toFile(`./public/${ctx.state.site}/${target}`)
|
await sharp.toFile(`${config.get('uploadFolder')}/${ctx.state.site}/${target}`)
|
||||||
|
|
||||||
let stat = await this.fs.stat(`./public/${ctx.state.site}/${target}`)
|
let stat = await this.fs.stat(`${config.get('uploadFolder')}/${ctx.state.site}/${target}`)
|
||||||
this.filesCacheAdd(ctx.state.site, target, stat.size)
|
this.filesCacheAdd(ctx.state.site, target, stat.size)
|
||||||
|
|
||||||
ctx.body[key] = {
|
ctx.body[key] = {
|
||||||
|
@ -159,7 +174,7 @@ export default class MediaRoutes {
|
||||||
}).then(
|
}).then(
|
||||||
function() {},
|
function() {},
|
||||||
function(err) {
|
function(err) {
|
||||||
throw new HttpError(`Error processing ${key}: ${err.message}`, 422)
|
throw new HttpError(422, `Error processing ${key}: ${err.message}`)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
|
@ -184,9 +199,9 @@ export default class MediaRoutes {
|
||||||
|
|
||||||
this.filesCacheRemove(site, ctx.params.filename)
|
this.filesCacheRemove(site, ctx.params.filename)
|
||||||
|
|
||||||
await this.fs.unlink(`./public/${site}/${ctx.params.filename}`)
|
await this.fs.unlink(`${config.get('uploadFolder')}/${site}/${ctx.params.filename}`)
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
throw new HttpError(`Error removing ${site}/${ctx.params.filename}: ${err.message}`, 422)
|
throw new HttpError(422, `Error removing ${site}/${ctx.params.filename}: ${err.message}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx.status = 204
|
ctx.status = 204
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { HttpError } from '../error.mjs'
|
import { HttpError } from 'flaska'
|
||||||
import decode from '../jwt/decode.mjs'
|
import decode from '../jwt/decode.mjs'
|
||||||
import config from '../config.mjs'
|
import config from '../config.mjs'
|
||||||
|
|
||||||
export function verifyToken(ctx) {
|
export function verifyToken(ctx) {
|
||||||
let token = ctx.query.get('token')
|
let token = ctx.query.get('token')
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new HttpError('Token is missing in query', 422)
|
throw new HttpError(422, 'Token is missing in query')
|
||||||
}
|
}
|
||||||
|
|
||||||
let org = config.get('sites')
|
let org = config.get('sites')
|
||||||
|
@ -21,14 +21,14 @@ export function verifyToken(ctx) {
|
||||||
return decoded.iss
|
return decoded.iss
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.log.error(err, 'Error decoding token: ' + token)
|
ctx.log.error(err, 'Error decoding token: ' + token)
|
||||||
throw new HttpError('Token was invalid', 422)
|
throw new HttpError(422, 'Token was invalid')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function throwIfNotPublic(site) {
|
export function throwIfNotPublic(site) {
|
||||||
let sites = config.get('sites')
|
let sites = config.get('sites')
|
||||||
if (!sites[site] || sites[site].public !== true) {
|
if (!sites[site] || sites[site].public !== true) {
|
||||||
throw new HttpError(`Requested site ${site} did not exist`, 404)
|
throw new HttpError(404, `Requested site ${site} did not exist`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,34 +48,34 @@ export function verifyBody(ctx) {
|
||||||
|
|
||||||
for (let key of keys) {
|
for (let key of keys) {
|
||||||
if (key === 'filename' || key === 'path') {
|
if (key === 'filename' || key === 'path') {
|
||||||
throw new HttpError('Body item with name filename or path is not allowed', 422)
|
throw new HttpError(422, 'Body item with name filename or path is not allowed')
|
||||||
}
|
}
|
||||||
let item = ctx.req.body[key]
|
let item = ctx.req.body[key]
|
||||||
|
|
||||||
if (typeof(item) !== 'object'
|
if (typeof(item) !== 'object'
|
||||||
|| !item
|
|| !item
|
||||||
|| Array.isArray(item)) {
|
|| Array.isArray(item)) {
|
||||||
throw new HttpError(`Body item ${key} was not valid`, 422)
|
throw new HttpError(422, `Body item ${key} was not valid`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof(item.format) !== 'string'
|
if (typeof(item.format) !== 'string'
|
||||||
|| !item.format
|
|| !item.format
|
||||||
|| validObjectOperations.includes(item.format)
|
|| validObjectOperations.includes(item.format)
|
||||||
|| item.format === 'out') {
|
|| item.format === 'out') {
|
||||||
throw new HttpError(`Body item ${key} missing valid format`, 422)
|
throw new HttpError(422, `Body item ${key} missing valid format`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof(item[item.format]) !== 'object'
|
if (typeof(item[item.format]) !== 'object'
|
||||||
|| !item[item.format]
|
|| !item[item.format]
|
||||||
|| Array.isArray(item[item.format])) {
|
|| Array.isArray(item[item.format])) {
|
||||||
throw new HttpError(`Body item ${key} options for format ${item.format} was not valid`, 422)
|
throw new HttpError(422, `Body item ${key} options for format ${item.format} was not valid`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.out != null) {
|
if (item.out != null) {
|
||||||
if (typeof(item.out) !== 'string'
|
if (typeof(item.out) !== 'string'
|
||||||
|| (item.out !== '' && item.out !== 'file' && item.out !== 'base64')
|
|| (item.out !== '' && item.out !== 'file' && item.out !== 'base64')
|
||||||
) {
|
) {
|
||||||
throw new HttpError(`Body item ${key} key out was invalid`, 422)
|
throw new HttpError(422, `Body item ${key} key out was invalid`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ export function verifyBody(ctx) {
|
||||||
if (item[operation] != null) {
|
if (item[operation] != null) {
|
||||||
if (typeof(item[operation]) !== 'object'
|
if (typeof(item[operation]) !== 'object'
|
||||||
|| Array.isArray(item[operation])) {
|
|| Array.isArray(item[operation])) {
|
||||||
throw new HttpError(`Body item ${key} key ${operation} was invalid`, 422)
|
throw new HttpError(422, `Body item ${key} key ${operation} was invalid`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,7 @@ export function verifyBody(ctx) {
|
||||||
for (let operation of validNumberOperations) {
|
for (let operation of validNumberOperations) {
|
||||||
if (item[operation] != null) {
|
if (item[operation] != null) {
|
||||||
if (typeof(item[operation]) !== 'number') {
|
if (typeof(item[operation]) !== 'number') {
|
||||||
throw new HttpError(`Body item ${key} key ${operation} was invalid`, 422)
|
throw new HttpError(422, `Body item ${key} key ${operation} was invalid`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
151
api/server.mjs
151
api/server.mjs
|
@ -5,82 +5,97 @@ import TestRoutes from './test/routes.mjs'
|
||||||
import MediaRoutes from './media/routes.mjs'
|
import MediaRoutes from './media/routes.mjs'
|
||||||
|
|
||||||
import config from './config.mjs'
|
import config from './config.mjs'
|
||||||
import log from './log.mjs'
|
|
||||||
|
|
||||||
const app = new Flaska({
|
export default class Server {
|
||||||
log: log,
|
constructor(http, port, core, opts = {}) {
|
||||||
})
|
Object.assign(this, opts)
|
||||||
|
this.http = http
|
||||||
|
this.port = port
|
||||||
|
this.core = core
|
||||||
|
|
||||||
app.before(function(ctx) {
|
this.jsonHandler = JsonHandler
|
||||||
ctx.__started = performance.now()
|
this.queryHandler = QueryHandler
|
||||||
ctx.log = ctx.log.child({
|
|
||||||
ip: ctx.req.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.after(function(ctx) {
|
this.flaskOptions = {
|
||||||
let ended = performance.now() - ctx.__started
|
log: this.core.log,
|
||||||
|
|
||||||
let status = ''
|
|
||||||
let level = 'info'
|
|
||||||
if (ctx.status >= 400) {
|
|
||||||
status = ctx.status + ' '
|
|
||||||
level = 'warn'
|
|
||||||
}
|
|
||||||
if (ctx.status >= 500) {
|
|
||||||
level = 'error'
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.log[level]({
|
|
||||||
duration: Math.round(ended),
|
|
||||||
status: ctx.status,
|
|
||||||
}, `<-- ${status}${ctx.method} ${ctx.url}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.onerror(function(err, ctx) {
|
|
||||||
|
|
||||||
if (err.status && err.status >= 400 && err.status < 500) {
|
|
||||||
if (err.body && err.body.request) {
|
|
||||||
ctx.log.warn({ request: err.body.request}, err.message)
|
|
||||||
} else {
|
|
||||||
ctx.log.warn(err.message)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ctx.log.error(err)
|
|
||||||
}
|
|
||||||
ctx.status = err.status || 500
|
|
||||||
|
|
||||||
if (err instanceof HttpError) {
|
this.routes = {
|
||||||
ctx.body = err.body || {
|
test: new TestRoutes(),
|
||||||
status: ctx.status,
|
media: new MediaRoutes(),
|
||||||
message: err.message,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.body = {
|
|
||||||
status: ctx.status,
|
|
||||||
message: err.message,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
const test = new TestRoutes()
|
run() {
|
||||||
app.get('/', test.static.bind(test))
|
// Create our server
|
||||||
app.get('/error', test.error.bind(test))
|
this.flaska = new Flaska(this.flaskOptions, this.http)
|
||||||
|
|
||||||
const media = new MediaRoutes()
|
// configure our server
|
||||||
media.init().then(function() {}, function(err) {
|
if (config.get('NODE_ENV') === 'development') {
|
||||||
log.error(err, 'Error initing media')
|
this.flaska.devMode()
|
||||||
})
|
}
|
||||||
app.get('/media', [QueryHandler()], media.listFiles.bind(media))
|
|
||||||
app.get('/media/:site', media.listPublicFiles.bind(media))
|
|
||||||
app.post('/media', [QueryHandler()], media.upload.bind(media))
|
|
||||||
app.post('/media/noprefix', [QueryHandler()], media.uploadNoPrefix.bind(media))
|
|
||||||
app.post('/media/resize', [QueryHandler()], media.resize.bind(media))
|
|
||||||
app.post('/media/resize/:filename', [QueryHandler(), JsonHandler()], media.resizeExisting.bind(media))
|
|
||||||
app.delete('/media/:filename', [QueryHandler()], media.remove.bind(media))
|
|
||||||
|
|
||||||
app.listen(config.get('server:port'), function(a,b) {
|
this.flaska.before(function(ctx) {
|
||||||
log.info(`Server listening at ${config.get('server:port')}`)
|
ctx.__started = performance.now()
|
||||||
})
|
ctx.log = ctx.log.child({
|
||||||
|
ip: ctx.req.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
export default app
|
this.flaska.after(function(ctx) {
|
||||||
|
let ended = performance.now() - ctx.__started
|
||||||
|
|
||||||
|
let status = ''
|
||||||
|
let level = 'info'
|
||||||
|
if (ctx.status >= 400) {
|
||||||
|
status = ctx.status + ' '
|
||||||
|
level = 'warn'
|
||||||
|
}
|
||||||
|
if (ctx.status >= 500) {
|
||||||
|
level = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.log[level]({
|
||||||
|
duration: Math.round(ended),
|
||||||
|
status: ctx.status,
|
||||||
|
}, `<-- ${status}${ctx.method} ${ctx.url}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.flaska.onerror(function(err, ctx) {
|
||||||
|
if (err.status && err.status >= 400 && err.status < 500) {
|
||||||
|
if (err.body && err.body.request) {
|
||||||
|
ctx.log.warn({ request: err.body.request}, err.message)
|
||||||
|
} else {
|
||||||
|
ctx.log.warn(err.message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.log.error(err)
|
||||||
|
}
|
||||||
|
ctx.status = err.status || 500
|
||||||
|
|
||||||
|
if (err instanceof HttpError) {
|
||||||
|
ctx.body = err.body || {
|
||||||
|
status: ctx.status,
|
||||||
|
message: err.message,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.body = {
|
||||||
|
status: ctx.status,
|
||||||
|
message: err.message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register our routes
|
||||||
|
let keys = Object.keys(this.routes)
|
||||||
|
for (let key of keys) {
|
||||||
|
this.routes[key].register(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start listening
|
||||||
|
|
||||||
|
return this.flaska.listenAsync(this.port).then(() => {
|
||||||
|
this.core.log.info(`Server is listening on port ${this.port} uploading to ${config.get('uploadFolder')}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,11 @@ export default class TestRoutes {
|
||||||
Object.assign(this, { })
|
Object.assign(this, { })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
register(server) {
|
||||||
|
server.flaska.get('/', this.static.bind(this))
|
||||||
|
server.flaska.get('/error', this.error.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
static(ctx) {
|
static(ctx) {
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
name: config.get('name'),
|
name: config.get('name'),
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
{
|
|
||||||
"bunyan": {
|
|
||||||
"name": "storage-upload-test",
|
|
||||||
"streams": [{
|
|
||||||
"stream": "process.stdout",
|
|
||||||
"level": "error"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"sites": {
|
|
||||||
"development": {
|
|
||||||
"keys": {
|
|
||||||
"default@HS256": "asdf1234"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"existing": {
|
|
||||||
"public": true,
|
|
||||||
"keys": {
|
|
||||||
"default@HS256": "asdf1234"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
34
dev.mjs
Normal file
34
dev.mjs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import fs from 'fs'
|
||||||
|
import { ServiceCore } from 'service-core'
|
||||||
|
import * as index from './index.mjs'
|
||||||
|
|
||||||
|
const port = 4040
|
||||||
|
|
||||||
|
var core = new ServiceCore('storage-upload', import.meta.url, port, '')
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
"sites": {
|
||||||
|
"development": {
|
||||||
|
"keys": {
|
||||||
|
"default@HS256": "asdf1234"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"existing": {
|
||||||
|
"public": true,
|
||||||
|
"keys": {
|
||||||
|
"default@HS256": "asdf1234"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
config = JSON.parse(fs.readFileSync('./config.json'))
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
config.port = port
|
||||||
|
|
||||||
|
core.setConfig(config)
|
||||||
|
core.init(index).then(function() {
|
||||||
|
return core.run()
|
||||||
|
})
|
11
index.mjs
Normal file
11
index.mjs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import config from './api/config.mjs'
|
||||||
|
|
||||||
|
export function start(http, port, ctx) {
|
||||||
|
config.sources[1].store = ctx.config
|
||||||
|
|
||||||
|
return import('./api/server.mjs')
|
||||||
|
.then(function(module) {
|
||||||
|
let server = new module.default(http, port, ctx)
|
||||||
|
return server.run()
|
||||||
|
})
|
||||||
|
}
|
17
package.json
17
package.json
|
@ -8,9 +8,20 @@
|
||||||
"start:bunyan": "node api/server.mjs | bunyan",
|
"start:bunyan": "node api/server.mjs | bunyan",
|
||||||
"test": "set NODE_ENV=test&& eltro test/**/*.test.mjs -r dot",
|
"test": "set NODE_ENV=test&& eltro test/**/*.test.mjs -r dot",
|
||||||
"test:linux": "NODE_ENV=test eltro 'test/**/*.test.mjs' -r dot",
|
"test:linux": "NODE_ENV=test eltro 'test/**/*.test.mjs' -r dot",
|
||||||
"test:watch": "npm-watch test"
|
"test:watch": "npm-watch test",
|
||||||
|
"dev": "npm-watch dev:server",
|
||||||
|
"dev:server": "node dev.mjs | bunyan"
|
||||||
},
|
},
|
||||||
"watch": {
|
"watch": {
|
||||||
|
"dev:server": {
|
||||||
|
"patterns": [
|
||||||
|
"api/*",
|
||||||
|
"{index,dev}.mjs"
|
||||||
|
],
|
||||||
|
"extensions": "js,mjs",
|
||||||
|
"quiet": true,
|
||||||
|
"inherit": true
|
||||||
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
"{api,test}/*"
|
"{api,test}/*"
|
||||||
|
@ -31,13 +42,13 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/nfp-projects/storage-upload#readme",
|
"homepage": "https://github.com/nfp-projects/storage-upload#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bunyan-lite": "^1.2.0",
|
|
||||||
"flaska": "^1.2.3",
|
"flaska": "^1.2.3",
|
||||||
"formidable": "^1.2.2",
|
"formidable": "^1.2.2",
|
||||||
"nconf-lite": "^2.0.0",
|
"nconf-lite": "^2.0.0",
|
||||||
"sharp": "^0.30.3"
|
"sharp": "^0.30.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eltro": "^1.2.3"
|
"eltro": "^1.2.3",
|
||||||
|
"service-core": "^3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
27
test.js
27
test.js
|
@ -1,27 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
const os = require('os');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const env = process.env;
|
|
||||||
|
|
||||||
const mkdirSync = function (dirPath) {
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
|
||||||
} catch (err) {
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if (err.code !== 'EEXIST') {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cachePath = function () {
|
|
||||||
const npmCachePath = env.npm_config_cache || /* istanbul ignore next */
|
|
||||||
(env.APPDATA ? path.join(env.APPDATA, 'npm-cache') : path.join(os.homedir(), '.npm'));
|
|
||||||
mkdirSync(npmCachePath);
|
|
||||||
const libvipsCachePath = path.join(npmCachePath, '_libvips');
|
|
||||||
mkdirSync(libvipsCachePath);
|
|
||||||
return libvipsCachePath;
|
|
||||||
};
|
|
||||||
|
|
||||||
cachePath()
|
|
|
@ -1 +0,0 @@
|
||||||
{"name":"storage-upload","hostname":"JonatanPC","pid":26596,"level":30,"path":"/","status":200,"ms":4,"msg":"Request finished","time":"2021-10-10T23:55:12.390Z","v":0}
|
|
|
@ -1,29 +1,66 @@
|
||||||
import { stub } from 'eltro'
|
import { stub } from 'eltro'
|
||||||
|
import { ServiceCore } from 'service-core'
|
||||||
import Client from './helper.client.mjs'
|
import Client from './helper.client.mjs'
|
||||||
import defaults from '../api/defaults.mjs'
|
import defaults from '../api/defaults.mjs'
|
||||||
import serv from '../api/server.mjs'
|
import * as index from '../index.mjs'
|
||||||
|
|
||||||
serv.log = {
|
export const port = 5030
|
||||||
|
|
||||||
|
export const log = {
|
||||||
log: stub(),
|
log: stub(),
|
||||||
warn: stub(),
|
warn: stub(),
|
||||||
info: stub(),
|
info: stub(),
|
||||||
error: stub(),
|
error: stub(),
|
||||||
child: stub(),
|
child: stub(),
|
||||||
|
event: {
|
||||||
|
warn: stub(),
|
||||||
|
info: stub(),
|
||||||
|
error: stub(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.child.returns(log)
|
||||||
|
|
||||||
|
let serverRunning = false
|
||||||
|
|
||||||
|
export function startServer() {
|
||||||
|
if (serverRunning) return Promise.resolve()
|
||||||
|
serverRunning = true
|
||||||
|
|
||||||
|
var core = new ServiceCore('storage-upload', import.meta.url, port, '')
|
||||||
|
core.setConfig({
|
||||||
|
"port": port,
|
||||||
|
"sites": {
|
||||||
|
"development": {
|
||||||
|
"keys": {
|
||||||
|
"default@HS256": "asdf1234"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"existing": {
|
||||||
|
"public": true,
|
||||||
|
"keys": {
|
||||||
|
"default@HS256": "asdf1234"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
core.log = log
|
||||||
|
|
||||||
|
return core.init(index).then(function() {
|
||||||
|
return core.run()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
serv.log.child.returns(serv.log)
|
|
||||||
|
|
||||||
export const server = serv
|
|
||||||
|
|
||||||
export function createClient() {
|
export function createClient() {
|
||||||
return new Client()
|
return new Client(port)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resetLog() {
|
export function resetLog() {
|
||||||
serv.log.log.reset()
|
log.log.reset()
|
||||||
serv.log.info.reset()
|
log.info.reset()
|
||||||
serv.log.warn.reset()
|
log.warn.reset()
|
||||||
serv.log.error.reset()
|
log.error.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createContext(opts) {
|
export function createContext(opts) {
|
||||||
|
|
|
@ -4,8 +4,7 @@ import fs from 'fs/promises'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
import { server, resetLog } from '../helper.server.mjs'
|
import { createClient, startServer, log, resetLog } from '../helper.server.mjs'
|
||||||
import Client from '../helper.client.mjs'
|
|
||||||
import encode from '../../api/jwt/encode.mjs'
|
import encode from '../../api/jwt/encode.mjs'
|
||||||
|
|
||||||
let __dirname = path.dirname(fileURLToPath(import.meta.url))
|
let __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
@ -17,10 +16,15 @@ function resolve(file) {
|
||||||
const currYear = new Date().getFullYear().toString()
|
const currYear = new Date().getFullYear().toString()
|
||||||
|
|
||||||
t.describe('Media (API)', () => {
|
t.describe('Media (API)', () => {
|
||||||
const client = new Client()
|
let client
|
||||||
const secret = 'asdf1234'
|
let secret = 'asdf1234'
|
||||||
let testFiles = []
|
let testFiles = []
|
||||||
|
|
||||||
|
t.before(function() {
|
||||||
|
client = createClient()
|
||||||
|
return startServer()
|
||||||
|
})
|
||||||
|
|
||||||
t.after(function() {
|
t.after(function() {
|
||||||
return Promise.all(testFiles.map(function(file) {
|
return Promise.all(testFiles.map(function(file) {
|
||||||
return fs.unlink(resolve(`../../public/${file}`)).catch(function() {})
|
return fs.unlink(resolve(`../../public/${file}`)).catch(function() {})
|
||||||
|
@ -33,8 +37,8 @@ t.describe('Media (API)', () => {
|
||||||
t.timeout(10000).describe('POST /media', function temp() {
|
t.timeout(10000).describe('POST /media', function temp() {
|
||||||
t.test('should require authentication', async () => {
|
t.test('should require authentication', async () => {
|
||||||
resetLog()
|
resetLog()
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 0)
|
assert.strictEqual(log.warn.callCount, 0)
|
||||||
let err = await assert.isRejected(
|
let err = await assert.isRejected(
|
||||||
client.upload('/media',
|
client.upload('/media',
|
||||||
resolve('test.png')
|
resolve('test.png')
|
||||||
|
@ -45,19 +49,19 @@ t.describe('Media (API)', () => {
|
||||||
assert.match(err.message, /[Tt]oken/)
|
assert.match(err.message, /[Tt]oken/)
|
||||||
assert.match(err.message, /[Mm]issing/)
|
assert.match(err.message, /[Mm]issing/)
|
||||||
|
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 2)
|
assert.strictEqual(log.warn.callCount, 2)
|
||||||
assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string')
|
assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
|
||||||
assert.match(server.log.warn.firstCall[0], /[Tt]oken/)
|
assert.match(log.warn.firstCall[0], /[Tt]oken/)
|
||||||
assert.match(server.log.warn.firstCall[0], /[Mm]issing/)
|
assert.match(log.warn.firstCall[0], /[Mm]issing/)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should verify token correctly', async () => {
|
t.test('should verify token correctly', async () => {
|
||||||
const assertToken = 'asdf.asdf.asdf'
|
const assertToken = 'asdf.asdf.asdf'
|
||||||
resetLog()
|
resetLog()
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 0)
|
assert.strictEqual(log.warn.callCount, 0)
|
||||||
assert.strictEqual(server.log.info.callCount, 0)
|
assert.strictEqual(log.info.callCount, 0)
|
||||||
|
|
||||||
let err = await assert.isRejected(
|
let err = await assert.isRejected(
|
||||||
client.upload('/media?token=' + assertToken,
|
client.upload('/media?token=' + assertToken,
|
||||||
|
@ -69,13 +73,13 @@ t.describe('Media (API)', () => {
|
||||||
assert.match(err.message, /[Tt]oken/)
|
assert.match(err.message, /[Tt]oken/)
|
||||||
assert.match(err.message, /[Ii]nvalid/)
|
assert.match(err.message, /[Ii]nvalid/)
|
||||||
|
|
||||||
assert.strictEqual(server.log.error.callCount, 1)
|
assert.strictEqual(log.error.callCount, 1)
|
||||||
assert.strictEqual(server.log.warn.callCount, 2)
|
assert.strictEqual(log.warn.callCount, 2)
|
||||||
assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string')
|
assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
|
||||||
assert.match(server.log.warn.firstCall[0], /[Tt]oken/)
|
assert.match(log.warn.firstCall[0], /[Tt]oken/)
|
||||||
assert.match(server.log.warn.firstCall[0], /[Ii]nvalid/)
|
assert.match(log.warn.firstCall[0], /[Ii]nvalid/)
|
||||||
assert.ok(server.log.error.lastCall[0] instanceof Error)
|
assert.ok(log.error.lastCall[0] instanceof Error)
|
||||||
assert.match(server.log.error.lastCall[1], new RegExp(assertToken))
|
assert.match(log.error.lastCall[1], new RegExp(assertToken))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should upload file and create file', async () => {
|
t.test('should upload file and create file', async () => {
|
||||||
|
@ -111,8 +115,8 @@ t.describe('Media (API)', () => {
|
||||||
t.timeout(10000).describe('POST /media/noprefix', function temp() {
|
t.timeout(10000).describe('POST /media/noprefix', function temp() {
|
||||||
t.test('should require authentication', async () => {
|
t.test('should require authentication', async () => {
|
||||||
resetLog()
|
resetLog()
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 0)
|
assert.strictEqual(log.warn.callCount, 0)
|
||||||
let err = await assert.isRejected(
|
let err = await assert.isRejected(
|
||||||
client.upload('/media/noprefix',
|
client.upload('/media/noprefix',
|
||||||
resolve('test.png')
|
resolve('test.png')
|
||||||
|
@ -123,19 +127,19 @@ t.describe('Media (API)', () => {
|
||||||
assert.match(err.message, /[Tt]oken/)
|
assert.match(err.message, /[Tt]oken/)
|
||||||
assert.match(err.message, /[Mm]issing/)
|
assert.match(err.message, /[Mm]issing/)
|
||||||
|
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 2)
|
assert.strictEqual(log.warn.callCount, 2)
|
||||||
assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string')
|
assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
|
||||||
assert.match(server.log.warn.firstCall[0], /[Tt]oken/)
|
assert.match(log.warn.firstCall[0], /[Tt]oken/)
|
||||||
assert.match(server.log.warn.firstCall[0], /[Mm]issing/)
|
assert.match(log.warn.firstCall[0], /[Mm]issing/)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should verify token correctly', async () => {
|
t.test('should verify token correctly', async () => {
|
||||||
const assertToken = 'asdf.asdf.asdf'
|
const assertToken = 'asdf.asdf.asdf'
|
||||||
resetLog()
|
resetLog()
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 0)
|
assert.strictEqual(log.warn.callCount, 0)
|
||||||
assert.strictEqual(server.log.info.callCount, 0)
|
assert.strictEqual(log.info.callCount, 0)
|
||||||
|
|
||||||
let err = await assert.isRejected(
|
let err = await assert.isRejected(
|
||||||
client.upload('/media/noprefix?token=' + assertToken,
|
client.upload('/media/noprefix?token=' + assertToken,
|
||||||
|
@ -147,13 +151,13 @@ t.describe('Media (API)', () => {
|
||||||
assert.match(err.message, /[Tt]oken/)
|
assert.match(err.message, /[Tt]oken/)
|
||||||
assert.match(err.message, /[Ii]nvalid/)
|
assert.match(err.message, /[Ii]nvalid/)
|
||||||
|
|
||||||
assert.strictEqual(server.log.error.callCount, 1)
|
assert.strictEqual(log.error.callCount, 1)
|
||||||
assert.strictEqual(server.log.warn.callCount, 2)
|
assert.strictEqual(log.warn.callCount, 2)
|
||||||
assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string')
|
assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
|
||||||
assert.match(server.log.warn.firstCall[0], /[Tt]oken/)
|
assert.match(log.warn.firstCall[0], /[Tt]oken/)
|
||||||
assert.match(server.log.warn.firstCall[0], /[Ii]nvalid/)
|
assert.match(log.warn.firstCall[0], /[Ii]nvalid/)
|
||||||
assert.ok(server.log.error.lastCall[0] instanceof Error)
|
assert.ok(log.error.lastCall[0] instanceof Error)
|
||||||
assert.match(server.log.error.lastCall[1], new RegExp(assertToken))
|
assert.match(log.error.lastCall[1], new RegExp(assertToken))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should upload and create file with no prefix', async () => {
|
t.test('should upload and create file with no prefix', async () => {
|
||||||
|
@ -227,8 +231,8 @@ t.describe('Media (API)', () => {
|
||||||
t.timeout(10000).describe('POST /media/resize', function temp() {
|
t.timeout(10000).describe('POST /media/resize', function temp() {
|
||||||
t.test('should require authentication', async () => {
|
t.test('should require authentication', async () => {
|
||||||
resetLog()
|
resetLog()
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 0)
|
assert.strictEqual(log.warn.callCount, 0)
|
||||||
let err = await assert.isRejected(
|
let err = await assert.isRejected(
|
||||||
client.upload('/media/resize',
|
client.upload('/media/resize',
|
||||||
resolve('test.png')
|
resolve('test.png')
|
||||||
|
@ -239,19 +243,19 @@ t.describe('Media (API)', () => {
|
||||||
assert.match(err.message, /[Tt]oken/)
|
assert.match(err.message, /[Tt]oken/)
|
||||||
assert.match(err.message, /[Mm]issing/)
|
assert.match(err.message, /[Mm]issing/)
|
||||||
|
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 2)
|
assert.strictEqual(log.warn.callCount, 2)
|
||||||
assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string')
|
assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
|
||||||
assert.match(server.log.warn.firstCall[0], /[Tt]oken/)
|
assert.match(log.warn.firstCall[0], /[Tt]oken/)
|
||||||
assert.match(server.log.warn.firstCall[0], /[Mm]issing/)
|
assert.match(log.warn.firstCall[0], /[Mm]issing/)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should verify token correctly', async () => {
|
t.test('should verify token correctly', async () => {
|
||||||
const assertToken = 'asdf.asdf.asdf'
|
const assertToken = 'asdf.asdf.asdf'
|
||||||
resetLog()
|
resetLog()
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 0)
|
assert.strictEqual(log.warn.callCount, 0)
|
||||||
assert.strictEqual(server.log.info.callCount, 0)
|
assert.strictEqual(log.info.callCount, 0)
|
||||||
|
|
||||||
let err = await assert.isRejected(
|
let err = await assert.isRejected(
|
||||||
client.upload('/media/resize?token=' + assertToken,
|
client.upload('/media/resize?token=' + assertToken,
|
||||||
|
@ -263,13 +267,13 @@ t.describe('Media (API)', () => {
|
||||||
assert.match(err.message, /[Tt]oken/)
|
assert.match(err.message, /[Tt]oken/)
|
||||||
assert.match(err.message, /[Ii]nvalid/)
|
assert.match(err.message, /[Ii]nvalid/)
|
||||||
|
|
||||||
assert.strictEqual(server.log.error.callCount, 1)
|
assert.strictEqual(log.error.callCount, 1)
|
||||||
assert.strictEqual(server.log.warn.callCount, 2)
|
assert.strictEqual(log.warn.callCount, 2)
|
||||||
assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string')
|
assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
|
||||||
assert.match(server.log.warn.firstCall[0], /[Tt]oken/)
|
assert.match(log.warn.firstCall[0], /[Tt]oken/)
|
||||||
assert.match(server.log.warn.firstCall[0], /[Ii]nvalid/)
|
assert.match(log.warn.firstCall[0], /[Ii]nvalid/)
|
||||||
assert.ok(server.log.error.lastCall[0] instanceof Error)
|
assert.ok(log.error.lastCall[0] instanceof Error)
|
||||||
assert.match(server.log.error.lastCall[1], new RegExp(assertToken))
|
assert.match(log.error.lastCall[1], new RegExp(assertToken))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should upload file and create file', async () => {
|
t.test('should upload file and create file', async () => {
|
||||||
|
@ -458,8 +462,8 @@ t.describe('Media (API)', () => {
|
||||||
|
|
||||||
t.test('should require authentication', async () => {
|
t.test('should require authentication', async () => {
|
||||||
resetLog()
|
resetLog()
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 0)
|
assert.strictEqual(log.warn.callCount, 0)
|
||||||
let err = await assert.isRejected(
|
let err = await assert.isRejected(
|
||||||
client.post(`/media/resize/${sourceFilename}`, {})
|
client.post(`/media/resize/${sourceFilename}`, {})
|
||||||
)
|
)
|
||||||
|
@ -468,19 +472,19 @@ t.describe('Media (API)', () => {
|
||||||
assert.match(err.message, /[Tt]oken/)
|
assert.match(err.message, /[Tt]oken/)
|
||||||
assert.match(err.message, /[Mm]issing/)
|
assert.match(err.message, /[Mm]issing/)
|
||||||
|
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 2)
|
assert.strictEqual(log.warn.callCount, 2)
|
||||||
assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string')
|
assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
|
||||||
assert.match(server.log.warn.firstCall[0], /[Tt]oken/)
|
assert.match(log.warn.firstCall[0], /[Tt]oken/)
|
||||||
assert.match(server.log.warn.firstCall[0], /[Mm]issing/)
|
assert.match(log.warn.firstCall[0], /[Mm]issing/)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should verify token correctly', async () => {
|
t.test('should verify token correctly', async () => {
|
||||||
const assertToken = 'asdf.asdf.asdf'
|
const assertToken = 'asdf.asdf.asdf'
|
||||||
resetLog()
|
resetLog()
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 0)
|
assert.strictEqual(log.warn.callCount, 0)
|
||||||
assert.strictEqual(server.log.info.callCount, 0)
|
assert.strictEqual(log.info.callCount, 0)
|
||||||
|
|
||||||
let err = await assert.isRejected(
|
let err = await assert.isRejected(
|
||||||
client.post(`/media/resize/${sourceFilename}?token=${assertToken}`, {})
|
client.post(`/media/resize/${sourceFilename}?token=${assertToken}`, {})
|
||||||
|
@ -490,13 +494,13 @@ t.describe('Media (API)', () => {
|
||||||
assert.match(err.message, /[Tt]oken/)
|
assert.match(err.message, /[Tt]oken/)
|
||||||
assert.match(err.message, /[Ii]nvalid/)
|
assert.match(err.message, /[Ii]nvalid/)
|
||||||
|
|
||||||
assert.strictEqual(server.log.error.callCount, 1)
|
assert.strictEqual(log.error.callCount, 1)
|
||||||
assert.strictEqual(server.log.warn.callCount, 2)
|
assert.strictEqual(log.warn.callCount, 2)
|
||||||
assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string')
|
assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
|
||||||
assert.match(server.log.warn.firstCall[0], /[Tt]oken/)
|
assert.match(log.warn.firstCall[0], /[Tt]oken/)
|
||||||
assert.match(server.log.warn.firstCall[0], /[Ii]nvalid/)
|
assert.match(log.warn.firstCall[0], /[Ii]nvalid/)
|
||||||
assert.ok(server.log.error.lastCall[0] instanceof Error)
|
assert.ok(log.error.lastCall[0] instanceof Error)
|
||||||
assert.match(server.log.error.lastCall[1], new RegExp(assertToken))
|
assert.match(log.error.lastCall[1], new RegExp(assertToken))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should create multiple sizes for existing file', async () => {
|
t.test('should create multiple sizes for existing file', async () => {
|
||||||
|
@ -596,8 +600,8 @@ t.describe('Media (API)', () => {
|
||||||
t.timeout(10000).describe('DELETE /media/:filename', function temp() {
|
t.timeout(10000).describe('DELETE /media/:filename', function temp() {
|
||||||
t.test('should require authentication', async () => {
|
t.test('should require authentication', async () => {
|
||||||
resetLog()
|
resetLog()
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 0)
|
assert.strictEqual(log.warn.callCount, 0)
|
||||||
let err = await assert.isRejected(
|
let err = await assert.isRejected(
|
||||||
client.del('/media/20220105_101610_test1.jpg',
|
client.del('/media/20220105_101610_test1.jpg',
|
||||||
resolve('test.png')
|
resolve('test.png')
|
||||||
|
@ -608,19 +612,19 @@ t.describe('Media (API)', () => {
|
||||||
assert.match(err.message, /[Tt]oken/)
|
assert.match(err.message, /[Tt]oken/)
|
||||||
assert.match(err.message, /[Mm]issing/)
|
assert.match(err.message, /[Mm]issing/)
|
||||||
|
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 2)
|
assert.strictEqual(log.warn.callCount, 2)
|
||||||
assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string')
|
assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
|
||||||
assert.match(server.log.warn.firstCall[0], /[Tt]oken/)
|
assert.match(log.warn.firstCall[0], /[Tt]oken/)
|
||||||
assert.match(server.log.warn.firstCall[0], /[Mm]issing/)
|
assert.match(log.warn.firstCall[0], /[Mm]issing/)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should verify token correctly', async () => {
|
t.test('should verify token correctly', async () => {
|
||||||
const assertToken = 'asdf.asdf.asdf'
|
const assertToken = 'asdf.asdf.asdf'
|
||||||
resetLog()
|
resetLog()
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 0)
|
assert.strictEqual(log.warn.callCount, 0)
|
||||||
assert.strictEqual(server.log.info.callCount, 0)
|
assert.strictEqual(log.info.callCount, 0)
|
||||||
|
|
||||||
let err = await assert.isRejected(
|
let err = await assert.isRejected(
|
||||||
client.del('/media/20220105_101610_test1.jpg?token=' + assertToken,
|
client.del('/media/20220105_101610_test1.jpg?token=' + assertToken,
|
||||||
|
@ -632,13 +636,13 @@ t.describe('Media (API)', () => {
|
||||||
assert.match(err.message, /[Tt]oken/)
|
assert.match(err.message, /[Tt]oken/)
|
||||||
assert.match(err.message, /[Ii]nvalid/)
|
assert.match(err.message, /[Ii]nvalid/)
|
||||||
|
|
||||||
assert.strictEqual(server.log.error.callCount, 1)
|
assert.strictEqual(log.error.callCount, 1)
|
||||||
assert.strictEqual(server.log.warn.callCount, 2)
|
assert.strictEqual(log.warn.callCount, 2)
|
||||||
assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string')
|
assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
|
||||||
assert.match(server.log.warn.firstCall[0], /[Tt]oken/)
|
assert.match(log.warn.firstCall[0], /[Tt]oken/)
|
||||||
assert.match(server.log.warn.firstCall[0], /[Ii]nvalid/)
|
assert.match(log.warn.firstCall[0], /[Ii]nvalid/)
|
||||||
assert.ok(server.log.error.lastCall[0] instanceof Error)
|
assert.ok(log.error.lastCall[0] instanceof Error)
|
||||||
assert.match(server.log.error.lastCall[1], new RegExp(assertToken))
|
assert.match(log.error.lastCall[1], new RegExp(assertToken))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should remove the file', async () => {
|
t.test('should remove the file', async () => {
|
||||||
|
@ -691,27 +695,27 @@ t.describe('Media (API)', () => {
|
||||||
t.describe('GET /media', function() {
|
t.describe('GET /media', function() {
|
||||||
t.test('should require authentication', async () => {
|
t.test('should require authentication', async () => {
|
||||||
resetLog()
|
resetLog()
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 0)
|
assert.strictEqual(log.warn.callCount, 0)
|
||||||
let err = await assert.isRejected(client.get('/media'))
|
let err = await assert.isRejected(client.get('/media'))
|
||||||
|
|
||||||
assert.strictEqual(err.status, 422)
|
assert.strictEqual(err.status, 422)
|
||||||
assert.match(err.message, /[Tt]oken/)
|
assert.match(err.message, /[Tt]oken/)
|
||||||
assert.match(err.message, /[Mm]issing/)
|
assert.match(err.message, /[Mm]issing/)
|
||||||
|
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 2)
|
assert.strictEqual(log.warn.callCount, 2)
|
||||||
assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string')
|
assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
|
||||||
assert.match(server.log.warn.firstCall[0], /[Tt]oken/)
|
assert.match(log.warn.firstCall[0], /[Tt]oken/)
|
||||||
assert.match(server.log.warn.firstCall[0], /[Mm]issing/)
|
assert.match(log.warn.firstCall[0], /[Mm]issing/)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should verify token correctly', async () => {
|
t.test('should verify token correctly', async () => {
|
||||||
const assertToken = 'asdf.asdf.asdf'
|
const assertToken = 'asdf.asdf.asdf'
|
||||||
resetLog()
|
resetLog()
|
||||||
assert.strictEqual(server.log.error.callCount, 0)
|
assert.strictEqual(log.error.callCount, 0)
|
||||||
assert.strictEqual(server.log.warn.callCount, 0)
|
assert.strictEqual(log.warn.callCount, 0)
|
||||||
assert.strictEqual(server.log.info.callCount, 0)
|
assert.strictEqual(log.info.callCount, 0)
|
||||||
|
|
||||||
let err = await assert.isRejected(client.get('/media?token=' + assertToken))
|
let err = await assert.isRejected(client.get('/media?token=' + assertToken))
|
||||||
|
|
||||||
|
@ -719,13 +723,13 @@ t.describe('Media (API)', () => {
|
||||||
assert.match(err.message, /[Tt]oken/)
|
assert.match(err.message, /[Tt]oken/)
|
||||||
assert.match(err.message, /[Ii]nvalid/)
|
assert.match(err.message, /[Ii]nvalid/)
|
||||||
|
|
||||||
assert.strictEqual(server.log.error.callCount, 1)
|
assert.strictEqual(log.error.callCount, 1)
|
||||||
assert.strictEqual(server.log.warn.callCount, 2)
|
assert.strictEqual(log.warn.callCount, 2)
|
||||||
assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string')
|
assert.strictEqual(typeof(log.warn.firstCall[0]), 'string')
|
||||||
assert.match(server.log.warn.firstCall[0], /[Tt]oken/)
|
assert.match(log.warn.firstCall[0], /[Tt]oken/)
|
||||||
assert.match(server.log.warn.firstCall[0], /[Ii]nvalid/)
|
assert.match(log.warn.firstCall[0], /[Ii]nvalid/)
|
||||||
assert.ok(server.log.error.lastCall[0] instanceof Error)
|
assert.ok(log.error.lastCall[0] instanceof Error)
|
||||||
assert.match(server.log.error.lastCall[1], new RegExp(assertToken))
|
assert.match(log.error.lastCall[1], new RegExp(assertToken))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should return list of files in specified folder', async () => {
|
t.test('should return list of files in specified folder', async () => {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import { Eltro as t, assert, stub } from 'eltro'
|
import { Eltro as t, assert, stub } from 'eltro'
|
||||||
|
import { HttpError } from 'flaska'
|
||||||
import { createContext } from '../helper.server.mjs'
|
import { createContext } from '../helper.server.mjs'
|
||||||
|
|
||||||
import MediaRoutes from '../../api/media/routes.mjs'
|
import MediaRoutes from '../../api/media/routes.mjs'
|
||||||
import { HttpError } from '../../api/error.mjs'
|
|
||||||
|
|
||||||
t.before(function() {
|
t.before(function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
|
|
|
@ -1,23 +1,32 @@
|
||||||
import { Eltro as t, assert} from 'eltro'
|
import { Eltro as t, assert} from 'eltro'
|
||||||
|
import { HttpError } from 'flaska'
|
||||||
|
|
||||||
import { createContext } from '../helper.server.mjs'
|
import { createContext } from '../helper.server.mjs'
|
||||||
import { verifyToken, verifyBody, throwIfNotPublic } from '../../api/media/security.mjs'
|
import { verifyToken, verifyBody, throwIfNotPublic } from '../../api/media/security.mjs'
|
||||||
import { HttpError } from '../../api/error.mjs'
|
|
||||||
import encode from '../../api/jwt/encode.mjs'
|
import encode from '../../api/jwt/encode.mjs'
|
||||||
import config from '../../api/config.mjs'
|
import config from '../../api/config.mjs'
|
||||||
|
|
||||||
t.describe('#throwIfNotPublic()', function() {
|
t.describe('#throwIfNotPublic()', function() {
|
||||||
|
let backup = {}
|
||||||
|
|
||||||
t.before(function() {
|
t.before(function() {
|
||||||
config.set('sites', {
|
backup = config.sources[1].store
|
||||||
justatest: {
|
config.sources[1].store = {
|
||||||
|
sites: {
|
||||||
|
justatest: {
|
||||||
|
},
|
||||||
|
justatest2: {
|
||||||
|
public: false,
|
||||||
|
},
|
||||||
|
justatest3: {
|
||||||
|
public: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
justatest2: {
|
}
|
||||||
public: false,
|
})
|
||||||
},
|
|
||||||
justatest3: {
|
t.after(function() {
|
||||||
public: true,
|
config.sources[1].store = backup
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should throw for sites that do not exist or are null', function() {
|
t.test('should throw for sites that do not exist or are null', function() {
|
||||||
|
@ -53,14 +62,22 @@ t.describe('#throwIfNotPublic()', function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.describe('#verifyToken()', function() {
|
t.describe('#verifyToken()', function() {
|
||||||
|
let backup = {}
|
||||||
t.before(function() {
|
t.before(function() {
|
||||||
config.set('sites', {
|
backup = config.sources[1].store
|
||||||
justatest: {
|
config.sources[1].store = {
|
||||||
keys: {
|
sites: {
|
||||||
'default@HS512': 'mysharedkey',
|
justatest: {
|
||||||
}
|
keys: {
|
||||||
|
'default@HS512': 'mysharedkey',
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.after(function() {
|
||||||
|
config.sources[1].store = backup
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should fail if query token is missing', function() {
|
t.test('should fail if query token is missing', function() {
|
||||||
|
|
|
@ -1,39 +1,41 @@
|
||||||
import { Eltro as t, assert} from 'eltro'
|
import { Eltro as t, assert} from 'eltro'
|
||||||
|
|
||||||
import { createClient, server, resetLog } from './helper.server.mjs'
|
import { createClient, startServer, log, resetLog } from './helper.server.mjs'
|
||||||
|
|
||||||
t.describe('Server', function() {
|
t.describe('Server', function() {
|
||||||
let client
|
let client
|
||||||
|
|
||||||
t.before(function() {
|
t.before(function() {
|
||||||
client = createClient()
|
client = createClient()
|
||||||
|
|
||||||
|
return startServer()
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should run', async function() {
|
t.test('should run', async function() {
|
||||||
resetLog()
|
resetLog()
|
||||||
assert.strictEqual(server.log.info.callCount, 0)
|
assert.strictEqual(log.info.callCount, 0)
|
||||||
let data = await client.get('/')
|
let data = await client.get('/')
|
||||||
|
|
||||||
assert.ok(data)
|
assert.ok(data)
|
||||||
assert.ok(data.name)
|
assert.ok(data.name)
|
||||||
assert.ok(data.version)
|
assert.ok(data.version)
|
||||||
assert.strictEqual(server.log.info.callCount, 1)
|
assert.strictEqual(log.info.callCount, 1)
|
||||||
assert.strictEqual(server.log.info.lastCall[0].status, 200)
|
assert.strictEqual(log.info.lastCall[0].status, 200)
|
||||||
assert.match(server.log.info.lastCall[1], /\<-- GET \//)
|
assert.match(log.info.lastCall[1], /\<-- GET \//)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should handle errors fine', async function() {
|
t.test('should handle errors fine', async function() {
|
||||||
resetLog()
|
resetLog()
|
||||||
assert.strictEqual(server.log.warn.callCount, 0)
|
assert.strictEqual(log.warn.callCount, 0)
|
||||||
let data = await assert.isRejected(client.get('/error'))
|
let data = await assert.isRejected(client.get('/error'))
|
||||||
|
|
||||||
assert.ok(data)
|
assert.ok(data)
|
||||||
assert.ok(data.body)
|
assert.ok(data.body)
|
||||||
assert.strictEqual(data.body.status, 500)
|
assert.strictEqual(data.body.status, 500)
|
||||||
assert.match(data.body.message, /test/)
|
assert.match(data.body.message, /test/)
|
||||||
assert.strictEqual(server.log.error.firstCall[0].message, 'This is a test')
|
assert.strictEqual(log.error.firstCall[0].message, 'This is a test')
|
||||||
assert.strictEqual(server.log.error.callCount, 2)
|
assert.strictEqual(log.error.callCount, 2)
|
||||||
assert.strictEqual(server.log.error.secondCall[0].status, 500)
|
assert.strictEqual(log.error.secondCall[0].status, 500)
|
||||||
assert.match(server.log.error.secondCall[1], /\<-- 500 GET \/error/)
|
assert.match(log.error.secondCall[1], /\<-- 500 GET \/error/)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue