1 | import sinon from "sinon";
|
2 |
|
3 | import expectjs from "expect.js";
|
4 |
|
5 | import HttpClient from "../lib/HttpClient";
|
6 | import NullLogger from "../lib/NullLogger";
|
7 |
|
8 | import http from "http";
|
9 |
|
10 | var expect = require("sinon-expect").enhance(expectjs, sinon, "was");
|
11 |
|
12 | var logger = new NullLogger();
|
13 | var fakeHttp = {
|
14 | request: function() {}
|
15 | };
|
16 | var fakeRequest = {
|
17 | end: function() {},
|
18 | on: function() {}
|
19 | };
|
20 |
|
21 | var defaultHeaders = {
|
22 | "Content-Type": "application/x-www-form-urlencoded",
|
23 | Accept: "application/json"
|
24 | };
|
25 |
|
26 | var client = null;
|
27 |
|
28 | describe("HttpClient Object", function() {
|
29 | afterEach(function() {
|
30 | fakeHttp.request.restore();
|
31 | });
|
32 |
|
33 | it("should support requests over https", function() {
|
34 | var mock = sinon.mock(fakeHttp);
|
35 | mock
|
36 | .expects("request")
|
37 | .once()
|
38 | .withArgs({
|
39 | headers: defaultHeaders,
|
40 | host: "api.nexmo.com",
|
41 | method: "GET",
|
42 | path: "/api",
|
43 | port: 443
|
44 | })
|
45 | .returns(fakeRequest);
|
46 |
|
47 | var client = new HttpClient({
|
48 | https: fakeHttp,
|
49 | port: 443,
|
50 | logger: logger
|
51 | });
|
52 |
|
53 | client.request(
|
54 | {
|
55 | host: "api.nexmo.com",
|
56 | path: "/api"
|
57 | },
|
58 | "GET",
|
59 | {
|
60 | some: "data"
|
61 | }
|
62 | );
|
63 | });
|
64 |
|
65 | it("should support requests over http", function() {
|
66 | var mock = sinon.mock(fakeHttp);
|
67 | mock
|
68 | .expects("request")
|
69 | .once()
|
70 | .withArgs({
|
71 | headers: defaultHeaders,
|
72 | host: "api.nexmo.com",
|
73 | method: "GET",
|
74 | path: "/api",
|
75 | port: 80
|
76 | })
|
77 | .returns(fakeRequest);
|
78 |
|
79 | var client = new HttpClient({
|
80 | http: fakeHttp,
|
81 | port: 80,
|
82 | logger: logger
|
83 | });
|
84 |
|
85 | client.request(
|
86 | {
|
87 | host: "api.nexmo.com",
|
88 | path: "/api"
|
89 | },
|
90 | "GET",
|
91 | {
|
92 | some: "data"
|
93 | }
|
94 | );
|
95 | });
|
96 |
|
97 | it("should be possible to set the host", function() {
|
98 | var mock = sinon.mock(fakeHttp);
|
99 | mock
|
100 | .expects("request")
|
101 | .once()
|
102 | .withArgs({
|
103 | headers: defaultHeaders,
|
104 | host: "rest.nexmo.com",
|
105 | method: "GET",
|
106 | path: "/api",
|
107 | port: 80
|
108 | })
|
109 | .returns(fakeRequest);
|
110 |
|
111 | var client = new HttpClient({
|
112 | http: fakeHttp,
|
113 | port: 80,
|
114 | logger: logger
|
115 | });
|
116 |
|
117 | client.request(
|
118 | {
|
119 | host: "rest.nexmo.com",
|
120 | path: "/api"
|
121 | },
|
122 | "GET",
|
123 | {
|
124 | some: "data"
|
125 | }
|
126 | );
|
127 | });
|
128 |
|
129 | it("should be possible to set the path", function() {
|
130 | var mock = sinon.mock(fakeHttp);
|
131 | mock
|
132 | .expects("request")
|
133 | .once()
|
134 | .withArgs({
|
135 | headers: defaultHeaders,
|
136 | host: "api.nexmo.com",
|
137 | method: "GET",
|
138 | path: "/some_path",
|
139 | port: 80
|
140 | })
|
141 | .returns(fakeRequest);
|
142 |
|
143 | var client = new HttpClient({
|
144 | http: fakeHttp,
|
145 | port: 80,
|
146 | logger: logger
|
147 | });
|
148 |
|
149 | client.request(
|
150 | {
|
151 | host: "api.nexmo.com",
|
152 | path: "/some_path"
|
153 | },
|
154 | "GET",
|
155 | {
|
156 | some: "data"
|
157 | }
|
158 | );
|
159 | });
|
160 |
|
161 | it("should be possible to set the method", function() {
|
162 | var mock = sinon.mock(fakeHttp);
|
163 | mock
|
164 | .expects("request")
|
165 | .once()
|
166 | .withArgs({
|
167 | headers: defaultHeaders,
|
168 | host: "api.nexmo.com",
|
169 | method: "POST",
|
170 | path: "/api",
|
171 | port: 443
|
172 | })
|
173 | .returns(fakeRequest);
|
174 |
|
175 | var client = new HttpClient({
|
176 | https: fakeHttp,
|
177 | logger: logger
|
178 | });
|
179 |
|
180 | client.request(
|
181 | {
|
182 | host: "api.nexmo.com",
|
183 | path: "/api"
|
184 | },
|
185 | "POST",
|
186 | {
|
187 | some: "data"
|
188 | }
|
189 | );
|
190 | });
|
191 |
|
192 | it("should be possible to set the timeout", function() {
|
193 | var mock = sinon.mock(fakeHttp);
|
194 | mock
|
195 | .expects("request")
|
196 | .once()
|
197 | .withArgs({
|
198 | headers: defaultHeaders,
|
199 | host: "api.nexmo.com",
|
200 | method: "POST",
|
201 | path: "/api",
|
202 | port: 443,
|
203 | timeout: 5000
|
204 | })
|
205 | .returns(fakeRequest);
|
206 |
|
207 | var client = new HttpClient({
|
208 | https: fakeHttp,
|
209 | logger: logger,
|
210 | timeout: 5000
|
211 | });
|
212 |
|
213 | client.request(
|
214 | {
|
215 | host: "api.nexmo.com",
|
216 | path: "/api"
|
217 | },
|
218 | "POST",
|
219 | {
|
220 | some: "data"
|
221 | }
|
222 | );
|
223 | });
|
224 |
|
225 | it("should not override the method when method and callback are undefined", function() {
|
226 | var mock = sinon.mock(fakeHttp);
|
227 | mock
|
228 | .expects("request")
|
229 | .once()
|
230 | .withArgs({
|
231 | headers: defaultHeaders,
|
232 | host: "api.nexmo.com",
|
233 | method: "POST",
|
234 | path: "/api",
|
235 | port: 443
|
236 | })
|
237 | .returns(fakeRequest);
|
238 |
|
239 | var client = new HttpClient({
|
240 | https: fakeHttp,
|
241 | logger: logger
|
242 | });
|
243 |
|
244 | client.request({
|
245 | host: "api.nexmo.com",
|
246 | path: "/api",
|
247 | method: "POST"
|
248 | });
|
249 | });
|
250 |
|
251 | it("should log requests", function() {
|
252 | var mock = sinon.mock(fakeHttp);
|
253 | mock.expects("request").returns(fakeRequest);
|
254 |
|
255 | var logged = false;
|
256 | var testLogger = {
|
257 | info: function() {
|
258 | logged = true;
|
259 | }
|
260 | };
|
261 | var client = new HttpClient({
|
262 | https: fakeHttp,
|
263 | logger: testLogger
|
264 | });
|
265 |
|
266 | client.request(
|
267 | {
|
268 | host: "api.nexmo.com",
|
269 | path: "/api"
|
270 | },
|
271 | "GET",
|
272 | {
|
273 | some: "data"
|
274 | }
|
275 | );
|
276 |
|
277 | expect(logged).to.be(true);
|
278 | });
|
279 |
|
280 | it("should allow User-Agent header to be set via options", function() {
|
281 | var expectedUserAgent = "nexmo-node/1.0.0/v4.4.7";
|
282 |
|
283 | var mock = sinon.mock(fakeHttp);
|
284 | mock
|
285 | .expects("request")
|
286 | .once()
|
287 | .withArgs({
|
288 | headers: {
|
289 | "Content-Type": "application/x-www-form-urlencoded",
|
290 | Accept: "application/json",
|
291 | "User-Agent": expectedUserAgent
|
292 | },
|
293 | host: "api.nexmo.com",
|
294 | method: "POST",
|
295 | path: "/api",
|
296 | port: 443
|
297 | })
|
298 | .returns(fakeRequest);
|
299 |
|
300 | var client = new HttpClient({
|
301 | https: fakeHttp,
|
302 | logger: logger,
|
303 | userAgent: expectedUserAgent
|
304 | });
|
305 |
|
306 | client.request(
|
307 | {
|
308 | host: "api.nexmo.com",
|
309 | path: "/api"
|
310 | },
|
311 | "POST",
|
312 | {
|
313 | some: "data"
|
314 | }
|
315 | );
|
316 | });
|
317 | });
|
318 |
|
319 | describe("parseResponse", function() {
|
320 | beforeEach(function() {
|
321 | client = new HttpClient({
|
322 | https: fakeHttp,
|
323 | logger: logger
|
324 | });
|
325 | });
|
326 |
|
327 | it("should parse a 500+ status code as an error", function() {
|
328 | var callback = sinon.spy();
|
329 | const headers = { "content-type": "application/json" };
|
330 | const response = { statusCode: 504, headers: headers };
|
331 | client.__parseResponse(response, [""], "GET", callback);
|
332 | expect(callback).was.calledWith(
|
333 | { message: "Server Error", statusCode: 504, headers: headers },
|
334 | null
|
335 | );
|
336 | });
|
337 |
|
338 | it("should parse a 400-499 status code as a JSON error", function() {
|
339 | var callback = sinon.spy();
|
340 | const headers = { "content-type": "application/json" };
|
341 | const response = { statusCode: 404, headers: headers };
|
342 | client.__parseResponse(
|
343 | response,
|
344 | ['{ "error" : "error" }'],
|
345 | "GET",
|
346 | callback
|
347 | );
|
348 | expect(callback).was.calledWith(
|
349 | { statusCode: 404, body: { error: "error" }, headers: headers },
|
350 | null
|
351 | );
|
352 | });
|
353 |
|
354 | it("should parse a 204 status code as null", function() {
|
355 | var callback = sinon.spy();
|
356 | const headers = { "content-type": "application/json" };
|
357 | const response = { statusCode: 204, headers: headers };
|
358 | client.__parseResponse(response, [""], "GET", callback);
|
359 | expect(callback).was.calledWith(null, null);
|
360 | });
|
361 |
|
362 | it("should parse a 200-299 status code as a JSON object", function() {
|
363 | var callback = sinon.spy();
|
364 | const response = {
|
365 | statusCode: 201,
|
366 | headers: { "content-type": "application/json" }
|
367 | };
|
368 | client.__parseResponse(response, ['{ "data" : "data" }'], "GET", callback);
|
369 | expect(callback).was.calledWith(null, { data: "data" });
|
370 | });
|
371 |
|
372 | it("should not try and parse successful DELETE request to JSON", function() {
|
373 | var callback = sinon.spy();
|
374 | const response = {
|
375 | statusCode: 201,
|
376 | headers: { "content-type": "application/json" }
|
377 | };
|
378 | client.__parseResponse(response, [""], "DELETE", callback);
|
379 | expect(callback).was.calledWith(null, [""]);
|
380 | });
|
381 |
|
382 | it("should catch invalid json and expose the data in the error", function() {
|
383 | var callback = sinon.spy();
|
384 | const response = {
|
385 | statusCode: 201,
|
386 | headers: { "content-type": "application/json" }
|
387 | };
|
388 | const data = "not_json";
|
389 | client.__parseResponse(response, [data], "GET", callback);
|
390 | expect(callback).was.calledWith(
|
391 | sinon.match({
|
392 | message: "The API response could not be parsed.",
|
393 | body: data,
|
394 | statusCode: 201
|
395 | }),
|
396 | null
|
397 | );
|
398 | });
|
399 |
|
400 | it("should not error with invalid JSON if parsing is disabled", function() {
|
401 | var callback = sinon.spy();
|
402 | const response = {
|
403 | statusCode: 200,
|
404 | headers: { "content-type": "application/json" }
|
405 | };
|
406 | const data = "not_json";
|
407 | client.__parseResponse(response, [data], "GET", callback, true);
|
408 | expect(callback).was.calledWith(null, data);
|
409 | });
|
410 |
|
411 | it("should parse binary data", function() {
|
412 | var callback = sinon.spy();
|
413 | var data = new Buffer("data");
|
414 | const response = {
|
415 | statusCode: 200,
|
416 | headers: { "content-type": "application/octet-stream" }
|
417 | };
|
418 | client.__parseResponse(response, data, "GET", callback);
|
419 | expect(callback).was.calledWith(null, data);
|
420 | });
|
421 |
|
422 | it("should set a default retry-after of 200 for a GET with a 429 response", function() {
|
423 | var callback = sinon.spy();
|
424 | const headers = {};
|
425 | const response = { statusCode: 429, headers: headers };
|
426 | client.__parseResponse(response, [""], "GET", callback);
|
427 | expect(callback).was.calledWith(
|
428 | { statusCode: 429, body: "", headers: { "retry-after": 200 } },
|
429 | null
|
430 | );
|
431 | });
|
432 |
|
433 | it("should set a default retry-after of 500 for a POST with a 429 response", function() {
|
434 | var callback = sinon.spy();
|
435 | const headers = {};
|
436 | const response = { statusCode: 429, headers: headers };
|
437 | client.__parseResponse(response, [""], "POST", callback);
|
438 | expect(callback).was.calledWith(
|
439 | { statusCode: 429, body: "", headers: { "retry-after": 500 } },
|
440 | null
|
441 | );
|
442 | });
|
443 |
|
444 | it("should use the server returned retry-after header with a 429 response", function() {
|
445 | var callback = sinon.spy();
|
446 | const headers = { "retry-after": 400 };
|
447 | const response = { statusCode: 429, headers: headers };
|
448 | client.__parseResponse(response, [""], "GET", callback);
|
449 | expect(callback).was.calledWith(
|
450 | { statusCode: 429, body: "", headers: { "retry-after": 400 } },
|
451 | null
|
452 | );
|
453 | });
|
454 |
|
455 | it("should not modify the default headers", function() {
|
456 | var mock = sinon.mock(http);
|
457 | mock
|
458 | .expects("request")
|
459 | .once()
|
460 | .withArgs({
|
461 | headers: {
|
462 |
|
463 |
|
464 | "Content-Type": "application/json",
|
465 | Accept: "application/json"
|
466 | },
|
467 | host: "api.nexmo.com",
|
468 | method: "GET",
|
469 | path: "/api",
|
470 | port: 80
|
471 | })
|
472 | .returns(fakeRequest);
|
473 |
|
474 | var client = new HttpClient({
|
475 | port: 80,
|
476 | logger: logger
|
477 | });
|
478 |
|
479 |
|
480 | expect(client.headers["Content-Type"]).to.eql(
|
481 | "application/x-www-form-urlencoded"
|
482 | );
|
483 |
|
484 |
|
485 | client.request(
|
486 | {
|
487 | host: "api.nexmo.com",
|
488 | path: "/api",
|
489 | headers: {
|
490 | "Content-Type": "application/json"
|
491 | }
|
492 | },
|
493 | "GET",
|
494 | function() {}
|
495 | );
|
496 |
|
497 |
|
498 | expect(client.headers["Content-Type"]).to.eql(
|
499 | "application/x-www-form-urlencoded"
|
500 | );
|
501 |
|
502 | mock.verify();
|
503 | });
|
504 | });
|