hmpo-model 3.1.0 → 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,96 +1,170 @@
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 hmpoLogger = require('hmpo-logger');
7
+ const { URL } = require('url');
8
+
9
+ const DEFAULT_TIMEOUT = 60000;
8
10
 
9
11
  class RemoteModel extends LocalModel {
10
12
  constructor(attributes, options) {
11
13
  super(attributes, options);
12
-
14
+ this.got = got;
15
+ this.options.label = this.options.label || kebabCase(this.constructor.name);
13
16
  this.setLogger();
14
17
  }
15
18
 
16
19
  setLogger() {
17
- this.logger = hmpoLogger.get(`:${kebabCase(this.constructor.name)}`);
20
+ try {
21
+ const hmpoLogger = require('hmpo-logger');
22
+ this.logger = hmpoLogger.get(':' + this.options.label);
23
+ } catch (e) {
24
+ console.error('Error setting logger, using console instead!', e);
25
+ this.logger = { outbound: console.log, trimHtml: html => html };
26
+ }
18
27
  }
19
28
 
20
- fetch(callback) {
21
- 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);
22
35
  this.request(config, callback);
23
36
  }
24
37
 
25
- save(callback) {
26
- this.prepare((err, data) => {
27
-
28
- if (err) { return callback(err); }
29
-
30
- data = JSON.stringify(data);
31
-
32
- let config = this.requestConfig({
33
- method: 'POST',
34
- headers: {
35
- 'Content-Type': 'application/json',
36
- 'Content-Length': Buffer.byteLength(data)
37
- }
38
- });
39
-
40
- this.request(config, data, callback);
41
-
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);
42
47
  });
43
48
  }
44
49
 
45
- delete(callback) {
46
- 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);
47
56
  this.request(config, callback);
48
57
  }
49
58
 
50
- requestConfig(config) {
51
- let retConfig = _.clone(config);
59
+ prepare(callback) {
60
+ debug('prepare');
61
+ callback(null, this.toJSON());
62
+ }
52
63
 
53
- retConfig.uri = this.url();
64
+ requestConfig(config, args) {
65
+ const retConfig = Object.assign({}, config);
54
66
 
55
- let auth = this.auth();
67
+ retConfig.url = this.url(retConfig.url || retConfig.uri, args);
56
68
 
69
+ retConfig.timeout = this.timeout(retConfig.timeout);
70
+
71
+ const auth = this.auth(retConfig.auth);
57
72
  if (auth) {
58
- retConfig.auth = auth;
73
+ retConfig.username = auth.username || auth.user;
74
+ retConfig.password = auth.password || auth.pass;
59
75
  }
76
+ delete retConfig.auth;
60
77
 
61
- if (this.options.headers) {
62
- retConfig.headers = _.defaults(retConfig.headers || {}, this.options.headers);
78
+ const agent = this.proxy(retConfig.proxy, retConfig.url);
79
+ if (agent) {
80
+ retConfig.agent = agent;
63
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);
64
88
 
65
89
  return retConfig;
66
90
  }
67
91
 
68
- request(settings, body, callback) {
69
- if (typeof body === 'function' && arguments.length === 2) {
70
- callback = body;
71
- 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
+ };
72
117
  }
118
+ return timeout;
119
+ }
73
120
 
74
- // This is cloned and set so that the post body is not kept around in memory
75
- // settings is a shallow object, so the requestSettings is entirely made up of copied properties
76
- let requestSettings = _.clone(settings);
77
- requestSettings.body = body;
121
+ proxy(proxy = this.options.proxy, url) {
122
+ if (!proxy || !url) return;
78
123
 
79
- let startTime = process.hrtime();
124
+ if (typeof proxy === 'string') proxy = { proxy };
80
125
 
81
- let _callback = (err, data, statusCode) => {
126
+ const isHttps = (new URL(url).protocol === 'https:');
82
127
 
83
- // This uses node's "high resolution time" to determine response time.
84
- // The calculation is to translate seconds & nanoseconds into a number of format 1.234 seconds
85
- let endTime = process.hrtime(startTime);
86
- 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
+ }
87
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) => {
88
162
  if (err) {
89
163
  this.hookFail({settings, statusCode, responseTime, err, data});
90
164
  this.logError({settings, statusCode, responseTime, err, data});
91
165
  this.emit('fail', err, data, settings, statusCode, responseTime);
92
166
  } else {
93
- this.hookSuccess({settings, statusCode, responseTime});
167
+ this.hookSuccess({data, settings, statusCode, responseTime});
94
168
  this.logSuccess({settings, statusCode, responseTime});
95
169
  this.emit('success', data, settings, statusCode, responseTime);
96
170
  }
@@ -99,25 +173,27 @@ class RemoteModel extends LocalModel {
99
173
  }
100
174
  };
101
175
 
102
- requestLib(requestSettings, (err, response) => {
103
- if (err) {
176
+ this.got(settings)
177
+ .catch(err => {
178
+ debug('request got error', err);
179
+ setResponseTime();
104
180
  if (err.code === 'ETIMEDOUT') {
105
181
  err.message = 'Connection timed out';
106
182
  err.status = 504;
107
183
  }
108
- err.status = err.status || (response && response.statusCode) || 503;
109
- return _callback(err, null, err.status);
110
- }
111
- this.handleResponse(response, _callback);
112
- });
113
-
114
- this.hookSync({settings});
115
- this.logSync({settings});
116
- 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
+ });
117
192
  }
118
193
 
119
194
  handleResponse(response, callback) {
120
- let data = {};
195
+ debug('handleResponse', response);
196
+ let data;
121
197
  try {
122
198
  data = JSON.parse(response.body || '{}');
123
199
  } catch (err) {
@@ -129,53 +205,37 @@ class RemoteModel extends LocalModel {
129
205
  }
130
206
 
131
207
  parseResponse(statusCode, data, callback) {
132
- if (statusCode < 400) {
133
- try {
134
- data = this.parse(data);
135
- callback(null, data, statusCode);
136
- } catch (err) {
137
- callback(err, null, statusCode);
138
- }
139
- } else {
140
- 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);
141
213
  }
142
- }
143
214
 
144
- prepare(callback) {
145
- 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);
146
222
  }
147
223
 
148
224
  parse(data) {
225
+ debug('parse', data);
226
+ if (data && typeof data === 'object') this.set(data);
149
227
  return data;
150
228
  }
151
229
 
152
230
  parseError(statusCode, data) {
153
- return _.extend({ status: statusCode }, data);
154
- }
155
-
156
- url() {
157
- return this.options.url;
158
- }
159
-
160
- auth(credentials) {
161
- if (!credentials) return;
162
-
163
- if (typeof credentials === 'string') {
164
- let auth = credentials.split(':');
165
- credentials = {
166
- user: auth.shift(),
167
- pass: auth.join(':'),
168
- sendImmediately: true
169
- };
170
- }
171
-
172
- return credentials;
231
+ debug('parseError, statusCode, data');
232
+ return Object.assign({ status: statusCode }, data);
173
233
  }
174
234
 
175
235
  logMeta(tokenData) {
176
236
  let data = {
177
237
  outVerb: tokenData.settings.method,
178
- outRequest: tokenData.settings.uri
238
+ outRequest: tokenData.settings.url
179
239
  };
180
240
 
181
241
  if (tokenData.statusCode) {
@@ -193,7 +253,7 @@ class RemoteModel extends LocalModel {
193
253
  data.outErrorBody = this.logger.trimHtml(tokenData.err.body);
194
254
  }
195
255
 
196
- _.extend(data, this.options.logging);
256
+ Object.assign(data, this.options.logging);
197
257
 
198
258
  return data;
199
259
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmpo-model",
3
- "version": "3.1.0",
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
- "hmpo-logger": "^4.0.2",
28
+ "debug": "^4.3.3",
29
+ "got": "^11.8.3",
30
+ "hpagent": "^0.1.2",
29
31
  "lodash.kebabcase": "^4.1.1",
30
- "request": "^2.88.2",
31
- "underscore": "^1.12.0"
32
+ "underscore": "^1.13.1"
32
33
  },
33
34
  "devDependencies": {
34
- "chai": "^4.3.0",
35
- "eslint": "^7.20.0",
36
- "mocha": "^7.2.0",
35
+ "chai": "^4.3.4",
36
+ "eslint": "^8.3.0",
37
+ "hmpo-logger": "^4.1.3",
38
+ "mocha": "^9.1.3",
37
39
  "nyc": "^15.1.0",
38
40
  "proxyquire": "^2.0.0",
39
- "sinon": "^9.2.4",
40
- "sinon-chai": "^3.5.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,39 +26,60 @@ 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);
31
32
  });
32
33
 
33
34
  describe('constructor', () => {
35
+ let Model;
34
36
 
35
- it('should call setLogger', () => {
36
- let Model = require('../../lib/remote-model');
37
+ beforeEach(() => {
38
+ Model = require('../../lib/remote-model');
37
39
  sinon.stub(Model.prototype, 'setLogger');
40
+ });
38
41
 
42
+ it('should call setLogger', () => {
39
43
  model = new Model();
40
-
41
44
  model.setLogger.should.have.been.calledWithExactly();
42
45
  });
43
46
 
47
+ it('should set model label name', () => {
48
+ model = new Model();
49
+ model.options.label.should.equal('remote-model');
50
+ });
51
+
44
52
  afterEach(() => {
45
53
  Model.prototype.setLogger.restore();
46
54
  });
47
55
  });
48
56
 
49
57
  describe('setLogger', () => {
50
- let getStub = sinon.stub();
51
- let Model = proxyquire('../../lib/remote-model', {
52
- 'hmpo-logger': {
53
- get: getStub
54
- }
58
+ beforeEach(() => {
59
+ sinon.stub(logger, 'get').returns('logger');
55
60
  });
56
61
 
57
- model = new Model();
62
+ afterEach(() => {
63
+ logger.get.restore();
64
+ });
65
+
66
+ it('should set up a new hmpo-logger', () => {
67
+ model = new Model();
68
+
69
+ logger.get.should.have.been.calledWithExactly(':remote-model');
70
+ model.logger.should.equal('logger');
71
+ });
58
72
 
59
- getStub.should.have.been.calledWithExactly(':remote-model');
73
+ it('should use console log and a trimHtml pass-through if hmpo-logger is not available', () => {
74
+ logger.get.throws(new Error());
75
+
76
+ model = new Model();
77
+
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);
82
+ });
60
83
  });
61
84
 
62
85
  describe('fetch', () => {
@@ -77,6 +100,15 @@ describe('Remote Model', () => {
77
100
  method: 'GET'
78
101
  });
79
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
+
80
112
  it('should call request', () => {
81
113
  model.requestConfig.returns(mocks.config);
82
114
 
@@ -102,9 +134,9 @@ describe('Remote Model', () => {
102
134
  it('should call prepare', () => {
103
135
  model.save(cb);
104
136
 
105
- model.prepare.should.have.been.called;
106
-
137
+ model.prepare.should.have.been.calledWithExactly(sinon.match.func);
107
138
  });
139
+
108
140
  it('should call callback with an error', () => {
109
141
  let error = new Error('error');
110
142
  model.prepare.yields(error);
@@ -112,7 +144,6 @@ describe('Remote Model', () => {
112
144
  model.save(cb);
113
145
 
114
146
  cb.should.have.been.calledWith(error);
115
-
116
147
  });
117
148
 
118
149
  context('on prepare success', () => {
@@ -128,20 +159,27 @@ describe('Remote Model', () => {
128
159
  it('should use requestConfig', () => {
129
160
  model.save(cb);
130
161
 
131
- model.requestConfig.should.have.been.calledWith({
162
+ model.requestConfig.should.have.been.calledWithExactly({
132
163
  method: 'POST',
133
- headers: {
134
- 'Content-Type': 'application/json',
135
- 'Content-Length': Buffer.byteLength(JSON.stringify(preparedData))
136
- }
137
- });
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' });
138
175
  });
176
+
139
177
  it('should call request', () => {
140
178
  model.requestConfig.returns(mocks.config);
141
179
 
142
180
  model.save(cb);
143
181
 
144
- model.request.should.have.been.calledWith(mocks.config, JSON.stringify(preparedData), cb);
182
+ model.request.should.have.been.calledWith(mocks.config, cb);
145
183
  });
146
184
  });
147
185
 
@@ -165,6 +203,15 @@ describe('Remote Model', () => {
165
203
  method: 'DELETE'
166
204
  });
167
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
+
168
215
  it('should call request', () => {
169
216
  model.requestConfig.returns(mocks.config);
170
217
 
@@ -175,50 +222,227 @@ describe('Remote Model', () => {
175
222
  });
176
223
 
177
224
  describe('requestConfig', () => {
178
- beforeEach(() => {
179
- sinon.stub(model, 'url');
180
- 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
+ });
181
231
 
182
- model.url.returns('http://example.net');
183
- });
232
+ config.url.should.equal('https://example.com/options');
233
+ });
184
234
 
185
- afterEach(() => {
186
- model.url.restore();
187
- 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
+ });
188
265
  });
189
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
+ });
190
273
 
191
- it('should use url', () => {
192
- model.requestConfig({
193
- 'method': 'VERB'
274
+ config.username.should.equal('options');
275
+ config.password.should.equal('pass:word');
276
+ config.should.not.have.property('auth');
277
+ });
278
+
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');
194
289
  });
195
290
 
196
- model.url.should.have.been.calledWithExactly();
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
+ });
197
304
  });
198
305
 
199
- it('should use auth', () => {
200
- model.requestConfig({
201
- '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
+ });
202
340
  });
203
341
 
204
- model.auth.should.have.been.calledWithExactly();
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
+ });
359
+ });
360
+
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
+ });
205
388
  });
206
389
 
207
- it('should add auth to config if provided', () => {
208
- model.auth.returns({
209
- user: 'username',
210
- 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');
211
399
  });
212
400
 
213
- let returnedConfig = model.requestConfig({
214
- '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
+ });
215
413
  });
216
414
 
217
- returnedConfig.should.deep.include({
218
- auth: {
219
- user: 'username',
220
- pass: 'password'
221
- }
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
+ });
222
446
  });
223
447
  });
224
448
 
@@ -330,14 +554,15 @@ describe('Remote Model', () => {
330
554
  });
331
555
 
332
556
  describe('request', () => {
333
- let settings, body, requestSettings;
557
+ let settings, requestSettings, mocks;
334
558
 
335
559
  beforeEach(() => {
336
- let Model = proxyquire('../../lib/remote-model', {
337
- 'request': mocks.request
338
- });
339
-
560
+ mocks = {};
561
+ mocks.got = sinon.stub().returns(mocks);
562
+ mocks.then = sinon.stub().returns(mocks);
563
+ mocks.catch = sinon.stub().returns(mocks);
340
564
  model = new Model();
565
+ model.got = mocks.got;
341
566
 
342
567
  sinon.stub(model, 'logSync');
343
568
  sinon.stub(model, 'logSuccess');
@@ -358,22 +583,11 @@ describe('Remote Model', () => {
358
583
  model.emit.restore();
359
584
  });
360
585
 
361
- it('should invoke request with settings including a body', () => {
362
- requestSettings.body = body;
363
-
364
- model.request(settings, body, cb);
365
-
366
- mocks.request.should.have.been.called;
367
- mocks.request.should.have.been.calledWith(requestSettings);
368
- });
369
-
370
586
  it('should invoke request with request settings', () => {
371
- requestSettings.body = undefined;
372
-
373
587
  model.request(settings, cb);
374
588
 
375
- mocks.request.should.have.been.called;
376
- mocks.request.should.have.been.calledWith(requestSettings);
589
+ mocks.got.should.have.been.called;
590
+ mocks.got.should.have.been.calledWith(requestSettings);
377
591
  });
378
592
 
379
593
  it('should log sync messages', () => {
@@ -405,7 +619,7 @@ describe('Remote Model', () => {
405
619
  });
406
620
 
407
621
  it('should work without a callback', () => {
408
- mocks.request.yields(new Error('Random Error'), {});
622
+ mocks.catch.yields(new Error('Random Error'));
409
623
 
410
624
  model.request(settings);
411
625
 
@@ -424,7 +638,9 @@ describe('Remote Model', () => {
424
638
  'statusCode': 418
425
639
  };
426
640
 
427
- mocks.request.yields(error, response);
641
+ error.response = response;
642
+
643
+ mocks.catch.yields(error);
428
644
  });
429
645
 
430
646
  it('should log error messages', () => {
@@ -507,7 +723,7 @@ describe('Remote Model', () => {
507
723
 
508
724
  context('on success', () => {
509
725
  beforeEach(() => {
510
- mocks.request.yields(null, {
726
+ mocks.then.yields({
511
727
  'body': JSON.stringify({'data': 'value'}),
512
728
  'statusCode': 200
513
729
  });
@@ -541,6 +757,7 @@ describe('Remote Model', () => {
541
757
  model.request(settings, cb);
542
758
 
543
759
  hook.should.have.been.calledWithExactly({
760
+ data: {'data': 'value'},
544
761
  statusCode: 200,
545
762
  settings: settings,
546
763
  responseTime: sinon.match.number
@@ -613,6 +830,7 @@ describe('Remote Model', () => {
613
830
 
614
831
  afterEach(() => {
615
832
  model.parse.restore();
833
+
616
834
  model.parseError.restore();
617
835
  });
618
836
 
@@ -686,6 +904,17 @@ describe('Remote Model', () => {
686
904
  it('returns data passed', () => {
687
905
  model.parse({ data: 1 }).should.eql({ data: 1 });
688
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
+ });
689
918
  });
690
919
 
691
920
  describe('parseError', () => {
@@ -715,12 +944,11 @@ describe('Remote Model', () => {
715
944
  });
716
945
 
717
946
  it('should return parsed credentials if credentials is a string', () => {
718
- let credentials = model.auth('username:password');
947
+ let credentials = model.auth('username:pass:word');
719
948
 
720
949
  credentials.should.deep.equal({
721
- user: 'username',
722
- pass: 'password',
723
- sendImmediately: true
950
+ username: 'username',
951
+ password: 'pass:word'
724
952
  });
725
953
 
726
954
  });
@@ -739,21 +967,21 @@ describe('Remote Model', () => {
739
967
 
740
968
 
741
969
  describe('logging', () => {
742
- let logger;
970
+ let mocks;
743
971
 
744
972
  beforeEach(() => {
745
- logger = {
973
+ mocks = {
746
974
  outbound: sinon.stub(),
747
975
  trimHtml: sinon.stub()
748
976
  };
749
-
750
- let Model = proxyquire('../../lib/remote-model', {
751
- 'hmpo-logger': {
752
- get: () => logger,
753
- }
754
- });
977
+ sinon.stub(logger, 'get').returns(mocks);
755
978
 
756
979
  model = new Model();
980
+
981
+ });
982
+
983
+ afterEach(() => {
984
+ logger.get.restore();
757
985
  });
758
986
 
759
987
  describe('logSync', () => {
@@ -761,7 +989,7 @@ describe('Remote Model', () => {
761
989
  let args = {
762
990
  settings: {
763
991
  method: 'VERB',
764
- uri: 'http://example.org'
992
+ url: 'http://example.org'
765
993
  }
766
994
  };
767
995
 
@@ -772,7 +1000,7 @@ describe('Remote Model', () => {
772
1000
 
773
1001
  model.logSync(args);
774
1002
 
775
- logger.outbound.should.have.been.calledWithExactly(
1003
+ mocks.outbound.should.have.been.calledWithExactly(
776
1004
  'Model request sent :outVerb :outRequest',
777
1005
  argsAsMeta
778
1006
  );
@@ -784,7 +1012,7 @@ describe('Remote Model', () => {
784
1012
  let args = {
785
1013
  settings: {
786
1014
  method: 'VERB',
787
- uri: 'http://example.org'
1015
+ url: 'http://example.org'
788
1016
  },
789
1017
  statusCode: 418,
790
1018
  responseTime: 1000
@@ -799,7 +1027,7 @@ describe('Remote Model', () => {
799
1027
 
800
1028
  model.logError(args);
801
1029
 
802
- logger.outbound.should.have.been.calledWithExactly(
1030
+ mocks.outbound.should.have.been.calledWithExactly(
803
1031
  'Model request failed :outVerb :outRequest :outResponseCode :outError',
804
1032
  argsAsMeta
805
1033
  );
@@ -811,7 +1039,7 @@ describe('Remote Model', () => {
811
1039
  let args = {
812
1040
  settings: {
813
1041
  method: 'VERB',
814
- uri: 'http://example.org'
1042
+ url: 'http://example.org'
815
1043
  },
816
1044
  statusCode: 418,
817
1045
  responseTime: 1000
@@ -826,7 +1054,7 @@ describe('Remote Model', () => {
826
1054
 
827
1055
  model.logSuccess(args);
828
1056
 
829
- logger.outbound.should.have.been.calledWithExactly(
1057
+ mocks.outbound.should.have.been.calledWithExactly(
830
1058
  'Model request success :outVerb :outRequest :outResponseCode',
831
1059
  argsAsMeta
832
1060
  );
@@ -841,7 +1069,7 @@ describe('Remote Model', () => {
841
1069
  data = {
842
1070
  settings: {
843
1071
  method: 'VERB',
844
- uri: 'http://example.org'
1072
+ url: 'http://example.org'
845
1073
  },
846
1074
  statusCode: 418,
847
1075
  responseTime: 3000,
@@ -916,7 +1144,7 @@ describe('Remote Model', () => {
916
1144
  });
917
1145
 
918
1146
  it('should be present with error', () => {
919
- logger.trimHtml.returns('Html Body');
1147
+ mocks.trimHtml.returns('Html Body');
920
1148
 
921
1149
  let result = model.logMeta(data);
922
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