Finished unit test, refactored to use flaska instead of koa
This commit is contained in:
parent
2b53b48a5d
commit
11c1133696
19 changed files with 422 additions and 211 deletions
|
@ -1,11 +1,18 @@
|
||||||
|
|
||||||
|
export class HttpError extends Error {
|
||||||
|
constructor(message, status = 500) {
|
||||||
|
super(message)
|
||||||
|
this.status = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function errorMiddleware(ctx, next) {
|
export async function errorMiddleware(ctx, next) {
|
||||||
try {
|
try {
|
||||||
await next()
|
await next()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.status = 422
|
ctx.status = e.status || 500
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
status: 422,
|
status: ctx.status,
|
||||||
message: e.message,
|
message: e.message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,17 +2,24 @@ import crypto from 'crypto'
|
||||||
import * as base64UrlSafe from './base64urlsafe.mjs'
|
import * as base64UrlSafe from './base64urlsafe.mjs'
|
||||||
import defaults from '../defaults.mjs'
|
import defaults from '../defaults.mjs'
|
||||||
|
|
||||||
export default function encode(header, orgBody, privateKeyPassword = null, disableAutofill = false) {
|
export default function encode(orgHeader, orgBody, privateKeyPassword = null, disableAutofill = false) {
|
||||||
let body = defaults(orgBody)
|
|
||||||
if (
|
if (
|
||||||
typeof header !== 'object' ||
|
typeof orgHeader !== 'object' ||
|
||||||
Array.isArray(header) ||
|
typeof orgBody !== 'object'
|
||||||
typeof body !== 'object' ||
|
|
||||||
Array.isArray(body)
|
|
||||||
) {
|
) {
|
||||||
throw new Error('both header and body should be of type object')
|
throw new Error('both header and body should be of type object')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let header = defaults(orgHeader)
|
||||||
|
let body = defaults(orgBody)
|
||||||
|
|
||||||
|
if (!header.alg) {
|
||||||
|
header.alg = 'HS256'
|
||||||
|
}
|
||||||
|
if (!header.typ) {
|
||||||
|
header.typ = 'JWT'
|
||||||
|
}
|
||||||
|
|
||||||
let hmacAlgo = null
|
let hmacAlgo = null
|
||||||
switch (header.alg) {
|
switch (header.alg) {
|
||||||
case 'HS256':
|
case 'HS256':
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
import { verifyToken } from './security.mjs'
|
import * as security from './security.mjs'
|
||||||
import { uploadFile } from './multer.mjs'
|
import * as multer from './multer.mjs'
|
||||||
|
|
||||||
export async function upload(ctx) {
|
export default class MediaRoutes {
|
||||||
let site = await verifyToken(ctx)
|
constructor(opts = {}) {
|
||||||
|
Object.assign(this, {
|
||||||
|
security: opts.security || security,
|
||||||
|
multer: opts.multer || multer,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let result = await uploadFile(ctx, site)
|
async upload(ctx) {
|
||||||
|
let site = await this.security.verifyToken(ctx)
|
||||||
|
|
||||||
ctx.body = {
|
let result = await this.multer.uploadFile(ctx, site)
|
||||||
filename: result.filename,
|
|
||||||
path: `/${site}/${result.filename}`
|
ctx.body = {
|
||||||
|
filename: result.filename,
|
||||||
|
path: `/${site}/${result.filename}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
// import * as jwt from '../jwt.mjs'
|
import { HttpError } from '../error.mjs'
|
||||||
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) {
|
||||||
if (!ctx.query.token) {
|
let token = ctx.query.get('token')
|
||||||
throw new Error('Token is missing in query')
|
if (!token) {
|
||||||
|
throw new HttpError('Token is missing in query', 422)
|
||||||
}
|
}
|
||||||
|
|
||||||
let decoded = decode(ctx.query.token, config.get('sites'), [])
|
try {
|
||||||
|
let decoded = decode(token, config.get('sites'), [])
|
||||||
return decoded.iss
|
return decoded.iss
|
||||||
|
} catch (err) {
|
||||||
|
ctx.log.error(err, 'Error decoding token: ' + token)
|
||||||
|
throw new HttpError('Token was invalid', 422)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import Router from 'koa-router'
|
|
||||||
|
|
||||||
import * as test from './test/routes.mjs'
|
|
||||||
import * as media from './media/routes.mjs'
|
|
||||||
|
|
||||||
const router = new Router()
|
|
||||||
|
|
||||||
router.get('/', test.testStatic)
|
|
||||||
router.get('/error', test.testError)
|
|
||||||
router.post('/media', media.upload)
|
|
||||||
|
|
||||||
export default router
|
|
|
@ -1,7 +1,14 @@
|
||||||
|
import { performance } from 'perf_hooks'
|
||||||
import Koa from 'koa-lite'
|
import Koa from 'koa-lite'
|
||||||
|
import { Flaska, QueryHandler } from 'flaska'
|
||||||
|
|
||||||
|
import TestRoutes from './test/routes.mjs'
|
||||||
|
import MediaRoutes from './media/routes.mjs'
|
||||||
|
|
||||||
import config from './config.mjs'
|
import config from './config.mjs'
|
||||||
import log from './log.mjs'
|
import log from './log.mjs'
|
||||||
|
|
||||||
|
/*
|
||||||
import router from './router.mjs'
|
import router from './router.mjs'
|
||||||
import { errorMiddleware } from './error.mjs'
|
import { errorMiddleware } from './error.mjs'
|
||||||
|
|
||||||
|
@ -15,4 +22,51 @@ const server = app.listen(config.get('server:port'), function(a,b) {
|
||||||
log.info(`Server listening at ${config.get('server:port')}`)
|
log.info(`Server listening at ${config.get('server:port')}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default server
|
export default server */
|
||||||
|
|
||||||
|
const app = new Flaska({
|
||||||
|
log: log,
|
||||||
|
})
|
||||||
|
|
||||||
|
app.before(function(ctx) {
|
||||||
|
ctx.__started = performance.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.after(function(ctx) {
|
||||||
|
let ended = performance.now() - ctx.__started
|
||||||
|
let logger = ctx.log.info
|
||||||
|
if (ctx.status >= 400) {
|
||||||
|
logger = ctx.log.warn
|
||||||
|
}
|
||||||
|
logger({
|
||||||
|
path: ctx.url,
|
||||||
|
status: ctx.status,
|
||||||
|
ms: Math.round(ended),
|
||||||
|
}, 'Request finished')
|
||||||
|
})
|
||||||
|
|
||||||
|
app.onerror(function(err, ctx) {
|
||||||
|
if (err.status && err.status >= 400 && err.status < 500) {
|
||||||
|
ctx.log.warn(err.message)
|
||||||
|
} else {
|
||||||
|
ctx.log.error(err)
|
||||||
|
}
|
||||||
|
ctx.status = err.status || 500
|
||||||
|
ctx.body = {
|
||||||
|
status: ctx.status,
|
||||||
|
message: err.message,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const test = new TestRoutes()
|
||||||
|
app.get('/', test.static.bind(test))
|
||||||
|
app.get('/error', test.error.bind(test))
|
||||||
|
|
||||||
|
const media = new MediaRoutes()
|
||||||
|
app.post('/media', [QueryHandler()], media.upload.bind(media))
|
||||||
|
|
||||||
|
app.listen(config.get('server:port'), function(a,b) {
|
||||||
|
log.info(`Server listening at ${config.get('server:port')}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default app
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
import config from '../config.mjs'
|
import config from '../config.mjs'
|
||||||
|
|
||||||
export async function testStatic(ctx) {
|
export default class TestRoutes {
|
||||||
ctx.body = {
|
constructor(opts = {}) {
|
||||||
name: config.get('name'),
|
Object.assign(this, { })
|
||||||
version: config.get('version'),
|
}
|
||||||
environment: config.get('NODE_ENV'),
|
|
||||||
|
static(ctx) {
|
||||||
|
ctx.body = {
|
||||||
|
name: config.get('name'),
|
||||||
|
version: config.get('version'),
|
||||||
|
environment: config.get('NODE_ENV'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error(ctx) {
|
||||||
|
throw new Error('This is a test')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function testError(ctx) {
|
|
||||||
throw new Error('This is a test')
|
|
||||||
}
|
|
||||||
|
|
15
config/config.test.json
Normal file
15
config/config.test.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"bunyan": {
|
||||||
|
"name": "storage-upload-test",
|
||||||
|
"streams": [{
|
||||||
|
"stream": "process.stdout",
|
||||||
|
"level": "error"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sites": {
|
||||||
|
"development": {
|
||||||
|
"default@HS256": "asdf1234"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon index.js",
|
"dev": "nodemon index.js",
|
||||||
"start": "node --experimental-modules api/server.mjs",
|
"start": "node --experimental-modules api/server.mjs",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -20,12 +21,11 @@
|
||||||
"homepage": "https://github.com/nfp-projects/storage-upload#readme",
|
"homepage": "https://github.com/nfp-projects/storage-upload#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bunyan-lite": "^1.1.1",
|
"bunyan-lite": "^1.1.1",
|
||||||
"koa-lite": "^2.10.1",
|
"flaska": "^0.9.5",
|
||||||
"koa-router": "^7.2.1",
|
|
||||||
"multer": "^1.3.0",
|
"multer": "^1.3.0",
|
||||||
"nconf-lite": "^2.0.0"
|
"nconf-lite": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eltro": "^1.1.0"
|
"eltro": "^1.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1
test.json
Normal file
1
test.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"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,4 +1,6 @@
|
||||||
import http from 'http'
|
import http from 'http'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
import path from 'path'
|
||||||
import { URL } from 'url'
|
import { URL } from 'url'
|
||||||
import defaults from '../api/defaults.mjs'
|
import defaults from '../api/defaults.mjs'
|
||||||
import config from '../api/config.mjs'
|
import config from '../api/config.mjs'
|
||||||
|
@ -50,6 +52,7 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
|
||||||
if (output.status) {
|
if (output.status) {
|
||||||
let err = new Error(`Request failed [${output.status}]: ${output.message}`)
|
let err = new Error(`Request failed [${output.status}]: ${output.message}`)
|
||||||
err.body = output
|
err.body = output
|
||||||
|
err.status = output.status
|
||||||
return reject(err)
|
return reject(err)
|
||||||
}
|
}
|
||||||
resolve(output)
|
resolve(output)
|
||||||
|
@ -59,8 +62,37 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Client.prototype.get = function(path = '/') {
|
Client.prototype.get = function(url = '/') {
|
||||||
return this.customRequest('GET', path, null)
|
return this.customRequest('GET', url, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.prototype.upload = function(url, file, method = 'POST') {
|
||||||
|
return fs.readFile(file).then(data => {
|
||||||
|
const crlf = '\r\n'
|
||||||
|
const filename = path.basename(file)
|
||||||
|
const boundary = `---------${Math.random().toString(16)}`
|
||||||
|
const headers = [
|
||||||
|
`Content-Disposition: form-data; name="file"; filename="${filename}"` + crlf,
|
||||||
|
]
|
||||||
|
const multipartBody = Buffer.concat([
|
||||||
|
Buffer.from(
|
||||||
|
`${crlf}--${boundary}${crlf}` +
|
||||||
|
headers.join('') + crlf
|
||||||
|
),
|
||||||
|
data,
|
||||||
|
Buffer.from(
|
||||||
|
`${crlf}--${boundary}--`
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
return this.customRequest(method, url, multipartBody, {
|
||||||
|
timeout: 5000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data; boundary=' + boundary,
|
||||||
|
'Content-Length': multipartBody.length,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -1,17 +1,38 @@
|
||||||
// import _ from 'lodash'
|
import { stub } from 'eltro'
|
||||||
// import sinon from 'sinon'
|
|
||||||
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 '../api/server.mjs'
|
import serv from '../api/server.mjs'
|
||||||
|
|
||||||
|
serv.log = {
|
||||||
|
log: stub(),
|
||||||
|
warn: stub(),
|
||||||
|
info: stub(),
|
||||||
|
error: stub(),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const server = serv
|
||||||
|
|
||||||
export function createClient() {
|
export function createClient() {
|
||||||
return new Client()
|
return new Client()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resetLog() {
|
||||||
|
serv.log.log.reset()
|
||||||
|
serv.log.info.reset()
|
||||||
|
serv.log.warn.reset()
|
||||||
|
serv.log.error.reset()
|
||||||
|
}
|
||||||
|
|
||||||
export function createContext(opts) {
|
export function createContext(opts) {
|
||||||
return defaults(opts, {
|
return defaults(opts, {
|
||||||
query: { },
|
query: new Map(),
|
||||||
req: { },
|
req: { },
|
||||||
res: { },
|
res: { },
|
||||||
|
log: {
|
||||||
|
log: stub(),
|
||||||
|
warn: stub(),
|
||||||
|
info: stub(),
|
||||||
|
error: stub(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Eltro as t, assert} from 'eltro'
|
import { Eltro as t, assert} from 'eltro'
|
||||||
import encode from '../../api/jwt/encode.mjs'
|
import encode from '../../api/jwt/encode.mjs'
|
||||||
|
import decode from '../../api/jwt/decode.mjs'
|
||||||
|
|
||||||
t.describe('encode', function() {
|
t.describe('encode', function() {
|
||||||
t.test('should faile with invalid header and body', function() {
|
t.test('should faile with invalid header and body', function() {
|
||||||
|
@ -10,12 +11,18 @@ t.describe('encode', function() {
|
||||||
/both header and body should be of type object/
|
/both header and body should be of type object/
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
t.test('should faile with empty header and body', function() {
|
t.test('should faile with invalid alg type', function() {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
function() {
|
function() {
|
||||||
encode({}, {})
|
encode({ alg: 'asdf' }, {})
|
||||||
},
|
},
|
||||||
/Only alg HS256, HS384 and HS512 are supported/
|
/Only alg HS256, HS384 and HS512 are supported/
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
t.test('should have default header options', function() {
|
||||||
|
const assertTest = '1234'
|
||||||
|
let token = encode(null, { iss: 'test', test: assertTest }, 'bla')
|
||||||
|
let decoded = decode(token, { 'test': { 'default@HS256': 'bla' } }, [])
|
||||||
|
assert.strictEqual(decoded.test, assertTest)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
import { Eltro as t, assert} from 'eltro'
|
|
||||||
import fs from 'fs'
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
import createClient from '../helper.client.mjs'
|
|
||||||
import config from '../../api/config.mjs'
|
|
||||||
import jwt from '../../api/jwt.mjs'
|
|
||||||
|
|
||||||
let __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
||||||
|
|
||||||
console.log(__dirname)
|
|
||||||
|
|
||||||
t.describe('Media (API)', () => {
|
|
||||||
let testFile
|
|
||||||
let client = createClient()
|
|
||||||
|
|
||||||
t.before(() => {
|
|
||||||
config.set('sites', {
|
|
||||||
development: 'hello-world'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.after(done => {
|
|
||||||
if (testFile) {
|
|
||||||
// path.resolve(path.join(__dirname, 'fixtures', file))
|
|
||||||
return fs.unlink( appRoot.resolve(`/public${testFile}`), done)
|
|
||||||
}
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
|
|
||||||
t.describe('POST /media', function temp() {
|
|
||||||
this.timeout(10000)
|
|
||||||
|
|
||||||
t.test('should require authentication', async () => {
|
|
||||||
let err = await assert.isRejected(
|
|
||||||
client.sendFileAsync('/media',
|
|
||||||
appRoot.resolve('/test/media/test.png')))
|
|
||||||
|
|
||||||
assert.strictEqual(err.status, 422)
|
|
||||||
assert.match(err.message, /[Tt]oken/)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.test('should upload file and create file', async () => {
|
|
||||||
let token = jwt.sign({ site: 'development' }, 'hello-world')
|
|
||||||
|
|
||||||
let data = await assert.isFulfilled(
|
|
||||||
client.sendFileAsync(
|
|
||||||
`/media?token=${token}`,
|
|
||||||
appRoot.resolve('/test/media/test.png')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.ok(data)
|
|
||||||
assert.ok(data.filename)
|
|
||||||
assert.ok(data.path)
|
|
||||||
|
|
||||||
testFile = data.path
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
94
test/media/api.test.mjs
Normal file
94
test/media/api.test.mjs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import { Eltro as t, assert} from 'eltro'
|
||||||
|
import fs from 'fs'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
import { server, resetLog } from '../helper.server.mjs'
|
||||||
|
import Client from '../helper.client.mjs'
|
||||||
|
import config from '../../api/config.mjs'
|
||||||
|
import encode from '../../api/jwt/encode.mjs'
|
||||||
|
|
||||||
|
let __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
function resolve(file) {
|
||||||
|
return path.resolve(path.join(__dirname, file))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.describe('Media (API)', () => {
|
||||||
|
const client = new Client()
|
||||||
|
const secret = 'asdf1234'
|
||||||
|
let testFile
|
||||||
|
|
||||||
|
t.after(function(done) {
|
||||||
|
if (testFile) {
|
||||||
|
return fs.unlink(resolve(`../../public/${testFile}`), done)
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
let err = await assert.isRejected(
|
||||||
|
client.upload('/media',
|
||||||
|
resolve('test.png')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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/)
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
let err = await assert.isRejected(
|
||||||
|
client.upload('/media?token=' + assertToken,
|
||||||
|
resolve('test.png')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.strictEqual(err.status, 422)
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.test('should upload file and create file', async () => {
|
||||||
|
let token = encode(null, { iss: 'development' }, secret)
|
||||||
|
|
||||||
|
let data = await assert.isFulfilled(
|
||||||
|
client.upload(
|
||||||
|
`/media?token=${token}`,
|
||||||
|
resolve('test.png')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(data)
|
||||||
|
assert.ok(data.filename)
|
||||||
|
assert.ok(data.path)
|
||||||
|
|
||||||
|
testFile = data.path
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,73 +0,0 @@
|
||||||
import assert from 'assert-extended'
|
|
||||||
import sinon from 'sinon'
|
|
||||||
|
|
||||||
import { createContext } from '../helper.server'
|
|
||||||
|
|
||||||
describe('Media (Routes)', () => {
|
|
||||||
const multer = require('../../api/media/multer')
|
|
||||||
const routes = require('../../api/media/routes')
|
|
||||||
const security = require('../../api/media/security')
|
|
||||||
const config = require('../../config')
|
|
||||||
|
|
||||||
let sandbox
|
|
||||||
let ctx
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
sandbox = sinon.sandbox.create()
|
|
||||||
ctx = createContext({
|
|
||||||
req: {
|
|
||||||
file: { },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
sandbox.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#upload', () => {
|
|
||||||
let stubVerifyToken
|
|
||||||
let stubUpload
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
stubVerifyToken = sandbox.stub(security, 'verifyToken')
|
|
||||||
stubUpload = sandbox.stub(multer, 'uploadFile').resolves({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should call security correctly', async () => {
|
|
||||||
const assertError = new Error('temp')
|
|
||||||
stubVerifyToken.rejects(assertError)
|
|
||||||
|
|
||||||
let err = await assert.isRejected(routes.upload(ctx))
|
|
||||||
|
|
||||||
assert.ok(stubVerifyToken.called)
|
|
||||||
assert.strictEqual(err, assertError)
|
|
||||||
assert.strictEqual(stubVerifyToken.firstCall.args[0], ctx)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should call upload correctly', async () => {
|
|
||||||
const assertSiteName = 'benshapiro'
|
|
||||||
const assertError = new Error('hello')
|
|
||||||
stubVerifyToken.resolves(assertSiteName)
|
|
||||||
stubUpload.rejects(assertError)
|
|
||||||
|
|
||||||
let err = await assert.isRejected(routes.upload(ctx))
|
|
||||||
|
|
||||||
assert.ok(stubUpload.called)
|
|
||||||
assert.strictEqual(err, assertError)
|
|
||||||
assert.strictEqual(stubUpload.firstCall.args[0], ctx)
|
|
||||||
assert.strictEqual(stubUpload.firstCall.args[1], assertSiteName)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should otherwise set context status to 204 and file in result', async () => {
|
|
||||||
const assertFilename = 'asdfsafd'
|
|
||||||
const assertSite = 'mario'
|
|
||||||
stubVerifyToken.resolves(assertSite)
|
|
||||||
stubUpload.resolves({ filename: assertFilename })
|
|
||||||
await routes.upload(ctx)
|
|
||||||
|
|
||||||
assert.strictEqual(ctx.body.filename, assertFilename)
|
|
||||||
assert.strictEqual(ctx.body.path, `/${assertSite}/${assertFilename}`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
64
test/media/routes.test.mjs
Normal file
64
test/media/routes.test.mjs
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { Eltro as t, assert, stub } from 'eltro'
|
||||||
|
import { createContext } from '../helper.server.mjs'
|
||||||
|
|
||||||
|
import MediaRoutes from '../../api/media/routes.mjs'
|
||||||
|
|
||||||
|
t.describe('#upload', () => {
|
||||||
|
const stubVerify = stub()
|
||||||
|
const stubUpload = stub()
|
||||||
|
|
||||||
|
const routes = new MediaRoutes({
|
||||||
|
security: { verifyToken: stubVerify },
|
||||||
|
multer: { uploadFile: stubUpload },
|
||||||
|
})
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
stubVerify.reset()
|
||||||
|
stubUpload.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
t.test('should call security correctly', async () => {
|
||||||
|
reset()
|
||||||
|
|
||||||
|
let ctx = createContext()
|
||||||
|
const assertError = new Error('temp')
|
||||||
|
stubVerify.rejects(assertError)
|
||||||
|
|
||||||
|
let err = await assert.isRejected(routes.upload(ctx))
|
||||||
|
|
||||||
|
assert.ok(stubVerify.called)
|
||||||
|
assert.strictEqual(err, assertError)
|
||||||
|
assert.strictEqual(stubVerify.firstCall[0], ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.test('should call upload correctly', async () => {
|
||||||
|
reset()
|
||||||
|
|
||||||
|
let ctx = createContext()
|
||||||
|
const assertSiteName = 'benshapiro'
|
||||||
|
const assertError = new Error('hello')
|
||||||
|
stubVerify.resolves(assertSiteName)
|
||||||
|
stubUpload.rejects(assertError)
|
||||||
|
|
||||||
|
let err = await assert.isRejected(routes.upload(ctx))
|
||||||
|
|
||||||
|
assert.ok(stubUpload.called)
|
||||||
|
assert.strictEqual(err, assertError)
|
||||||
|
assert.strictEqual(stubUpload.firstCall[0], ctx)
|
||||||
|
assert.strictEqual(stubUpload.firstCall[1], assertSiteName)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.test('should otherwise set context status to 204 and file in result', async () => {
|
||||||
|
reset()
|
||||||
|
|
||||||
|
let ctx = createContext()
|
||||||
|
const assertFilename = 'asdfsafd'
|
||||||
|
const assertSite = 'mario'
|
||||||
|
stubVerify.resolves(assertSite)
|
||||||
|
stubUpload.resolves({ filename: assertFilename })
|
||||||
|
await routes.upload(ctx)
|
||||||
|
|
||||||
|
assert.strictEqual(ctx.body.filename, assertFilename)
|
||||||
|
assert.strictEqual(ctx.body.path, `/${assertSite}/${assertFilename}`)
|
||||||
|
})
|
||||||
|
})
|
|
@ -2,6 +2,7 @@ import { Eltro as t, assert} from 'eltro'
|
||||||
|
|
||||||
import { createContext } from '../helper.server.mjs'
|
import { createContext } from '../helper.server.mjs'
|
||||||
import { verifyToken } from '../../api/media/security.mjs'
|
import { verifyToken } 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'
|
||||||
|
|
||||||
|
@ -16,51 +17,73 @@ t.describe('#verifyToken()', function() {
|
||||||
|
|
||||||
t.test('should fail if query token is missing', function() {
|
t.test('should fail if query token is missing', function() {
|
||||||
let ctx = createContext({ })
|
let ctx = createContext({ })
|
||||||
delete ctx.query.token
|
ctx.query.delete('token')
|
||||||
|
|
||||||
assert.throws(function() { verifyToken(ctx) }, /[Mm]issing/)
|
assert.throws(function() { verifyToken(ctx) }, function(err) {
|
||||||
|
assert.ok(err instanceof HttpError)
|
||||||
|
assert.ok(err instanceof Error)
|
||||||
|
assert.strictEqual(err.status, 422)
|
||||||
|
assert.match(err.message, /[Qq]uery/)
|
||||||
|
assert.match(err.message, /[Tt]oken/)
|
||||||
|
return true
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function assertInvalidToken(err) {
|
||||||
|
assert.ok(err instanceof HttpError)
|
||||||
|
assert.ok(err instanceof Error)
|
||||||
|
assert.strictEqual(err.status, 422)
|
||||||
|
assert.match(err.message, /[Ii]nvalid/)
|
||||||
|
assert.match(err.message, /[Tt]oken/)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
t.test('should fail if token is invalid', function() {
|
t.test('should fail if token is invalid', function() {
|
||||||
let ctx = createContext({ })
|
let ctx = createContext({ })
|
||||||
ctx.query.token = 'asdfasdgassdga'
|
ctx.query.set('token', 'asdfasdgassdga')
|
||||||
|
|
||||||
assert.throws(function() { verifyToken(ctx) })
|
assert.throws(function() { verifyToken(ctx) }, assertInvalidToken)
|
||||||
|
assert.ok(ctx.log.error.lastCall)
|
||||||
|
assert.match(ctx.log.error.lastCall[0].message, /3 dots/)
|
||||||
|
|
||||||
ctx.query.token = 'asdfasdgassdga.asdfasdg.sadfsadfas'
|
ctx.query.set('token', 'asdfasdgassdga.asdfasdg.sadfsadfas')
|
||||||
|
|
||||||
assert.throws(function() { verifyToken(ctx) }, /invalid/)
|
assert.throws(function() { verifyToken(ctx) }, assertInvalidToken)
|
||||||
|
assert.match(ctx.log.error.lastCall[0].message, /[Ii]nvalid/)
|
||||||
|
|
||||||
ctx.query.token = encode(
|
ctx.query.set('token', encode(
|
||||||
{ typ: 'JWT', alg: 'HS256' },
|
{ typ: 'JWT', alg: 'HS256' },
|
||||||
{ iss: 'justatest' },
|
{ iss: 'justatest' },
|
||||||
'mysharedkey'
|
'mysharedkey'
|
||||||
)
|
))
|
||||||
assert.throws(function() { verifyToken(ctx) }, /pubkey/)
|
assert.throws(function() { verifyToken(ctx) }, assertInvalidToken)
|
||||||
|
assert.match(ctx.log.error.lastCall[0].message, /pubkey/)
|
||||||
|
|
||||||
ctx.query.token = encode(
|
ctx.query.set('token', encode(
|
||||||
{ typ: 'JWT', alg: 'HS512' },
|
{ typ: 'JWT', alg: 'HS512' },
|
||||||
{ iss: 'notexist' },
|
{ iss: 'notexist' },
|
||||||
'mysharedkey'
|
'mysharedkey'
|
||||||
)
|
))
|
||||||
assert.throws(function() { verifyToken(ctx) }, /notexist/)
|
assert.throws(function() { verifyToken(ctx) }, assertInvalidToken)
|
||||||
|
assert.match(ctx.log.error.lastCall[0].message, /notexist/)
|
||||||
|
|
||||||
ctx.query.token = encode(
|
ctx.query.set('token', encode(
|
||||||
{ typ: 'JWT', alg: 'HS512' },
|
{ typ: 'JWT', alg: 'HS512' },
|
||||||
{ iss: 'justatest' },
|
{ iss: 'justatest' },
|
||||||
'mysharedkey2'
|
'mysharedkey2'
|
||||||
)
|
))
|
||||||
assert.throws(function() { verifyToken(ctx) }, /HS512/)
|
assert.throws(function() { verifyToken(ctx) }, assertInvalidToken)
|
||||||
assert.throws(function() { verifyToken(ctx) }, /[vV]erification/)
|
assert.match(ctx.log.error.lastCall[0].message, /HS512/)
|
||||||
|
assert.match(ctx.log.error.lastCall[0].message, /[vV]erification/)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should otherwise return the issuer', function() {
|
t.test('should otherwise return the issuer', function() {
|
||||||
let ctx = createContext({ })
|
let ctx = createContext({ })
|
||||||
ctx.query.token = encode(
|
ctx.query.set('token', encode(
|
||||||
{ typ: 'JWT', alg: 'HS512' },
|
{ typ: 'JWT', alg: 'HS512' },
|
||||||
{ iss: 'justatest' },
|
{ iss: 'justatest' },
|
||||||
'mysharedkey'
|
'mysharedkey'
|
||||||
)
|
))
|
||||||
let site = verifyToken(ctx)
|
let site = verifyToken(ctx)
|
||||||
assert.strictEqual(site, 'justatest')
|
assert.strictEqual(site, 'justatest')
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,28 +1,40 @@
|
||||||
import { Eltro as t, assert} from 'eltro'
|
import { Eltro as t, assert} from 'eltro'
|
||||||
|
|
||||||
import * as server from './helper.server.mjs'
|
import { createClient, server, resetLog } from './helper.server.mjs'
|
||||||
|
|
||||||
t.describe('Server', function() {
|
t.describe('Server', function() {
|
||||||
let client
|
let client
|
||||||
|
|
||||||
t.before(function() {
|
t.before(function() {
|
||||||
client = server.createClient()
|
client = createClient()
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should run', async function() {
|
t.test('should run', async function() {
|
||||||
|
resetLog()
|
||||||
|
assert.strictEqual(server.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(server.log.info.lastCall[0].status, 200)
|
||||||
|
assert.strictEqual(server.log.info.lastCall[0].path, '/')
|
||||||
|
assert.strictEqual(typeof(server.log.info.lastCall[0].ms), 'number')
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should handle errors fine', async function() {
|
t.test('should handle errors fine', async function() {
|
||||||
|
resetLog()
|
||||||
|
assert.strictEqual(server.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, 422)
|
assert.strictEqual(data.body.status, 500)
|
||||||
assert.match(data.body.message, /test/)
|
assert.match(data.body.message, /test/)
|
||||||
|
assert.strictEqual(server.log.warn.callCount, 1)
|
||||||
|
assert.strictEqual(server.log.warn.lastCall[0].status, 500)
|
||||||
|
assert.strictEqual(server.log.warn.lastCall[0].path, '/error')
|
||||||
|
assert.strictEqual(typeof(server.log.warn.lastCall[0].ms), 'number')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue