nodeunit-api-client 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/httpclient.js +186 -118
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodeunit-api-client",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Lightweight HTTP/HTTPS client with built-in testing assertions for Nodeunit",
5
5
  "main": "src/httpclient.js",
6
6
  "exports": {
package/src/httpclient.js CHANGED
@@ -1,207 +1,275 @@
1
1
  'use strict';
2
- var querystring = require('querystring'),
3
- underscore = require('underscore'),
4
- debug;
5
2
 
3
+ var http = require('http'),
4
+ https = require('https'),
5
+ querystring = require('querystring'),
6
+ underscore = require('underscore');
6
7
 
7
8
  /**
8
9
  * @param {object} Options:
10
+ * https (false) - Set to true for HTTPS calls
9
11
  * auth ('username:password')
10
12
  * host ('localhost')
11
- * port (80)
13
+ * port (80 for http, 443 for https)
12
14
  * path ('') - Base path URL e.g. '/api'
13
- * headers ({}) - Test that these headers are present on every response (unless overridden)
14
- * status (null) - Test that every response has this status (unless overridden)
15
- * https (false) - https/http
15
+ * headers ({}) - Default headers for all requests
16
+ * status (null) - Expected status code for all responses
17
+ * timeout (0) - Request timeout in milliseconds (0 = no timeout)
16
18
  * @param options
17
19
  */
18
20
  var HttpClient = module.exports = function(options) {
19
21
  options = options || {};
20
22
 
23
+ this.https = options.https || false;
21
24
  this.auth = options.auth || undefined;
22
25
  this.host = options.host || 'localhost';
23
- this.port = options.port || 80;
26
+ this.port = options.port || (this.https ? 443 : 80);
24
27
  this.path = options.path || '';
25
28
  this.headers = options.headers || {};
26
29
  this.status = options.status;
27
- this.http = require(options.https ? 'https' : 'http');
28
- debug = options.debug ? true : false;
30
+ this.timeout = options.timeout || 0;
29
31
  };
30
32
 
31
33
  HttpClient.create = function(options) {
32
34
  return new HttpClient(options);
33
35
  };
34
36
 
35
- var methods = ['get', 'post', 'head', 'put', 'del', 'trace', 'options', 'connect'];
37
+ var methods = ['get', 'post', 'head', 'put', 'del', 'trace', 'options', 'connect', 'patch'];
36
38
 
37
39
  /**
38
- * Performs a testable http request
40
+ * Performs HTTP/HTTPS request
39
41
  *
40
- * @param {Assert}
41
- * Nodeunit assert object
42
- * @param {string} [route=undefined]
43
- * http uri to test
44
- * @param {object} [req=undefined]
45
- * Object containing request related attributes like headers or body.
46
- * @param {object} [res=undefined]
47
- * Object to compare with the response of the http call
48
- * @param {Function} [cb=undefined]
49
- * Callback that will be called after the http call. Receives the http response object.
42
+ * @param {object|null} assert - Assert object for testing (can be null)
43
+ * @param {string} path - Request path
44
+ * @param {object} req - Request options (headers, data, auth, etc.)
45
+ * @param {object} res - Expected response options (status, headers, body, etc.)
46
+ * @param {Function} cb - Callback function
50
47
  */
51
48
  methods.forEach(function(method) {
52
49
  HttpClient.prototype[method] = function(assert, path, req, res, cb) {
53
50
  var self = this;
54
51
 
55
- //Handle different signatures
56
- if (arguments.length == 3) {
57
- //(assert, path, cb)
52
+ // Initialize default values
53
+ req = req || {};
54
+ res = res || {};
55
+
56
+ // Handle different signatures
57
+ if (arguments.length === 2) {
58
+ // (assert, path) or (null, path)
59
+ cb = null;
60
+ } else if (arguments.length === 3) {
58
61
  if (typeof req === 'function') {
62
+ // (assert, path, cb)
59
63
  cb = req;
60
64
  req = {};
61
65
  res = {};
62
- }
63
-
64
- //(assert, path, res)
65
- else {
66
+ } else {
67
+ // (assert, path, res)
66
68
  cb = null;
67
69
  res = req;
68
70
  req = {};
69
71
  }
70
- }
71
-
72
- //(assert, path, req, cb)
73
- if (arguments.length == 4) {
74
- if (typeof res == 'function') {
72
+ } else if (arguments.length === 4) {
73
+ if (typeof res === 'function') {
74
+ // (assert, path, req, cb)
75
75
  cb = res;
76
76
  res = {};
77
77
  }
78
+ // else: (assert, path, req, res)
78
79
  }
80
+ // arguments.length === 5: (assert, path, req, res, cb)
79
81
 
80
- //Also accepted:
81
- //(assert, path, req, res)
82
- //(assert, path, req, res, cb)
82
+ // Generate full path
83
+ var fullPath = this.path + (path || '');
83
84
 
84
- //Generate path based on base path, route path and querystring params
85
- var fullPath = this.path + path;
85
+ // Add query parameters for GET-like methods
86
+ if (['post', 'put', 'patch'].indexOf(method) === -1) {
87
+ var queryData = req.query || req.data;
88
+ if (queryData && typeof queryData === 'object') {
89
+ var queryStr = querystring.stringify(queryData);
90
+ if (queryStr) {
91
+ fullPath += (fullPath.indexOf('?') === -1 ? '?' : '&') + queryStr;
92
+ }
93
+ }
94
+ }
86
95
 
87
- //Don't add to querystring if POST or PUT
88
- if (['post', 'put'].indexOf(method) === -1) {
89
- var data = req.data;
96
+ // Prepare request headers
97
+ var requestHeaders = underscore.extend({}, this.headers, req.headers || {});
90
98
 
91
- if (data) {fullPath += '?' + querystring.stringify(data);}
92
- }
99
+ // Clean undefined headers
100
+ Object.keys(requestHeaders).forEach(function(key) {
101
+ if (requestHeaders[key] === undefined || requestHeaders[key] === null) {
102
+ delete requestHeaders[key];
103
+ }
104
+ });
93
105
 
94
106
  var options = {
95
107
  host: this.host,
96
108
  port: this.port,
97
109
  path: fullPath,
98
- method: method == 'del' ? 'DELETE' : method.toUpperCase(),
99
- headers: underscore.extend({}, this.headers, req.headers)
110
+ method: method === 'del' ? 'DELETE' : method.toUpperCase(),
111
+ headers: requestHeaders,
112
+ rejectUnauthorized: false // Disable SSL verification
100
113
  };
101
114
 
115
+ // Set authentication
102
116
  if (req.auth) {
103
117
  options.auth = req.auth;
104
118
  } else if (this.auth) {
105
119
  options.auth = this.auth;
106
120
  }
107
121
 
108
- var request = this.http.request(options);
122
+ // Set timeout
123
+ if (req.timeout || this.timeout) {
124
+ options.timeout = req.timeout || this.timeout;
125
+ }
126
+
127
+ // Choose HTTP or HTTPS module
128
+ var requestModule = this.https ? https : http;
129
+ var request = requestModule.request(options);
130
+
131
+ // Set request timeout
132
+ if (options.timeout) {
133
+ request.setTimeout(options.timeout, function() {
134
+ request.abort();
135
+ var error = new Error('Request timeout after ' + options.timeout + 'ms');
136
+ error.code = 'TIMEOUT';
137
+ if (cb) {
138
+ return cb(null, error);
139
+ }
140
+ });
141
+ }
109
142
 
110
- //Write POST & PUTdata
111
- if (['post', 'put'].indexOf(method) != -1) {
112
- var data = req.data || req.body;
143
+ // Handle request body for POST/PUT/PATCH
144
+ if (['post', 'put', 'patch'].indexOf(method) !== -1) {
145
+ var bodyData = req.body || req.data;
113
146
 
114
- if (data) {
115
- if (typeof data == 'object') {
116
- request.setHeader('content-type', 'application/json');
117
- request.write(JSON.stringify(data));
118
- } else {
119
- request.write(data);
147
+ if (bodyData) {
148
+ if (typeof bodyData === 'object') {
149
+ // JSON data
150
+ var jsonData = JSON.stringify(bodyData);
151
+ request.setHeader('Content-Type', 'application/json');
152
+ request.setHeader('Content-Length', Buffer.byteLength(jsonData));
153
+ request.write(jsonData);
154
+ } else if (typeof bodyData === 'string') {
155
+ // String data
156
+ request.setHeader('Content-Length', Buffer.byteLength(bodyData));
157
+ request.write(bodyData);
158
+ } else if (Buffer.isBuffer(bodyData)) {
159
+ // Buffer data
160
+ request.setHeader('Content-Length', bodyData.length);
161
+ request.write(bodyData);
120
162
  }
121
163
  }
122
164
  }
123
165
 
124
- if (debug) {httpClientLogger.log('REQUEST', request);}
125
- //Send
166
+ // Handle form data
167
+ if (req.form && typeof req.form === 'object') {
168
+ var formData = querystring.stringify(req.form);
169
+ request.setHeader('Content-Type', 'application/x-www-form-urlencoded');
170
+ request.setHeader('Content-Length', Buffer.byteLength(formData));
171
+ request.write(formData);
172
+ }
173
+
174
+ // Send request
126
175
  request.end();
127
176
 
177
+ // Handle response
128
178
  request.on('response', function(response) {
129
- if (debug) {httpClientLogger.log('RESPONSE', response);}
130
-
131
179
  response.setEncoding('utf8');
180
+ response.body = '';
132
181
 
133
182
  response.on('data', function(chunk) {
134
- if (response.body) {response.body += chunk;} else {response.body = chunk;}
183
+ response.body += chunk;
135
184
  });
136
185
 
137
- //Handle the response; run response tests and hand back control to test
138
186
  response.on('end', function() {
139
- //Add parsed JSON
140
- var contentType = response.headers['content-type'];
141
- if (contentType && contentType.indexOf('application/json') != -1) {
142
- if (typeof response.body != 'undefined') {
143
- //Catch errors on JSON.parse and attempt to handle cases where the response.body contains html
144
- try {
145
- response.data = JSON.parse(response.body);
146
- } catch (err) {
147
- console.log('JSON.parse response.body error:');
148
- console.log(err);
149
- if (debug) {httpClientLogger.log('RESPONSE.BODY', response.body);}
150
- var responseTest = response.body.split('{');
151
- if (responseTest.length > 1) {
152
- var actualResponse = '{' + responseTest[1];
153
- try {
154
- response.data = JSON.parse(actualResponse);
155
- console.log('JSON.parse second attempt success.');
156
- } catch (err) {
157
- console.log('JSON.parse error on second parse attempt.');
158
- console.log(err);
159
- if (debug) {httpClientLogger.log('FILTERED RESPONSE.BODY', actualResponse);}
160
- }
161
- }
162
- }
187
+ // Parse JSON if content-type indicates JSON
188
+ var contentType = response.headers['content-type'] || '';
189
+ if (contentType.indexOf('application/json') !== -1) {
190
+ try {
191
+ response.data = JSON.parse(response.body);
192
+ } catch (e) {
193
+ response.data = null;
194
+ response.parseError = e;
163
195
  }
164
196
  }
165
197
 
166
- //Run tests on the response
167
- (function testResponse() {
168
- //Can pass in falsy value to prevent running tests
169
- if (!assert) {return;}
198
+ // Add convenience properties
199
+ response.ok = response.statusCode >= 200 && response.statusCode < 300;
200
+ response.clientError = response.statusCode >= 400 && response.statusCode < 500;
201
+ response.serverError = response.statusCode >= 500;
170
202
 
171
- //Status code
172
- var status = res.status || self.status;
173
- if (status) {
174
- assert.equal(response.statusCode, status);
175
- }
203
+ // Run response tests if assert is provided
204
+ if (assert) {
205
+ try {
206
+ // Test status code
207
+ var expectedStatus = res.status || self.status;
208
+ if (expectedStatus) {
209
+ assert.equal(response.statusCode, expectedStatus,
210
+ 'Expected status ' + expectedStatus + ' but got ' + response.statusCode);
211
+ }
176
212
 
177
- //Headers
178
- var headers = underscore.extend({}, self.headers, res.headers);
179
- for (var key in headers) {
180
- assert.equal(response.headers[key], headers[key]);
181
- }
213
+ // Test headers
214
+ var expectedHeaders = underscore.extend({}, self.headers, res.headers || {});
215
+ Object.keys(expectedHeaders).forEach(function(key) {
216
+ var expected = expectedHeaders[key];
217
+ var actual = response.headers[key.toLowerCase()];
218
+ if (expected !== undefined) {
219
+ assert.equal(actual, expected,
220
+ 'Expected header "' + key + '" to be "' + expected + '" but got "' + actual + '"');
221
+ }
222
+ });
182
223
 
183
- //Body
184
- if (res.body) {
185
- assert.equal(response.body, res.body);
186
- }
224
+ // Test response body
225
+ if (res.body !== undefined) {
226
+ assert.equal(response.body, res.body, 'Response body mismatch');
227
+ }
187
228
 
188
- //JSON data
189
- if (res.data) {
190
- assert.deepEqual(response.data, res.data);
191
- }
192
- })();
229
+ // Test JSON data
230
+ if (res.data !== undefined) {
231
+ assert.deepEqual(response.data, res.data, 'Response data mismatch');
232
+ }
193
233
 
234
+ // Test response properties
235
+ if (res.ok !== undefined) {
236
+ assert.equal(response.ok, res.ok, 'Response ok status mismatch');
237
+ }
194
238
 
195
- //Done, return control to test
196
- if (cb) {return cb(response);} else {return assert.done();}
239
+ } catch (assertError) {
240
+ if (cb) {
241
+ return cb(response, assertError);
242
+ } else if (assert.done) {
243
+ return assert.done(assertError);
244
+ }
245
+ throw assertError;
246
+ }
247
+ }
248
+
249
+ // Call callback or assert.done()
250
+ if (cb) {
251
+ return cb(response);
252
+ } else if (assert && assert.done) {
253
+ return assert.done();
254
+ }
197
255
  });
198
256
  });
257
+
258
+ // Handle request errors
259
+ request.on('error', function(error) {
260
+ error.request = {
261
+ method: options.method,
262
+ url: (self.https ? 'https' : 'http') + '://' + self.host + ':' + self.port + fullPath,
263
+ headers: options.headers
264
+ };
265
+
266
+ if (cb) {
267
+ return cb(null, error);
268
+ } else if (assert && assert.done) {
269
+ return assert.done(error);
270
+ }
271
+
272
+ throw error;
273
+ });
199
274
  };
200
275
  });
201
-
202
- var httpClientLogger = {
203
- log: function(header, data) {
204
- console.log(header);
205
- console.log(data);
206
- }
207
- };