diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9cf9495 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index cd6db72..eaa5293 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -51,7 +51,6 @@ on_success: if [ $? -eq 0 ] ; then echo "Release already exists, nothing to do."; else - ./test/7zas a -mx9 "${CURR_VER}_sc-core.7z" package.json index.mjs cli.mjs core bin echo "Creating release on gitea" RELEASE_RESULT=$(curl \ -X POST \ @@ -59,13 +58,6 @@ on_success: -H "Content-Type: application/json" \ https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases \ -d "{\"tag_name\":\"v${CURR_VER}\",\"name\":\"v${CURR_VER}\",\"body\":\"Automatic release from Appveyor from ${APPVEYOR_REPO_COMMIT} :\n\n${APPVEYOR_REPO_COMMIT_MESSAGE}\"}") - RELEASE_ID=$(echo $RELEASE_RESULT | jq -r .id) - echo "Adding ${CURR_VER}_sc-core.7z to release ${RELEASE_ID}" - curl \ - -X POST \ - -H "Authorization: token $deploytoken" \ - -F "attachment=@${CURR_VER}_sc-core.7z" \ - https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases/$RELEASE_ID/assets echo '//registry.npmjs.org/:_authToken=${npmtoken}' > ~/.npmrc echo "Publishing new version to npm" npm publish diff --git a/flaska.mjs b/flaska.mjs index 11725d1..aa0ac11 100644 --- a/flaska.mjs +++ b/flaska.mjs @@ -57,6 +57,33 @@ export function QueryHandler() { } } +export function JsonHandler() { + return async function(ctx) { + const buffers = []; + + for await (const chunk of ctx.req) { + buffers.push(chunk); + } + + const data = Buffer.concat(buffers).toString(); + + ctx.req.body = JSON.parse(data) + } +} + +export class HttpError extends Error { + constructor(statusCode, message, body = null) { + super(message); + + Error.captureStackTrace(this, HttpError); + + let proto = Object.getPrototypeOf(this); + proto.name = 'HttpError'; + + this.status = statusCode + } +} + export class FlaskaRouter { constructor() { this.root = new Branch() @@ -547,12 +574,48 @@ export class Flaska { } } - listen(port, cb) { + listen(port, orgIp, orgcb) { + let ip = orgIp + let cb = orgcb + if (!cb && typeof(orgIp) === 'function') { + ip = '::' + cb = orgIp + } if (typeof(port) !== 'number') { throw new Error('Flaska.listen() called with non-number in port') } this.compile() this.server = this.http.createServer(this.requestStart.bind(this)) - this.server.listen(port, cb) + + this.server.listen(port, ip, cb) + } + + listenAsync(port, ip = '::') { + if (typeof(port) !== 'number') { + return Promise.reject(new Error('Flaska.listen() called with non-number in port')) + } + + this.compile() + this.server = this.http.createServer(this.requestStart.bind(this)) + + return new Promise((res, rej) => { + this.server.listen(port, ip, function(err) { + if (err) return rej(err) + return res() + }) + }) + } + + closeAsync() { + if (!this.server) return Promise.resolve() + + return new Promise((res, rej) => { + this.server.close(function(err) { + if (err) { return rej(err) } + + // Waiting 0.1 second for it to close down + setTimeout(function() { res() }, 100) + }) + }) } } diff --git a/package.json b/package.json index 4d0a61d..503656d 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,22 @@ { "name": "flaska", - "version": "0.9.8", + "version": "0.9.9", "description": "Flaska is a micro web-framework for node. It is designed to be fast, simple and lightweight, and is distributed as a single file module with no dependencies.", "main": "flaska.mjs", "scripts": { - "test": "eltro" + "test": "eltro", + "test:watch": "npm-watch test" + }, + "watch": { + "test": { + "patterns": [ + "test/*", + "flaska.mjs" + ], + "extensions": "mjs", + "quiet": true, + "inherit": true + } }, "type": "module", "repository": { @@ -27,7 +39,8 @@ }, "homepage": "https://github.com/nfp-projects/bottle-node#readme", "devDependencies": { - "eltro": "^1.1.0" + "eltro": "^1.3.1", + "formidable": "^1.2.2" }, "files": [ "flaska.mjs", diff --git a/test/client.mjs b/test/client.mjs index 63b7395..15b0858 100644 --- a/test/client.mjs +++ b/test/client.mjs @@ -15,7 +15,6 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) { return new Promise((resolve, reject) => { const opts = defaults(defaults(options, { - agent: false, method: method, timeout: 500, protocol: urlObj.protocol, @@ -24,6 +23,7 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) { host: urlObj.hostname, port: Number(urlObj.port), path: urlObj.pathname + urlObj.search, + headers: {}, })) const req = http.request(opts) @@ -33,7 +33,6 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) { req.on('error', reject) req.on('timeout', function() { - console.log(req.destroy()) reject(new Error(`Request ${method} ${path} timed out`)) }) req.on('response', res => { @@ -45,6 +44,7 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) { }) res.on('end', function () { + if (!output) return resolve(null) try { output = JSON.parse(output) } catch (e) { @@ -63,6 +63,63 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) { }) } -Client.prototype.get = function(path = '/') { - return this.customRequest('GET', path, null) -} \ No newline at end of file +Client.prototype.get = function(url = '/') { + return this.customRequest('GET', url, null) +} + +Client.prototype.post = function(url = '/', body = {}) { + let parsed = JSON.stringify(body) + return this.customRequest('POST', url, parsed, { + headers: { + 'Content-Type': 'application/json', + 'Content-Length': parsed.length, + }, + }) +} + +Client.prototype.del = function(url = '/', body = {}) { + return this.customRequest('DELETE', url, JSON.stringify(body)) +} + +const random = (length = 8) => { + // Declare all characters + let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + // Pick characers randomly + let str = ''; + for (let i = 0; i < length; i++) { + str += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return str; +} + +Client.prototype.upload = function(url, file, method = 'POST', body = {}) { + return fs.readFile(file).then(data => { + const crlf = '\r\n' + const filename = path.basename(file) + const boundary = `---------${random(32)}` + + const multipartBody = Buffer.concat([ + Buffer.from( + `${crlf}--${boundary}${crlf}` + + `Content-Disposition: form-data; name="file"; filename="${filename}"` + crlf + crlf + ), + data, + Buffer.concat(Object.keys(body).map(function(key) { + return Buffer.from('' + + `${crlf}--${boundary}${crlf}` + + `Content-Disposition: form-data; name="${key}"` + crlf + crlf + + JSON.stringify(body[key]) + ) + })), + Buffer.from(`${crlf}--${boundary}--`), + ]) + + return this.customRequest(method, url, multipartBody, { + timeout: 5000, + headers: { + 'Content-Type': 'multipart/form-data; boundary=' + boundary, + 'Content-Length': multipartBody.length, + }, + }) + }) +} diff --git a/test/flaska.api.test.mjs b/test/flaska.api.test.mjs index 9da12fb..50448b6 100644 --- a/test/flaska.api.test.mjs +++ b/test/flaska.api.test.mjs @@ -1,6 +1,6 @@ -import { Eltro as t, assert} from 'eltro' +import { Eltro as t, assert, stub, spy } from 'eltro' import { Flaska } from '../flaska.mjs' -import { spy, createCtx, fakeHttp } from './helper.mjs' +import { createCtx, fakeHttp } from './helper.mjs' const faker = fakeHttp() @@ -441,12 +441,39 @@ t.describe('#listen()', function() { t.test('call http correctly', function() { const assertPort = 325897235 + const assertIp = 'asdga' const assertCb = function() { } let checkPort = null + let checkIp = null let checkListenCb = null - let testFaker = fakeHttp(null, function(port, cb) { + let testFaker = fakeHttp(null, function(port, ip, cb) { checkPort = port + checkIp = ip + checkListenCb = cb + }) + let flaska = new Flaska({}, testFaker) + assert.ok(flaska.requestStart) + flaska.requestStart = function() { + checkInternalThis = this + checkIsTrue = true + } + flaska.listen(assertPort, assertIp, assertCb) + assert.strictEqual(checkPort, assertPort) + assert.strictEqual(checkIp, assertIp) + assert.strictEqual(checkListenCb, assertCb) + }) + + t.test('call http correctly if only port is specified', function() { + const assertPort = 325897235 + const assertCb = function() { } + let checkPort = null + let checkIp = null + let checkListenCb = null + + let testFaker = fakeHttp(null, function(port, ip, cb) { + checkPort = port + checkIp = ip checkListenCb = cb }) let flaska = new Flaska({}, testFaker) @@ -457,6 +484,7 @@ t.describe('#listen()', function() { } flaska.listen(assertPort, assertCb) assert.strictEqual(checkPort, assertPort) + assert.strictEqual(checkIp, '::') assert.strictEqual(checkListenCb, assertCb) }) @@ -484,3 +512,131 @@ t.describe('#listen()', function() { assert.strictEqual(checkInternalThis, flaska) }) }) + +t.describe('#listenAsync()', function() { + t.test('it should throw if missing port', async function() { + let flaska = new Flaska({}, faker) + + let tests = [ + undefined, + 'asdf', + '123', + [], + {}, + function() {}, + ] + let errors = await Promise.all(tests.map(x => assert.isRejected(flaska.listenAsync(x)))) + assert.strictEqual(errors.length, tests.length) + for (let i = 0; i < errors.length; i++) { + assert.match(errors[i].message, /[Pp]ort/) + } + }) + + t.test('it should automatically call compile', async function() { + let assertCalled = false + let flaska = new Flaska({}, faker) + flaska.compile = function() { assertCalled = true } + await flaska.listenAsync(404) + assert.strictEqual(assertCalled, true) + }) + + t.test('call http correctly', async function() { + const assertPort = 325897235 + const assertIp = 'asdf' + let checkPort = null + let checkIp = null + + let testFaker = fakeHttp(null, function(port, ip, cb) { + checkPort = port + checkIp = ip + cb() + }) + let flaska = new Flaska({}, testFaker) + assert.ok(flaska.requestStart) + flaska.requestStart = function() { + checkInternalThis = this + checkIsTrue = true + } + await flaska.listenAsync(assertPort, assertIp) + assert.strictEqual(checkPort, assertPort) + assert.strictEqual(checkIp, assertIp) + }) + + t.test('call http correctly if only port specified', async function() { + const assertPort = 325897235 + let checkPort = null + let checkIp = null + + let testFaker = fakeHttp(null, function(port, ip, cb) { + checkPort = port + checkIp = ip + cb() + }) + let flaska = new Flaska({}, testFaker) + assert.ok(flaska.requestStart) + flaska.requestStart = function() { + checkInternalThis = this + checkIsTrue = true + } + await flaska.listenAsync(assertPort) + assert.strictEqual(checkPort, assertPort) + assert.strictEqual(checkIp, '::') + }) + + t.test('register requestStart if no async', async function() { + let checkIsTrue = false + let checkInternalThis = null + let checkHandler = null + + let testFaker = fakeHttp(function(cb) { + checkHandler = cb + }) + let flaska = new Flaska({}, testFaker) + assert.ok(flaska.requestStart) + flaska.requestStart = function() { + checkInternalThis = this + checkIsTrue = true + } + await flaska.listenAsync(404) + assert.strictEqual(typeof(checkHandler), 'function') + assert.notStrictEqual(checkHandler, flaska.requestStart) + assert.notStrictEqual(checkIsTrue, true) + assert.notStrictEqual(checkInternalThis, flaska) + checkHandler() + assert.strictEqual(checkIsTrue, true) + assert.strictEqual(checkInternalThis, flaska) + }) +}) + +t.describe('#closeAsync()', function() { + t.test('it return if server is null', function() { + let flaska = new Flaska() + flaska.server = null + return flaska.closeAsync() + }) + + t.test('it should call close on server correctly', async function() { + const assertError = new Error('Pirate Fight') + let flaska = new Flaska() + flaska.server = { + close: stub() + } + flaska.server.close.returnWith(function(cb) { + cb(assertError) + }) + let err = await assert.isRejected(flaska.closeAsync()) + assert.strictEqual(err, assertError) + }) + + t.test('should otherwise work', async function() { + let flaska = new Flaska() + flaska.server = { + close: stub() + } + flaska.server.close.returnWith(function(cb) { + cb(null, { a: 1 }) + }) + let res = await flaska.closeAsync() + assert.notOk(res) + }) +}) diff --git a/test/flaska.body.test.mjs b/test/flaska.body.test.mjs new file mode 100644 index 0000000..e69de29 diff --git a/test/flaska.formidable.test.mjs b/test/flaska.formidable.test.mjs new file mode 100644 index 0000000..e69de29 diff --git a/test/flaska.in.test.mjs b/test/flaska.in.test.mjs index 8930ac3..e270d2f 100644 --- a/test/flaska.in.test.mjs +++ b/test/flaska.in.test.mjs @@ -1,6 +1,6 @@ -import { Eltro as t, assert} from 'eltro' +import { Eltro as t, spy, assert} from 'eltro' import { Flaska } from '../flaska.mjs' -import { fakeHttp, createReq, spy, createRes } from './helper.mjs' +import { fakeHttp, createReq, createRes } from './helper.mjs' const faker = fakeHttp() diff --git a/test/flaska.out.test.mjs b/test/flaska.out.test.mjs index 6d9175e..a07b5e7 100644 --- a/test/flaska.out.test.mjs +++ b/test/flaska.out.test.mjs @@ -1,6 +1,6 @@ -import { Eltro as t, assert} from 'eltro' +import { Eltro as t, spy, assert} from 'eltro' import { Flaska, FlaskaRouter } from '../flaska.mjs' -import { fakeHttp, createCtx, spy } from './helper.mjs' +import { fakeHttp, createCtx } from './helper.mjs' const fakerHttp = fakeHttp() const fakeStream = { pipeline: spy() } diff --git a/test/helper.mjs b/test/helper.mjs index 55c8fc1..34135e0 100644 --- a/test/helper.mjs +++ b/test/helper.mjs @@ -1,40 +1,19 @@ +import { spy } from 'eltro' + const indexMap = [ 'firstCall', 'secondCall', 'thirdCall', ] -export function spy() { - let calls = [] - let called = 0 - let func = function(...args) { - func.called = true - calls.push(args) - if (called < indexMap.length) { - func[indexMap[called]] = args - } - called++ - func.callCount = called - } - func.called = false - func.callCount = called - func.onCall = function(i) { - return calls[i] - } - for (let i = 0; i < indexMap.length; i++) { - func[indexMap] = null - } - return func -} - export function fakeHttp(inj1, inj2) { let intermediate = { createServer: function(cb) { if (inj1) inj1(cb) intermediate.fakeRequest = cb return { - listen: function(port, cb) { - if (inj2) inj2(port, cb) + listen: function(port, ip, cb) { + if (inj2) inj2(port, ip, cb) else if (cb) cb() } } diff --git a/test/http.test.mjs b/test/http.test.mjs index abb30e6..c85c4fe 100644 --- a/test/http.test.mjs +++ b/test/http.test.mjs @@ -6,24 +6,36 @@ const port = 51024 const flaska = new Flaska({}) const client = new Client(port) +let reqBody = null + flaska.get('/', function(ctx) { ctx.body = { status: true } }) - -t.before(function(cb) { - flaska.listen(port, cb) +flaska.post('/test', function(ctx) { + ctx.body = { success: true } + // console.log(ctx) }) -t.describe('', function() { - t.test('/ should return status true', function() { - return client.get().then(function(body) { +t.before(function() { + return flaska.listenAsync(port) +}) + +t.describe('/', function() { + t.test('should return status true', function() { + return client.get('/').then(function(body) { assert.deepEqual(body, { status: true }) }) }) }) -t.after(function(cb) { - setTimeout(function() { - flaska.server.close(cb) - }, 1000) +t.describe('/test', function() { + t.test('should return success and store body', async function() { + reqBody = null + let body = await client.post('/test') + assert.deepEqual(body, { success: true }) + }) +}) + +t.after(function() { + return flaska.closeAsync() }) diff --git a/test/middlewares.test.mjs b/test/middlewares.test.mjs index d9f829b..ea81b50 100644 --- a/test/middlewares.test.mjs +++ b/test/middlewares.test.mjs @@ -1,5 +1,5 @@ import { Eltro as t, assert} from 'eltro' -import { QueryHandler } from '../flaska.mjs' +import { QueryHandler, JsonHandler } from '../flaska.mjs' t.describe('#QueryHandler()', function() { let queryHandler = QueryHandler() @@ -22,3 +22,27 @@ t.describe('#QueryHandler()', function() { assert.strictEqual(ctx.query.get('ITEM2'), 'hello world') }) }) + +t.describe('#JsonHandler()', function() { + let jsonHandler = JsonHandler() + + t.test('should return a handler', function() { + assert.strictEqual(typeof(jsonHandler), 'function') + }) + + t.test('should support separating query from request url', async function() { + const assertBody = { a: 1, temp: 'test', hello: 'world'} + let parsed = JSON.stringify(assertBody) + + const ctx = { + req: [ + Promise.resolve(Buffer.from(parsed.slice(0, parsed.length / 2))), + Promise.resolve(Buffer.from(parsed.slice(parsed.length / 2))), + ] + } + + await jsonHandler(ctx) + assert.notStrictEqual(ctx.req.body, assertBody) + assert.deepStrictEqual(ctx.req.body, assertBody) + }) +}) diff --git a/test/test.png b/test/test.png new file mode 100644 index 0000000..33533b9 Binary files /dev/null and b/test/test.png differ