sfmc-sdk 0.5.0 → 0.6.1

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.
package/lib/auth.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const axios = require('axios');
4
- const { isConnectionError } = require('./util');
4
+ const { isConnectionError, RestError } = require('./util');
5
5
  const AVAIALABLE_SCOPES = [
6
6
  'accounts_read',
7
7
  'accounts_write',
@@ -154,21 +154,21 @@ module.exports = class Auth {
154
154
  }
155
155
  if (Boolean(forceRefresh) || _isExpired(this.authObject)) {
156
156
  try {
157
+ remainingAttempts--;
157
158
  this.authObject = await _requestToken(this.authObject);
158
159
  } catch (ex) {
159
160
  if (
160
161
  this.options.retryOnConnectionError &&
161
- remainingAttempts &&
162
+ remainingAttempts > 0 &&
162
163
  isConnectionError(ex.code)
163
164
  ) {
164
165
  if (this.options?.eventHandlers?.onConnectionError) {
165
166
  this.options.eventHandlers.onConnectionError(ex, remainingAttempts);
166
167
  }
167
- remainingAttempts--;
168
168
  return this.getAccessToken(forceRefresh, remainingAttempts);
169
- } else {
170
- throw ex;
171
169
  }
170
+
171
+ throw new RestError(ex);
172
172
  }
173
173
 
174
174
  if (this.options?.eventHandlers?.onRefresh) {
@@ -180,6 +180,7 @@ module.exports = class Auth {
180
180
 
181
181
  /**
182
182
  * Helper to get back list of scopes supported by SDK
183
+ *
183
184
  * @returns {Array[String]} array of potential scopes
184
185
  */
185
186
  getSupportedScopes() {
package/lib/rest.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const axios = require('axios');
4
- const { isObject, isConnectionError } = require('./util');
4
+ const { isObject, isPayload, isConnectionError, RestError } = require('./util');
5
5
  const pLimit = require('p-limit');
6
6
 
7
7
  module.exports = class Rest {
@@ -175,23 +175,35 @@ module.exports = class Rest {
175
175
  requestOptions.headers = {
176
176
  Authorization: `Bearer ` + this.auth.authObject.access_token,
177
177
  };
178
- const res = await axios(requestOptions);
179
- return res.data;
178
+ remainingAttempts--;
179
+ if (this.options?.eventHandlers?.logRequest) {
180
+ this.options.eventHandlers.logRequest(requestOptions);
181
+ }
182
+ const response = await axios(requestOptions);
183
+ if (this.options?.eventHandlers?.logResponse) {
184
+ this.options.eventHandlers.logResponse({
185
+ data: response.data,
186
+ status: response.status,
187
+ });
188
+ }
189
+ return response.data;
180
190
  } catch (ex) {
181
191
  if (
182
192
  this.options.retryOnConnectionError &&
183
- remainingAttempts &&
193
+ remainingAttempts > 0 &&
184
194
  isConnectionError(ex.code)
185
195
  ) {
186
- remainingAttempts--;
196
+ if (this.options?.eventHandlers?.onConnectionError) {
197
+ this.options.eventHandlers.onConnectionError(ex, remainingAttempts);
198
+ }
187
199
  return this._apiRequest(requestOptions, remainingAttempts);
188
200
  } else if (ex.response && ex.response.status === 401 && remainingAttempts) {
189
201
  // force refresh due to url related issue
190
202
  await this.auth.getAccessToken(true);
191
203
  //only retry once on refresh since there should be no reason for this token to be invalid
192
- return this._apiRequest(requestOptions, 0);
204
+ return this._apiRequest(requestOptions, 1);
193
205
  } else {
194
- throw ex;
206
+ throw new RestError(ex);
195
207
  }
196
208
  }
197
209
  }
@@ -202,7 +214,7 @@ module.exports = class Rest {
202
214
  * @param {object} options API request opptions
203
215
  */
204
216
  function _checkPayload(options) {
205
- if (!isObject(options.data)) {
217
+ if (!isPayload(options.data)) {
206
218
  throw new Error(`${options.method} requests require a payload in options.data`);
207
219
  }
208
220
  }
package/lib/soap.js CHANGED
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
  const axios = require('axios');
3
3
  const { XMLBuilder, XMLParser } = require('fast-xml-parser');
4
- const { isObject, isConnectionError } = require('./util');
4
+
5
+ const { isObject, isConnectionError, SOAPError } = require('./util');
5
6
 
6
7
  module.exports = class Soap {
7
8
  /**
@@ -96,6 +97,8 @@ module.exports = class Soap {
96
97
  }
97
98
  status = resultsBatch.OverallStatus;
98
99
  if (status === 'MoreDataAvailable') {
100
+ //as requestParams is by default optional, ensure object exists in this case
101
+ requestParams = requestParams || {};
99
102
  requestParams.continueRequest = resultsBatch.RequestID;
100
103
  if (this.options?.eventHandlers?.onLoop) {
101
104
  this.options.eventHandlers.onLoop(type, resultsBulk);
@@ -367,22 +370,25 @@ module.exports = class Soap {
367
370
  this.options.eventHandlers.logRequest(requestOptions);
368
371
  }
369
372
  let response;
373
+ remainingAttempts--;
370
374
  try {
371
375
  response = await axios(requestOptions);
372
376
  } catch (ex) {
373
377
  if (
374
378
  this.options.retryOnConnectionError &&
375
- remainingAttempts &&
379
+ remainingAttempts > 0 &&
376
380
  isConnectionError(ex.code)
377
381
  ) {
378
- remainingAttempts--;
382
+ if (this.options?.eventHandlers?.onConnectionError) {
383
+ this.options.eventHandlers.onConnectionError(ex, remainingAttempts);
384
+ }
379
385
  return this._apiRequest(options, remainingAttempts);
380
386
  } else if (ex.response) {
381
387
  // if the response is received, then continue parsing and check for errors later
382
388
  response = ex.response;
383
389
  } else {
384
390
  // if no response, then throw
385
- throw ex;
391
+ throw new SOAPError(ex);
386
392
  }
387
393
  }
388
394
  if (this.options?.eventHandlers?.logResponse) {
@@ -399,13 +405,17 @@ module.exports = class Soap {
399
405
  // need to wait as it may error
400
406
  return await _parseResponse(response, options.key);
401
407
  } catch (ex) {
402
- if (ex.errorMessage === 'Token Expired' && remainingAttempts) {
408
+ if (ex.message === 'Token Expired' && remainingAttempts) {
403
409
  // force refresh due to url related issue
404
410
  await this.auth.getAccessToken(true);
405
411
  // set to no more retries as after token refresh it should always work
406
- return this._apiRequest(options, 0);
407
- } else {
412
+ return this._apiRequest(options, 1);
413
+ } else if (ex instanceof SOAPError) {
414
+ //rethrow as is already handled/parsed
408
415
  throw ex;
416
+ } else {
417
+ //unknown error
418
+ throw new SOAPError(ex, response, null);
409
419
  }
410
420
  }
411
421
  }
@@ -483,11 +493,12 @@ async function _parseResponse(response, key) {
483
493
  }
484
494
  // checks overall status error
485
495
  if (['Error', 'Has Errors'].includes(soapBody[key].OverallStatus)) {
486
- throw new SOAPError(response, soapBody[key]);
496
+ throw new SOAPError(null, response, soapBody[key]);
487
497
  }
488
498
  return soapBody[key];
489
499
  }
490
- throw new SOAPError(response, soapBody);
500
+ // something else went wrong but payload parsed
501
+ throw new SOAPError(null, response, soapBody);
491
502
  }
492
503
  /**
493
504
  * Method checks options object for validity
@@ -514,29 +525,3 @@ function validateOptions(options, additional) {
514
525
  }
515
526
  }
516
527
  }
517
-
518
- class SOAPError extends Error {
519
- constructor(response, soapBody) {
520
- // Content Error
521
- if (soapBody && ['Error', 'Has Errors'].includes(soapBody.OverallStatus)) {
522
- super('One or more errors in the Results');
523
- }
524
- // Payload Error
525
- else if (soapBody && soapBody['soap:Fault']) {
526
- super('Error in SOAP Payload');
527
- const fault = soapBody['soap:Fault'];
528
- this.errorCode = fault.faultcode;
529
- this.errorMessage = fault.faultstring;
530
- }
531
- // Request Error
532
- else if (response.status > 299) {
533
- super('Error with SOAP Request');
534
- }
535
- // Fallback Error
536
- else {
537
- super('Unknown Error or Unhandled Request');
538
- }
539
- this.response = response;
540
- this.JSON = soapBody;
541
- }
542
- }
package/lib/util.js CHANGED
@@ -6,6 +6,15 @@
6
6
  */
7
7
  module.exports.isObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]';
8
8
 
9
+ /**
10
+ * Method to check if Object passed is a valid payload for API calls
11
+ *
12
+ * @param {object} obj Object to check
13
+ * @returns {boolean} true if is a valid payload
14
+ */
15
+ module.exports.isPayload = (obj) =>
16
+ Object.prototype.toString.call(obj) === '[object Object]' || Array.isArray(obj);
17
+
9
18
  /**
10
19
  * Method to check if it is a connection error
11
20
  *
@@ -14,3 +23,70 @@ module.exports.isObject = (obj) => Object.prototype.toString.call(obj) === '[obj
14
23
  */
15
24
  module.exports.isConnectionError = (code) =>
16
25
  code && ['ETIMEDOUT', 'EHOSTUNREACH', 'ENOTFOUND', 'ECONNRESET', 'ECONNABORTED'].includes(code);
26
+
27
+ /**
28
+ * CustomError type for handling REST (including Auth) based errors
29
+ *
30
+ * @class RestError
31
+ * @augments {Error}
32
+ */
33
+ module.exports.RestError = class RestError extends Error {
34
+ constructor(ex) {
35
+ // Expired Error
36
+ if (ex.response?.data?.message) {
37
+ super(ex.response.data.message);
38
+ this.code = ex.response.data.errorcode || ex.code;
39
+ }
40
+ // Unauthenticated
41
+ else if (ex.response?.data?.error_description) {
42
+ super(ex.response.data.error_description);
43
+ this.code = ex.response.data.error || ex.code;
44
+ } else {
45
+ super(ex.message);
46
+ this.code = ex.code;
47
+ }
48
+ this.response = ex.response;
49
+ this.name = this.constructor.name;
50
+ if (Error.captureStackTrace) {
51
+ Error.captureStackTrace(this, RestError);
52
+ }
53
+ }
54
+ };
55
+
56
+ /**
57
+ * CustomError type for handling SOAP based errors
58
+ *
59
+ * @class SOAPError
60
+ * @augments {Error}
61
+ */
62
+ module.exports.SOAPError = class SOAPError extends Error {
63
+ constructor(ex, response, soapBody) {
64
+ // Content Error
65
+ if (soapBody && ['Error', 'Has Errors'].includes(soapBody.OverallStatus)) {
66
+ super('One or more errors in the Results');
67
+ this.code = soapBody.OverallStatus;
68
+ }
69
+ // Payload Error
70
+ else if (soapBody && soapBody['soap:Fault']) {
71
+ const fault = soapBody['soap:Fault'];
72
+ super(fault.faultstring);
73
+ this.code = fault.faultcode;
74
+ }
75
+ // Request Error
76
+ else if (response?.status > 299) {
77
+ super('Error with SOAP Request');
78
+ this.code = response?.status;
79
+ }
80
+ // Fallback Error
81
+ else {
82
+ super(ex.message);
83
+ this.code = ex.code;
84
+ }
85
+ this.response = response;
86
+ this.json = soapBody;
87
+ this.name = this.constructor.name;
88
+ if (Error.captureStackTrace) {
89
+ Error.captureStackTrace(this, SOAPError);
90
+ }
91
+ }
92
+ };
package/package.json CHANGED
@@ -1,46 +1,50 @@
1
1
  {
2
- "name": "sfmc-sdk",
3
- "version": "0.5.0",
4
- "description": "Libarary to simplify SFMC requests with updated dependencies and less overhead",
5
- "main": "./lib/index.js",
6
- "scripts": {
7
- "test": "nyc --reporter=text mocha",
8
- "lint": "eslint ./lib",
9
- "lint:fix": "eslint ./lib --fix"
10
- },
11
- "repository": {
12
- "type": "git",
13
- "url": "https://github.com/DougMidgley/SFMC-SDK.git"
14
- },
15
- "author": "Doug Midgley <douglasmidgley@gmail.com>",
16
- "license": "BSD-3-Clause",
17
- "dependencies": {
18
- "axios": "^0.26.0",
19
- "fast-xml-parser": "4.0.3",
20
- "p-limit": "3.1.0"
21
- },
22
- "keywords": [
23
- "fuel",
24
- "exacttarget",
25
- "salesforce",
26
- "marketing",
27
- "cloud",
28
- "soap",
29
- "rest",
30
- "auth",
31
- "sdk"
32
- ],
33
- "devDependencies": {
34
- "assert": "2.0.0",
35
- "axios-mock-adapter": "1.20.0",
36
- "chai": "4.3.6",
37
- "eslint-config-prettier": "8.4.0",
38
- "eslint-plugin-jsdoc": "37.9.4",
39
- "eslint-plugin-mocha": "10.0.3",
40
- "eslint-plugin-prettier": "4.0.0",
41
- "mocha": "9.2.1",
42
- "nyc": "15.1.0",
43
- "prettier-eslint": "13.0.0",
44
- "sinon": "13.0.1"
45
- }
2
+ "name": "sfmc-sdk",
3
+ "version": "0.6.1",
4
+ "description": "Libarary to simplify SFMC requests with updated dependencies and less overhead",
5
+ "main": "./lib/index.js",
6
+ "scripts": {
7
+ "test": "nyc --reporter=text mocha",
8
+ "lint": "eslint ./lib && eslint ./test",
9
+ "lint:fix": "eslint ./lib --fix && eslint ./test --fix"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/DougMidgley/SFMC-SDK.git"
14
+ },
15
+ "author": "Doug Midgley <douglasmidgley@gmail.com>",
16
+ "license": "BSD-3-Clause",
17
+ "dependencies": {
18
+ "axios": "^0.27.2",
19
+ "fast-xml-parser": "4.0.8",
20
+ "p-limit": "3.1.0"
21
+ },
22
+ "keywords": [
23
+ "fuel",
24
+ "exacttarget",
25
+ "salesforce",
26
+ "marketing",
27
+ "cloud",
28
+ "soap",
29
+ "rest",
30
+ "auth",
31
+ "sdk"
32
+ ],
33
+ "devDependencies": {
34
+ "assert": "2.0.0",
35
+ "axios-mock-adapter": "1.21.1",
36
+ "chai": "4.3.6",
37
+ "eslint-config-prettier": "8.5.0",
38
+ "eslint-plugin-jsdoc": "39.3.3",
39
+ "eslint-plugin-mocha": "10.0.5",
40
+ "eslint-plugin-prettier": "4.0.0",
41
+ "mocha": "10.0.0",
42
+ "nyc": "15.1.0",
43
+ "prettier-eslint": "15.0.1",
44
+ "sinon": "14.0.0"
45
+ },
46
+ "engines": {
47
+ "npm": ">=6.14.4",
48
+ "node": ">=14.0.0"
49
+ }
46
50
  }
package/test/auth.test.js CHANGED
@@ -4,11 +4,11 @@ const { defaultSdk, mock } = require('./utils.js');
4
4
  const resources = require('./resources/auth.json');
5
5
  const { isConnectionError } = require('../lib/util');
6
6
 
7
- describe('auth', () => {
8
- afterEach(() => {
7
+ describe('auth', function () {
8
+ afterEach(function () {
9
9
  mock.reset();
10
10
  });
11
- it('should return an auth payload with token', async () => {
11
+ it('should return an auth payload with token', async function () {
12
12
  //given
13
13
  const { success } = resources;
14
14
 
@@ -20,7 +20,7 @@ describe('auth', () => {
20
20
  assert.lengthOf(mock.history.post, 1);
21
21
  return;
22
22
  });
23
- it('should return an auth payload with previous token and one request', async () => {
23
+ it('should return an auth payload with previous token and one request', async function () {
24
24
  //given
25
25
  const { success } = resources;
26
26
  mock.onPost(success.url).reply(success.status, success.response);
@@ -33,7 +33,7 @@ describe('auth', () => {
33
33
  assert.lengthOf(mock.history.post, 1);
34
34
  return;
35
35
  });
36
- it('should return an unauthorized error', async () => {
36
+ it('should return an unauthorized error', async function () {
37
37
  //given
38
38
  const { unauthorized } = resources;
39
39
  mock.onPost(unauthorized.url).reply(unauthorized.status, unauthorized.response);
@@ -49,10 +49,10 @@ describe('auth', () => {
49
49
 
50
50
  return;
51
51
  });
52
- it('should return an incorrect account_id error', async () => {
52
+ it('should return an incorrect account_id error', async function () {
53
53
  try {
54
54
  //given
55
- sfmc = new SDK({
55
+ new SDK({
56
56
  client_id: 'XXXXX',
57
57
  client_secret: 'YYYYYY',
58
58
  auth_url: 'https://mct0l7nxfq2r988t1kxfy8sc47ma.auth.marketingcloudapis.com/',
@@ -68,10 +68,10 @@ describe('auth', () => {
68
68
  }
69
69
  return;
70
70
  });
71
- it('should return an incorrect auth_url error', async () => {
71
+ it('should return an incorrect auth_url error', async function () {
72
72
  try {
73
73
  //given
74
- sfmc = new SDK({
74
+ new SDK({
75
75
  client_id: 'XXXXX',
76
76
  client_secret: 'YYYYYY',
77
77
  auth_url: 'https://x.auth.marketingcloudapis.com/',
@@ -87,10 +87,10 @@ describe('auth', () => {
87
87
  }
88
88
  return;
89
89
  });
90
- it('should return an incorrect client_id error', async () => {
90
+ it('should return an incorrect client_id error', async function () {
91
91
  try {
92
92
  //given
93
- sfmc = new SDK({
93
+ new SDK({
94
94
  client_id: '',
95
95
  client_secret: 'YYYYYY',
96
96
  auth_url: 'https://mct0l7nxfq2r988t1kxfy8sc47ma.auth.marketingcloudapis.com/',
@@ -103,10 +103,10 @@ describe('auth', () => {
103
103
  }
104
104
  return;
105
105
  });
106
- it('should return an incorrect client_key error', async () => {
106
+ it('should return an incorrect client_key error', async function () {
107
107
  try {
108
108
  //given
109
- sfmc = new SDK({
109
+ new SDK({
110
110
  client_id: 'XXXXX',
111
111
  client_secret: '',
112
112
  auth_url: 'https://mct0l7nxfq2r988t1kxfy8sc47ma.auth.marketingcloudapis.com/',
@@ -119,10 +119,10 @@ describe('auth', () => {
119
119
  }
120
120
  return;
121
121
  });
122
- it('should return an invalid scope error', async () => {
122
+ it('should return an invalid scope error', async function () {
123
123
  try {
124
124
  //given
125
- sfmc = new SDK({
125
+ new SDK({
126
126
  client_id: 'XXXXX',
127
127
  client_secret: 'YYYYYY',
128
128
  auth_url: 'https://mct0l7nxfq2r988t1kxfy8sc47ma.auth.marketingcloudapis.com/',
@@ -136,10 +136,10 @@ describe('auth', () => {
136
136
  }
137
137
  return;
138
138
  });
139
- it('should return an invalid scope type error', async () => {
139
+ it('should return an invalid scope type error', async function () {
140
140
  try {
141
141
  //given
142
- sfmc = new SDK({
142
+ new SDK({
143
143
  client_id: 'XXXXX',
144
144
  client_secret: 'YYYYYY',
145
145
  auth_url: 'https://mct0l7nxfq2r988t1kxfy8sc47ma.auth.marketingcloudapis.com/',
@@ -154,7 +154,7 @@ describe('auth', () => {
154
154
  return;
155
155
  });
156
156
 
157
- it('RETRY: should return an success, after a connection issues', async () => {
157
+ it('RETRY: should return an success, after a connection issues', async function () {
158
158
  //given
159
159
  const { success } = resources;
160
160
 
@@ -169,7 +169,7 @@ describe('auth', () => {
169
169
  assert.lengthOf(mock.history.post, 2);
170
170
  return;
171
171
  });
172
- it('FAILED RETRY: should return an error, after multiple connection issues', async () => {
172
+ it('FAILED RETRY: should return an error, after multiple connection issues', async function () {
173
173
  //given
174
174
  const { success } = resources;
175
175
 
@@ -2,7 +2,7 @@
2
2
  "success": {
3
3
  "url": "https://mct0l7nxfq2r988t1kxfy8sc47ma.auth.marketingcloudapis.com/v2/token",
4
4
  "response": {
5
- "access_token": "eyJhbGciOiJIUzI1NiIsImtpZCI6IjQiLCJ2ZXIiOiIxIiwidHlwIjoiSldUIn0.eyJhY2Nlc3NfdG9rZW4iOiI3UU9IYmJTd3JFOXRTYkdlTTQ4ZktXTXgiLCJjbGllbnRfaWQiOiIwbTBoZXQ2bXFrbnp2MHVlYnFsdm9vNDEiLCJlaWQiOjcyODE2OTgsInN0YWNrX2tleSI6IlM3IiwicGxhdGZvcm1fdmVyc2lvbiI6MiwiY2xpZW50X3R5cGUiOiJTZXJ2ZXJUb1NlcnZlciJ9.5TciRkgyCXpV232vUowRvGCR03-zT5d1NRBcZrIn1Z8.8_hnocm2nm2WkEu4KHNpyFrG60_TQ52XAVYmYSU8yyd452YL3Mzb6k_ieT9B2CWRI3dSvDARFK9bz59yjz1HWsUjP0ENXeyO6wEA8MpeVSkxlD6u6Q73ZtK2sAwsaFoSmx96HpjjhMWKxNEuESTl_Hp2Qfv_mXC2LNluVq3fYYKn7VSr4zaRTRB",
5
+ "access_token": "TESTTOKEN",
6
6
  "token_type": "Bearer",
7
7
  "expires_in": 1079,
8
8
  "scope": "offline documents_and_images_read documents_and_images_write saved_content_read saved_content_write automations_execute automations_read automations_write journeys_execute journeys_read journeys_write email_read email_send email_write push_read push_send push_write sms_read sms_send sms_write social_post social_publish social_read social_write web_publish web_read web_write audiences_read audiences_write list_and_subscribers_read list_and_subscribers_write data_extensions_read data_extensions_write file_locations_read file_locations_write tracking_events_read calendar_read calendar_write campaign_read campaign_write accounts_read accounts_write users_read users_write webhooks_read webhooks_write workflows_write approvals_write tags_write approvals_read tags_read workflows_read ott_chat_messaging_read ott_chat_messaging_send ott_channels_read ott_channels_write marketing_cloud_connect_read marketing_cloud_connect_write marketing_cloud_connect_send event_notification_callback_create event_notification_callback_read event_notification_callback_update event_notification_callback_delete event_notification_subscription_create event_notification_subscription_read event_notification_subscription_update event_notification_subscription_delete tracking_events_write key_manage_view key_manage_rotate key_manage_revoke dfu_configure journeys_aspr journeys_delete package_manager_package package_manager_deploy deep_linking_asset_read deep_linking_asset_write deep_linking_asset_delete deep_linking_settings_read deep_linking_settings_write",
@@ -356,6 +356,11 @@
356
356
  "serviceMessageID": "36f9ab27-aaee-46d5-b41f-4b7a61fc645a"
357
357
  }
358
358
  },
359
+ "dataExtensionUpsert": {
360
+ "status": 200,
361
+ "url": "https://mct0l7nxfq2r988t1kxfy8sc47ma.rest.marketingcloudapis.com/hub/v1/dataevents/key:key/rowset",
362
+ "response": [{ "keys": { "primaryKey": 1 }, "values": { "name": "test" } }]
363
+ },
359
364
  "campaignDelete": {
360
365
  "status": 200,
361
366
  "url": "https://mct0l7nxfq2r988t1kxfy8sc47ma.rest.marketingcloudapis.com/hub/v1/campaigns/12656",
package/test/rest.test.js CHANGED
@@ -1,21 +1,21 @@
1
1
  const assert = require('chai').assert;
2
2
  const { defaultSdk, mock } = require('./utils.js');
3
+ const SDK = require('../lib');
3
4
  const resources = require('./resources/rest.json');
4
5
  const authResources = require('./resources/auth.json');
5
6
  const { isConnectionError } = require('../lib/util');
6
7
 
7
- describe('rest', () => {
8
- beforeEach(() => {
8
+ describe('rest', function () {
9
+ beforeEach(function () {
9
10
  mock.onPost(authResources.success.url).reply(
10
11
  authResources.success.status,
11
12
  authResources.success.response
12
13
  );
13
14
  });
14
- afterEach(() => {
15
+ afterEach(function () {
15
16
  mock.reset();
16
17
  });
17
-
18
- it('GET Bulk: should return 6 journey items', async () => {
18
+ it('GET Bulk: should return 6 journey items', async function () {
19
19
  //given
20
20
  const { journeysPage1, journeysPage2 } = resources;
21
21
  mock.onGet(journeysPage1.url).reply(journeysPage1.status, journeysPage1.response);
@@ -28,7 +28,7 @@ describe('rest', () => {
28
28
  assert.lengthOf(mock.history.get, 2);
29
29
  return;
30
30
  });
31
- it('GET: should return 5 journey items', async () => {
31
+ it('GET: should return 5 journey items', async function () {
32
32
  //given
33
33
  const { journeysPage1 } = resources;
34
34
  mock.onGet(journeysPage1.url).reply(journeysPage1.status, journeysPage1.response);
@@ -42,7 +42,7 @@ describe('rest', () => {
42
42
  assert.lengthOf(mock.history.get, 1);
43
43
  return;
44
44
  });
45
- it('GETCOLLECTION: should return 2 identical payloads', async () => {
45
+ it('GETCOLLECTION: should return 2 identical payloads', async function () {
46
46
  //given
47
47
  const { journeysPage1 } = resources;
48
48
  mock.onGet(journeysPage1.url).reply(journeysPage1.status, journeysPage1.response);
@@ -58,7 +58,7 @@ describe('rest', () => {
58
58
  assert.lengthOf(mock.history.get, 2);
59
59
  return;
60
60
  });
61
- it('POST: should create Event Definition', async () => {
61
+ it('POST: should create Event Definition', async function () {
62
62
  //given
63
63
  const { eventcreate } = resources;
64
64
  mock.onPost(eventcreate.url).reply(eventcreate.status, eventcreate.response);
@@ -91,7 +91,23 @@ describe('rest', () => {
91
91
  assert.lengthOf(mock.history.post, 2);
92
92
  return;
93
93
  });
94
- it('PUT: should update Event Definition', async () => {
94
+ it('POST: should add an entry to a Data Extension', async function () {
95
+ //given
96
+ const { dataExtensionUpsert } = resources;
97
+ mock.onPost(dataExtensionUpsert.url).reply(
98
+ dataExtensionUpsert.status,
99
+ dataExtensionUpsert.response
100
+ );
101
+ // when
102
+ const payload = await defaultSdk().rest.post('hub/v1/dataevents/key:key/rowset', [
103
+ { keys: { primaryKey: 1 }, values: { name: 'test' } },
104
+ ]);
105
+ // then
106
+ assert.deepEqual(payload, dataExtensionUpsert.response);
107
+ assert.lengthOf(mock.history.post, 2);
108
+ return;
109
+ });
110
+ it('PUT: should update Event Definition', async function () {
95
111
  //given
96
112
  const { eventupdate } = resources;
97
113
  mock.onPut(eventupdate.url).reply(eventupdate.status, eventupdate.response);
@@ -114,7 +130,7 @@ describe('rest', () => {
114
130
  assert.lengthOf(mock.history.put, 1);
115
131
  return;
116
132
  });
117
- it('PATCH: should update Contact', async () => {
133
+ it('PATCH: should update Contact', async function () {
118
134
  //given
119
135
  const { contactPatch } = resources;
120
136
  mock.onPatch(contactPatch.url).reply(contactPatch.status, contactPatch.response);
@@ -164,7 +180,7 @@ describe('rest', () => {
164
180
  assert.lengthOf(mock.history.patch, 1);
165
181
  return;
166
182
  });
167
- it('DELETE: should delete Campaign', async () => {
183
+ it('DELETE: should delete Campaign', async function () {
168
184
  //given
169
185
  const { campaignDelete } = resources;
170
186
  mock.onDelete(campaignDelete.url).reply(campaignDelete.status, campaignDelete.response);
@@ -176,7 +192,7 @@ describe('rest', () => {
176
192
  assert.lengthOf(mock.history.delete, 1);
177
193
  return;
178
194
  });
179
- it('should retry auth one time on first failure then work', async () => {
195
+ it('should retry auth one time on first failure then work', async function () {
180
196
  //given
181
197
  mock.reset(); // needed to avoid before hook being used
182
198
  const { expired, success } = authResources;
@@ -195,7 +211,7 @@ describe('rest', () => {
195
211
  assert.lengthOf(mock.history.delete, 1);
196
212
  return;
197
213
  });
198
- it('should retry auth one time on first failure then fail', async () => {
214
+ it('should retry auth one time on first failure then fail', async function () {
199
215
  //given
200
216
  mock.reset(); // needed to avoid before hook being used
201
217
  const { unauthorized } = authResources;
@@ -214,7 +230,7 @@ describe('rest', () => {
214
230
  }
215
231
  return;
216
232
  });
217
- it('should fail to delete campaign', async () => {
233
+ it('should fail to delete campaign', async function () {
218
234
  //given
219
235
  const { campaignFailed } = resources;
220
236
  mock.onDelete(campaignFailed.url).reply(campaignFailed.status, campaignFailed.response);
@@ -233,7 +249,7 @@ describe('rest', () => {
233
249
  assert.lengthOf(mock.history.delete, 1);
234
250
  return;
235
251
  });
236
- it('RETRY: should return 5 journey items, after a connection error', async () => {
252
+ it('RETRY: should return 5 journey items, after a connection error', async function () {
237
253
  //given
238
254
  const { journeysPage1 } = resources;
239
255
  mock.onGet(journeysPage1.url)
@@ -250,7 +266,7 @@ describe('rest', () => {
250
266
  assert.lengthOf(mock.history.get, 2);
251
267
  return;
252
268
  });
253
- it('FAILED RETRY: should return error, after 2 connection errors', async () => {
269
+ it('FAILED RETRY: should return error, after 2 connection timeout errors', async function () {
254
270
  //given
255
271
  const { journeysPage1 } = resources;
256
272
  mock.onGet(journeysPage1.url).timeout();
@@ -267,4 +283,75 @@ describe('rest', () => {
267
283
 
268
284
  return;
269
285
  });
286
+ it('FAILED RETRY: should return error, after 2 ECONNRESET errors', async function () {
287
+ //given
288
+ const { journeysPage1 } = resources;
289
+
290
+ mock.onGet(journeysPage1.url).reply(() => {
291
+ const connectionError = new Error();
292
+ connectionError.code = 'ECONNRESET';
293
+ throw connectionError;
294
+ });
295
+ // when
296
+ try {
297
+ await defaultSdk().rest.get('interaction/v1/interactions?$pageSize=5&$page=1');
298
+ assert.fail();
299
+ } catch (ex) {
300
+ // then
301
+ assert.isTrue(isConnectionError(ex.code));
302
+ }
303
+ assert.lengthOf(mock.history.post, 1);
304
+ assert.lengthOf(mock.history.get, 2);
305
+
306
+ return;
307
+ });
308
+ it('LogRequest & Response: should run middleware for logging ', async function () {
309
+ //given
310
+ const { journeysPage1 } = resources;
311
+ mock.onGet(journeysPage1.url).reply(journeysPage1.status, journeysPage1.response);
312
+ // when
313
+ let expectedRequest;
314
+ let expectedResponse;
315
+ const sdk = new SDK(
316
+ {
317
+ client_id: 'XXXXX',
318
+ client_secret: 'YYYYYY',
319
+ auth_url: 'https://mct0l7nxfq2r988t1kxfy8sc47ma.auth.marketingcloudapis.com/',
320
+ account_id: 1111111,
321
+ },
322
+ {
323
+ eventHandlers: {
324
+ logRequest: (reqObj) => {
325
+ expectedRequest = reqObj;
326
+ },
327
+
328
+ logResponse: (resObj) => {
329
+ expectedResponse = resObj;
330
+ },
331
+ onConnectionError: () => {
332
+ return;
333
+ },
334
+ },
335
+ retryOnConnectionError: true,
336
+ requestAttempts: 2,
337
+ }
338
+ );
339
+ // when
340
+ await sdk.rest.get('interaction/v1/interactions?$pageSize=5&$page=1');
341
+ // then
342
+ assert.deepEqual(
343
+ {
344
+ method: 'GET',
345
+ url: 'interaction/v1/interactions?$pageSize=5&$page=1',
346
+ baseURL: 'https://mct0l7nxfq2r988t1kxfy8sc47ma.rest.marketingcloudapis.com/',
347
+ headers: {
348
+ Authorization: 'Bearer TESTTOKEN',
349
+ },
350
+ },
351
+ expectedRequest
352
+ );
353
+ assert.equal(200, expectedResponse.status);
354
+ assert.equal(5, expectedResponse.data.items.length);
355
+ return;
356
+ });
270
357
  });
package/test/soap.test.js CHANGED
@@ -19,18 +19,18 @@ const addHandler = (metadata) => {
19
19
  ).reply(metadata.status, metadata.response);
20
20
  };
21
21
 
22
- describe('soap', () => {
23
- beforeEach(() => {
22
+ describe('soap', function () {
23
+ beforeEach(function () {
24
24
  mock.onPost(authResources.success.url).reply(
25
25
  authResources.success.status,
26
26
  authResources.success.response
27
27
  );
28
28
  });
29
- afterEach(() => {
29
+ afterEach(function () {
30
30
  mock.reset();
31
31
  });
32
32
 
33
- it('retrieve: should return 1 data extension', async () => {
33
+ it('retrieve: should return 1 data extension', async function () {
34
34
  //given
35
35
  addHandler(resources.retrieveDataExtension);
36
36
  // when
@@ -56,7 +56,7 @@ describe('soap', () => {
56
56
  assert.lengthOf(mock.history.post, 2);
57
57
  return;
58
58
  });
59
- it('retrieveBulk: should return 2 data extensions', async () => {
59
+ it('retrieveBulk: should return 2 data extensions', async function () {
60
60
  //given
61
61
  mock.onPost(
62
62
  '/Service.asmx',
@@ -102,7 +102,7 @@ describe('soap', () => {
102
102
  assert.lengthOf(mock.history.post, 3);
103
103
  return;
104
104
  });
105
- it('failed: should fail to create 1 subscriber', async () => {
105
+ it('failed: should fail to create 1 subscriber', async function () {
106
106
  //given
107
107
  addHandler(resources.subscriberFailed);
108
108
  // when
@@ -122,13 +122,13 @@ describe('soap', () => {
122
122
  // then
123
123
  assert.fail();
124
124
  } catch (ex) {
125
- assert.deepEqual(ex.JSON, resources.subscriberFailed.parsed);
125
+ assert.deepEqual(ex.json, resources.subscriberFailed.parsed);
126
126
  assert.lengthOf(mock.history.post, 2);
127
127
  }
128
128
 
129
129
  return;
130
130
  });
131
- it('create: should create 1 subscriber', async () => {
131
+ it('create: should create 1 subscriber', async function () {
132
132
  //given
133
133
  addHandler(resources.subscriberCreated);
134
134
  // when
@@ -151,7 +151,7 @@ describe('soap', () => {
151
151
 
152
152
  return;
153
153
  });
154
- it('update: should update 1 subscriber', async () => {
154
+ it('update: should update 1 subscriber', async function () {
155
155
  //given
156
156
  addHandler(resources.subscriberUpdated);
157
157
  // when
@@ -174,7 +174,7 @@ describe('soap', () => {
174
174
 
175
175
  return;
176
176
  });
177
- it('expired: should return an error of expired token', async () => {
177
+ it('expired: should return an error of expired token', async function () {
178
178
  //given
179
179
  addHandler(resources.expiredToken);
180
180
  // when
@@ -185,13 +185,14 @@ describe('soap', () => {
185
185
  });
186
186
  } catch (ex) {
187
187
  // then
188
- assert.equal(ex.errorMessage, 'Token Expired');
188
+ assert.equal(ex.message, 'Token Expired');
189
189
  assert.lengthOf(mock.history.post, 4);
190
+
190
191
  return;
191
192
  }
192
193
  assert.fail();
193
194
  });
194
- it('bad Request: should return an error of bad request', async () => {
195
+ it('bad Request: should return an error of bad request', async function () {
195
196
  //given
196
197
  addHandler(resources.badRequest);
197
198
  // when
@@ -207,7 +208,7 @@ describe('soap', () => {
207
208
  }
208
209
  assert.fail();
209
210
  });
210
- it('Delete: should delete a subscriber', async () => {
211
+ it('Delete: should delete a subscriber', async function () {
211
212
  //given
212
213
  addHandler(resources.subscriberDeleted);
213
214
  // when
@@ -219,7 +220,7 @@ describe('soap', () => {
219
220
  assert.lengthOf(mock.history.post, 2);
220
221
  return;
221
222
  });
222
- it('Describe: should describe the subscriber type', async () => {
223
+ it('Describe: should describe the subscriber type', async function () {
223
224
  //given
224
225
  addHandler(resources.subscriberDescribed);
225
226
  // when
@@ -229,7 +230,7 @@ describe('soap', () => {
229
230
  assert.lengthOf(mock.history.post, 2);
230
231
  return;
231
232
  });
232
- it('Execute: should unsubscribe subscriber', async () => {
233
+ it('Execute: should unsubscribe subscriber', async function () {
233
234
  //given
234
235
  addHandler(resources.subscribeUnsub);
235
236
  // when
@@ -242,7 +243,7 @@ describe('soap', () => {
242
243
  assert.lengthOf(mock.history.post, 2);
243
244
  return;
244
245
  });
245
- it('Perform: should unsubscribe subscriber', async () => {
246
+ it('Perform: should unsubscribe subscriber', async function () {
246
247
  //given
247
248
  addHandler(resources.queryPerform);
248
249
  // when
@@ -254,7 +255,7 @@ describe('soap', () => {
254
255
  assert.lengthOf(mock.history.post, 2);
255
256
  return;
256
257
  });
257
- it('Schedule: should schedule an Automation', async () => {
258
+ it('Schedule: should schedule an Automation', async function () {
258
259
  //given
259
260
  addHandler(resources.automationSchedule);
260
261
  // when
@@ -279,7 +280,7 @@ describe('soap', () => {
279
280
  assert.lengthOf(mock.history.post, 2);
280
281
  return;
281
282
  });
282
- it('RETRY: should return 1 data extension, after a connection error', async () => {
283
+ it('RETRY: should return 1 data extension, after a connection error', async function () {
283
284
  //given
284
285
 
285
286
  mock.onPost('/Service.asmx')
@@ -322,7 +323,7 @@ describe('soap', () => {
322
323
  assert.lengthOf(mock.history.post, 3);
323
324
  return;
324
325
  });
325
- it('FAILED RETRY: should return error, after multiple connection error', async () => {
326
+ it('FAILED RETRY: should return error, after multiple connection error', async function () {
326
327
  //given
327
328
 
328
329
  mock.onPost('/Service.asmx').timeout();
package/test/utils.js CHANGED
@@ -113,7 +113,7 @@ exports.defaultSdk = () => {
113
113
  },
114
114
  },
115
115
  retryOnConnectionError: true,
116
- requestAttempts: 1,
116
+ requestAttempts: 2,
117
117
  }
118
118
  );
119
119
  };