Use service-core

This commit is contained in:
Jonatan Nilsson 2022-08-13 21:52:45 +00:00
parent 9a0fca4a22
commit 666c4bee89
20 changed files with 436 additions and 393 deletions

View file

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

View file

@ -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

View file

@ -1,7 +0,0 @@
export class HttpError extends Error {
constructor(message, status = 500) {
super(message)
this.status = status
}
}

View file

@ -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

View file

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

View file

@ -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

View file

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

View file

@ -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')}`)
})
}
}

View file

@ -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'),

View file

@ -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
View 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
View 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()
})
}

View file

@ -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"
}
}

27
test.js
View file

@ -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()

View file

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

View file

@ -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) {

View file

@ -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 () => {

View file

@ -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([

View file

@ -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() {

View file

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