This repository has been archived by the owner on Feb 27, 2022. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(response): add base response class
- Loading branch information
1 parent
1e5edd1
commit 805be5a
Showing
3 changed files
with
370 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
'use strict' | ||
|
||
/* | ||
* adonis-vow | ||
* | ||
* (c) Harminder Virk <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
const Macroable = require('macroable') | ||
const debug = require('debug')('adonis:vow') | ||
const nodeCookie = require('node-cookie') | ||
const cookieParser = require('../../lib/cookieParser') | ||
|
||
module.exports = function (Config) { | ||
debug('creating isolated response for the suite') | ||
|
||
/** | ||
* This is base response class to be used | ||
* by other response clients. For example: | ||
* Api client and browser client. | ||
* | ||
* @class BaseResponse | ||
* @constructor | ||
*/ | ||
class Response extends Macroable { | ||
constructor (headers) { | ||
super() | ||
this._headers = headers || {} | ||
this._cookieString = this._parseCookies() | ||
this._cookies = null | ||
this._plainCookies = null | ||
} | ||
|
||
/** | ||
* Add a new hook before request starts | ||
* | ||
* @method before | ||
* @static | ||
* | ||
* @param {Function} fn | ||
* | ||
* @chainable | ||
*/ | ||
static before (fn) { | ||
this._hooks.before.push(fn) | ||
return this | ||
} | ||
|
||
/** | ||
* Add a new hook for after request completes | ||
* | ||
* @method after | ||
* @static | ||
* | ||
* @param {Function} fn | ||
* | ||
* @chainable | ||
*/ | ||
static after (fn) { | ||
this._hooks.after.push(fn) | ||
return this | ||
} | ||
|
||
/** | ||
* Hydrate request constructor properties, macros | ||
* and getters | ||
* | ||
* @method hydrate | ||
* @static | ||
* | ||
* @return {void} | ||
*/ | ||
static hydrate () { | ||
super.hydrate() | ||
this._hooks = { | ||
before: [], | ||
after: [] | ||
} | ||
} | ||
|
||
/** | ||
* Parses the headers cookie into a string. This | ||
* method will remove expired cookies | ||
* | ||
* @method _parseCookies | ||
* | ||
* @return {String} | ||
* | ||
* @private | ||
*/ | ||
_parseCookies () { | ||
const setCookieHeader = this._headers['set-cookie'] || [] | ||
const cookies = cookieParser.filterExpired(setCookieHeader.map((cookie) => cookieParser.parse(cookie))) | ||
return cookies.map((cookie) => `${cookie.key}=${cookie.value}`).join(';') | ||
} | ||
|
||
/** | ||
* A object of response cookies | ||
* | ||
* @attribute cookies | ||
* | ||
* @return {Object} | ||
*/ | ||
get cookies () { | ||
const appKey = this.constructor.Config.get('app.appKey') | ||
if (!this._cookies) { | ||
this._cookies = nodeCookie.parse({ headers: { cookie: this._cookieString } }, appKey, !!appKey) | ||
} | ||
return this._cookies | ||
} | ||
|
||
/** | ||
* A object of response plain cookies | ||
* | ||
* @attribute cookies | ||
* | ||
* @return {Object} | ||
*/ | ||
get plainCookies () { | ||
if (!this._plainCookies) { | ||
this._plainCookies = nodeCookie.parse({ headers: { cookie: this._cookieString } }) | ||
} | ||
return this._plainCookies | ||
} | ||
|
||
/** | ||
* Execute request hooks in sequence | ||
* | ||
* @method exec | ||
* | ||
* @param {String} event - Must be `before` or `after` | ||
* | ||
* @return {void} | ||
*/ | ||
async exec (event) { | ||
if (event !== 'before' && event !== 'after') { | ||
throw new Error(`${event} is not a valid hook event for vow response`) | ||
} | ||
|
||
const hooks = this.constructor._hooks[event] | ||
for (const hook of hooks) { | ||
await hook(this) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Properties for macroable | ||
*/ | ||
Response._macros = {} | ||
Response._getters = {} | ||
|
||
/** | ||
* For hooks | ||
*/ | ||
Response._hooks = { | ||
before: [], | ||
after: [] | ||
} | ||
|
||
/** | ||
* Reference to the Config provider | ||
*/ | ||
Response.Config = Config | ||
|
||
return Response | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
'use strict' | ||
|
||
/* | ||
* adonis-vow | ||
* | ||
* (c) Harminder Virk <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
const http = require('http') | ||
const test = require('japa') | ||
const superagent = require('superagent') | ||
const { Config } = require('@adonisjs/sink') | ||
const ResponseManager = require('../../src/Response') | ||
const nodeCookie = require('node-cookie') | ||
const PORT = '3333' | ||
const BASE_URL = `http://localhost:${PORT}` | ||
|
||
const sleep = function (time) { | ||
return new Promise((resolve) => { | ||
setTimeout(resolve, time) | ||
}) | ||
} | ||
|
||
test.group('Response', () => { | ||
test('instantiate response', (assert) => { | ||
const Response = ResponseManager(new Config()) | ||
const response = new Response() | ||
assert.instanceOf(response, Response) | ||
}) | ||
|
||
test('define response getter', (assert) => { | ||
const Response = ResponseManager(new Config()) | ||
Response.getter('foo', () => 'bar') | ||
const response = new Response() | ||
assert.equal(response.foo, 'bar') | ||
}) | ||
|
||
test('define response macro', (assert) => { | ||
const Response = ResponseManager(new Config()) | ||
Response.macro('foo', () => 'bar') | ||
const response = new Response() | ||
assert.equal(response.foo(), 'bar') | ||
}) | ||
|
||
test('add before response hook', (assert) => { | ||
const Response = ResponseManager(new Config()) | ||
const fn = function () {} | ||
Response.before(fn) | ||
assert.deepEqual(Response._hooks.before, [fn]) | ||
}) | ||
|
||
test('add after response hook', (assert) => { | ||
const Response = ResponseManager(new Config()) | ||
const fn = function () {} | ||
Response.after(fn) | ||
assert.deepEqual(Response._hooks.after, [fn]) | ||
}) | ||
|
||
test('execute hooks in sequence', async (assert) => { | ||
const Response = ResponseManager(new Config()) | ||
|
||
const stack = [] | ||
Response.before(() => { | ||
stack.push('1') | ||
}) | ||
|
||
Response.before(async () => { | ||
await sleep(100) | ||
stack.push('2') | ||
}) | ||
|
||
Response.before(() => { | ||
stack.push('3') | ||
}) | ||
|
||
const response = new Response() | ||
await response.exec('before') | ||
assert.deepEqual(stack, ['1', '2', '3']) | ||
}) | ||
|
||
test('throw exception when invalid hook type is passed', async (assert) => { | ||
assert.plan(1) | ||
|
||
const Response = ResponseManager(new Config()) | ||
const response = new Response() | ||
|
||
try { | ||
await response.exec('foo') | ||
} catch ({ message }) { | ||
assert.equal(message, 'foo is not a valid hook event for vow response') | ||
} | ||
}) | ||
|
||
test('should have access to response instance inside hook', async (assert) => { | ||
assert.plan(1) | ||
const Response = ResponseManager(new Config()) | ||
|
||
Response.before((req) => { | ||
assert.deepEqual(req, response) | ||
}) | ||
|
||
const response = new Response() | ||
await response.exec('before') | ||
}) | ||
|
||
test('read response cookies', async (assert) => { | ||
const server = http.createServer((req, res) => { | ||
nodeCookie.create(res, 'user', 'virk') | ||
res.end() | ||
}).listen(PORT) | ||
|
||
const res = await superagent.get(BASE_URL) | ||
const Response = ResponseManager(new Config()) | ||
const response = new Response(res.headers) | ||
assert.deepEqual(response.cookies, { user: 'virk' }) | ||
server.close() | ||
}) | ||
|
||
test('read multiple cookies', async (assert) => { | ||
const server = http.createServer((req, res) => { | ||
nodeCookie.create(res, 'user', 'virk') | ||
nodeCookie.create(res, 'age', 22) | ||
res.end() | ||
}).listen(PORT) | ||
|
||
const res = await superagent.get(BASE_URL) | ||
const Response = ResponseManager(new Config()) | ||
const response = new Response(res.headers) | ||
assert.deepEqual(response.cookies, { user: 'virk', age: '22' }) | ||
server.close() | ||
}) | ||
|
||
test('read encrypted cookies', async (assert) => { | ||
const config = new Config() | ||
config.set('app.appKey', 'alongrandomstring') | ||
|
||
const server = http.createServer((req, res) => { | ||
nodeCookie.create(res, 'user', 'virk', {}, config.get('app.appKey'), true) | ||
res.end() | ||
}).listen(PORT) | ||
|
||
const res = await superagent.get(BASE_URL) | ||
const Response = ResponseManager(config) | ||
const response = new Response(res.headers) | ||
assert.deepEqual(response.cookies, { user: 'virk' }) | ||
server.close() | ||
}) | ||
|
||
test('return cookies with options', async (assert) => { | ||
const config = new Config() | ||
config.set('app.appKey', 'alongrandomstring') | ||
|
||
const server = http.createServer((req, res) => { | ||
nodeCookie.create(res, 'user', 'virk', { path: '/' }) | ||
res.end() | ||
}).listen(PORT) | ||
|
||
const res = await superagent.get(BASE_URL) | ||
const Response = ResponseManager(config) | ||
const response = new Response(res.headers) | ||
assert.deepEqual(response.plainCookies, { user: 'virk' }) | ||
server.close() | ||
}) | ||
|
||
test('return null when unable to unparse cookies', async (assert) => { | ||
const config = new Config() | ||
config.set('app.appKey', 'alongrandomstring') | ||
|
||
const server = http.createServer((req, res) => { | ||
nodeCookie.create(res, 'user', 'virk', {}) | ||
res.end() | ||
}).listen(PORT) | ||
|
||
const res = await superagent.get(BASE_URL) | ||
const Response = ResponseManager(config) | ||
const response = new Response(res.headers) | ||
assert.deepEqual(response.cookies, { user: null }) | ||
server.close() | ||
}) | ||
|
||
test('read plain cookies', async (assert) => { | ||
const config = new Config() | ||
config.set('app.appKey', 'alongrandomstring') | ||
|
||
const server = http.createServer((req, res) => { | ||
nodeCookie.create(res, 'user', 'virk', {}) | ||
res.end() | ||
}).listen(PORT) | ||
|
||
const res = await superagent.get(BASE_URL) | ||
const Response = ResponseManager(config) | ||
const response = new Response(res.headers) | ||
assert.deepEqual(response.plainCookies, { user: 'virk' }) | ||
server.close() | ||
}) | ||
}) |