1
|
|
|
/* eslint-env mocha */ |
2
|
|
|
/* eslint-disable no-unused-expressions */ |
3
|
|
|
/* global transportFactory, Net */ |
4
|
|
|
|
5
|
|
|
var Sinon = require('sinon') |
6
|
|
|
var Chai = require('chai') |
7
|
|
|
var expect = Chai.expect |
8
|
|
|
var SDK = require('../../../../lib') |
9
|
|
|
var Clients = SDK.Http |
10
|
|
|
var Method = Clients.Method |
11
|
|
|
var Client = Clients.Basic |
12
|
|
|
var Loggers = SDK.Logger |
13
|
|
|
|
14
|
|
|
Chai.use(require('chai-as-promised')) |
15
|
|
|
Chai.use(require('chai-string')) |
16
|
|
|
|
17
|
|
|
function branchStopper () { |
18
|
|
|
Chai.assert(false, 'This branch should have never been executed') |
19
|
|
|
} |
20
|
|
|
|
21
|
|
|
describe('Integration', function () { |
22
|
|
|
describe('/http', function () { |
23
|
|
|
describe('/basic.js', function () { |
24
|
|
|
describe('.Client', function () { |
25
|
|
|
var clientFactory = function (transport, options) { |
26
|
|
|
options = options || {} |
27
|
|
|
options.logger = {level: Loggers.Level.Trace} |
28
|
|
|
return new Client(options, transport) |
29
|
|
|
} |
30
|
|
|
|
31
|
|
|
var transport |
32
|
|
|
var client |
33
|
|
|
var BASE_URL = 'http://localhost' |
34
|
|
|
|
35
|
|
|
beforeEach(function () { |
36
|
|
|
transport = transportFactory({code: 200, headers: [], text: '{}'}) |
37
|
|
|
client = clientFactory(transport, {url: BASE_URL}) |
38
|
|
|
}) |
39
|
|
|
|
40
|
|
|
describe('< new', function () { |
41
|
|
|
it('uses Net.httpRequestAsync transport if none passed', function () { |
42
|
|
|
Net.httpRequestAsync = Sinon.spy(function () { |
43
|
|
|
return Promise.resolve({code: 200}) |
44
|
|
|
}) |
45
|
|
|
var client = clientFactory() |
46
|
|
|
var path = '/path' |
47
|
|
|
return client |
48
|
|
|
.request(Method.Get, path) |
49
|
|
|
.then(function () { |
50
|
|
|
expect(Net.httpRequestAsync.callCount).to.eq(1) |
51
|
|
|
expect(Net.httpRequestAsync.getCall(0).args[0]).to.endWith(path) |
52
|
|
|
}) |
53
|
|
|
}) |
54
|
|
|
|
55
|
|
|
it('tolerates invalid options input', function () { |
56
|
|
|
var client = new Client(null, transport) |
57
|
|
|
var path = '/path' |
58
|
|
|
return client |
59
|
|
|
.request(Method.Get, path) |
60
|
|
|
.then(function () { |
61
|
|
|
expect(transport.callCount).to.eq(1) |
62
|
|
|
expect(transport.getCall(0).args[0]).to.endWith(path) |
63
|
|
|
}) |
64
|
|
|
}) |
65
|
|
|
}) |
66
|
|
|
|
67
|
|
|
describe('#request', function () { |
68
|
|
|
it('concatenates base url with provided url', function () { |
69
|
|
|
var path = '/search' |
70
|
|
|
return client |
71
|
|
|
.request(Method.Get, path) |
72
|
|
|
.then(function () { |
73
|
|
|
expect(transport.getCall(0).args[0]).to.eq(BASE_URL + path) |
74
|
|
|
}) |
75
|
|
|
}) |
76
|
|
|
|
77
|
|
|
it('refuses falsey method', function () { |
78
|
|
|
return client |
79
|
|
|
.request(null, '/') |
80
|
|
|
.then(branchStopper, function (error) { |
81
|
|
|
var ExceptionClass = Clients.InvalidConfigurationException |
82
|
|
|
expect(error).to.be.instanceOf(ExceptionClass) |
83
|
|
|
}) |
84
|
|
|
}) |
85
|
|
|
|
86
|
|
|
it('ignores falsey path', function () { |
87
|
|
|
return client |
88
|
|
|
.request(Method.Get) |
89
|
|
|
.then(function () { |
90
|
|
|
expect(transport.getCall(0).args[0]).to.eq(BASE_URL) |
91
|
|
|
}) |
92
|
|
|
}) |
93
|
|
|
|
94
|
|
|
it('encodes and passes provided headers', function () { |
95
|
|
|
var headers = { |
96
|
|
|
Cookie: ['a=b', 'c=d'], |
97
|
|
|
Authorization: 'Basic amM6MDQ1MQ==' |
98
|
|
|
} |
99
|
|
|
var expectation = [ |
100
|
|
|
'Cookie: a=b', |
101
|
|
|
'Cookie: c=d', |
102
|
|
|
'Authorization: Basic amM6MDQ1MQ==' |
103
|
|
|
] |
104
|
|
|
return client |
105
|
|
|
.request(Method.Get, '/', null, null, headers) |
106
|
|
|
.then(function () { |
107
|
|
|
var options = transport.getCall(0).args[1] |
108
|
|
|
expect(options.headers).to.deep.eq(expectation) |
109
|
|
|
}) |
110
|
|
|
}) |
111
|
|
|
|
112
|
|
|
it('encodes and passes provided query', function () { |
113
|
|
|
var query = { |
114
|
|
|
category: 'books', |
115
|
|
|
filter: ['title=Once upon*', 'author=*John*'] |
116
|
|
|
} |
117
|
|
|
var expectation = '?category=books&filter=title%3dOnce%20upon*&filter=author%3d*John*' |
118
|
|
|
return client |
119
|
|
|
.request(Method.Get, '/', query) |
120
|
|
|
.then(function () { |
121
|
|
|
var url = transport.getCall(0).args[0] |
122
|
|
|
expect(url.toLowerCase()).to.endWith(expectation.toLowerCase()) |
123
|
|
|
}) |
124
|
|
|
}) |
125
|
|
|
|
126
|
|
|
it('passes through payload', function () { |
127
|
|
|
var payload = '{}' |
128
|
|
|
return client |
129
|
|
|
.request(Method.Post, '/', null, payload) |
130
|
|
|
.then(function () { |
131
|
|
|
var options = transport.getCall(0).args[1] |
132
|
|
|
expect(options.postData).to.deep.eq(payload) |
133
|
|
|
}) |
134
|
|
|
}) |
135
|
|
|
|
136
|
|
|
it('decodes and returns response', function () { |
137
|
|
|
var headers = [ |
138
|
|
|
{key: 'Server', value: 'nginx/1.9.9'}, |
139
|
|
|
{key: 'Set-Cookie', value: 'lang=en'} |
140
|
|
|
] |
141
|
|
|
var dummy = {code: 200, headers: headers, text: '{}'} |
142
|
|
|
var transport = transportFactory(dummy) |
143
|
|
|
var expectation = { |
144
|
|
|
Server: ['nginx/1.9.9'], |
145
|
|
|
'Set-Cookie': ['lang=en'] |
146
|
|
|
} |
147
|
|
|
return clientFactory(transport) |
148
|
|
|
.request(Method.Get, '/test') |
149
|
|
|
.then(function (response) { |
150
|
|
|
expect(response.code).to.eq(dummy.code) |
151
|
|
|
expect(response.headers).to.deep.eq(expectation) |
152
|
|
|
expect(response.payload).to.eq(dummy.text) |
153
|
|
|
}) |
154
|
|
|
}) |
155
|
|
|
|
156
|
|
|
it('ignores method override header on GET/POST request', function () { |
157
|
|
|
var options = {methodOverrideHeader: 'X-HMO'} |
158
|
|
|
var client = clientFactory(transport, options) |
159
|
|
|
return client |
160
|
|
|
.request(Method.Get, '/') |
161
|
|
|
.then(function () { |
162
|
|
|
var options = transport.getCall(0).args[1] |
163
|
|
|
expect(options.headers).to.be.empty |
164
|
|
|
}) |
165
|
|
|
}) |
166
|
|
|
|
167
|
|
|
it('prevents non-GET/POST request without method override header', function () { |
168
|
|
|
var transport = transportFactory() |
169
|
|
|
var client = clientFactory(transport) |
170
|
|
|
|
171
|
|
|
return client |
172
|
|
|
.request(Method.Patch, '/') |
173
|
|
|
.then(branchStopper, function (error) { |
174
|
|
|
expect(error).to.be.instanceOf(Clients.InvalidConfigurationException) |
175
|
|
|
expect(transport.callCount).to.eq(0) |
176
|
|
|
}) |
177
|
|
|
}) |
178
|
|
|
|
179
|
|
|
it('adds method override header for non-GET/POST request', function () { |
180
|
|
|
var transport = transportFactory({code: 200}) |
181
|
|
|
var options = {methodOverrideHeader: 'X-HMO'} |
182
|
|
|
var client = clientFactory(transport, options) |
183
|
|
|
var method = Method.Patch |
184
|
|
|
|
185
|
|
|
return client |
186
|
|
|
.request(method, '/') |
187
|
|
|
.then(function () { |
188
|
|
|
var headers = transport.getCall(0).args[1].headers |
189
|
|
|
expect(headers).to.deep.eq(['X-HMO: ' + method]) |
190
|
|
|
}) |
191
|
|
|
}) |
192
|
|
|
|
193
|
|
|
it('merges and overwrites default headers', function () { |
194
|
|
|
var headers = {overwritten: ['a', 'b'], kept: ['c', 'd']} |
195
|
|
|
var options = {headers: headers} |
196
|
|
|
var overrides = {introduced: ['e', 'f'], overwritten: ['g']} |
197
|
|
|
var expectation = [ |
198
|
|
|
'introduced: e', |
199
|
|
|
'introduced: f', |
200
|
|
|
'overwritten: g', |
201
|
|
|
'kept: c', |
202
|
|
|
'kept: d' |
203
|
|
|
] |
204
|
|
|
var client = clientFactory(transport, options) |
205
|
|
|
return client |
206
|
|
|
.request(Method.Get, '/', [], null, overrides) |
207
|
|
|
.then(function () { |
208
|
|
|
var headers = transport.getCall(0).args[1].headers |
209
|
|
|
expect(headers.sort()).to.deep.eq(expectation.sort()) |
210
|
|
|
}) |
211
|
|
|
}) |
212
|
|
|
|
213
|
|
|
var errorTypes = { |
214
|
|
|
NetworkError: { |
215
|
|
|
code: -1, |
216
|
|
|
exception: Clients.NetworkException |
217
|
|
|
}, |
218
|
|
|
ServerError: { |
219
|
|
|
code: 500, |
220
|
|
|
exception: Clients.ServerErrorException |
221
|
|
|
}, |
222
|
|
|
ClientError: { |
223
|
|
|
code: 400, |
224
|
|
|
exception: Clients.ClientErrorException |
225
|
|
|
}, |
226
|
|
|
NotFound: { |
227
|
|
|
code: 404, |
228
|
|
|
exception: Clients.NotFoundException |
229
|
|
|
} |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
Object.keys(errorTypes).forEach(function (errorType) { |
233
|
|
|
var error = errorTypes[errorType] |
234
|
|
|
it('retries on ' + errorType + ' error if told so', function () { |
235
|
|
|
var transport = transportFactory({code: error.code}, {code: 200}) |
236
|
|
|
var opts = {} |
237
|
|
|
var client |
238
|
|
|
|
239
|
|
|
opts['retryOn' + errorType] = true |
240
|
|
|
client = clientFactory(transport, opts) |
241
|
|
|
|
242
|
|
|
return client |
243
|
|
|
.get('/test') |
244
|
|
|
.then(function (response) { |
245
|
|
|
expect(response.code).to.eq(200) |
246
|
|
|
expect(transport.callCount).to.eq(2) |
247
|
|
|
}) |
248
|
|
|
}) |
249
|
|
|
|
250
|
|
|
it('does not retry on ' + errorType + ' error unless told so', function () { |
251
|
|
|
var transport = transportFactory({code: error.code}, {code: 200}) |
252
|
|
|
var opts = {} |
253
|
|
|
var client |
254
|
|
|
|
255
|
|
|
opts['retryOn' + errorType] = false |
256
|
|
|
opts['throwOn' + errorType] = true |
257
|
|
|
client = clientFactory(transport, opts) |
258
|
|
|
|
259
|
|
|
return client |
260
|
|
|
.get('/test') |
261
|
|
|
.then(branchStopper, function (e) { |
262
|
|
|
expect(e).to.be.instanceOf(error.exception) |
263
|
|
|
expect(transport.callCount).to.eq(1) |
264
|
|
|
}) |
265
|
|
|
}) |
266
|
|
|
|
267
|
|
|
if (errorType === 'NetworkError') { |
268
|
|
|
return |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
it('does not throw on ' + errorType + ' error if told so', function () { |
272
|
|
|
var transport = transportFactory({code: error.code}) |
273
|
|
|
var opts = {} |
274
|
|
|
var client |
275
|
|
|
|
276
|
|
|
opts['retryOn' + errorType] = false |
277
|
|
|
opts['throwOn' + errorType] = false |
278
|
|
|
client = clientFactory(transport, opts) |
279
|
|
|
|
280
|
|
|
return client |
281
|
|
|
.get('/test') |
282
|
|
|
.then(function (response) { |
283
|
|
|
expect(response.code).eq(error.code) |
284
|
|
|
expect(transport.callCount).to.eq(1) |
285
|
|
|
}) |
286
|
|
|
}) |
287
|
|
|
}) |
288
|
|
|
|
289
|
|
|
it('retries not more times than specified in settings', function () { |
290
|
|
|
var transport = transportFactory({code: 500}) |
291
|
|
|
var options = { |
292
|
|
|
retries: 4, |
293
|
|
|
throwOnServerError: false, |
294
|
|
|
retryOnServerError: true |
295
|
|
|
} |
296
|
|
|
var client = clientFactory(transport, options) |
297
|
|
|
|
298
|
|
|
return client |
299
|
|
|
.request(Method.Get, '/') |
300
|
|
|
.then(function (response) { |
301
|
|
|
expect(response.code).to.eq(500) |
302
|
|
|
expect(transport.callCount).to.eq(5) |
303
|
|
|
}) |
304
|
|
|
}) |
305
|
|
|
|
306
|
|
|
it('handles response with unknown code', function () { |
307
|
|
|
var transport = transportFactory({code: -500}) |
308
|
|
|
var client = clientFactory(transport, {retryOnNetworkError: false}) |
309
|
|
|
|
310
|
|
|
return client |
311
|
|
|
.request(Method.Get, '/') |
312
|
|
|
.then(branchStopper, function (error) { |
313
|
|
|
expect(error).to.be.instanceOf(Clients.NetworkException) |
314
|
|
|
expect(error.code).to.eq(-500) |
315
|
|
|
}) |
316
|
|
|
}) |
317
|
|
|
|
318
|
|
|
it('handles response with no code', function () { |
319
|
|
|
var transport = transportFactory({}) |
320
|
|
|
var client = clientFactory(transport, {retryOnNetworkError: false}) |
321
|
|
|
|
322
|
|
|
return client |
323
|
|
|
.request(Method.Get, '/') |
324
|
|
|
.then(branchStopper, function (error) { |
325
|
|
|
expect(error).to.be.instanceOf(Clients.NetworkException) |
326
|
|
|
expect(error.code).to.eq(-1) |
327
|
|
|
}) |
328
|
|
|
}) |
329
|
|
|
}) |
330
|
|
|
|
331
|
|
|
var methods = ['get', 'head'] |
332
|
|
|
methods.forEach(function (method) { |
333
|
|
|
describe('#' + method, function () { |
334
|
|
|
it('provides ' + method + ' request method', function () { |
335
|
|
|
var options = {methodOverrideHeader: 'X-HMO'} |
336
|
|
|
var client = clientFactory(transport, options) |
337
|
|
|
var query = {alpha: 'beta'} |
338
|
|
|
var headers = {'X-Gamma': 'delta'} |
339
|
|
|
return client[method]('/', query, headers) |
340
|
|
|
.then(function () { |
341
|
|
|
var call = transport.getCall(0) |
342
|
|
|
expect(call.args[1].headers).to.contain('X-Gamma: delta') |
343
|
|
|
expect(call.args[1].postData).to.be.not.ok |
344
|
|
|
expect(call.args[0]).to.eq('/?alpha=beta') |
345
|
|
|
}) |
346
|
|
|
}) |
347
|
|
|
}) |
348
|
|
|
}) |
349
|
|
|
|
350
|
|
|
methods = ['post', 'put', 'patch', 'delete'] |
351
|
|
|
methods.forEach(function (method) { |
352
|
|
|
describe('#' + method, function () { |
353
|
|
|
it('provides ' + method + ' request method', function () { |
354
|
|
|
var options = {methodOverrideHeader: 'X-HMO'} |
355
|
|
|
var client = clientFactory(transport, options) |
356
|
|
|
var payload = '{"alpha": "beta"}' |
357
|
|
|
var headers = {'X-Gamma': 'delta'} |
358
|
|
|
var query = {alpha: 'beta'} |
359
|
|
|
return client[method]('/', payload, headers, query) |
360
|
|
|
.then(function () { |
361
|
|
|
var call = transport.getCall(0) |
362
|
|
|
expect(call.args[1].headers).to.contain('X-Gamma: delta') |
363
|
|
|
expect(call.args[1].postData).to.eq(payload) |
364
|
|
|
expect(call.args[0]).to.eq('/?alpha=beta') |
365
|
|
|
}) |
366
|
|
|
}) |
367
|
|
|
}) |
368
|
|
|
}) |
369
|
|
|
}) |
370
|
|
|
}) |
371
|
|
|
}) |
372
|
|
|
}) |
373
|
|
|
|