├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── lib
├── batchRequestor.js
├── headerExtractor.js
├── httpClient.js
└── multiFetch.js
├── package.json
└── test
├── batchRequestor.js
├── httpClient.js
├── multiFetch.js
└── multiFetch_customHost.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - "0.11"
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 marmelab
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |  |
4 | Archived Repository
5 | This code is no longer maintained. Feel free to fork it, but use it at your own risks.
6 | |
7 |
8 |
9 |
10 | # koa-multifetch
11 |
12 | A simple [koa.js](http://koajs.com/) middleware to multiplex several HTTP requests into one. Very useful on mobile devices where latency can kill performance. Based on [Facebook's Batch Requests philosophy](https://developers.facebook.com/docs/graph-api/making-multiple-requests), and inspired by [multifetch](https://github.com/e-conomic/multifetch) for Express.
13 |
14 | ## Installation
15 |
16 | ```sh
17 | npm install koa-multifetch
18 | ```
19 |
20 | After installing the package, mount the middleware on any URL, using [koa-route](https://github.com/koajs/route). For example, to add `/multi` as a multifetch endpoint:
21 |
22 | ```js
23 | var multifetch = require('koa-multifetch')();
24 | var koaRoute = require('koa-route');
25 | app.use(koaRoute.post('/multi', multifetch));
26 | ```
27 |
28 | By default koa-multifetch use relative url. In this example all request will be prepended with "/multi".
29 |
30 | But koa-multifetch can take an object of options as parameter, object that can have two properties:
31 |
32 | 1) If you want to allow koa-multifetch to send request anywhere on your site, enable absoluteUrl option with absolute property by calling
33 | ```js
34 | var multifetch = require('koa-multifetch')({absolute: true}); // enable absolute url, absolute is false by default
35 | ```
36 |
37 | 2) If you want to force koa-multifetch to send request on a specific host (for exemple if you are behind a load balancer on level 7), enable custom host option with header_host property by calling
38 | ```js
39 | var multifetch = require('koa-multifetch')({header_host: 'http://localhost:3000'}); // set a custom host for multiFetch requests
40 | ```
41 |
42 | koa-multifetch can be mounted both as a GET and POST route.
43 |
44 | ## Usage
45 |
46 | Send a request to the route where the middleware is listening, passing the requests to fetch in parallel as a JSON object in the request body. For instance, to fetch `/products/1` and `/users` in parallel, make the following request:
47 |
48 | ```
49 | POST /multi HTTP/1.1
50 | Content-Type: application/json
51 | {
52 | "product": "/products/1",
53 | "all_users": "/users"
54 | }
55 | ```
56 |
57 | The middleware will call both HTTP resources, and return a response with a composite body once all the requests are fetched:
58 | ```js
59 | {
60 | "product": {
61 | "code":"200",
62 | "headers":[
63 | { "name": "Content-Type", "value": "text/javascript; charset=UTF-8" }
64 | ],
65 | "body": "{ id: 1, name: \"ipad2\", stock: 34 }"
66 | },
67 | "all_users": {
68 | "code":"200",
69 | "headers":[
70 | { "name": "Content-Type", "value": "text/javascript; charset=UTF-8" }
71 | ],
72 | "body": "[{ id: 2459, login: \"john\" }, { id: 7473, login: \"jane\" }]"
73 | }
74 | }
75 | ```
76 |
77 | Any header present in the multifetch request will be automatically added to all sub-requests.
78 |
79 | **Tip**: When mounted as a GET route, koa-multifetch reads the query parameters to determine the requests to fetch:
80 |
81 | ```
82 | GET /multi?product=/product/1&all_users=/users HTTP/1.1
83 | ```
84 |
85 | ## License
86 |
87 | koa-multifetch is licensed under the [MIT Licence](LICENSE), courtesy of [marmelab](http://marmelab.com).
88 |
--------------------------------------------------------------------------------
/lib/batchRequestor.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var headerExtractor = require('./headerExtractor');
4 |
5 | module.exports = function (requestor) {
6 | return function* batchRequestor(requests, baseUrl, headers) {
7 | var results = {},
8 | resultsHeaders = {},
9 | requestsThunks = [],
10 | i = 0,
11 | name;
12 |
13 | for (name in requests) {
14 | requestsThunks[i] = requestor.get(baseUrl + requests[name], headers);
15 | results[name] = i;
16 | i++;
17 | }
18 |
19 | var resultsArray = yield requestsThunks, totalResults = 0;
20 |
21 | for (name in results) {
22 | results[name] = resultsArray[results[name]];
23 | if (!results[name]) {
24 | continue;
25 | }
26 |
27 | ++totalResults;
28 |
29 | resultsHeaders[name] = headerExtractor.extract(results[name].headers);
30 | }
31 |
32 | var responseHeaders = headerExtractor.compute(resultsHeaders, totalResults);
33 |
34 | return { body: results, headers: responseHeaders };
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/lib/headerExtractor.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | /* Extracted from https://github.com/hapijs/wreck library */
5 | _parseCacheControl: function (field) {
6 | /*
7 | Cache-Control = 1#cache-directive
8 | cache-directive = token [ "=" ( token / quoted-string ) ]
9 | token = [^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+
10 | quoted-string = "(?:[^"\\]|\\.)*"
11 | */
12 |
13 | // 1: directive = 2: token 3: quoted-string
14 | var regex = /(?:^|(?:\s*\,\s*))([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)(?:\=(?:([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)|(?:\"((?:[^"\\]|\\.)*)\")))?/g;
15 |
16 | var header = {};
17 | var err = field.replace(regex, function ($0, $1, $2, $3) {
18 | var value = $2 || $3;
19 | header[$1] = value ? value.toLowerCase() : true;
20 |
21 | return '';
22 | });
23 |
24 | var integerFieldValue;
25 | for (var integerField in ['max-age', 's-maxage']) {
26 | if (integerField in header) {
27 | try {
28 | integerFieldValue = parseInt(header[integerField], 10);
29 | if (isNaN(integerFieldValue)) {
30 | return null;
31 | }
32 |
33 | header[integerField] = integerFieldValue;
34 | }
35 | catch (err) { }
36 | }
37 | }
38 |
39 | return (err ? null : header);
40 | },
41 |
42 | extract: function (headers) {
43 | var header,
44 | cacheControl = false,
45 | varies = [];
46 |
47 | for (var i in headers) {
48 | header = headers[i];
49 |
50 | switch (header.name.toLowerCase()) {
51 | case 'cache-control':
52 | var parsedCacheControl = this._parseCacheControl(header.value);
53 |
54 | cacheControl = {
55 | maxAge: 0,
56 | sharedMaxAge: 0,
57 | 'private': false,
58 | 'public': false,
59 | noCache: false
60 | };
61 |
62 | if ('no-cache' in parsedCacheControl) {
63 | cacheControl.noCache = true;
64 |
65 | break;
66 | }
67 |
68 | if ('private' in parsedCacheControl) {
69 | cacheControl['private'] = true;
70 | }
71 |
72 | if ('public' in parsedCacheControl) {
73 | cacheControl['public'] = true;
74 | }
75 |
76 | if ('max-age' in parsedCacheControl) {
77 | cacheControl.maxAge = parsedCacheControl['max-age'];
78 | }
79 |
80 | if ('s-maxage' in parsedCacheControl) {
81 | cacheControl.sharedMaxAge = parsedCacheControl['s-maxage'];
82 | }
83 |
84 | break;
85 |
86 | case 'vary':
87 | header.value.split(',').forEach(function (vary) {
88 | varies.push(vary.trim());
89 | });
90 | break;
91 | }
92 | }
93 |
94 | return {
95 | cacheControl: cacheControl,
96 | varies: varies
97 | };
98 | },
99 |
100 | compute: function (headers, totalResults) {
101 | var totalCacheControl = 0, name;
102 |
103 | for (name in headers) {
104 | if (false === headers[name].cacheControl) {
105 | continue;
106 | }
107 |
108 | // if one of header contain no-cache so force to no-cache
109 | if (headers[name].cacheControl.noCache) {
110 | return { 'Cache-Control': 'no-cache' };
111 | }
112 |
113 | ++totalCacheControl;
114 | }
115 |
116 | if (totalCacheControl !== totalResults) {
117 | return {};
118 | }
119 |
120 | var responseHeaders = {},
121 | privateCache = false,
122 | publicCache = false,
123 | maxMaxAge = 0,
124 | maxSharedMaxAge = 0,
125 | currentCacheControl = {},
126 | currentVaries = [],
127 | varies = [];
128 |
129 | for (name in headers) {
130 | currentCacheControl = headers[name].cacheControl;
131 | currentVaries = headers[name].varies;
132 |
133 | if (currentCacheControl.maxAge > maxMaxAge) {
134 | maxMaxAge = currentCacheControl.maxAge;
135 | }
136 |
137 | if (currentCacheControl.sharedMaxAge > maxSharedMaxAge) {
138 | maxSharedMaxAge = currentCacheControl.sharedMaxAge;
139 | }
140 |
141 | if (currentCacheControl['private']) {
142 | privateCache = true;
143 | }
144 |
145 | if (currentCacheControl['public']) {
146 | publicCache = true;
147 | }
148 |
149 | for (var i in currentVaries) {
150 | if (-1 === varies.indexOf(currentVaries[i])) {
151 | varies.push(currentVaries[i]);
152 | }
153 | }
154 | }
155 |
156 | if (privateCache || publicCache || maxMaxAge || maxSharedMaxAge) {
157 | var cacheControl = [];
158 |
159 | if (privateCache) {
160 | cacheControl.push('private');
161 | } else if (publicCache) {
162 | cacheControl.push('public');
163 | }
164 |
165 | if (maxMaxAge) {
166 | cacheControl.push('max-age=' + maxMaxAge);
167 | }
168 |
169 | if (maxSharedMaxAge) {
170 | cacheControl.push('s-maxage=' + maxSharedMaxAge);
171 | }
172 |
173 | responseHeaders['Cache-Control'] = cacheControl.join(", ");
174 | }
175 |
176 | if (varies.length) {
177 | responseHeaders.Vary = varies.join(", ");
178 | }
179 |
180 | return responseHeaders;
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/lib/httpClient.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function (superagent) {
4 |
5 | // co compatible thunk with node style callback
6 | var getUrl = function (url, headers) {
7 | return function (cb) {
8 |
9 | var request = superagent.get(url)
10 | .on('error', function (error) {
11 | // throw error;
12 | cb(error);
13 | });
14 |
15 | for(var name in headers) {
16 | request.set(name, headers[name]);
17 | }
18 |
19 | request.end(function (res) {
20 | // return res;
21 | cb(null, res);
22 | });
23 | };
24 | };
25 |
26 | function* get(url, headers) {
27 |
28 | headers = headers || {};
29 | try {
30 | var response = yield getUrl(url, headers);
31 | } catch (error) {
32 | switch(error.code) {
33 | case 'ENOTFOUND':
34 | return {
35 | code: 404,
36 | };
37 | case 'ECONNREFUSED':
38 | return {
39 | code: 403,
40 | };
41 | default:
42 | return {
43 | code: 500,
44 | body: error.code + ': ' + error.message
45 | };
46 | }
47 | }
48 |
49 | if (response.code) {
50 | return response;
51 | }
52 |
53 | var result = {
54 | code: response.status,
55 | headers: [],
56 | body: response.body
57 | };
58 | for(var name in response.headers) {
59 | if (!response.headers.hasOwnProperty(name)) {
60 | continue;
61 | }
62 | result.headers.push({
63 | name: name,
64 | value: response.headers[name]
65 | });
66 | }
67 |
68 | return result;
69 | }
70 |
71 | return {
72 | get: get
73 | };
74 | };
75 |
--------------------------------------------------------------------------------
/lib/multiFetch.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var request = require('superagent');
4 | var coBody = require('co-body');
5 | var batchRequestor = require('./batchRequestor')(require('./httpClient')(require('superagent')));
6 |
7 | module.exports = function (options) {
8 | options = options || {};
9 | var er = 0;
10 | return function* batch() {
11 | var data;
12 | try {
13 | data = yield coBody(this);
14 | } catch (e) {
15 | data = this.request.query;
16 | }
17 |
18 | if (options.header_host) {
19 | this.headers.host = options.header_host;
20 | }
21 |
22 | var baseUrl = options.absolute ? this.headers.host : this.headers.host + this.req._parsedUrl.pathname;
23 |
24 | var result = yield batchRequestor(data, baseUrl, this.headers);
25 |
26 | this.body = result.body;
27 | for (var name in result.headers) {
28 | this.set(name, result.headers[name]);
29 | }
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "koa-multifetch",
3 | "version": "0.6.1",
4 | "description": "A Koa.js middleware for performing internal batch requests",
5 | "main": "lib/multiFetch.js",
6 | "directories": {
7 | "test": "test"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git://github.com/marmelab/koa-multifetch.git"
12 | },
13 | "author": "marmelab",
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/marmelab/koa-multifetch/issues"
17 | },
18 | "homepage": "https://github.com/marmelab/koa-multifetch",
19 | "engines": {
20 | "node": ">=0.11"
21 | },
22 | "scripts": {
23 | "test": "NODE_ENV=test node_modules/mocha/bin/mocha --harmony test/*.js test/**/*.js"
24 | },
25 | "devDependencies": {
26 | "chai": "~1.9.2",
27 | "koa": "~0.13.0",
28 | "koa-route": "~2.2.0",
29 | "mocha": "~1.21.5",
30 | "mockery": "^1.4.0",
31 | "supertest": "~0.14.0"
32 | },
33 | "dependencies": {
34 | "co": "~3.1.0",
35 | "co-body": "~1.0.0",
36 | "superagent": "~0.20.0"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/test/batchRequestor.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 | var co = require('co');
3 |
4 | describe('batchRequestor', function () {
5 | var mockRequestor, batchRequestor;
6 |
7 | beforeEach(function () {
8 | mockRequestor = {
9 | get: function* (url, headers) {
10 | this.getCall++;
11 | this.headers = headers;
12 |
13 | return { code: 200, body: 'result for ' + url, headers: {} };
14 | },
15 | headers: null,
16 | getCall: 0
17 | };
18 | batchRequestor = require('../lib/batchRequestor')(mockRequestor);
19 | });
20 |
21 | it('should return result for all given url', co(function* () {
22 | var result = yield batchRequestor({resource1: '/resource1/5', resource2: '/resource2'}, '/host/api');
23 | assert.deepEqual(result.body, {
24 | resource1: { code: 200, body: 'result for /host/api/resource1/5', headers: {} },
25 | resource2: { code: 200, body: 'result for /host/api/resource2', headers: {} }
26 | });
27 | }));
28 |
29 | it ('should call requestor once for every request with baseurl + requestUrl', co(function* () {
30 | yield batchRequestor({resource1: '/resource1/5', resource2: '/resource2'}, '/host/api');
31 |
32 | assert.equal(mockRequestor.getCall, 2);
33 | mockRequestor.getCall = 0;
34 |
35 | yield batchRequestor({resource1: '/host/api/resource1/5', resource2: '/host/api/resource2', resource3: '/host/api/resource3'}, '/host/api');
36 | assert.equal(mockRequestor.getCall, 3);
37 | }));
38 |
39 | it('should use provided headers if any for every request', co(function* () {
40 | var expectedHeaders = { Accept: 'text/json'};
41 |
42 | yield batchRequestor({resource1: 'api/resource1/5', resource2: 'api/resource2'}, '/host/api', { Accept: 'text/json'});
43 | assert.deepEqual(mockRequestor.headers, expectedHeaders);
44 |
45 | assert.equal(mockRequestor.getCall, 2);
46 | }));
47 |
48 | it('should include Cache-Control and Vary headers extracted from results', co(function* () {
49 | var responseHeaders = {};
50 | mockRequestor = {
51 | get: function* (url, headers) {
52 | responseHeaders = {};
53 | if ('/api/1' === url) {
54 | responseHeaders = [
55 | { name: 'Cache-Control', value: 'public, max-age=300' },
56 | { name: 'Vary', value: 'Cookie' }
57 | ];
58 | }
59 | if ('/api/2' === url) {
60 | responseHeaders = [
61 | { name: 'Cache-Control', value: 'public, max-age=3600, s-maxage=3600' },
62 | { name: 'Vary', value: 'User-Agent' }
63 | ];
64 | }
65 |
66 | return { code: 200, body: 'result for ' + url, headers: responseHeaders };
67 | }
68 | }
69 | batchRequestor = require('../lib/batchRequestor')(mockRequestor);
70 |
71 | var results = yield batchRequestor({resource1: 'api/1', resource2: 'api/2'}, '/');
72 | assert.deepEqual(results.headers, { 'Cache-Control': 'public, max-age=3600, s-maxage=3600', Vary: 'Cookie, User-Agent' });
73 | }));
74 |
75 | it('should not include Cache-Control and Vary header if at least one of results omit them', co(function* () {
76 | var responseHeaders = {};
77 | mockRequestor = {
78 | get: function* (url, headers) {
79 | responseHeaders = {};
80 | if ('/api/1' === url) {
81 | responseHeaders = [
82 | { name: 'Cache-Control', value: 'public, max-age=300' },
83 | { name: 'Vary', value: 'Cookie' }
84 | ];
85 | }
86 |
87 | return { code: 200, body: 'result for ' + url, headers: responseHeaders };
88 | }
89 | }
90 | batchRequestor = require('../lib/batchRequestor')(mockRequestor);
91 |
92 | results = yield batchRequestor({ resource1: 'api/1', resource2: 'api/2' }, '/');
93 | assert.deepEqual(results.headers, {});
94 | }));
95 |
96 | it('should set Cache-Control if at least one of results have no-cache directive', co(function* () {
97 | var responseHeaders = {};
98 | mockRequestor = {
99 | get: function* (url, headers) {
100 | responseHeaders = {};
101 | if ('/api/1' === url) {
102 | responseHeaders = [
103 | { name: 'Cache-Control', value: 'no-cache' }
104 | ];
105 | }
106 |
107 | return { code: 200, body: 'result for ' + url, headers: responseHeaders };
108 | }
109 | }
110 | batchRequestor = require('../lib/batchRequestor')(mockRequestor);
111 |
112 | results = yield batchRequestor({ resource1: 'api/1', resource2: 'api/2' }, '/');
113 | assert.deepEqual(results.headers, { 'Cache-Control': 'no-cache' });
114 | }));
115 | });
116 |
--------------------------------------------------------------------------------
/test/httpClient.js:
--------------------------------------------------------------------------------
1 | var co = require('co');
2 | var assert = require('chai').assert;
3 |
4 | describe('httpClient', function () {
5 | var httpClient, superAgentMock;
6 |
7 | beforeEach(function () {
8 | superAgentMock = {
9 | get: function (){return this;},
10 | set: function () {return this;},
11 | on: function () {return this;},
12 | end: function (cb) {
13 | cb({});
14 | }
15 | };
16 | httpClient = require('../lib/httpClient')(superAgentMock);
17 | });
18 |
19 | it('should return code 500 and its detail on unknown error', co(function* () {
20 | superAgentMock.on = function (type, cb) {
21 | var error = new Error('unknown error');
22 | error.code = 'UNEXPECTED';
23 | cb(error);
24 | };
25 |
26 | var response = yield httpClient.get('wrong/url');
27 |
28 | assert.equal(response.code, 500);
29 | assert.equal(response.body, 'UNEXPECTED: unknown error');
30 | }));
31 |
32 | it('should return res with code 404 on inexistant url', co(function* () {
33 | superAgentMock.on = function (type, cb) {
34 | cb({code: 'ENOTFOUND'});
35 | };
36 |
37 | assert.deepEqual(yield httpClient.get('wrong/url'), {
38 | code: 404
39 | });
40 | }));
41 |
42 | it('should send get request to given url using provided agent returning response code headers and body', co(function* () {
43 | superAgentMock.end = function (cb) {
44 | cb({
45 | status: 200,
46 | headers: {
47 | "Content-Type": "text/javascript; charset=UTF-8"
48 | },
49 | body: 'result'
50 | });
51 | };
52 |
53 | assert.deepEqual(yield httpClient.get('api/resources'), {
54 | code: 200,
55 | headers: [
56 | { "name": "Content-Type", "value": "text/javascript; charset=UTF-8" }
57 | ],
58 | body: 'result'
59 | });
60 | }));
61 |
62 | it('should set the headers values on the request if provided', co(function* () {
63 |
64 | var setCall = 0;
65 |
66 | superAgentMock.set = function (name, value) {
67 | setCall++;
68 | var expectation = {
69 | 'Accept': 'text/javascript; charset=UTF-8',
70 | 'Access-Token': '4589edf54'
71 | };
72 | assert.equal(expectation[name], value);
73 |
74 | return this;
75 | };
76 |
77 | yield httpClient.get('api/resources', {'Access-Token': '4589edf54', 'Accept': 'text/javascript; charset=UTF-8'});
78 |
79 | assert.equal(setCall, 2, 'set method should have been called twice');
80 | }));
81 |
82 | it('should parse the response headers', co(function* () {
83 |
84 | superAgentMock.end = function (cb) {
85 | cb({
86 | status: 200,
87 | headers: {
88 | 'response-header': 'some header',
89 | 'one-more': 'another header'
90 | },
91 | body: 'result'
92 | });
93 | };
94 |
95 | assert.deepEqual((yield httpClient.get('api/resources')).headers, [
96 | { "name": "response-header", "value": "some header" },
97 | { "name": "one-more", "value": "another header" }
98 | ]);
99 |
100 | }));
101 |
102 | });
103 |
--------------------------------------------------------------------------------
/test/multiFetch.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 | var co = require('co');
3 | var koa = require('koa');
4 | var koaRoute = require('koa-route');
5 | var request = require('supertest');
6 | var batch = require('../lib/multiFetch');
7 |
8 | describe('multifetch', function () {
9 | var app;
10 |
11 | before(function () {
12 | app = koa();
13 | app.use(koaRoute.post('/api', batch()));
14 | app.use(koaRoute.get('/api', batch()));
15 | app.use(koaRoute.get('/api/resource1', function* () {
16 | this.set('Custom-Header', 'why not');
17 | this.body = {result: 'resource1'};
18 | }));
19 | app.use(koaRoute.get('/api/resource2/:id', function* (id) {
20 | this.set('Other-Custom-Header', 'useful');
21 | this.body = {result: 'resource2/' + id};
22 | }));
23 | app.use(koaRoute.get('/api/boom', function* (id) {
24 | throw new Error('boom');
25 | }));
26 | app.use(koaRoute.get('/api/protected', function* (id) {
27 | if (this.get('Authorization') === 'Token abcdef') {
28 | this.body = true;
29 | return;
30 | }
31 | this.status = 403;
32 | }));
33 | });
34 |
35 | it ('should return code 404 if url is not found', function (done) {
36 | request.agent(app.listen())
37 | .post('/api')
38 | .send({wrong: '/wrong'})
39 | .expect(function (res) {
40 | assert.equal(res.body.wrong.code, 404);
41 | assert.deepEqual(res.body.wrong.body, {});
42 | })
43 | .end(done);
44 | });
45 |
46 | it ('should return code 500 if server error occur', function (done) {
47 | request.agent(app.listen())
48 | .post('/api')
49 | .send({boom: '/boom'})
50 | .expect(function (res) {
51 | assert.equal(res.body.boom.code, 500);
52 | })
53 | .end(done);
54 | });
55 |
56 | describe('GET', function () {
57 |
58 | it('should call each passed request and return their result', function (done) {
59 | request.agent(app.listen())
60 | .get('/api?resource1=/resource1&resource2=/resource2/5')
61 | .expect(function (res) {
62 | assert.equal(res.body.resource1.code, 200);
63 | assert.deepEqual(res.body.resource1.body, {result: 'resource1'});
64 | assert.equal(res.body.resource2.code, 200);
65 | assert.deepEqual(res.body.resource2.body, {result: 'resource2/5'});
66 | })
67 | .end(done);
68 | });
69 |
70 | it('should use main request headers on each sub request', co(function* () {
71 | var response = yield function (done) {
72 | request.agent(app.listen())
73 | .get('/api?protected=/protected')
74 | .set('Authorization', 'Token wrongtoken')
75 | .end(done);
76 | };
77 |
78 | assert.equal(response.body.protected.code, 403);
79 |
80 | yield function (done) {
81 | request.agent(app.listen())
82 | .get('/api?protected=/protected')
83 | .set('Authorization', 'Token abcdef')
84 | .expect(200)
85 | .end(done);
86 | };
87 |
88 | assert.equal(response.body.protected.code, 403);
89 | }));
90 |
91 | it('should return the header for each request', function (done) {
92 | request.agent(app.listen())
93 | .get('/api?resource1=/resource1&resource2=/resource2/5')
94 | .expect(function (res) {
95 | assert.include(res.body.resource1.headers, {name: 'custom-header', value: 'why not'});
96 | assert.include(res.body.resource2.headers, {name: 'other-custom-header', value: 'useful'});
97 | })
98 | .end(done);
99 | });
100 |
101 | });
102 |
103 | describe('POST', function () {
104 |
105 | it ('should call each passed request and return their result', function (done) {
106 | request.agent(app.listen())
107 | .post('/api')
108 | .send({resource1: '/resource1', resource2: '/resource2/5'})
109 | .expect(function (res) {
110 | assert.equal(res.body.resource1.code, 200);
111 | assert.deepEqual(res.body.resource1.body, {result: 'resource1'});
112 | assert.equal(res.body.resource2.code, 200);
113 | assert.deepEqual(res.body.resource2.body, {result: 'resource2/5'});
114 | })
115 | .end(done);
116 | });
117 |
118 | it ('should return the header for each request', function (done) {
119 | request.agent(app.listen())
120 | .post('/api')
121 | .send({resource1: '/resource1', resource2: '/resource2/5'})
122 | .expect(function (res) {
123 | assert.include(res.body.resource1.headers, {name: 'custom-header', value: 'why not'});
124 | assert.include(res.body.resource2.headers, {name: 'other-custom-header', value: 'useful'});
125 | })
126 | .end(done);
127 | });
128 |
129 | });
130 |
131 | describe('absolute url to true', function () {
132 |
133 | before (function () {
134 | app.use(koaRoute.post('/absolute_path', batch({absolute: true})));
135 | });
136 |
137 | describe('POST', function () {
138 |
139 | it('should call each passed request and return their result', co(function* () {
140 | var response = yield function (done) {
141 | request.agent(app.listen())
142 | .post('/absolute_path')
143 | .send({resource1: '/api/resource1', resource2: '/api/resource2/5'})
144 | .end(done);
145 | };
146 |
147 | assert.equal(response.body.resource1.code, 200);
148 | assert.deepEqual(response.body.resource1.body, {result: 'resource1'});
149 | assert.equal(response.body.resource2.code, 200);
150 | assert.deepEqual(response.body.resource2.body, {result: 'resource2/5'});
151 | }));
152 |
153 | it ('should return the header for each request', function (done) {
154 | request.agent(app.listen())
155 | .post('/absolute_path')
156 | .send({resource1: '/api/resource1', resource2: '/api/resource2/5'})
157 | .expect(function (res) {
158 | assert.include(res.body.resource1.headers, {name: 'custom-header', value: 'why not'});
159 | assert.include(res.body.resource2.headers, {name: 'other-custom-header', value: 'useful'});
160 | })
161 | .end(done);
162 | });
163 |
164 | });
165 | });
166 |
167 | after(function () {
168 | delete app;
169 | });
170 |
171 | });
172 |
--------------------------------------------------------------------------------
/test/multiFetch_customHost.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 | var co = require('co');
3 | var koa = require('koa');
4 | var koaRoute = require('koa-route');
5 | var request = require('supertest');
6 | var mockery = require('mockery');
7 |
8 | describe('multifetch with custom header_host', function () {
9 | var app;
10 | var hostTest = null;
11 |
12 | before(function () {
13 | var mock_httpClient = function () {
14 | return {
15 | get: function *(url, headers) {
16 | hostTest = headers.host;
17 | }
18 | };
19 | };
20 | mockery.registerAllowable('./httpClient', true);
21 | mockery.enable({
22 | useCleanCache: true,
23 | warnOnReplace: false,
24 | warnOnUnregistered: false
25 | });
26 | mockery.registerMock('./httpClient', mock_httpClient);
27 | var batch = require('../lib/multiFetch');
28 |
29 | app = koa();
30 | app.use(koaRoute.get('/api', batch({header_host: 'http://localhost:3000'})));
31 | app.use(koaRoute.get('/api/resource1', function* () {
32 | this.set('Custom-Header', 'why not');
33 | this.body = {result: 'resource1'};
34 | }));
35 | });
36 |
37 | it('should set custom host on htttClient subrequest', co(function* () {
38 | var response = yield function (done) {
39 | request.agent(app.listen())
40 | .get('/api?resource1=/resource1')
41 | .end(done);
42 | };
43 | assert.equal(hostTest, 'http://localhost:3000');
44 | }));
45 |
46 | after(function () {
47 | mockery.disable();
48 | delete app;
49 | });
50 |
51 | });
52 |
--------------------------------------------------------------------------------