hmpo-model 3.2.2 → 4.0.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.
@@ -0,0 +1,21 @@
1
+ # This config is equivalent to both the '.circleci/extended/orb-free.yml' and the base '.circleci/config.yml'
2
+ version: 2.1
3
+
4
+ # Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects.
5
+ # See: https://circleci.com/docs/2.0/orb-intro/
6
+ orbs:
7
+ node: circleci/node@4.7
8
+
9
+ # Invoke jobs via workflows
10
+ # See: https://circleci.com/docs/2.0/configuration-reference/#workflows
11
+ workflows:
12
+ sample: # This is the name of the workflow, feel free to change it to better match your workflow.
13
+ # Inside the workflow, you define the jobs you want to run.
14
+ jobs:
15
+ - node/test:
16
+ # This is the node version to use for the `cimg/node` tag
17
+ # Relevant tags can be found on the CircleCI Developer Hub
18
+ # https://circleci.com/developer/images/image/cimg/node
19
+ version: '16.10'
20
+ # If you are using yarn, change the line below from "npm" to "yarn"
21
+ pkg-manager: npm
package/README.md CHANGED
@@ -2,88 +2,134 @@
2
2
  * localModel - Simple model for data persistance
3
3
  * remoteModel - Simple model for interacting with http/rest apis.
4
4
 
5
+ ## Upgrading
6
+
7
+ The deprecated `request` library has been replaced with `got`. The API is very similar, and some args are translated, like auth, and proxy.
8
+ The new `got` library doesn't automativally use the proxy environment variables so you would need to use something like `global-agent` in your
9
+ app if you need to specify proxies by environment arguments.
10
+
11
+ The `request` method no longer takes a body. This should be inserted as `json`, `body`, or `form` into the `requestConfig` method.
12
+
5
13
  ## Local Model Usage
6
- ### get
14
+ ### `get(name)`
7
15
  * gets a model property via a key
8
16
 
9
- ### set
17
+ ### `set(name, value)` or `set({ name: value })`
10
18
  * sets a property on the model to a value and dispatches events
11
19
 
12
- ### unset
20
+ ### `unset(name)`
13
21
  * unsets a property
14
22
 
15
- ### reset
23
+ ### `reset([options])`
16
24
  * resets a model
17
25
  * suppresses `change` event notifications if `options.silent` is set
18
26
 
19
- ### increment
27
+ ### `increment(name)`
20
28
  * Increments a property
21
29
 
22
- ### toJSON
30
+ ### `toJSON()`
23
31
  * returns a JSON representation of the data in the model
24
32
 
25
33
  ## Remote Model Usage
26
34
 
27
35
  Normally this would be used as an abstract class and extended with your own implementation.
28
36
 
29
- Implementations would normally define at least a `url` method to define the target of API calls.
30
-
31
- There are three methods for API interaction corresponding to GET, POST, and DELETE http methods:
32
-
33
- ### `fetch`
37
+ Implementations would normally define at least a `url():url` method to define the target of API calls.
34
38
 
39
+ Example implimentation:
35
40
  ```javascript
36
- var model = new Model();
37
- model.fetch(function (err, data, responseTime) {
41
+ class MyModel extends HmpoModel {
42
+ url() {
43
+ return super.url('https://my.example.com/url')
44
+ }
45
+
46
+ auth() {
47
+ return super.auth('username:password');
48
+ }
49
+
50
+ requestConfig(config) {
51
+ config.proxy = 'http://proxy.example.com:3128'
52
+ return super.requestConfig(config);
53
+ }
54
+
55
+ // add data to JSON post body
56
+ prepare(callback) {
57
+ super.prepare((err, data) => {
58
+ if (err) return callback(err);
59
+ data.foo = 'bar';
60
+ callback(null, data);
61
+ });
62
+ }
63
+
64
+ // transform returned data
65
+ parse(data) {
66
+ data.additionalItem = true;
67
+ return super.parse(data);
68
+ }
69
+ }
70
+
71
+ const model = new MyModel();
72
+ model.set('boo', 'baz');
73
+ model.save((err, data, responseTime) => {
74
+ if (err) return console.error(err);
38
75
  console.log(data);
39
76
  });
40
77
  ```
41
78
 
42
- ### `save`
79
+ There are three methods for API interaction corresponding to GET, POST, and DELETE http methods:
80
+
81
+ ### `fetch([args, ][callback])`
82
+
83
+ `fetch` performs a `GET` request on the url
43
84
 
44
85
  ```javascript
45
- var model = new Model();
46
- model.set({
47
- property: 'properties are sent as JSON request body by default'
48
- });
49
- model.save(function (err, data, responseTime) {
86
+ const model = new Model();
87
+ model.fetch((err, data, responseTime) => {
50
88
  console.log(data);
51
89
  });
52
90
  ```
91
+ #### Request
92
+ - Request args for the `got` library, can be set by overriding the `requestConfig({}):{}` method.
93
+
94
+ - The `url` can be configured either by setting a default in the model options or `requestConfig()` data, or by overriding the `url(default, args):url` method.
95
+
96
+ - `proxy`, `timeout`, and basic `auth` can be set in the same way, using model options, setting in `requestConfig()`, or by overriding a method.
97
+ - Specifying a `proxy` will set up a proxy tunneling `agent` for the request.
98
+ - Specifying a numeric `timeout` will set the same timeout for all `got` timeout values.
99
+ - Basic `auth` can be a colon separated string, or a `{username, password}` or `{user, pass}` object.
100
+
101
+ #### Response
102
+ - The returned body will be expected to be in JSON format.
103
+ - If `statusCode < 400` the JSON response will be set to the model.
104
+ This behaviour can be changed by overriding the `parse(data):data` method.
105
+ - If `statusCode >= 400` the data will be passed to the `parseError(statusCode, data):error` method, and the `fetch` callback will be called with the returned error.
106
+ - If response statuses need to be treated differently than the above, the `parseResponse(statusCode, data, cb)` method can be overridden.
107
+ - If the response body is not going to be JSON, the `handleResponse(response, cb)` method can be overridden.
108
+
109
+ ### `save([args, ][callback])`
53
110
 
54
- The method can also be overwritten by passing options
111
+ `save` performs a `POST` request on the url
55
112
 
56
113
  ```javascript
57
- var model = new Model();
114
+ const model = new Model();
58
115
  model.set({
59
- property: 'this will be sent as a PUT request'
116
+ property: 'properties are sent as JSON request body by default'
60
117
  });
61
- model.save({ method: 'PUT' }, function (err, data, responseTime) {
118
+ model.save((err, data, responseTime) => {
62
119
  console.log(data);
63
120
  });
64
121
  ```
65
122
 
66
- ### `delete`
123
+ - By default the post body will be a JSON encoded object containing all attributes set to the model using, extracted using `model.toJSON()`. This behaviour can be changed by overriding the `prepare(callback(err, data))` method.
124
+ - The response and body will be treated the same way as the `fetch` request above.
67
125
 
68
- ```javascript
69
- var model = new Model();
70
- model.delete(function (err, data) {
71
- console.log(data);
72
- });
73
- ```
126
+ ### `delete([args, ][callback])`
74
127
 
75
- If no `url` method is defined then the model will use the options parameter and [Node's url.format method](https://nodejs.org/api/url.html#url_url_format_urlobj) to construct a URL.
128
+ `delete` performs a `DELETE` request on the url
76
129
 
77
130
  ```javascript
78
- var model = new Model();
79
-
80
- // make a GET request to http://example.com:3000/foo/bar
81
- model.fetch({
82
- protocol: 'http',
83
- hostname: 'example.com',
84
- port: 3000,
85
- path: '/foo/bar'
86
- }, function (err, data, responseTime) {
131
+ const model = new Model();
132
+ model.delete((err, data, responseTime) => {
87
133
  console.log(data);
88
134
  });
89
135
  ```
@@ -98,7 +98,7 @@ class LocalModel extends EventEmitter {
98
98
  }
99
99
 
100
100
  toJSON() {
101
- return _.clone(this.attributes);
101
+ return Object.assign({}, this.attributes);
102
102
  }
103
103
  }
104
104
 
@@ -1,14 +1,17 @@
1
1
  'use strict';
2
2
 
3
+ const debug = require('debug')('hmpo:model:remote');
3
4
  const LocalModel = require('./local-model');
4
- const requestLib = require('request');
5
- const _ = require('underscore');
5
+ const got = require('got');
6
6
  const kebabCase = require('lodash.kebabcase');
7
+ const { URL } = require('url');
8
+
9
+ const DEFAULT_TIMEOUT = 60000;
7
10
 
8
11
  class RemoteModel extends LocalModel {
9
12
  constructor(attributes, options) {
10
13
  super(attributes, options);
11
-
14
+ this.got = got;
12
15
  this.options.label = this.options.label || kebabCase(this.constructor.name);
13
16
  this.setLogger();
14
17
  }
@@ -19,78 +22,143 @@ class RemoteModel extends LocalModel {
19
22
  this.logger = hmpoLogger.get(':' + this.options.label);
20
23
  } catch (e) {
21
24
  console.error('Error setting logger, using console instead!', e);
22
- this.logger = { outbound: console.log, trimHtml: _.identity };
25
+ this.logger = { outbound: console.log, trimHtml: html => html };
23
26
  }
24
27
  }
25
28
 
26
- fetch(callback) {
27
- let config = this.requestConfig({method: 'GET'});
29
+ fetch(args, callback) {
30
+ if (typeof args === 'function') {
31
+ callback = args;
32
+ args = undefined;
33
+ }
34
+ const config = this.requestConfig({method: 'GET'}, args);
28
35
  this.request(config, callback);
29
36
  }
30
37
 
31
- save(callback) {
32
- this.prepare((err, data) => {
33
-
34
- if (err) { return callback(err); }
35
-
36
- data = JSON.stringify(data);
37
-
38
- let config = this.requestConfig({
39
- method: 'POST',
40
- headers: {
41
- 'Content-Type': 'application/json',
42
- 'Content-Length': Buffer.byteLength(data)
43
- }
44
- });
45
-
46
- this.request(config, data, callback);
47
-
38
+ save(args, callback) {
39
+ if (typeof args === 'function') {
40
+ callback = args;
41
+ args = undefined;
42
+ }
43
+ this.prepare((err, json) => {
44
+ if (err) return callback(err);
45
+ const config = this.requestConfig({method: 'POST', json}, args);
46
+ this.request(config, callback);
48
47
  });
49
48
  }
50
49
 
51
- delete(callback) {
52
- let config = this.requestConfig({method: 'DELETE'});
50
+ delete(args, callback) {
51
+ if (typeof args === 'function') {
52
+ callback = args;
53
+ args = undefined;
54
+ }
55
+ const config = this.requestConfig({method: 'DELETE'}, args);
53
56
  this.request(config, callback);
54
57
  }
55
58
 
56
- requestConfig(config) {
57
- let retConfig = _.clone(config);
59
+ prepare(callback) {
60
+ debug('prepare');
61
+ callback(null, this.toJSON());
62
+ }
63
+
64
+ requestConfig(config, args) {
65
+ const retConfig = Object.assign({}, config);
58
66
 
59
- retConfig.uri = this.url();
67
+ retConfig.url = this.url(retConfig.url || retConfig.uri, args);
60
68
 
61
- let auth = this.auth();
69
+ retConfig.timeout = this.timeout(retConfig.timeout);
62
70
 
71
+ const auth = this.auth(retConfig.auth);
63
72
  if (auth) {
64
- retConfig.auth = auth;
73
+ retConfig.username = auth.username || auth.user;
74
+ retConfig.password = auth.password || auth.pass;
65
75
  }
76
+ delete retConfig.auth;
66
77
 
67
- if (this.options.headers) {
68
- retConfig.headers = _.defaults(retConfig.headers || {}, this.options.headers);
78
+ const agent = this.proxy(retConfig.proxy, retConfig.url);
79
+ if (agent) {
80
+ retConfig.agent = agent;
69
81
  }
82
+ delete retConfig.proxy;
83
+
84
+ const headers = Object.assign({}, this.options.headers, retConfig.headers);
85
+ if (Object.keys(headers).length) retConfig.headers = headers;
86
+
87
+ debug('requestConfig', retConfig);
70
88
 
71
89
  return retConfig;
72
90
  }
73
91
 
74
- request(settings, body, callback) {
75
- if (typeof body === 'function' && arguments.length === 2) {
76
- callback = body;
77
- body = undefined;
92
+ url(url = this.options.url) {
93
+ return url;
94
+ }
95
+
96
+ auth(auth = this.options.auth) {
97
+ if (typeof auth === 'string') {
98
+ const splitAuth = auth.split(':');
99
+ auth = {
100
+ username: splitAuth.shift(),
101
+ password: splitAuth.join(':')
102
+ };
103
+ }
104
+ return auth;
105
+ }
106
+
107
+ timeout(timeout = this.options.timeout || DEFAULT_TIMEOUT) {
108
+ if (typeof timeout === 'number') {
109
+ timeout = {
110
+ lookup: timeout,
111
+ connect: timeout,
112
+ secureConnect: timeout,
113
+ socket: timeout,
114
+ send: timeout,
115
+ response: timeout
116
+ };
78
117
  }
118
+ return timeout;
119
+ }
79
120
 
80
- // This is cloned and set so that the post body is not kept around in memory
81
- // settings is a shallow object, so the requestSettings is entirely made up of copied properties
82
- let requestSettings = _.clone(settings);
83
- requestSettings.body = body;
121
+ proxy(proxy = this.options.proxy, url) {
122
+ if (!proxy || !url) return;
84
123
 
85
- let startTime = process.hrtime();
124
+ if (typeof proxy === 'string') proxy = { proxy };
86
125
 
87
- let _callback = (err, data, statusCode) => {
126
+ const isHttps = (new URL(url).protocol === 'https:');
88
127
 
89
- // This uses node's "high resolution time" to determine response time.
90
- // The calculation is to translate seconds & nanoseconds into a number of format 1.234 seconds
91
- let endTime = process.hrtime(startTime);
92
- let responseTime = Number((endTime[0]*1000 + endTime[1]/1000000).toFixed(3));
128
+ if (isHttps) {
129
+ const { HttpsProxyAgent } = require('hpagent');
130
+ return {
131
+ https: new HttpsProxyAgent(Object.assign({
132
+ keepAlive: false,
133
+ maxSockets: 1,
134
+ maxFreeSockets: 1,
135
+ }, proxy))
136
+ };
137
+ } else {
138
+ const { HttpProxyAgent } = require('hpagent');
139
+ return {
140
+ http: new HttpProxyAgent(Object.assign({
141
+ keepAlive: false,
142
+ maxSockets: 1,
143
+ maxFreeSockets: 1,
144
+ }, proxy))
145
+ };
146
+ }
147
+ }
93
148
 
149
+ request(settings, callback) {
150
+ this.hookSync({settings});
151
+ this.logSync({settings});
152
+ this.emit('sync', settings);
153
+
154
+ let responseTime;
155
+ const startTime = process.hrtime.bigint();
156
+ const setResponseTime = () => {
157
+ const endTime = process.hrtime.bigint();
158
+ responseTime = Number((Number(endTime - startTime) / 1000000).toFixed(3));
159
+ };
160
+
161
+ const _callback = (err, data, statusCode) => {
94
162
  if (err) {
95
163
  this.hookFail({settings, statusCode, responseTime, err, data});
96
164
  this.logError({settings, statusCode, responseTime, err, data});
@@ -105,25 +173,27 @@ class RemoteModel extends LocalModel {
105
173
  }
106
174
  };
107
175
 
108
- requestLib(requestSettings, (err, response) => {
109
- if (err) {
176
+ this.got(settings)
177
+ .catch(err => {
178
+ debug('request got error', err);
179
+ setResponseTime();
110
180
  if (err.code === 'ETIMEDOUT') {
111
181
  err.message = 'Connection timed out';
112
182
  err.status = 504;
113
183
  }
114
- err.status = err.status || (response && response.statusCode) || 503;
115
- return _callback(err, null, err.status);
116
- }
117
- this.handleResponse(response, _callback);
118
- });
119
-
120
- this.hookSync({settings});
121
- this.logSync({settings});
122
- this.emit('sync', settings);
184
+ err.status = err.status || (err.response && err.response.statusCode) || 503;
185
+ return _callback(err, null, err.status, err.response);
186
+ })
187
+ .then(response => {
188
+ debug('request got response', response);
189
+ setResponseTime();
190
+ this.handleResponse(response, _callback);
191
+ });
123
192
  }
124
193
 
125
194
  handleResponse(response, callback) {
126
- let data = {};
195
+ debug('handleResponse', response);
196
+ let data;
127
197
  try {
128
198
  data = JSON.parse(response.body || '{}');
129
199
  } catch (err) {
@@ -135,53 +205,37 @@ class RemoteModel extends LocalModel {
135
205
  }
136
206
 
137
207
  parseResponse(statusCode, data, callback) {
138
- if (statusCode < 400) {
139
- try {
140
- data = this.parse(data);
141
- callback(null, data, statusCode);
142
- } catch (err) {
143
- callback(err, null, statusCode);
144
- }
145
- } else {
146
- callback(this.parseError(statusCode, data), data, statusCode);
208
+ debug('parseResponse', statusCode, data);
209
+
210
+ if (statusCode >= 400) {
211
+ const error = this.parseError(statusCode, data);
212
+ return callback(error, data, statusCode);
147
213
  }
148
- }
149
214
 
150
- prepare(callback) {
151
- callback(null, this.toJSON());
215
+ try {
216
+ data = this.parse(data);
217
+ } catch (err) {
218
+ return callback(err, null, statusCode);
219
+ }
220
+
221
+ callback(null, data, statusCode);
152
222
  }
153
223
 
154
224
  parse(data) {
225
+ debug('parse', data);
226
+ if (data && typeof data === 'object') this.set(data);
155
227
  return data;
156
228
  }
157
229
 
158
230
  parseError(statusCode, data) {
159
- return _.extend({ status: statusCode }, data);
160
- }
161
-
162
- url() {
163
- return this.options.url;
164
- }
165
-
166
- auth(credentials) {
167
- if (!credentials) return;
168
-
169
- if (typeof credentials === 'string') {
170
- let auth = credentials.split(':');
171
- credentials = {
172
- user: auth.shift(),
173
- pass: auth.join(':'),
174
- sendImmediately: true
175
- };
176
- }
177
-
178
- return credentials;
231
+ debug('parseError, statusCode, data');
232
+ return Object.assign({ status: statusCode }, data);
179
233
  }
180
234
 
181
235
  logMeta(tokenData) {
182
236
  let data = {
183
237
  outVerb: tokenData.settings.method,
184
- outRequest: tokenData.settings.uri
238
+ outRequest: tokenData.settings.url
185
239
  };
186
240
 
187
241
  if (tokenData.statusCode) {
@@ -199,7 +253,7 @@ class RemoteModel extends LocalModel {
199
253
  data.outErrorBody = this.logger.trimHtml(tokenData.err.body);
200
254
  }
201
255
 
202
- _.extend(data, this.options.logging);
256
+ Object.assign(data, this.options.logging);
203
257
 
204
258
  return data;
205
259
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmpo-model",
3
- "version": "3.2.2",
3
+ "version": "4.0.0",
4
4
  "description": "Simple model for interacting with http/rest apis.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -25,19 +25,21 @@
25
25
  },
26
26
  "homepage": "https://github.com/UKHomeOffice/passports-model",
27
27
  "dependencies": {
28
+ "debug": "^4.3.3",
29
+ "got": "^11.8.3",
30
+ "hpagent": "^0.1.2",
28
31
  "lodash.kebabcase": "^4.1.1",
29
- "request": "^2.88.2",
30
32
  "underscore": "^1.13.1"
31
33
  },
32
34
  "devDependencies": {
33
35
  "chai": "^4.3.4",
34
- "eslint": "^7.26.0",
36
+ "eslint": "^8.3.0",
35
37
  "hmpo-logger": "^4.1.3",
36
- "mocha": "^8.4.0",
38
+ "mocha": "^9.1.3",
37
39
  "nyc": "^15.1.0",
38
40
  "proxyquire": "^2.0.0",
39
- "sinon": "^10.0.0",
40
- "sinon-chai": "^3.6.0"
41
+ "sinon": "^12.0.1",
42
+ "sinon-chai": "^3.7.0"
41
43
  },
42
44
  "nyc": {
43
45
  "all": true,
@@ -1,14 +1,16 @@
1
1
  'use strict';
2
2
 
3
- const proxyquire = require('proxyquire');
3
+ const Model = require('../../lib/remote-model');
4
4
  const BaseModel = require('../../lib/local-model');
5
5
  const _ = require('underscore');
6
+ const logger = require('hmpo-logger');
7
+
8
+ const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent');
6
9
 
7
10
  describe('Remote Model', () => {
8
- let model, Model, cb, mocks;
11
+ let model, cb, mocks;
9
12
 
10
13
  beforeEach(() => {
11
- Model = require('../../lib/remote-model');
12
14
  model = new Model();
13
15
 
14
16
  cb = sinon.stub();
@@ -24,7 +26,6 @@ describe('Remote Model', () => {
24
26
  });
25
27
 
26
28
  it('should be an instance of LocalModel', () => {
27
- Model = require('../../lib/remote-model');
28
29
  model = new Model();
29
30
 
30
31
  model.should.be.an.instanceOf(BaseModel);
@@ -54,35 +55,30 @@ describe('Remote Model', () => {
54
55
  });
55
56
 
56
57
  describe('setLogger', () => {
57
- let getStub, Model;
58
-
59
58
  beforeEach(() => {
60
- getStub = sinon.stub();
61
- getStub.returns('logger');
59
+ sinon.stub(logger, 'get').returns('logger');
60
+ });
62
61
 
63
- Model = proxyquire('../../lib/remote-model', {
64
- 'hmpo-logger': {
65
- get: getStub
66
- }
67
- });
62
+ afterEach(() => {
63
+ logger.get.restore();
68
64
  });
69
65
 
70
66
  it('should set up a new hmpo-logger', () => {
71
67
  model = new Model();
72
68
 
73
- getStub.should.have.been.calledWithExactly(':remote-model');
69
+ logger.get.should.have.been.calledWithExactly(':remote-model');
74
70
  model.logger.should.equal('logger');
75
71
  });
76
72
 
77
- it('should use console if hmpo-logger is not available', () => {
78
- getStub.throws(new Error());
73
+ it('should use console log and a trimHtml pass-through if hmpo-logger is not available', () => {
74
+ logger.get.throws(new Error());
79
75
 
80
76
  model = new Model();
81
77
 
82
- model.logger.should.eql({
83
- outbound: console.log,
84
- trimHtml: _.identity
85
- });
78
+ model.logger.outbound.should.eql(console.log);
79
+ model.logger.trimHtml.should.be.a('function');
80
+ const html = {};
81
+ model.logger.trimHtml(html).should.equal(html);
86
82
  });
87
83
  });
88
84
 
@@ -104,6 +100,15 @@ describe('Remote Model', () => {
104
100
  method: 'GET'
105
101
  });
106
102
  });
103
+
104
+ it('should pass args onto requestConfig', () => {
105
+ model.fetch({ foo: 'bar' }, cb);
106
+
107
+ model.requestConfig.should.have.been.calledWithExactly({
108
+ method: 'GET'
109
+ }, { foo: 'bar' });
110
+ });
111
+
107
112
  it('should call request', () => {
108
113
  model.requestConfig.returns(mocks.config);
109
114
 
@@ -129,9 +134,9 @@ describe('Remote Model', () => {
129
134
  it('should call prepare', () => {
130
135
  model.save(cb);
131
136
 
132
- model.prepare.should.have.been.called;
133
-
137
+ model.prepare.should.have.been.calledWithExactly(sinon.match.func);
134
138
  });
139
+
135
140
  it('should call callback with an error', () => {
136
141
  let error = new Error('error');
137
142
  model.prepare.yields(error);
@@ -139,7 +144,6 @@ describe('Remote Model', () => {
139
144
  model.save(cb);
140
145
 
141
146
  cb.should.have.been.calledWith(error);
142
-
143
147
  });
144
148
 
145
149
  context('on prepare success', () => {
@@ -155,20 +159,27 @@ describe('Remote Model', () => {
155
159
  it('should use requestConfig', () => {
156
160
  model.save(cb);
157
161
 
158
- model.requestConfig.should.have.been.calledWith({
162
+ model.requestConfig.should.have.been.calledWithExactly({
159
163
  method: 'POST',
160
- headers: {
161
- 'Content-Type': 'application/json',
162
- 'Content-Length': Buffer.byteLength(JSON.stringify(preparedData))
163
- }
164
- });
164
+ json: preparedData
165
+ }, undefined);
166
+ });
167
+
168
+ it('should pass args onto requestConfig', () => {
169
+ model.save({ foo: 'bar' }, cb);
170
+
171
+ model.requestConfig.should.have.been.calledWithExactly({
172
+ method: 'POST',
173
+ json: preparedData
174
+ }, { foo: 'bar' });
165
175
  });
176
+
166
177
  it('should call request', () => {
167
178
  model.requestConfig.returns(mocks.config);
168
179
 
169
180
  model.save(cb);
170
181
 
171
- model.request.should.have.been.calledWith(mocks.config, JSON.stringify(preparedData), cb);
182
+ model.request.should.have.been.calledWith(mocks.config, cb);
172
183
  });
173
184
  });
174
185
 
@@ -192,6 +203,15 @@ describe('Remote Model', () => {
192
203
  method: 'DELETE'
193
204
  });
194
205
  });
206
+
207
+ it('should pass args onto requestConfig', () => {
208
+ model.delete({ foo: 'bar' }, cb);
209
+
210
+ model.requestConfig.should.have.been.calledWithExactly({
211
+ method: 'DELETE'
212
+ }, { foo: 'bar' });
213
+ });
214
+
195
215
  it('should call request', () => {
196
216
  model.requestConfig.returns(mocks.config);
197
217
 
@@ -202,50 +222,227 @@ describe('Remote Model', () => {
202
222
  });
203
223
 
204
224
  describe('requestConfig', () => {
205
- beforeEach(() => {
206
- sinon.stub(model, 'url');
207
- sinon.stub(model, 'auth');
225
+ describe('url', () => {
226
+ it('should use url from model options', () => {
227
+ model.options.url = 'https://example.com/options';
228
+ const config = model.requestConfig({
229
+ 'method': 'VERB'
230
+ });
208
231
 
209
- model.url.returns('http://example.net');
210
- });
232
+ config.url.should.equal('https://example.com/options');
233
+ });
211
234
 
212
- afterEach(() => {
213
- model.url.restore();
214
- model.auth.restore();
235
+ it('should use url from request config', () => {
236
+ model.options.url = 'https://example.com/options';
237
+ const config = model.requestConfig({
238
+ 'method': 'VERB',
239
+ 'url': 'https://example.com/config'
240
+ });
241
+
242
+ config.url.should.equal('https://example.com/config');
243
+ });
244
+
245
+ it('should use url returned by overridden url() method', () => {
246
+ model.options.url = 'https://example.com/options';
247
+ model.url = sinon.stub().returns('https://example.com/overridden');
248
+ const config = model.requestConfig({
249
+ 'method': 'VERB',
250
+ 'url': 'https://example.com/config'
251
+ });
252
+ model.url.should.have.been.calledWithExactly('https://example.com/config', undefined);
253
+ config.url.should.equal('https://example.com/overridden');
254
+ });
255
+
256
+ it('should pass args onto url method', () => {
257
+ model.url = sinon.stub().returns('https://example.com/overridden');
258
+ const config = model.requestConfig({
259
+ 'method': 'VERB',
260
+ 'url': 'https://example.com/config'
261
+ }, { foo: 'bar' });
262
+ model.url.should.have.been.calledWithExactly('https://example.com/config', { foo: 'bar' });
263
+ config.url.should.equal('https://example.com/overridden');
264
+ });
215
265
  });
216
266
 
267
+ describe('auth', () => {
268
+ it('should use auth from model options', () => {
269
+ model.options.auth = 'options:pass:word';
270
+ const config = model.requestConfig({
271
+ 'method': 'VERB'
272
+ });
217
273
 
218
- it('should use url', () => {
219
- model.requestConfig({
220
- 'method': 'VERB'
274
+ config.username.should.equal('options');
275
+ config.password.should.equal('pass:word');
276
+ config.should.not.have.property('auth');
221
277
  });
222
278
 
223
- model.url.should.have.been.calledWithExactly();
279
+ it('should use auth from config', () => {
280
+ model.options.auth = 'options:pass:word';
281
+ const config = model.requestConfig({
282
+ 'method': 'VERB',
283
+ 'auth': 'config:pass:word'
284
+ });
285
+
286
+ config.username.should.equal('config');
287
+ config.password.should.equal('pass:word');
288
+ config.should.not.have.property('auth');
289
+ });
290
+
291
+ it('should use auth from overidden auth() method', () => {
292
+ model.options.auth = 'options:pass:word';
293
+ model.auth = sinon.stub().returns({ user: 'overridden', pass: 'pass:word' });
294
+ const config = model.requestConfig({
295
+ 'method': 'VERB',
296
+ 'auth': 'config:pass:word'
297
+ });
298
+
299
+ model.auth.should.have.been.calledWithExactly('config:pass:word');
300
+ config.username.should.equal('overridden');
301
+ config.password.should.equal('pass:word');
302
+ config.should.not.have.property('auth');
303
+ });
224
304
  });
225
305
 
226
- it('should use auth', () => {
227
- model.requestConfig({
228
- 'method': 'VERB'
306
+ describe('timeout', () => {
307
+ it('should use a default timeout', () => {
308
+ const config = model.requestConfig({
309
+ 'method': 'VERB'
310
+ });
311
+
312
+ config.should.deep.include({
313
+ timeout: {
314
+ connect: 60000,
315
+ lookup: 60000,
316
+ response: 60000,
317
+ secureConnect: 60000,
318
+ send: 60000,
319
+ socket: 60000
320
+ }
321
+ });
322
+ });
323
+
324
+ it('should use timeout from model options', () => {
325
+ model.options.timeout = 1000;
326
+ const config = model.requestConfig({
327
+ 'method': 'VERB'
328
+ });
329
+
330
+ config.should.deep.include({
331
+ timeout: {
332
+ connect: 1000,
333
+ lookup: 1000,
334
+ response: 1000,
335
+ secureConnect: 1000,
336
+ send: 1000,
337
+ socket: 1000
338
+ }
339
+ });
340
+ });
341
+
342
+ it('should use timeout from config', () => {
343
+ model.options.timeout = 1000;
344
+ const config = model.requestConfig({
345
+ 'method': 'VERB',
346
+ 'timeout': 2000
347
+ });
348
+
349
+ config.should.deep.include({
350
+ timeout: {
351
+ connect: 2000,
352
+ lookup: 2000,
353
+ response: 2000,
354
+ secureConnect: 2000,
355
+ send: 2000,
356
+ socket: 2000
357
+ }
358
+ });
229
359
  });
230
360
 
231
- model.auth.should.have.been.calledWithExactly();
361
+ it('should use timeout from specified object', () => {
362
+ const config = model.requestConfig({
363
+ 'method': 'VERB',
364
+ 'timeout': { connect: 3000 }
365
+ });
366
+
367
+ config.should.deep.include({
368
+ timeout: {
369
+ connect: 3000,
370
+ }
371
+ });
372
+ });
373
+
374
+ it('should use timeout from overidden timeout() method', () => {
375
+ model.timeout = sinon.stub().returns({ connect: 4000 });
376
+ const config = model.requestConfig({
377
+ 'method': 'VERB',
378
+ 'timeout': 2000
379
+ });
380
+
381
+ model.timeout.should.have.been.calledWithExactly(2000);
382
+ config.should.deep.include({
383
+ timeout: {
384
+ connect: 4000,
385
+ }
386
+ });
387
+ });
232
388
  });
233
389
 
234
- it('should add auth to config if provided', () => {
235
- model.auth.returns({
236
- user: 'username',
237
- pass: 'password'
390
+ describe('proxy', () => {
391
+ it('should not set up http proxy if there is no url', () => {
392
+ const returnedConfig = model.requestConfig({
393
+ 'method': 'VERB',
394
+ 'proxy': 'http://proxy.example.com:8000'
395
+ });
396
+
397
+ returnedConfig.should.not.have.property('proxy');
398
+ returnedConfig.should.not.have.property('agent');
238
399
  });
239
400
 
240
- let returnedConfig = model.requestConfig({
241
- 'method': 'VERB'
401
+ it('should set up http proxy if specified', () => {
402
+ const returnedConfig = model.requestConfig({
403
+ 'method': 'VERB',
404
+ 'url': 'http://example.net',
405
+ 'proxy': 'http://proxy.example.com:8000'
406
+ });
407
+
408
+ sinon.assert.match(returnedConfig, {
409
+ agent: {
410
+ http: sinon.match.instanceOf(HttpProxyAgent)
411
+ }
412
+ });
242
413
  });
243
414
 
244
- returnedConfig.should.deep.include({
245
- auth: {
246
- user: 'username',
247
- pass: 'password'
248
- }
415
+ it('should set up https proxy if specified', () => {
416
+ const returnedConfig = model.requestConfig({
417
+ 'method': 'VERB',
418
+ 'url': 'https://example.net',
419
+ 'proxy': 'http://proxy.example.com:8000'
420
+ });
421
+
422
+ sinon.assert.match(returnedConfig, {
423
+ agent: {
424
+ https: sinon.match.instanceOf(HttpsProxyAgent)
425
+ }
426
+ });
427
+ });
428
+
429
+ it('should pass proxy options to the new proxy', () => {
430
+ const returnedConfig = model.requestConfig({
431
+ 'method': 'VERB',
432
+ 'url': 'http://example.net',
433
+ 'proxy': {
434
+ proxy: 'http://proxy.example.com:8000',
435
+ keepAlive: true
436
+ }
437
+ });
438
+
439
+ sinon.assert.match(returnedConfig, {
440
+ agent: {
441
+ http: {
442
+ keepAlive: true
443
+ }
444
+ }
445
+ });
249
446
  });
250
447
  });
251
448
 
@@ -357,14 +554,15 @@ describe('Remote Model', () => {
357
554
  });
358
555
 
359
556
  describe('request', () => {
360
- let settings, body, requestSettings;
557
+ let settings, requestSettings, mocks;
361
558
 
362
559
  beforeEach(() => {
363
- let Model = proxyquire('../../lib/remote-model', {
364
- 'request': mocks.request
365
- });
366
-
560
+ mocks = {};
561
+ mocks.got = sinon.stub().returns(mocks);
562
+ mocks.then = sinon.stub().returns(mocks);
563
+ mocks.catch = sinon.stub().returns(mocks);
367
564
  model = new Model();
565
+ model.got = mocks.got;
368
566
 
369
567
  sinon.stub(model, 'logSync');
370
568
  sinon.stub(model, 'logSuccess');
@@ -385,22 +583,11 @@ describe('Remote Model', () => {
385
583
  model.emit.restore();
386
584
  });
387
585
 
388
- it('should invoke request with settings including a body', () => {
389
- requestSettings.body = body;
390
-
391
- model.request(settings, body, cb);
392
-
393
- mocks.request.should.have.been.called;
394
- mocks.request.should.have.been.calledWith(requestSettings);
395
- });
396
-
397
586
  it('should invoke request with request settings', () => {
398
- requestSettings.body = undefined;
399
-
400
587
  model.request(settings, cb);
401
588
 
402
- mocks.request.should.have.been.called;
403
- mocks.request.should.have.been.calledWith(requestSettings);
589
+ mocks.got.should.have.been.called;
590
+ mocks.got.should.have.been.calledWith(requestSettings);
404
591
  });
405
592
 
406
593
  it('should log sync messages', () => {
@@ -432,7 +619,7 @@ describe('Remote Model', () => {
432
619
  });
433
620
 
434
621
  it('should work without a callback', () => {
435
- mocks.request.yields(new Error('Random Error'), {});
622
+ mocks.catch.yields(new Error('Random Error'));
436
623
 
437
624
  model.request(settings);
438
625
 
@@ -451,7 +638,9 @@ describe('Remote Model', () => {
451
638
  'statusCode': 418
452
639
  };
453
640
 
454
- mocks.request.yields(error, response);
641
+ error.response = response;
642
+
643
+ mocks.catch.yields(error);
455
644
  });
456
645
 
457
646
  it('should log error messages', () => {
@@ -534,7 +723,7 @@ describe('Remote Model', () => {
534
723
 
535
724
  context('on success', () => {
536
725
  beforeEach(() => {
537
- mocks.request.yields(null, {
726
+ mocks.then.yields({
538
727
  'body': JSON.stringify({'data': 'value'}),
539
728
  'statusCode': 200
540
729
  });
@@ -641,6 +830,7 @@ describe('Remote Model', () => {
641
830
 
642
831
  afterEach(() => {
643
832
  model.parse.restore();
833
+
644
834
  model.parseError.restore();
645
835
  });
646
836
 
@@ -714,6 +904,17 @@ describe('Remote Model', () => {
714
904
  it('returns data passed', () => {
715
905
  model.parse({ data: 1 }).should.eql({ data: 1 });
716
906
  });
907
+
908
+ it('sets the parsed data to the model', () => {
909
+ model.parse({ foo: 'bar' });
910
+ model.get('foo').should.equal('bar');
911
+ });
912
+
913
+ it('does not set if the data falsey', () => {
914
+ model.set = sinon.stub();
915
+ model.parse(null);
916
+ model.set.should.not.have.been.called;
917
+ });
717
918
  });
718
919
 
719
920
  describe('parseError', () => {
@@ -743,12 +944,11 @@ describe('Remote Model', () => {
743
944
  });
744
945
 
745
946
  it('should return parsed credentials if credentials is a string', () => {
746
- let credentials = model.auth('username:password');
947
+ let credentials = model.auth('username:pass:word');
747
948
 
748
949
  credentials.should.deep.equal({
749
- user: 'username',
750
- pass: 'password',
751
- sendImmediately: true
950
+ username: 'username',
951
+ password: 'pass:word'
752
952
  });
753
953
 
754
954
  });
@@ -767,21 +967,21 @@ describe('Remote Model', () => {
767
967
 
768
968
 
769
969
  describe('logging', () => {
770
- let logger;
970
+ let mocks;
771
971
 
772
972
  beforeEach(() => {
773
- logger = {
973
+ mocks = {
774
974
  outbound: sinon.stub(),
775
975
  trimHtml: sinon.stub()
776
976
  };
777
-
778
- let Model = proxyquire('../../lib/remote-model', {
779
- 'hmpo-logger': {
780
- get: () => logger,
781
- }
782
- });
977
+ sinon.stub(logger, 'get').returns(mocks);
783
978
 
784
979
  model = new Model();
980
+
981
+ });
982
+
983
+ afterEach(() => {
984
+ logger.get.restore();
785
985
  });
786
986
 
787
987
  describe('logSync', () => {
@@ -789,7 +989,7 @@ describe('Remote Model', () => {
789
989
  let args = {
790
990
  settings: {
791
991
  method: 'VERB',
792
- uri: 'http://example.org'
992
+ url: 'http://example.org'
793
993
  }
794
994
  };
795
995
 
@@ -800,7 +1000,7 @@ describe('Remote Model', () => {
800
1000
 
801
1001
  model.logSync(args);
802
1002
 
803
- logger.outbound.should.have.been.calledWithExactly(
1003
+ mocks.outbound.should.have.been.calledWithExactly(
804
1004
  'Model request sent :outVerb :outRequest',
805
1005
  argsAsMeta
806
1006
  );
@@ -812,7 +1012,7 @@ describe('Remote Model', () => {
812
1012
  let args = {
813
1013
  settings: {
814
1014
  method: 'VERB',
815
- uri: 'http://example.org'
1015
+ url: 'http://example.org'
816
1016
  },
817
1017
  statusCode: 418,
818
1018
  responseTime: 1000
@@ -827,7 +1027,7 @@ describe('Remote Model', () => {
827
1027
 
828
1028
  model.logError(args);
829
1029
 
830
- logger.outbound.should.have.been.calledWithExactly(
1030
+ mocks.outbound.should.have.been.calledWithExactly(
831
1031
  'Model request failed :outVerb :outRequest :outResponseCode :outError',
832
1032
  argsAsMeta
833
1033
  );
@@ -839,7 +1039,7 @@ describe('Remote Model', () => {
839
1039
  let args = {
840
1040
  settings: {
841
1041
  method: 'VERB',
842
- uri: 'http://example.org'
1042
+ url: 'http://example.org'
843
1043
  },
844
1044
  statusCode: 418,
845
1045
  responseTime: 1000
@@ -854,7 +1054,7 @@ describe('Remote Model', () => {
854
1054
 
855
1055
  model.logSuccess(args);
856
1056
 
857
- logger.outbound.should.have.been.calledWithExactly(
1057
+ mocks.outbound.should.have.been.calledWithExactly(
858
1058
  'Model request success :outVerb :outRequest :outResponseCode',
859
1059
  argsAsMeta
860
1060
  );
@@ -869,7 +1069,7 @@ describe('Remote Model', () => {
869
1069
  data = {
870
1070
  settings: {
871
1071
  method: 'VERB',
872
- uri: 'http://example.org'
1072
+ url: 'http://example.org'
873
1073
  },
874
1074
  statusCode: 418,
875
1075
  responseTime: 3000,
@@ -944,7 +1144,7 @@ describe('Remote Model', () => {
944
1144
  });
945
1145
 
946
1146
  it('should be present with error', () => {
947
- logger.trimHtml.returns('Html Body');
1147
+ mocks.trimHtml.returns('Html Body');
948
1148
 
949
1149
  let result = model.logMeta(data);
950
1150
 
package/.travis.yml DELETED
@@ -1,8 +0,0 @@
1
- language: node_js
2
- node_js:
3
- - "10"
4
- - "12"
5
- - "14"
6
- notifications:
7
- email: false
8
- sudo: false