nodeunit-api-client 1.4.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 +178 -146
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodeunit-api-client",
3
- "version": "1.4.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,161 +1,181 @@
1
1
  'use strict';
2
- var querystring = require('querystring'),
3
- underscore = require('underscore'),
4
- debug;
2
+
3
+ var http = require('http'),
4
+ https = require('https'),
5
+ querystring = require('querystring'),
6
+ underscore = require('underscore');
5
7
 
6
8
  /**
7
9
  * @param {object} Options:
10
+ * https (false) - Set to true for HTTPS calls
8
11
  * auth ('username:password')
9
12
  * host ('localhost')
10
- * port (80)
13
+ * port (80 for http, 443 for https)
11
14
  * path ('') - Base path URL e.g. '/api'
12
- * headers ({}) - Test that these headers are present on every response (unless overridden)
13
- * status (null) - Test that every response has this status (unless overridden)
14
- * 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)
15
18
  * @param options
16
19
  */
17
20
  var HttpClient = module.exports = function(options) {
18
21
  options = options || {};
19
22
 
23
+ this.https = options.https || false;
20
24
  this.auth = options.auth || undefined;
21
25
  this.host = options.host || 'localhost';
22
- this.port = options.port || (options.https ? 443 : 80);
26
+ this.port = options.port || (this.https ? 443 : 80);
23
27
  this.path = options.path || '';
24
28
  this.headers = options.headers || {};
25
29
  this.status = options.status;
26
- this.https = options.https || false;
27
- this.http = require(this.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
- //(assert, path, res)
64
- else {
66
+ } else {
67
+ // (assert, path, res)
65
68
  cb = null;
66
69
  res = req;
67
70
  req = {};
68
71
  }
69
- }
70
-
71
- //(assert, path, req, cb)
72
- if (arguments.length == 4) {
73
- if (typeof res == 'function') {
72
+ } else if (arguments.length === 4) {
73
+ if (typeof res === 'function') {
74
+ // (assert, path, req, cb)
74
75
  cb = res;
75
76
  res = {};
76
77
  }
78
+ // else: (assert, path, req, res)
77
79
  }
80
+ // arguments.length === 5: (assert, path, req, res, cb)
78
81
 
79
- //Also accepted:
80
- //(assert, path, req, res)
81
- //(assert, path, req, res, cb)
82
+ // Generate full path
83
+ var fullPath = this.path + (path || '');
82
84
 
83
- //Generate path based on base path, route path and querystring params
84
- var fullPath = this.path + path;
85
-
86
- //Don't add to querystring if POST or PUT
87
- if (['post', 'put'].indexOf(method) === -1) {
88
- var data = req.data;
89
- if (data) {
90
- fullPath += '?' + querystring.stringify(data);
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
+ }
91
93
  }
92
94
  }
93
95
 
96
+ // Prepare request headers
97
+ var requestHeaders = underscore.extend({}, this.headers, req.headers || {});
98
+
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
+ });
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 || {}),
100
- rejectUnauthorized: false // Pour éviter les erreurs de certificat SSL en dev
110
+ method: method === 'del' ? 'DELETE' : method.toUpperCase(),
111
+ headers: requestHeaders,
112
+ rejectUnauthorized: false // Disable SSL verification
101
113
  };
102
114
 
115
+ // Set authentication
103
116
  if (req.auth) {
104
117
  options.auth = req.auth;
105
118
  } else if (this.auth) {
106
119
  options.auth = this.auth;
107
120
  }
108
121
 
109
- var request = this.http.request(options);
122
+ // Set timeout
123
+ if (req.timeout || this.timeout) {
124
+ options.timeout = req.timeout || this.timeout;
125
+ }
110
126
 
111
- //Write POST & PUT data
112
- if (['post', 'put'].indexOf(method) != -1) {
113
- var data = req.data || req.body;
127
+ // Choose HTTP or HTTPS module
128
+ var requestModule = this.https ? https : http;
129
+ var request = requestModule.request(options);
114
130
 
115
- if (data) {
116
- if (typeof data == 'object') {
117
- options.headers['content-type'] = 'application/json';
118
- var jsonData = JSON.stringify(data);
119
- options.headers['content-length'] = Buffer.byteLength(jsonData);
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
+ }
142
+
143
+ // Handle request body for POST/PUT/PATCH
144
+ if (['post', 'put', 'patch'].indexOf(method) !== -1) {
145
+ var bodyData = req.body || req.data;
146
+
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));
120
153
  request.write(jsonData);
121
- } else {
122
- options.headers['content-length'] = Buffer.byteLength(data);
123
- request.write(data);
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);
124
162
  }
125
163
  }
126
164
  }
127
165
 
128
- if (debug) {
129
- httpClientLogger.log('REQUEST', {
130
- method: options.method,
131
- host: options.host,
132
- port: options.port,
133
- path: options.path,
134
- headers: options.headers
135
- });
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);
136
172
  }
137
173
 
138
- //Send
174
+ // Send request
139
175
  request.end();
140
176
 
141
- request.on('error', function(err) {
142
- console.error('Request error:', err);
143
- if (cb) {
144
- return cb(null, err);
145
- } else if (assert) {
146
- assert.ok(false, 'Request failed: ' + err.message);
147
- return assert.done();
148
- }
149
- });
150
-
177
+ // Handle response
151
178
  request.on('response', function(response) {
152
- if (debug) {
153
- httpClientLogger.log('RESPONSE', {
154
- statusCode: response.statusCode,
155
- headers: response.headers
156
- });
157
- }
158
-
159
179
  response.setEncoding('utf8');
160
180
  response.body = '';
161
181
 
@@ -163,81 +183,93 @@ methods.forEach(function(method) {
163
183
  response.body += chunk;
164
184
  });
165
185
 
166
- //Handle the response; run response tests and hand back control to test
167
186
  response.on('end', function() {
168
- //Add parsed JSON
169
- var contentType = response.headers['content-type'];
170
- if (contentType && contentType.indexOf('application/json') != -1) {
171
- if (response.body) {
172
- //Catch errors on JSON.parse and attempt to handle cases where the response.body contains html
173
- try {
174
- response.data = JSON.parse(response.body);
175
- } catch (err) {
176
- console.log('JSON.parse response.body error:');
177
- console.log(err);
178
- if (debug) {
179
- httpClientLogger.log('RESPONSE.BODY', response.body);
180
- }
181
- var responseTest = response.body.split('{');
182
- if (responseTest.length > 1) {
183
- var actualResponse = '{' + responseTest[1];
184
- try {
185
- response.data = JSON.parse(actualResponse);
186
- console.log('JSON.parse second attempt success.');
187
- } catch (err2) {
188
- console.log('JSON.parse error on second parse attempt.');
189
- console.log(err2);
190
- if (debug) {
191
- httpClientLogger.log('FILTERED RESPONSE.BODY', actualResponse);
192
- }
193
- }
194
- }
195
- }
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;
196
195
  }
197
196
  }
198
197
 
199
- //Run tests on the response
200
- (function testResponse() {
201
- //Can pass in falsy value to prevent running tests
202
- 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;
203
202
 
204
- //Status code
205
- var status = res.status || self.status;
206
- if (status) {
207
- assert.equal(response.statusCode, status, 'Status code mismatch');
208
- }
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
+ }
209
212
 
210
- //Headers
211
- var headers = underscore.extend({}, self.headers, res.headers);
212
- for (var key in headers) {
213
- assert.equal(response.headers[key], headers[key], 'Header mismatch: ' + key);
214
- }
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
+ });
215
223
 
216
- //Body
217
- if (res.body) {
218
- assert.equal(response.body, res.body, 'Body mismatch');
219
- }
224
+ // Test response body
225
+ if (res.body !== undefined) {
226
+ assert.equal(response.body, res.body, 'Response body mismatch');
227
+ }
220
228
 
221
- //JSON data
222
- if (res.data) {
223
- assert.deepEqual(response.data, res.data, 'Data mismatch');
229
+ // Test JSON data
230
+ if (res.data !== undefined) {
231
+ assert.deepEqual(response.data, res.data, 'Response data mismatch');
232
+ }
233
+
234
+ // Test response properties
235
+ if (res.ok !== undefined) {
236
+ assert.equal(response.ok, res.ok, 'Response ok status mismatch');
237
+ }
238
+
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;
224
246
  }
225
- })();
247
+ }
226
248
 
227
- //Done, return control to test
249
+ // Call callback or assert.done()
228
250
  if (cb) {
229
251
  return cb(response);
230
- } else if (assert) {
252
+ } else if (assert && assert.done) {
231
253
  return assert.done();
232
254
  }
233
255
  });
234
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
+ });
235
274
  };
236
275
  });
237
-
238
- var httpClientLogger = {
239
- log: function(header, data) {
240
- console.log('=== ' + header + ' ===');
241
- console.log(JSON.stringify(data, null, 2));
242
- }
243
- };