Skip to content
This repository was archived by the owner on Jul 24, 2024. It is now read-only.

Commit 676510f

Browse files
feat: Added 401 fallback to re-generate access_token (#853)
* feat: current changes * feat: updated request retry code * feat: limited auth to 401's only * feat: cleaned up code * feat: moved timeout into seperate function * feat: further improved code * feat: using bind on function * feat: access_token now being regenerated * feat: fixed bug * feat: fixed es-lint errors * feat: testing 429 * test: request 401 re auth test (#857) Co-authored-by: Robert Field <[email protected]> * feat: fixed failing 404 test * feat: removed test button * feat: updated errors to not silently fail * feat: updated error handling --------- Co-authored-by: Robert Field <[email protected]>
1 parent 694ade8 commit 676510f

File tree

7 files changed

+192
-30
lines changed

7 files changed

+192
-30
lines changed

examples/index.html

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
<!DOCTYPE html>
22
<html lang="en">
3-
43
<head>
5-
<meta charset="UTF-8">
6-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7-
<meta http-equiv="X-UA-Compatible" content="ie=edge">
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
87
<title>Moltin Example</title>
98
</head>
109

1110
<body>
1211
<div id="products"></div>
1312
<script src="/moltin.js"></script>
1413
<script charset="utf-8">
15-
1614
const Moltin = moltin.gateway({
1715
client_id: 'j6hSilXRQfxKohTndUuVrErLcSJWP15P347L6Im0M4'
1816
})
@@ -21,5 +19,4 @@
2119
Moltin.Products.All().then(({ data }) => console.log(data))
2220
</script>
2321
</body>
24-
2522
</html>

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
"url": "https://github.com/moltin/js-sdk/issues"
4545
},
4646
"devDependencies": {
47-
"babel-plugin-rewire": "^1.2.0",
4847
"@babel/core": "7.21.3",
4948
"@babel/eslint-parser": "7.21.3",
5049
"@babel/preset-env": "7.20.2",
@@ -54,9 +53,12 @@
5453
"@rollup/plugin-json": "4.1.0",
5554
"@rollup/plugin-node-resolve": "13.3.0",
5655
"@semantic-release/exec": "^6.0.3",
56+
"@sinonjs/fake-timers": "^11.1.0",
5757
"@types/chai": "4.3.4",
5858
"@types/mocha": "9.1.1",
5959
"@types/node": "18.0.6",
60+
"@types/sinonjs__fake-timers": "^8.1.2",
61+
"babel-plugin-rewire": "^1.2.0",
6062
"chai": "4.3.7",
6163
"commitizen": "4.2.5",
6264
"core-js": "3.29.1",

src/factories/request.js

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {
44
resetProps,
55
tokenInvalid,
66
getCredentials,
7-
isNode, resolveCredentialsStorageKey
7+
isNode,
8+
resolveCredentialsStorageKey
89
} from '../utils/helpers'
910

1011
const createAuthRequest = config => {
@@ -57,10 +58,34 @@ const fetchRetry = (
5758
version,
5859
headers,
5960
requestBody,
61+
authenticate,
6062
attempt = 1
6163
) =>
6264
new Promise((resolve, reject) => {
6365
const ver = version || config.version
66+
const updatedHeaders = headers
67+
68+
function retryTimeout(access_token) {
69+
if (access_token) {
70+
updatedHeaders.Authorization = `Bearer ${access_token}`
71+
}
72+
73+
setTimeout(() => {
74+
fetchRetry(
75+
config,
76+
uri,
77+
method,
78+
version,
79+
updatedHeaders,
80+
requestBody,
81+
authenticate,
82+
attempt + 1
83+
)
84+
.then(result => resolve(result))
85+
.catch(error => reject(error))
86+
}, attempt * config.retryDelay + Math.floor(Math.random() * config.retryJitter))
87+
}
88+
6489
config.auth.fetch
6590
.bind()(
6691
`${config.protocol}://${config.host}${ver ? `/${ver}` : ''}/${uri}`,
@@ -75,23 +100,16 @@ const fetchRetry = (
75100
if (response.ok) {
76101
resolve(response.json)
77102
}
78-
if (attempt < config.fetchMaxAttempts && response.status === 429) {
79-
setTimeout(
80-
() =>
81-
fetchRetry(
82-
config,
83-
uri,
84-
method,
85-
version,
86-
headers,
87-
requestBody,
88-
attempt + 1
89-
)
90-
.then(result => resolve(result))
91-
.catch(error => reject(error)),
92-
attempt * config.retryDelay +
93-
Math.floor(Math.random() * config.retryJitter)
94-
)
103+
if (attempt < config.fetchMaxAttempts) {
104+
if (response.status === 401 && config.reauth) {
105+
authenticate()
106+
.then(data => retryTimeout(data.access_token))
107+
.catch(error => reject(error))
108+
} else if (response.status === 429) {
109+
retryTimeout()
110+
} else {
111+
reject(response.json)
112+
}
95113
} else {
96114
reject(response.json)
97115
}
@@ -133,7 +151,10 @@ class RequestFactory {
133151
...(refresh_token && { refresh_token })
134152
}
135153

136-
storage.set(resolveCredentialsStorageKey(config.name), JSON.stringify(credentials))
154+
storage.set(
155+
resolveCredentialsStorageKey(config.name),
156+
JSON.stringify(credentials)
157+
)
137158
}
138159
}
139160
)
@@ -154,7 +175,7 @@ class RequestFactory {
154175
) {
155176
const { config, storage } = this
156177

157-
const storageKey = resolveCredentialsStorageKey(config.name);
178+
const storageKey = resolveCredentialsStorageKey(config.name)
158179
const credentials = getCredentials(storage, storageKey)
159180

160181
const req = cred => {
@@ -214,11 +235,22 @@ class RequestFactory {
214235
return wrapBody ? buildRequestBody(body) : JSON.stringify(body)
215236
}
216237

217-
return fetchRetry(config, uri, method, version, headers, requestBody)
238+
return fetchRetry(
239+
config,
240+
uri,
241+
method,
242+
version,
243+
headers,
244+
requestBody,
245+
this.authenticate.bind(this),
246+
1
247+
)
218248
}
219249

220250
if (tokenInvalid(config) && config.reauth && !config.store_id) {
221-
return this.authenticate().then(() => req(getCredentials(storage, storageKey)))
251+
return this.authenticate().then(() =>
252+
req(getCredentials(storage, storageKey))
253+
)
222254
}
223255

224256
if (instance) resetProps(instance)

test/factories.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,3 +1067,49 @@ export const applicationKeysArray = [
10671067
}
10681068
}
10691069
]
1070+
1071+
export const shopperCatalogProductResponse = {
1072+
data: {
1073+
id: '072541c8-1558-440b-979e-05b7880128fa',
1074+
type: 'product',
1075+
attributes: {
1076+
base_product: false,
1077+
commodity_type: 'physical',
1078+
created_at: '2023-05-17T11:21:02.014Z',
1079+
manage_stock: false,
1080+
name: 'Playstation 5 Controller',
1081+
price: {
1082+
USD: {
1083+
amount: 5000,
1084+
includes_tax: false
1085+
}
1086+
},
1087+
sku: 'ps5-controller',
1088+
slug: 'playstation-5-controller',
1089+
status: 'live',
1090+
updated_at: '2023-05-30T17:00:04.994Z',
1091+
published_at: '2023-05-31T10:21:28.184Z'
1092+
},
1093+
meta: {
1094+
catalog_id: 'bb06b811-95db-420d-a7ef-810eee1bb343',
1095+
catalog_source: 'pim',
1096+
pricebook_id: 'fe4a40ab-4bd1-4678-b94c-be799cbd58ac',
1097+
bread_crumb_nodes: ['50059701-ec6d-4829-b6d4-65831b98855e'],
1098+
bread_crumbs: {
1099+
'50059701-ec6d-4829-b6d4-65831b98855e': [
1100+
'06d5d325-487d-411d-9504-4bc8ca3b677b'
1101+
]
1102+
},
1103+
display_price: {
1104+
without_tax: {
1105+
amount: 5000,
1106+
currency: 'USD',
1107+
formatted: '$50.00'
1108+
}
1109+
}
1110+
}
1111+
},
1112+
links: {
1113+
self: '/catalog/products/072541c8-1558-440b-979e-05b7880128fa'
1114+
}
1115+
}

test/factories/request.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { assert } from 'chai'
2+
import nock from 'nock'
3+
import fakeTimers from '@sinonjs/fake-timers'
4+
import { gateway as MoltinGateway } from '../../src/moltin'
5+
import { shopperCatalogProductResponse } from '../factories'
6+
7+
const apiUrl = 'https://euwest.api.elasticpath.com'
8+
9+
describe('Moltin request', () => {
10+
const Moltin = MoltinGateway({
11+
client_id: 'XXX'
12+
})
13+
14+
let clock: fakeTimers.InstalledClock | undefined
15+
16+
beforeEach(() => {
17+
clock = fakeTimers.install({
18+
shouldAdvanceTime: true,
19+
advanceTimeDelta: 1
20+
})
21+
})
22+
23+
afterEach(() => {
24+
clock?.uninstall()
25+
})
26+
27+
it('Moltin request when 401 response should attempt to re-authenticate', () => {
28+
// Intercept the API request
29+
nock(apiUrl, {
30+
reqheaders: {
31+
Authorization: 'Bearer a550d8cbd4a4627013452359ab69694cd446615a'
32+
}
33+
})
34+
.post('/oauth/access_token')
35+
.reply(200, {
36+
token_type: 'Bearer',
37+
expires_in: 9999999999,
38+
expires: 9999999999,
39+
identifier: 'implicit',
40+
access_token: 'a550d8cbd4a4627013452359ab69694cd446615a'
41+
})
42+
43+
nock(apiUrl, {
44+
reqheaders: {
45+
Authorization: 'Bearer a550d8cbd4a4627013452359ab69694cd446615a'
46+
}
47+
})
48+
.get('/catalog/products/1')
49+
.reply(401, {
50+
errors: {
51+
title: 'Unauthorized',
52+
status: 401
53+
}
54+
})
55+
.get('/catalog/products/1')
56+
.reply(200, shopperCatalogProductResponse)
57+
58+
return Moltin.ShopperCatalog.Products.Get({ productId: '1' }).then(
59+
response => {
60+
assert.equal(response.data.attributes.name, 'Playstation 5 Controller')
61+
}
62+
)
63+
})
64+
})

test/tests.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,5 @@ require('./unit/one-time-password-token-request')
4242
require('./utils/helpers')
4343
require('./utils/throttle')
4444
require('./utils/configFetch')
45+
46+
require('./factories/request')

yarn.lock

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1561,6 +1561,20 @@
15611561
lodash "^4.17.4"
15621562
parse-json "^5.0.0"
15631563

1564+
"@sinonjs/commons@^3.0.0":
1565+
version "3.0.0"
1566+
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72"
1567+
integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==
1568+
dependencies:
1569+
type-detect "4.0.8"
1570+
1571+
"@sinonjs/fake-timers@^11.1.0":
1572+
version "11.1.0"
1573+
resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-11.1.0.tgz#5ad7b44514a61bbd04a3ddec863e21edd6efc2da"
1574+
integrity sha512-pUBaWhXoa9N0R/LeYKLqkrN9mqN3jwKBeMfbvlRtHUzLmk55o+0swncIuZBcSH/PpXDttRf/AcPF22pknAzORQ==
1575+
dependencies:
1576+
"@sinonjs/commons" "^3.0.0"
1577+
15641578
"@tootallnate/once@1":
15651579
version "1.1.2"
15661580
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@@ -1633,6 +1647,11 @@
16331647
dependencies:
16341648
"@types/node" "*"
16351649

1650+
"@types/sinonjs__fake-timers@^8.1.2":
1651+
version "8.1.2"
1652+
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz#bf2e02a3dbd4aecaf95942ecd99b7402e03fad5e"
1653+
integrity sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==
1654+
16361655
16371656
version "1.1.2"
16381657
resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
@@ -6798,7 +6817,7 @@ type-check@^0.4.0, type-check@~0.4.0:
67986817
dependencies:
67996818
prelude-ls "^1.2.1"
68006819

6801-
type-detect@^4.0.0, type-detect@^4.0.5:
6820+
type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5:
68026821
version "4.0.8"
68036822
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
68046823
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==

0 commit comments

Comments
 (0)