Skip to content

Commit 98415e1

Browse files
authored
[Feat][DEVX-634]: Support Async TokenCache Store (#1126)
* feat(sdk-client-v3): support async token cache storage - make the token cache assessors async - update related type definitions to match new function signature - pass tokenCacheKey as second param to the set function - add unit test to validate functinality * chore(changesets): add release changeset - add release changeset
1 parent 023d7c2 commit 98415e1

File tree

7 files changed

+144
-27
lines changed

7 files changed

+144
-27
lines changed

.changeset/little-moons-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@commercetools/ts-client': major
3+
---
4+
5+
Add support for async token cache stores

packages/platform-sdk/test/integration-tests/sdk-v3/error-cases.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ describe('testing error cases', () => {
8282
clientSecret: 'test',
8383
},
8484
tokenCache: {
85-
set: (cache) => {},
86-
get: (tokenCacheKey) => {
85+
set: async (cache) => {},
86+
get: async (tokenCacheKey) => {
8787
return {
8888
token: 'test',
8989
expirationTime: 9934431427363,
@@ -175,12 +175,12 @@ describe('testing error cases', () => {
175175
}
176176
)
177177

178-
expect(tokenCache.get().expirationTime).toEqual(expirationTime)
179-
expect(tokenCache.get().token).toEqual('x-invalid-token')
178+
expect((await tokenCache.get()).expirationTime).toEqual(expirationTime)
179+
expect((await tokenCache.get()).token).toEqual('x-invalid-token')
180180

181181
await apiRootV3.get().execute()
182-
expect(tokenCache.get().expirationTime).toBeGreaterThan(-1)
183-
expect(tokenCache.get().token).not.toEqual('x-invalid-token')
182+
expect((await tokenCache.get()).expirationTime).toBeGreaterThan(-1)
183+
expect((await tokenCache.get()).token).not.toEqual('x-invalid-token')
184184
})
185185

186186
it('should simulate concurrent token fetch request', async () => {
@@ -210,16 +210,16 @@ describe('testing error cases', () => {
210210
}
211211
)
212212

213-
expect(tokenCache.get().expirationTime).toEqual(expirationTime)
214-
expect(tokenCache.get().token).toEqual('an-expired-token')
213+
expect((await tokenCache.get()).expirationTime).toEqual(expirationTime)
214+
expect((await tokenCache.get()).token).toEqual('an-expired-token')
215215

216216
const fetch1 = apiRootV3.get().execute()
217217
const fetch2 = apiRootV3.get().execute()
218218
const fetch3 = apiRootV3.get().execute()
219219
await Promise.all([fetch1, fetch2, fetch3])
220220

221-
expect(tokenCache.get().expirationTime).toBeGreaterThan(-1)
222-
expect(tokenCache.get().token).not.toEqual('an-expired-token')
221+
expect((await tokenCache.get()).expirationTime).toBeGreaterThan(-1)
222+
expect((await tokenCache.get()).token).not.toEqual('an-expired-token')
223223
})
224224

225225
it('should move to next middleware for non error response', async () => {

packages/sdk-client-v3/src/middleware/auth-middleware/auth-request-executor.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
executeRequestOptions,
2+
ExecuteRequestOptions,
33
IBuiltRequestParams,
44
TokenInfo,
55
} from '../../types/types'
@@ -11,13 +11,14 @@ import {
1111
} from '../../utils'
1212
import { buildRequestForRefreshTokenFlow } from './auth-request-builder'
1313

14-
export async function executeRequest(options: executeRequestOptions) {
14+
export async function executeRequest(options: ExecuteRequestOptions) {
1515
const {
1616
httpClient,
1717
httpClientOptions,
1818
tokenCache,
1919
userOption,
2020
tokenCacheObject,
21+
tokenCacheKey,
2122
} = options
2223

2324
let url = options.url
@@ -83,7 +84,8 @@ export async function executeRequest(options: executeRequestOptions) {
8384
const expirationTime = calculateExpirationTime(expiresIn)
8485

8586
// cache new generated token, refreshToken and expiration time
86-
tokenCache.set({ token, expirationTime, refreshToken })
87+
const cache = { token, expirationTime, refreshToken }
88+
await tokenCache.set(cache, tokenCacheKey)
8789
return Promise.resolve(true)
8890
}
8991

packages/sdk-client-v3/src/middleware/auth-middleware/auth-request-processor.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export async function authProcessor<T extends AuthMiddlewareOptions>(
2525
const requestOptions = {
2626
request,
2727
tokenCache,
28+
tokenCacheKey,
2829
httpClient: options.httpClient || fetch,
2930
httpClientOptions: options.httpClientOptions,
3031
...builder(options),
@@ -45,7 +46,7 @@ export async function authProcessor<T extends AuthMiddlewareOptions>(
4546
await tokenFetchPromise
4647
tokenFetchPromise = null
4748

48-
tokenCacheObject = tokenCache.get(tokenCacheKey)
49+
tokenCacheObject = await tokenCache.get(tokenCacheKey)
4950
_response = await next(mergeAuthHeader(tokenCacheObject.token, request))
5051
}
5152

@@ -64,7 +65,7 @@ export async function authProcessor<T extends AuthMiddlewareOptions>(
6465
* If there is a token in the tokenCache, and it's not
6566
* expired, append the token in the `Authorization` header.
6667
*/
67-
tokenCacheObject = tokenCache.get(tokenCacheKey)
68+
tokenCacheObject = await tokenCache.get(tokenCacheKey)
6869
if (
6970
tokenCacheObject &&
7071
tokenCacheObject.token &&
@@ -86,6 +87,6 @@ export async function authProcessor<T extends AuthMiddlewareOptions>(
8687
}
8788

8889
// Now the token is present in the tokenCache and can be accessed
89-
tokenCacheObject = tokenCache.get(tokenCacheKey)
90+
tokenCacheObject = await tokenCache.get(tokenCacheKey)
9091
return next(mergeAuthHeader(tokenCacheObject.token, request))
9192
}

packages/sdk-client-v3/src/types/types.d.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ export type TokenStore = {
119119
}
120120

121121
export type TokenCache = {
122-
get: (tokenCacheOptions?: TokenCacheOptions) => TokenStore
123-
set: (cache: TokenStore, tokenCacheOptions?: TokenCacheOptions) => void
122+
get: (tokenCacheOptions?: TokenCacheOptions) => Promise<TokenStore>
123+
set: (cache: TokenStore, tokenCacheOptions?: TokenCacheOptions) => Promise<void>
124124
}
125125

126126
export type IBuiltRequestParams = {
@@ -146,7 +146,7 @@ export type RefreshAuthMiddlewareOptions = {
146146
}
147147

148148
/* Request */
149-
type requestBaseOptions = {
149+
type RequestBaseOptions = {
150150
url: string
151151
body: string
152152
basicAuth: string
@@ -157,13 +157,13 @@ type requestBaseOptions = {
157157
httpClientOptions?: object
158158
}
159159

160-
export type executeRequestOptions = requestBaseOptions & {
160+
export type ExecuteRequestOptions = RequestBaseOptions & {
161161
next: Next
162162
httpClient?: Function
163163
userOption?: AuthMiddlewareOptions | PasswordAuthMiddlewareOptions
164164
}
165165

166-
export type AuthMiddlewareBaseOptions = requestBaseOptions & {
166+
export type AuthMiddlewareBaseOptions = RequestBaseOptions & {
167167
request: MiddlewareRequest
168168
httpClient?: Function
169169
}

packages/sdk-client-v3/tests/auth/auth-request-executor.test.ts

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import fs from 'node:fs'
2+
import path from 'node:path'
3+
14
import { ClientResult } from '../../src'
25
import { executeRequest } from '../../src/middleware/auth-middleware/auth-request-executor'
36

4-
type U<T> = {
7+
type U<T, K> = {
58
get(): T
6-
set(value: T): void
9+
set(value: T, key: K): void
710
}
811

912
function createTestExecutorOptions(options) {
@@ -30,10 +33,10 @@ function createTestExecutorOptions(options) {
3033
}
3134
}
3235

33-
function cache<T = ClientResult>(): U<T> {
36+
function cache<T = ClientResult, K = object>(): U<T, K> {
3437
let value: T
3538
return {
36-
set: jest.fn((response: T) => {
39+
set: jest.fn((response: T, key: K) => {
3740
value = response
3841
}),
3942
get: jest.fn(() => {
@@ -126,6 +129,51 @@ describe('Auth request executor', () => {
126129
'refresh-access-token'
127130
)
128131
})
132+
133+
test('should call the tokenStore `set` method with both the cache and cacheKey', async () => {
134+
const tokenCacheKey = {
135+
host: 'test-host',
136+
projectKey: 'test-project-key',
137+
clientId: 'test-client-id',
138+
}
139+
140+
const options = createTestExecutorOptions({
141+
url: 'test-demo-uri',
142+
tokenCache: {
143+
...cache(),
144+
async set(cache, key) {
145+
// cache assertions
146+
expect(cache).toBeDefined()
147+
expect(typeof cache).toEqual('object')
148+
149+
// tokenCacheKey assertions
150+
expect(key).toBeDefined()
151+
expect(typeof key).toEqual('object')
152+
expect(key.host).toEqual(tokenCacheKey.host)
153+
expect(key.projectKey).toEqual(tokenCacheKey.projectKey)
154+
expect(key.clientId).toEqual(tokenCacheKey.clientId)
155+
},
156+
},
157+
tokenCacheKey,
158+
tokenCacheObject: {
159+
token: 'test-cached-token',
160+
expirationTime: Date.now() - 999,
161+
refreshToken: 'refresh-cache-token',
162+
},
163+
httpClient: jest.fn(() => ({
164+
statusCode: 200,
165+
data: {
166+
statusCode: 200,
167+
access_token: 'test-access-token',
168+
refresh_token: 'refresh-access-token',
169+
},
170+
})),
171+
})
172+
173+
const response = await executeRequest(options)
174+
expect(typeof response).toEqual('boolean')
175+
expect(response).toEqual(true)
176+
})
129177
})
130178

131179
describe('Auth request executor - rejected response', () => {
@@ -268,4 +316,65 @@ describe('Auth request executor', () => {
268316
)
269317
})
270318
})
319+
320+
describe('Async TokenStore accessor functions', () => {
321+
const file = path.join(__dirname, './cache.json')
322+
afterAll(() => {
323+
// unlink the file
324+
fs.promises.unlink(file)
325+
})
326+
327+
async function get() {
328+
try {
329+
const json = await fs.promises.readFile(file, { encoding: 'utf-8' })
330+
return JSON.parse(json)
331+
} catch (e) {
332+
/** noop */
333+
}
334+
}
335+
336+
async function set(val: object, key: object) {
337+
try {
338+
await fs.promises.writeFile(file, JSON.stringify(val))
339+
} catch (e) {
340+
/** noop */
341+
}
342+
}
343+
344+
test('Should test async behaviour of the tokenStore', async () => {
345+
const data = {
346+
statusCode: 200,
347+
access_token: 'async-test-access-token',
348+
refresh_token: 'async-refresh-access-token',
349+
}
350+
351+
const _cache = () => {
352+
return { set, get }
353+
}
354+
355+
const options = createTestExecutorOptions({
356+
url: 'async-test-demo-uri',
357+
tokenCache: _cache(),
358+
tokenCacheKey: {
359+
host: 'async-test-host',
360+
projectKey: 'async-test-project-key',
361+
clientId: 'async-test-client-id',
362+
},
363+
httpClient: jest.fn(() => ({
364+
statusCode: 200,
365+
data,
366+
})),
367+
})
368+
369+
const response = await executeRequest(options)
370+
expect(typeof response).toEqual('boolean')
371+
expect(response).toEqual(true)
372+
373+
const store = await _cache().get()
374+
375+
expect(typeof store).toEqual('object')
376+
expect(store.token).toEqual(data.access_token)
377+
expect(store.refreshToken).toEqual(data.refresh_token)
378+
})
379+
})
271380
})

packages/sdk-client-v3/tests/auth/auth-request-processor.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ describe('auth-request-processor', () => {
3636
host: '',
3737
},
3838
{
39-
set: () => {},
40-
get: () => ({
39+
set: async () => {},
40+
get: async () => ({
4141
token: 'token',
4242
expirationTime: 99999,
4343
}),

0 commit comments

Comments
 (0)