From 666c4bee8903b97ccd7e59f668387be35bed8cac Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Sat, 13 Aug 2022 21:52:45 +0000 Subject: [PATCH] Use service-core --- api/config.default.json | 22 ---- api/config.mjs | 92 +++++++-------- api/error.mjs | 7 -- api/log.mjs | 22 ---- api/media/formidable.mjs | 12 +- api/media/routes.mjs | 37 ++++-- api/media/security.mjs | 22 ++-- api/server.mjs | 151 +++++++++++++----------- api/test/routes.mjs | 5 + config/config.test.json | 23 ---- dev.mjs | 34 ++++++ index.mjs | 11 ++ package.json | 17 ++- test.js | 27 ----- test.json | 1 - test/helper.server.mjs | 57 +++++++-- test/media/api.test.mjs | 216 ++++++++++++++++++----------------- test/media/routes.test.mjs | 2 +- test/media/security.test.mjs | 49 +++++--- test/server.test.mjs | 22 ++-- 20 files changed, 436 insertions(+), 393 deletions(-) delete mode 100644 api/config.default.json delete mode 100644 api/error.mjs delete mode 100644 api/log.mjs delete mode 100644 config/config.test.json create mode 100644 dev.mjs create mode 100644 index.mjs delete mode 100644 test.js delete mode 100644 test.json diff --git a/api/config.default.json b/api/config.default.json deleted file mode 100644 index afbec0b..0000000 --- a/api/config.default.json +++ /dev/null @@ -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 -} diff --git a/api/config.mjs b/api/config.mjs index c7329ce..441acdc 100644 --- a/api/config.mjs +++ b/api/config.mjs @@ -1,67 +1,61 @@ import fs from 'fs' -import path from 'path' -import { fileURLToPath } from 'url' import Nconf from 'nconf-lite' +import { Util } from 'service-core' 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. nconf.inTest = () => nconf.get('NODE_ENV') === 'test' // Config follow the following priority check order: -// 1. package.json -// 2. Enviroment variables +// 1. Enviroment variables +// 2. package.json // 3. config/config.json // 4. config/config.default.json - -pckg = { - name: pckg.name, - version: pckg.version, - description: pckg.description, - author: pckg.author, - license: pckg.license, - homepage: pckg.homepage, -} +// Load enviroment variables as first priority +nconf.env({ + separator: '__', + whitelist: [ + 'NODE_ENV', + 'server__port', + 'server__host', + 'bunyan__name', + 'frontend__url', + 'fileSize', + 'name', + 'NODE_VERSION', + ], + parseValues: true, +}) -// Load overrides as first priority -nconf.overrides(pckg) +// Load empty overrides that can be overwritten later +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.env() - - -// Load any overrides from the appropriate config file -let configFile = '../config/config.json' - -/* istanbul ignore else */ -if (nconf.get('NODE_ENV') === 'test') { - configFile = '../config/config.test.json' -} - -/* istanbul ignore if */ -if (nconf.get('NODE_ENV') === 'production') { - configFile = '../config/config.production.json' -} - -nconf.file('main', path.resolve(path.join(__dirname, configFile))) - -// Load defaults -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) -} - +nconf.defaults({ + "name": pckg.name, + "version": pckg.version, + "NODE_ENV": "development", + "server": { + "port": 4040, + "host": "0.0.0.0" + }, + "bunyan": { + "name": "storage-upload", + "streams": [{ + "stream": "process.stdout", + "level": "debug" + } + ] + }, + "sites": { + }, + "uploadFolder": "./public", + "fileSize": 524288000 +}) export default nconf diff --git a/api/error.mjs b/api/error.mjs deleted file mode 100644 index 89dbc52..0000000 --- a/api/error.mjs +++ /dev/null @@ -1,7 +0,0 @@ - -export class HttpError extends Error { - constructor(message, status = 500) { - super(message) - this.status = status - } -} diff --git a/api/log.mjs b/api/log.mjs deleted file mode 100644 index aa6d7e5..0000000 --- a/api/log.mjs +++ /dev/null @@ -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 diff --git a/api/media/formidable.mjs b/api/media/formidable.mjs index e9dba0a..cdb01c8 100644 --- a/api/media/formidable.mjs +++ b/api/media/formidable.mjs @@ -1,5 +1,5 @@ import fs from 'fs' -import { HttpError } from '../error.mjs' +import { HttpError } from 'flaska' import formidable from 'formidable' import config from '../config.mjs' @@ -36,12 +36,12 @@ export function uploadFile(ctx, siteName, noprefix = false) { let prefix = '' var form = new formidable.IncomingForm() - form.uploadDir = `./public/${siteName}` + form.uploadDir = `${config.get('uploadFolder')}/${siteName}` form.maxFileSize = config.get('fileSize') form.parse(ctx.req, function(err, fields, files) { 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 Object.keys(fields).forEach(function(key) { @@ -51,13 +51,13 @@ export function uploadFile(ctx, siteName, noprefix = false) { }) 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() } - 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) - file.path = `./public/${siteName}/${prefix}${file.name}` + file.path = `${config.get('uploadFolder')}/${siteName}/${prefix}${file.name}` file.filename = `${prefix}${file.name}` return res(file) diff --git a/api/media/routes.mjs b/api/media/routes.mjs index d3dd6e8..900d204 100644 --- a/api/media/routes.mjs +++ b/api/media/routes.mjs @@ -1,7 +1,8 @@ import path from 'path' import sharp from 'sharp' 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 formidable from './formidable.mjs' @@ -17,13 +18,27 @@ export default class MediaRoutes { 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() { - return fs.readdir('./public').then(folders => { + return fs.readdir(config.get('uploadFolder')).then(folders => { return Promise.all(folders.map(folder => { - return fs.readdir('./public/' + folder) + return fs.readdir(config.get('uploadFolder') + '/' + folder) .then(files => { return Promise.all(files.map(file => { - return fs.stat(`./public/${folder}/${file}`) + return fs.stat(`${config.get('uploadFolder')}/${folder}/${file}`) .then(function(stat) { return { filename: file, size: stat.size } }) @@ -94,7 +109,7 @@ export default class MediaRoutes { 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) ctx.body = { @@ -124,7 +139,7 @@ export default class MediaRoutes { return Promise.resolve() .then(async () => { 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() for (let operation of allowedOperations) { @@ -147,9 +162,9 @@ export default class MediaRoutes { } 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) ctx.body[key] = { @@ -159,7 +174,7 @@ export default class MediaRoutes { }).then( function() {}, 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) - await this.fs.unlink(`./public/${site}/${ctx.params.filename}`) + await this.fs.unlink(`${config.get('uploadFolder')}/${site}/${ctx.params.filename}`) .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 diff --git a/api/media/security.mjs b/api/media/security.mjs index 36df233..2185cdb 100644 --- a/api/media/security.mjs +++ b/api/media/security.mjs @@ -1,11 +1,11 @@ -import { HttpError } from '../error.mjs' +import { HttpError } from 'flaska' import decode from '../jwt/decode.mjs' import config from '../config.mjs' export function verifyToken(ctx) { let token = ctx.query.get('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') @@ -21,14 +21,14 @@ export function verifyToken(ctx) { return decoded.iss } catch (err) { 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) { let sites = config.get('sites') 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) { 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] if (typeof(item) !== 'object' || !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' || !item.format || validObjectOperations.includes(item.format) || 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' || !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 (typeof(item.out) !== 'string' || (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 (typeof(item[operation]) !== 'object' || 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) { if (item[operation] != null) { 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`) } } } diff --git a/api/server.mjs b/api/server.mjs index 42416f2..28f7013 100644 --- a/api/server.mjs +++ b/api/server.mjs @@ -5,82 +5,97 @@ import TestRoutes from './test/routes.mjs' import MediaRoutes from './media/routes.mjs' import config from './config.mjs' -import log from './log.mjs' -const app = new Flaska({ - log: log, -}) +export default class Server { + constructor(http, port, core, opts = {}) { + Object.assign(this, opts) + this.http = http + this.port = port + this.core = core -app.before(function(ctx) { - ctx.__started = performance.now() - ctx.log = ctx.log.child({ - ip: ctx.req.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress, - }) -}) + this.jsonHandler = JsonHandler + this.queryHandler = QueryHandler -app.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}`) -}) - -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) + this.flaskOptions = { + log: this.core.log, } - } 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, + this.routes = { + test: new TestRoutes(), + media: new MediaRoutes(), } } -}) -const test = new TestRoutes() -app.get('/', test.static.bind(test)) -app.get('/error', test.error.bind(test)) + run() { + // Create our server + this.flaska = new Flaska(this.flaskOptions, this.http) + + // configure our server + if (config.get('NODE_ENV') === 'development') { + this.flaska.devMode() + } + + this.flaska.before(function(ctx) { + ctx.__started = performance.now() + ctx.log = ctx.log.child({ + ip: ctx.req.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress, + }) + }) + + 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, + } + } + }) -const media = new MediaRoutes() -media.init().then(function() {}, function(err) { - log.error(err, 'Error initing media') -}) -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)) + // Register our routes + let keys = Object.keys(this.routes) + for (let key of keys) { + this.routes[key].register(this) + } -app.listen(config.get('server:port'), function(a,b) { - log.info(`Server listening at ${config.get('server:port')}`) -}) + // Start listening -export default app + return this.flaska.listenAsync(this.port).then(() => { + this.core.log.info(`Server is listening on port ${this.port} uploading to ${config.get('uploadFolder')}`) + }) + } +} diff --git a/api/test/routes.mjs b/api/test/routes.mjs index 648e58f..eb48507 100644 --- a/api/test/routes.mjs +++ b/api/test/routes.mjs @@ -5,6 +5,11 @@ export default class TestRoutes { Object.assign(this, { }) } + register(server) { + server.flaska.get('/', this.static.bind(this)) + server.flaska.get('/error', this.error.bind(this)) + } + static(ctx) { ctx.body = { name: config.get('name'), diff --git a/config/config.test.json b/config/config.test.json deleted file mode 100644 index e213c70..0000000 --- a/config/config.test.json +++ /dev/null @@ -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" - } - } - } -} diff --git a/dev.mjs b/dev.mjs new file mode 100644 index 0000000..0653c69 --- /dev/null +++ b/dev.mjs @@ -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() +}) diff --git a/index.mjs b/index.mjs new file mode 100644 index 0000000..4a8dd8e --- /dev/null +++ b/index.mjs @@ -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() + }) +} diff --git a/package.json b/package.json index 68c18cc..92a1017 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,20 @@ "start:bunyan": "node api/server.mjs | bunyan", "test": "set 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": { + "dev:server": { + "patterns": [ + "api/*", + "{index,dev}.mjs" + ], + "extensions": "js,mjs", + "quiet": true, + "inherit": true + }, "test": { "patterns": [ "{api,test}/*" @@ -31,13 +42,13 @@ }, "homepage": "https://github.com/nfp-projects/storage-upload#readme", "dependencies": { - "bunyan-lite": "^1.2.0", "flaska": "^1.2.3", "formidable": "^1.2.2", "nconf-lite": "^2.0.0", "sharp": "^0.30.3" }, "devDependencies": { - "eltro": "^1.2.3" + "eltro": "^1.2.3", + "service-core": "^3.0.0" } } diff --git a/test.js b/test.js deleted file mode 100644 index fec7759..0000000 --- a/test.js +++ /dev/null @@ -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() \ No newline at end of file diff --git a/test.json b/test.json deleted file mode 100644 index 44e5709..0000000 --- a/test.json +++ /dev/null @@ -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} \ No newline at end of file diff --git a/test/helper.server.mjs b/test/helper.server.mjs index 1455c84..248ff2c 100644 --- a/test/helper.server.mjs +++ b/test/helper.server.mjs @@ -1,29 +1,66 @@ import { stub } from 'eltro' +import { ServiceCore } from 'service-core' import Client from './helper.client.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(), warn: stub(), info: stub(), error: 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() { - return new Client() + return new Client(port) } export function resetLog() { - serv.log.log.reset() - serv.log.info.reset() - serv.log.warn.reset() - serv.log.error.reset() + log.log.reset() + log.info.reset() + log.warn.reset() + log.error.reset() } export function createContext(opts) { diff --git a/test/media/api.test.mjs b/test/media/api.test.mjs index ab2ed0d..4dc1565 100644 --- a/test/media/api.test.mjs +++ b/test/media/api.test.mjs @@ -4,8 +4,7 @@ import fs from 'fs/promises' import { fileURLToPath } from 'url' import path from 'path' -import { server, resetLog } from '../helper.server.mjs' -import Client from '../helper.client.mjs' +import { createClient, startServer, log, resetLog } from '../helper.server.mjs' import encode from '../../api/jwt/encode.mjs' let __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -17,10 +16,15 @@ function resolve(file) { const currYear = new Date().getFullYear().toString() t.describe('Media (API)', () => { - const client = new Client() - const secret = 'asdf1234' + let client + let secret = 'asdf1234' let testFiles = [] + t.before(function() { + client = createClient() + return startServer() + }) + t.after(function() { return Promise.all(testFiles.map(function(file) { 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.test('should require authentication', async () => { resetLog() - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 0) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 0) let err = await assert.isRejected( client.upload('/media', resolve('test.png') @@ -45,19 +49,19 @@ t.describe('Media (API)', () => { assert.match(err.message, /[Tt]oken/) assert.match(err.message, /[Mm]issing/) - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 2) - assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string') - assert.match(server.log.warn.firstCall[0], /[Tt]oken/) - assert.match(server.log.warn.firstCall[0], /[Mm]issing/) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 2) + assert.strictEqual(typeof(log.warn.firstCall[0]), 'string') + assert.match(log.warn.firstCall[0], /[Tt]oken/) + assert.match(log.warn.firstCall[0], /[Mm]issing/) }) t.test('should verify token correctly', async () => { const assertToken = 'asdf.asdf.asdf' resetLog() - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 0) - assert.strictEqual(server.log.info.callCount, 0) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 0) + assert.strictEqual(log.info.callCount, 0) let err = await assert.isRejected( client.upload('/media?token=' + assertToken, @@ -69,13 +73,13 @@ t.describe('Media (API)', () => { assert.match(err.message, /[Tt]oken/) assert.match(err.message, /[Ii]nvalid/) - assert.strictEqual(server.log.error.callCount, 1) - assert.strictEqual(server.log.warn.callCount, 2) - assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string') - assert.match(server.log.warn.firstCall[0], /[Tt]oken/) - assert.match(server.log.warn.firstCall[0], /[Ii]nvalid/) - assert.ok(server.log.error.lastCall[0] instanceof Error) - assert.match(server.log.error.lastCall[1], new RegExp(assertToken)) + assert.strictEqual(log.error.callCount, 1) + assert.strictEqual(log.warn.callCount, 2) + assert.strictEqual(typeof(log.warn.firstCall[0]), 'string') + assert.match(log.warn.firstCall[0], /[Tt]oken/) + assert.match(log.warn.firstCall[0], /[Ii]nvalid/) + assert.ok(log.error.lastCall[0] instanceof Error) + assert.match(log.error.lastCall[1], new RegExp(assertToken)) }) 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.test('should require authentication', async () => { resetLog() - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 0) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 0) let err = await assert.isRejected( client.upload('/media/noprefix', resolve('test.png') @@ -123,19 +127,19 @@ t.describe('Media (API)', () => { assert.match(err.message, /[Tt]oken/) assert.match(err.message, /[Mm]issing/) - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 2) - assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string') - assert.match(server.log.warn.firstCall[0], /[Tt]oken/) - assert.match(server.log.warn.firstCall[0], /[Mm]issing/) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 2) + assert.strictEqual(typeof(log.warn.firstCall[0]), 'string') + assert.match(log.warn.firstCall[0], /[Tt]oken/) + assert.match(log.warn.firstCall[0], /[Mm]issing/) }) t.test('should verify token correctly', async () => { const assertToken = 'asdf.asdf.asdf' resetLog() - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 0) - assert.strictEqual(server.log.info.callCount, 0) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 0) + assert.strictEqual(log.info.callCount, 0) let err = await assert.isRejected( client.upload('/media/noprefix?token=' + assertToken, @@ -147,13 +151,13 @@ t.describe('Media (API)', () => { assert.match(err.message, /[Tt]oken/) assert.match(err.message, /[Ii]nvalid/) - assert.strictEqual(server.log.error.callCount, 1) - assert.strictEqual(server.log.warn.callCount, 2) - assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string') - assert.match(server.log.warn.firstCall[0], /[Tt]oken/) - assert.match(server.log.warn.firstCall[0], /[Ii]nvalid/) - assert.ok(server.log.error.lastCall[0] instanceof Error) - assert.match(server.log.error.lastCall[1], new RegExp(assertToken)) + assert.strictEqual(log.error.callCount, 1) + assert.strictEqual(log.warn.callCount, 2) + assert.strictEqual(typeof(log.warn.firstCall[0]), 'string') + assert.match(log.warn.firstCall[0], /[Tt]oken/) + assert.match(log.warn.firstCall[0], /[Ii]nvalid/) + assert.ok(log.error.lastCall[0] instanceof Error) + assert.match(log.error.lastCall[1], new RegExp(assertToken)) }) 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.test('should require authentication', async () => { resetLog() - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 0) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 0) let err = await assert.isRejected( client.upload('/media/resize', resolve('test.png') @@ -239,19 +243,19 @@ t.describe('Media (API)', () => { assert.match(err.message, /[Tt]oken/) assert.match(err.message, /[Mm]issing/) - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 2) - assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string') - assert.match(server.log.warn.firstCall[0], /[Tt]oken/) - assert.match(server.log.warn.firstCall[0], /[Mm]issing/) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 2) + assert.strictEqual(typeof(log.warn.firstCall[0]), 'string') + assert.match(log.warn.firstCall[0], /[Tt]oken/) + assert.match(log.warn.firstCall[0], /[Mm]issing/) }) t.test('should verify token correctly', async () => { const assertToken = 'asdf.asdf.asdf' resetLog() - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 0) - assert.strictEqual(server.log.info.callCount, 0) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 0) + assert.strictEqual(log.info.callCount, 0) let err = await assert.isRejected( client.upload('/media/resize?token=' + assertToken, @@ -263,13 +267,13 @@ t.describe('Media (API)', () => { assert.match(err.message, /[Tt]oken/) assert.match(err.message, /[Ii]nvalid/) - assert.strictEqual(server.log.error.callCount, 1) - assert.strictEqual(server.log.warn.callCount, 2) - assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string') - assert.match(server.log.warn.firstCall[0], /[Tt]oken/) - assert.match(server.log.warn.firstCall[0], /[Ii]nvalid/) - assert.ok(server.log.error.lastCall[0] instanceof Error) - assert.match(server.log.error.lastCall[1], new RegExp(assertToken)) + assert.strictEqual(log.error.callCount, 1) + assert.strictEqual(log.warn.callCount, 2) + assert.strictEqual(typeof(log.warn.firstCall[0]), 'string') + assert.match(log.warn.firstCall[0], /[Tt]oken/) + assert.match(log.warn.firstCall[0], /[Ii]nvalid/) + assert.ok(log.error.lastCall[0] instanceof Error) + assert.match(log.error.lastCall[1], new RegExp(assertToken)) }) t.test('should upload file and create file', async () => { @@ -458,8 +462,8 @@ t.describe('Media (API)', () => { t.test('should require authentication', async () => { resetLog() - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 0) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 0) let err = await assert.isRejected( client.post(`/media/resize/${sourceFilename}`, {}) ) @@ -468,19 +472,19 @@ t.describe('Media (API)', () => { assert.match(err.message, /[Tt]oken/) assert.match(err.message, /[Mm]issing/) - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 2) - assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string') - assert.match(server.log.warn.firstCall[0], /[Tt]oken/) - assert.match(server.log.warn.firstCall[0], /[Mm]issing/) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 2) + assert.strictEqual(typeof(log.warn.firstCall[0]), 'string') + assert.match(log.warn.firstCall[0], /[Tt]oken/) + assert.match(log.warn.firstCall[0], /[Mm]issing/) }) t.test('should verify token correctly', async () => { const assertToken = 'asdf.asdf.asdf' resetLog() - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 0) - assert.strictEqual(server.log.info.callCount, 0) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 0) + assert.strictEqual(log.info.callCount, 0) let err = await assert.isRejected( 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, /[Ii]nvalid/) - assert.strictEqual(server.log.error.callCount, 1) - assert.strictEqual(server.log.warn.callCount, 2) - assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string') - assert.match(server.log.warn.firstCall[0], /[Tt]oken/) - assert.match(server.log.warn.firstCall[0], /[Ii]nvalid/) - assert.ok(server.log.error.lastCall[0] instanceof Error) - assert.match(server.log.error.lastCall[1], new RegExp(assertToken)) + assert.strictEqual(log.error.callCount, 1) + assert.strictEqual(log.warn.callCount, 2) + assert.strictEqual(typeof(log.warn.firstCall[0]), 'string') + assert.match(log.warn.firstCall[0], /[Tt]oken/) + assert.match(log.warn.firstCall[0], /[Ii]nvalid/) + assert.ok(log.error.lastCall[0] instanceof Error) + assert.match(log.error.lastCall[1], new RegExp(assertToken)) }) 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.test('should require authentication', async () => { resetLog() - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 0) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 0) let err = await assert.isRejected( client.del('/media/20220105_101610_test1.jpg', resolve('test.png') @@ -608,19 +612,19 @@ t.describe('Media (API)', () => { assert.match(err.message, /[Tt]oken/) assert.match(err.message, /[Mm]issing/) - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 2) - assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string') - assert.match(server.log.warn.firstCall[0], /[Tt]oken/) - assert.match(server.log.warn.firstCall[0], /[Mm]issing/) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 2) + assert.strictEqual(typeof(log.warn.firstCall[0]), 'string') + assert.match(log.warn.firstCall[0], /[Tt]oken/) + assert.match(log.warn.firstCall[0], /[Mm]issing/) }) t.test('should verify token correctly', async () => { const assertToken = 'asdf.asdf.asdf' resetLog() - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 0) - assert.strictEqual(server.log.info.callCount, 0) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 0) + assert.strictEqual(log.info.callCount, 0) let err = await assert.isRejected( 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, /[Ii]nvalid/) - assert.strictEqual(server.log.error.callCount, 1) - assert.strictEqual(server.log.warn.callCount, 2) - assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string') - assert.match(server.log.warn.firstCall[0], /[Tt]oken/) - assert.match(server.log.warn.firstCall[0], /[Ii]nvalid/) - assert.ok(server.log.error.lastCall[0] instanceof Error) - assert.match(server.log.error.lastCall[1], new RegExp(assertToken)) + assert.strictEqual(log.error.callCount, 1) + assert.strictEqual(log.warn.callCount, 2) + assert.strictEqual(typeof(log.warn.firstCall[0]), 'string') + assert.match(log.warn.firstCall[0], /[Tt]oken/) + assert.match(log.warn.firstCall[0], /[Ii]nvalid/) + assert.ok(log.error.lastCall[0] instanceof Error) + assert.match(log.error.lastCall[1], new RegExp(assertToken)) }) t.test('should remove the file', async () => { @@ -691,27 +695,27 @@ t.describe('Media (API)', () => { t.describe('GET /media', function() { t.test('should require authentication', async () => { resetLog() - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 0) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 0) let err = await assert.isRejected(client.get('/media')) assert.strictEqual(err.status, 422) assert.match(err.message, /[Tt]oken/) assert.match(err.message, /[Mm]issing/) - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 2) - assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string') - assert.match(server.log.warn.firstCall[0], /[Tt]oken/) - assert.match(server.log.warn.firstCall[0], /[Mm]issing/) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 2) + assert.strictEqual(typeof(log.warn.firstCall[0]), 'string') + assert.match(log.warn.firstCall[0], /[Tt]oken/) + assert.match(log.warn.firstCall[0], /[Mm]issing/) }) t.test('should verify token correctly', async () => { const assertToken = 'asdf.asdf.asdf' resetLog() - assert.strictEqual(server.log.error.callCount, 0) - assert.strictEqual(server.log.warn.callCount, 0) - assert.strictEqual(server.log.info.callCount, 0) + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.warn.callCount, 0) + assert.strictEqual(log.info.callCount, 0) 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, /[Ii]nvalid/) - assert.strictEqual(server.log.error.callCount, 1) - assert.strictEqual(server.log.warn.callCount, 2) - assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string') - assert.match(server.log.warn.firstCall[0], /[Tt]oken/) - assert.match(server.log.warn.firstCall[0], /[Ii]nvalid/) - assert.ok(server.log.error.lastCall[0] instanceof Error) - assert.match(server.log.error.lastCall[1], new RegExp(assertToken)) + assert.strictEqual(log.error.callCount, 1) + assert.strictEqual(log.warn.callCount, 2) + assert.strictEqual(typeof(log.warn.firstCall[0]), 'string') + assert.match(log.warn.firstCall[0], /[Tt]oken/) + assert.match(log.warn.firstCall[0], /[Ii]nvalid/) + assert.ok(log.error.lastCall[0] instanceof Error) + assert.match(log.error.lastCall[1], new RegExp(assertToken)) }) t.test('should return list of files in specified folder', async () => { diff --git a/test/media/routes.test.mjs b/test/media/routes.test.mjs index 33d6e4b..d7736b6 100644 --- a/test/media/routes.test.mjs +++ b/test/media/routes.test.mjs @@ -1,9 +1,9 @@ import fs from 'fs/promises' import { Eltro as t, assert, stub } from 'eltro' +import { HttpError } from 'flaska' import { createContext } from '../helper.server.mjs' import MediaRoutes from '../../api/media/routes.mjs' -import { HttpError } from '../../api/error.mjs' t.before(function() { return Promise.all([ diff --git a/test/media/security.test.mjs b/test/media/security.test.mjs index cf4821a..7cb261c 100644 --- a/test/media/security.test.mjs +++ b/test/media/security.test.mjs @@ -1,23 +1,32 @@ import { Eltro as t, assert} from 'eltro' +import { HttpError } from 'flaska' import { createContext } from '../helper.server.mjs' import { verifyToken, verifyBody, throwIfNotPublic } from '../../api/media/security.mjs' -import { HttpError } from '../../api/error.mjs' import encode from '../../api/jwt/encode.mjs' import config from '../../api/config.mjs' t.describe('#throwIfNotPublic()', function() { + let backup = {} + t.before(function() { - config.set('sites', { - justatest: { + backup = config.sources[1].store + config.sources[1].store = { + sites: { + justatest: { + }, + justatest2: { + public: false, + }, + justatest3: { + public: true, + }, }, - justatest2: { - public: false, - }, - justatest3: { - public: true, - }, - }) + } + }) + + t.after(function() { + config.sources[1].store = backup }) 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() { + let backup = {} t.before(function() { - config.set('sites', { - justatest: { - keys: { - 'default@HS512': 'mysharedkey', - } + backup = config.sources[1].store + config.sources[1].store = { + sites: { + justatest: { + keys: { + 'default@HS512': 'mysharedkey', + } + }, }, - }) + } + }) + + t.after(function() { + config.sources[1].store = backup }) t.test('should fail if query token is missing', function() { diff --git a/test/server.test.mjs b/test/server.test.mjs index aba1000..9d9ef18 100644 --- a/test/server.test.mjs +++ b/test/server.test.mjs @@ -1,39 +1,41 @@ 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() { let client t.before(function() { client = createClient() + + return startServer() }) t.test('should run', async function() { resetLog() - assert.strictEqual(server.log.info.callCount, 0) + assert.strictEqual(log.info.callCount, 0) let data = await client.get('/') assert.ok(data) assert.ok(data.name) assert.ok(data.version) - assert.strictEqual(server.log.info.callCount, 1) - assert.strictEqual(server.log.info.lastCall[0].status, 200) - assert.match(server.log.info.lastCall[1], /\<-- GET \//) + assert.strictEqual(log.info.callCount, 1) + assert.strictEqual(log.info.lastCall[0].status, 200) + assert.match(log.info.lastCall[1], /\<-- GET \//) }) t.test('should handle errors fine', async function() { resetLog() - assert.strictEqual(server.log.warn.callCount, 0) + assert.strictEqual(log.warn.callCount, 0) let data = await assert.isRejected(client.get('/error')) assert.ok(data) assert.ok(data.body) assert.strictEqual(data.body.status, 500) assert.match(data.body.message, /test/) - assert.strictEqual(server.log.error.firstCall[0].message, 'This is a test') - assert.strictEqual(server.log.error.callCount, 2) - assert.strictEqual(server.log.error.secondCall[0].status, 500) - assert.match(server.log.error.secondCall[1], /\<-- 500 GET \/error/) + assert.strictEqual(log.error.firstCall[0].message, 'This is a test') + assert.strictEqual(log.error.callCount, 2) + assert.strictEqual(log.error.secondCall[0].status, 500) + assert.match(log.error.secondCall[1], /\<-- 500 GET \/error/) }) })