nodeunit-api-client 1.3.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 (3) hide show
  1. package/README.md +284 -0
  2. package/package.json +48 -0
  3. package/src/httpclient.js +207 -0
package/README.md ADDED
@@ -0,0 +1,284 @@
1
+ # nodeunit-api-client
2
+
3
+ [![npm version](https://img.shields.io/npm/v/nodeunit-api-client.svg)](https://www.npmjs.com/package/nodeunit-api-client)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Lightweight HTTP/HTTPS client with built-in testing assertions for Nodeunit.
7
+
8
+ ## ✨ Features
9
+
10
+ - ✅ Simple API for HTTP/HTTPS requests
11
+ - ✅ Built-in assertions for Nodeunit tests
12
+ - ✅ Support for GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE, CONNECT
13
+ - ✅ Automatic JSON parsing
14
+ - ✅ Configurable timeouts
15
+ - ✅ Basic authentication support
16
+ - ✅ Query string handling
17
+ - ✅ No external dependencies (uses native Node.js modules)
18
+
19
+ ## 📦 Installation
20
+
21
+ ```bash
22
+ npm install nodeunit-api-client
23
+ ```
24
+
25
+ ## 🚀 Quick Start
26
+
27
+ ### Basic Usage
28
+
29
+ ```javascript
30
+ const HttpClient = require('nodeunit-api-client');
31
+
32
+ const api = new HttpClient({
33
+ protocol: 'https',
34
+ host: 'api.example.com',
35
+ port: 443,
36
+ path: '/v1'
37
+ });
38
+
39
+ // Simple GET request
40
+ api.get(null, '/users', function(response) {
41
+ console.log(response.statusCode); // 200
42
+ console.log(response.data); // Parsed JSON
43
+ });
44
+ ```
45
+
46
+ ### With Nodeunit Tests
47
+
48
+ ```javascript
49
+ const HttpClient = require('nodeunit-api-client');
50
+
51
+ const api = new HttpClient({
52
+ protocol: 'https',
53
+ host: 'api.example.com'
54
+ });
55
+
56
+ exports.testAPI = {
57
+ 'should return user data': function(test) {
58
+ api.get(test, '/user/123', {}, {
59
+ status: 200,
60
+ headers: { 'content-type': 'application/json' }
61
+ }, function(response) {
62
+ test.deepEqual(response.data, { id: 123, name: 'John' });
63
+ test.done();
64
+ });
65
+ }
66
+ };
67
+ ```
68
+
69
+ ### POST with JSON Data
70
+
71
+ ```javascript
72
+ api.post(null, '/users', {
73
+ data: { name: 'Alice', email: 'alice@example.com' }
74
+ }, {}, function(response) {
75
+ console.log(response.statusCode); // 201
76
+ console.log(response.data);
77
+ });
78
+ ```
79
+
80
+ ### GET with Query Parameters
81
+
82
+ ```javascript
83
+ api.get(null, '/search', {
84
+ data: { q: 'nodejs', limit: 10 }
85
+ }, {}, function(response) {
86
+ // Request: GET /search?q=nodejs&limit=10
87
+ console.log(response.data);
88
+ });
89
+ ```
90
+
91
+ ## 📖 API Documentation
92
+
93
+ ### Constructor Options
94
+
95
+ ```javascript
96
+ new HttpClient({
97
+ protocol: 'http', // 'http' or 'https' (default: 'http')
98
+ host: 'localhost', // Server hostname
99
+ port: 80, // Server port (default: 80 for http, 443 for https)
100
+ path: '', // Base path (e.g. '/api/v1')
101
+ auth: 'user:pass', // Basic authentication
102
+ reqHeaders: {}, // Default request headers
103
+ headers: {}, // Default expected response headers (for assertions)
104
+ status: 200, // Default expected status code (for assertions)
105
+ timeout: 30000, // Request timeout in ms (default: 30000)
106
+ debug: false // Enable debug logs (default: false)
107
+ })
108
+ ```
109
+
110
+ ### Methods
111
+
112
+ All methods support multiple signatures:
113
+
114
+ ```javascript
115
+ // (assert, path, callback)
116
+ api.get(test, '/users', function(response) { ... });
117
+
118
+ // (assert, path, res)
119
+ api.get(test, '/users', { status: 200 });
120
+
121
+ // (assert, path, req, callback)
122
+ api.get(test, '/users', { headers: { 'x-api-key': 'secret' } }, function(response) { ... });
123
+
124
+ // (assert, path, req, res)
125
+ api.get(test, '/users', {}, { status: 200 });
126
+
127
+ // (assert, path, req, res, callback)
128
+ api.get(test, '/users', {}, { status: 200 }, function(response) { ... });
129
+ ```
130
+
131
+ **Available methods:**
132
+ - `get(assert, path, req, res, cb)`
133
+ - `post(assert, path, req, res, cb)`
134
+ - `put(assert, path, req, res, cb)`
135
+ - `del(assert, path, req, res, cb)` (DELETE)
136
+ - `head(assert, path, req, res, cb)`
137
+ - `options(assert, path, req, res, cb)`
138
+ - `trace(assert, path, req, res, cb)`
139
+ - `connect(assert, path, req, res, cb)`
140
+
141
+ ### Request Object (`req`)
142
+
143
+ ```javascript
144
+ {
145
+ headers: {}, // Request headers
146
+ data: {}, // Query params (GET) or body data (POST/PUT)
147
+ body: '', // Raw body (alternative to data)
148
+ auth: 'user:pass' // Override default auth
149
+ }
150
+ ```
151
+
152
+ ### Response Assertions (`res`)
153
+
154
+ ```javascript
155
+ {
156
+ status: 200, // Expected status code
157
+ headers: {}, // Expected response headers
158
+ body: '', // Expected raw body
159
+ data: {} // Expected parsed JSON data
160
+ }
161
+ ```
162
+
163
+ ### Response Object (in callback)
164
+
165
+ ```javascript
166
+ {
167
+ statusCode: 200, // HTTP status code
168
+ headers: {}, // Response headers
169
+ body: '', // Raw response body
170
+ data: {} // Parsed JSON (if content-type is application/json)
171
+ }
172
+ ```
173
+
174
+ ## 🧪 Testing
175
+
176
+ ```bash
177
+ npm test
178
+ ```
179
+
180
+ ## 📝 Examples
181
+
182
+ ### Basic Authentication
183
+
184
+ ```javascript
185
+ const api = new HttpClient({
186
+ protocol: 'https',
187
+ host: 'api.example.com',
188
+ auth: 'username:password'
189
+ });
190
+
191
+ api.get(null, '/protected', function(response) {
192
+ console.log(response.statusCode);
193
+ });
194
+ ```
195
+
196
+ ### Custom Headers
197
+
198
+ ```javascript
199
+ const api = new HttpClient({
200
+ protocol: 'https',
201
+ host: 'api.example.com',
202
+ reqHeaders: {
203
+ 'X-API-Key': 'your-api-key',
204
+ 'User-Agent': 'MyApp/1.0'
205
+ }
206
+ });
207
+
208
+ api.get(null, '/data', function(response) {
209
+ console.log(response.data);
210
+ });
211
+ ```
212
+
213
+ ### Error Handling
214
+
215
+ ```javascript
216
+ api.get(null, '/endpoint', function(response) {
217
+ if (response instanceof Error) {
218
+ console.error('Request failed:', response.message);
219
+ return;
220
+ }
221
+
222
+ console.log('Success:', response.data);
223
+ });
224
+ ```
225
+
226
+ ### Complete Test Example
227
+
228
+ ```javascript
229
+ const HttpClient = require('nodeunit-api-client');
230
+
231
+ const api = new HttpClient({
232
+ protocol: 'https',
233
+ host: 'jsonplaceholder.typicode.com'
234
+ });
235
+
236
+ exports.apiTests = {
237
+ 'GET /posts/1': function(test) {
238
+ api.get(test, '/posts/1', {}, {
239
+ status: 200,
240
+ headers: { 'content-type': 'application/json; charset=utf-8' }
241
+ }, function(response) {
242
+ test.equal(response.data.id, 1);
243
+ test.ok(response.data.title);
244
+ test.done();
245
+ });
246
+ },
247
+
248
+ 'POST /posts': function(test) {
249
+ api.post(test, '/posts', {
250
+ data: {
251
+ title: 'foo',
252
+ body: 'bar',
253
+ userId: 1
254
+ }
255
+ }, {
256
+ status: 201
257
+ }, function(response) {
258
+ test.equal(response.data.title, 'foo');
259
+ test.done();
260
+ });
261
+ }
262
+ };
263
+ ```
264
+ ```bash
265
+ Copyright (c) 2024 Sabri
266
+
267
+ Permission is hereby granted, free of charge, to any person obtaining a copy
268
+ of this software and associated documentation files (the "Software"), to deal
269
+ in the Software without restriction, including without limitation the rights
270
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
271
+ copies of the Software, and to permit persons to whom the Software is
272
+ furnished to do so, subject to the following conditions:
273
+
274
+ The above copyright notice and this permission notice shall be included in all
275
+ copies or substantial portions of the Software.
276
+
277
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
278
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
279
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
280
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
281
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
282
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
283
+ SOFTWARE.
284
+ ```
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "nodeunit-api-client",
3
+ "version": "1.3.0",
4
+ "description": "Lightweight HTTP/HTTPS client with built-in testing assertions for Nodeunit",
5
+ "main": "src/httpclient.js",
6
+ "exports": {
7
+ ".": "./src/httpclient.js"
8
+ },
9
+ "keywords": [
10
+ "http",
11
+ "https",
12
+ "client",
13
+ "testing",
14
+ "nodeunit",
15
+ "test",
16
+ "assertions",
17
+ "api",
18
+ "rest"
19
+ ],
20
+ "author": "Sabri <ssabri1996@gmail.com>",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/ssabri1996/nodeunit-httpclient.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/ssabri1996/nodeunit-httpclient/issues"
28
+ },
29
+ "homepage": "https://github.com/ssabri1996/nodeunit-httpclient#readme",
30
+ "scripts": {
31
+ "test": "npx nodeunit test/httpclient.test.js",
32
+ "prepublishOnly": "npm test"
33
+ },
34
+ "engines": {
35
+ "node": ">=14"
36
+ },
37
+ "files": [
38
+ "src/httpclient.js",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "devDependencies": {
43
+ "nodeunit": "^0.11.3"
44
+ },
45
+ "dependencies": {
46
+ "underscore": "^1.13.7"
47
+ }
48
+ }
@@ -0,0 +1,207 @@
1
+ 'use strict';
2
+ var querystring = require('querystring'),
3
+ underscore = require('underscore'),
4
+ debug;
5
+
6
+
7
+ /**
8
+ * @param {object} Options:
9
+ * auth ('username:password')
10
+ * host ('localhost')
11
+ * port (80)
12
+ * 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
16
+ * @param options
17
+ */
18
+ var HttpClient = module.exports = function(options) {
19
+ options = options || {};
20
+
21
+ this.auth = options.auth || undefined;
22
+ this.host = options.host || 'localhost';
23
+ this.port = options.port || 80;
24
+ this.path = options.path || '';
25
+ this.headers = options.headers || {};
26
+ this.status = options.status;
27
+ this.http = require(options.https ? 'https' : 'http');
28
+ debug = options.debug ? true : false;
29
+ };
30
+
31
+ HttpClient.create = function(options) {
32
+ return new HttpClient(options);
33
+ };
34
+
35
+ var methods = ['get', 'post', 'head', 'put', 'del', 'trace', 'options', 'connect'];
36
+
37
+ /**
38
+ * Performs a testable http request
39
+ *
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.
50
+ */
51
+ methods.forEach(function(method) {
52
+ HttpClient.prototype[method] = function(assert, path, req, res, cb) {
53
+ var self = this;
54
+
55
+ //Handle different signatures
56
+ if (arguments.length == 3) {
57
+ //(assert, path, cb)
58
+ if (typeof req === 'function') {
59
+ cb = req;
60
+ req = {};
61
+ res = {};
62
+ }
63
+
64
+ //(assert, path, res)
65
+ else {
66
+ cb = null;
67
+ res = req;
68
+ req = {};
69
+ }
70
+ }
71
+
72
+ //(assert, path, req, cb)
73
+ if (arguments.length == 4) {
74
+ if (typeof res == 'function') {
75
+ cb = res;
76
+ res = {};
77
+ }
78
+ }
79
+
80
+ //Also accepted:
81
+ //(assert, path, req, res)
82
+ //(assert, path, req, res, cb)
83
+
84
+ //Generate path based on base path, route path and querystring params
85
+ var fullPath = this.path + path;
86
+
87
+ //Don't add to querystring if POST or PUT
88
+ if (['post', 'put'].indexOf(method) === -1) {
89
+ var data = req.data;
90
+
91
+ if (data) {fullPath += '?' + querystring.stringify(data);}
92
+ }
93
+
94
+ var options = {
95
+ host: this.host,
96
+ port: this.port,
97
+ path: fullPath,
98
+ method: method == 'del' ? 'DELETE' : method.toUpperCase(),
99
+ headers: underscore.extend({}, this.headers, req.headers)
100
+ };
101
+
102
+ if (req.auth) {
103
+ options.auth = req.auth;
104
+ } else if (this.auth) {
105
+ options.auth = this.auth;
106
+ }
107
+
108
+ var request = this.http.request(options);
109
+
110
+ //Write POST & PUTdata
111
+ if (['post', 'put'].indexOf(method) != -1) {
112
+ var data = req.data || req.body;
113
+
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);
120
+ }
121
+ }
122
+ }
123
+
124
+ if (debug) {httpClientLogger.log('REQUEST', request);}
125
+ //Send
126
+ request.end();
127
+
128
+ request.on('response', function(response) {
129
+ if (debug) {httpClientLogger.log('RESPONSE', response);}
130
+
131
+ response.setEncoding('utf8');
132
+
133
+ response.on('data', function(chunk) {
134
+ if (response.body) {response.body += chunk;} else {response.body = chunk;}
135
+ });
136
+
137
+ //Handle the response; run response tests and hand back control to test
138
+ 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
+ }
163
+ }
164
+ }
165
+
166
+ //Run tests on the response
167
+ (function testResponse() {
168
+ //Can pass in falsy value to prevent running tests
169
+ if (!assert) {return;}
170
+
171
+ //Status code
172
+ var status = res.status || self.status;
173
+ if (status) {
174
+ assert.equal(response.statusCode, status);
175
+ }
176
+
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
+ }
182
+
183
+ //Body
184
+ if (res.body) {
185
+ assert.equal(response.body, res.body);
186
+ }
187
+
188
+ //JSON data
189
+ if (res.data) {
190
+ assert.deepEqual(response.data, res.data);
191
+ }
192
+ })();
193
+
194
+
195
+ //Done, return control to test
196
+ if (cb) {return cb(response);} else {return assert.done();}
197
+ });
198
+ });
199
+ };
200
+ });
201
+
202
+ var httpClientLogger = {
203
+ log: function(header, data) {
204
+ console.log(header);
205
+ console.log(data);
206
+ }
207
+ };