Skip to content

Commit 01a6aa2

Browse files
committed
fix: decouple axios version from core SDK
1 parent bdd6bcc commit 01a6aa2

File tree

5 files changed

+191
-129
lines changed

5 files changed

+191
-129
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@smartthings/core-sdk": patch
3+
---
4+
5+
Decouple from axios. Dependents of the core SDK no longer need to use the same version of
6+
axios, or even use axios at all.

src/authenticator.ts

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'
22
import { MutexInterface } from 'async-mutex'
33

4-
import { EndpointClientConfig } from './endpoint-client'
4+
import { EndpointClientConfig, HttpClientHeaders } from './endpoint-client'
55

66

77
/**
@@ -13,37 +13,26 @@ import { EndpointClientConfig } from './endpoint-client'
1313
export interface Authenticator {
1414
login?(): Promise<void>
1515
logout?(): Promise<void>
16-
refresh?(requestConfig: AxiosRequestConfig, clientConfig: EndpointClientConfig): Promise<void>
16+
refresh?(clientConfig: EndpointClientConfig): Promise<HttpClientHeaders>
1717
acquireRefreshMutex?(): Promise<MutexInterface.Releaser>
1818

1919
/**
20-
* Performs required authentication steps to add credentials to the axios config, typically via Bearer Auth headers.
21-
* Expected to call other functions such as @see refresh as needed to return valid credentials.
22-
*
23-
* @param requestConfig AxiosRequestConfig to add credentials to and return otherwise unmodified
24-
*/
25-
authenticate(requestConfig: AxiosRequestConfig): Promise<AxiosRequestConfig>
26-
27-
/**
28-
* Performs required authentication steps and returns credentials as a string value
20+
* Performs required authentication steps and returns credentials as a set of HTTP headers which
21+
* must be included in authenticated requests.
2922
* Expected to perform any required steps (such as token refresh) needed to return valid credentials.
3023
*
3124
* @returns {string} valid auth token
3225
*/
33-
authenticateGeneric?(): Promise<string>
26+
authenticate(): Promise<HttpClientHeaders>
3427
}
3528

3629

3730
/**
3831
* For use in tests or on endpoints that don't need any authentication.
3932
*/
4033
export class NoOpAuthenticator implements Authenticator {
41-
authenticate(requestConfig: AxiosRequestConfig): Promise<AxiosRequestConfig> {
42-
return Promise.resolve(requestConfig)
43-
}
44-
45-
authenticateGeneric(): Promise<string> {
46-
return Promise.resolve('')
34+
authenticate(): Promise<HttpClientHeaders> {
35+
return Promise.resolve({})
4736
}
4837
}
4938

@@ -57,18 +46,17 @@ export class BearerTokenAuthenticator implements Authenticator {
5746
// simple
5847
}
5948

60-
authenticate(requestConfig: AxiosRequestConfig): Promise<AxiosRequestConfig> {
49+
authenticate(): Promise<HttpClientHeaders> {
50+
/*
6151
return Promise.resolve({
6252
...requestConfig,
6353
headers: {
6454
...requestConfig.headers,
6555
Authorization: `Bearer ${this.token}`,
6656
},
6757
})
68-
}
69-
70-
authenticateGeneric(): Promise<string> {
71-
return Promise.resolve(this.token)
58+
*/
59+
return Promise.resolve({ Authorization: `Bearer ${this.token}` })
7260
}
7361
}
7462

@@ -101,17 +89,11 @@ export class RefreshTokenAuthenticator implements Authenticator {
10189
// simple
10290
}
10391

104-
authenticate(requestConfig: AxiosRequestConfig): Promise<AxiosRequestConfig> {
105-
return Promise.resolve({
106-
...requestConfig,
107-
headers: {
108-
...requestConfig.headers,
109-
Authorization: `Bearer ${this.token}`,
110-
},
111-
})
92+
authenticate(): Promise<HttpClientHeaders> {
93+
return Promise.resolve({ Authorization: `Bearer ${this.token}` })
11294
}
11395

114-
async refresh(requestConfig: AxiosRequestConfig, clientConfig: EndpointClientConfig): Promise<void> {
96+
async refresh(clientConfig: EndpointClientConfig): Promise<HttpClientHeaders> {
11597
const refreshData: RefreshData = await this.tokenStore.getRefreshData()
11698
const headers = {
11799
'Content-Type': 'application/x-www-form-urlencoded',
@@ -133,8 +115,8 @@ export class RefreshTokenAuthenticator implements Authenticator {
133115
refreshToken: response.data.refresh_token,
134116
}
135117
this.token = authData.authToken
136-
requestConfig.headers = { ...(requestConfig.headers ?? {}), Authorization: `Bearer ${this.token}` }
137-
return this.tokenStore.putAuthData(authData)
118+
await this.tokenStore.putAuthData(authData)
119+
return { Authorization: `Bearer ${this.token}` }
138120
}
139121

140122
throw Error(`error ${response.status} refreshing token, with message ${response.data}`)

src/endpoint-client.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export class EndpointClient {
174174
}
175175
}
176176

177-
let axiosConfig: AxiosRequestConfig = {
177+
const axiosConfig: AxiosRequestConfig = {
178178
url: this.url(path),
179179
method,
180180
headers: options?.headerOverrides ? { ...headers, ...options.headerOverrides } : headers,
@@ -183,7 +183,8 @@ export class EndpointClient {
183183
paramsSerializer: params => qs.stringify(params, { indices: false }),
184184
}
185185

186-
axiosConfig = await this.config.authenticator.authenticate(axiosConfig)
186+
const authHeaders = await this.config.authenticator.authenticate()
187+
axiosConfig.headers = { ...axiosConfig.headers, ...authHeaders }
187188

188189
if (this.logger.isDebugEnabled()) {
189190
this.logger.debug(`making axios request: ${scrubConfig(axiosConfig)}`)
@@ -208,6 +209,7 @@ export class EndpointClient {
208209
return response.data
209210
} catch (error: any) {
210211
if (this.logger.isTraceEnabled()) {
212+
// https://www.npmjs.com/package/axios#handling-errors
211213
if (error.response) {
212214
// server responded with non-200 response code
213215
this.logger.trace(`axios response ${error.response.status}: data=${JSON.stringify(error.response.data)}`)
@@ -222,14 +224,16 @@ export class EndpointClient {
222224
if (this.config.authenticator.acquireRefreshMutex) {
223225
const release = await this.config.authenticator.acquireRefreshMutex()
224226
try {
225-
await this.config.authenticator.refresh(axiosConfig, this.config)
227+
const newAuthHeaders = await this.config.authenticator.refresh(this.config)
228+
axiosConfig.headers = { ...axiosConfig.headers, ...newAuthHeaders }
226229
const response = await axios.request(axiosConfig)
227230
return response.data
228231
} finally {
229232
release()
230233
}
231234
} else {
232-
await this.config.authenticator.refresh(axiosConfig, this.config)
235+
const newAuthHeaders = await this.config.authenticator.refresh(this.config)
236+
axiosConfig.headers = { ...axiosConfig.headers, ...newAuthHeaders }
233237
const response = await axios.request(axiosConfig)
234238
return response.data
235239
}

test/unit/authenticator.test.ts

Lines changed: 14 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -28,50 +28,24 @@ describe('authenticators', () => {
2828
jest.clearAllMocks()
2929
})
3030

31-
const config = { url: 'https://api.smartthings.com', headers: { Test: 'test' } }
31+
test('NoOpAuthenticator returns no headers', async () => {
32+
const authenticator = new NoOpAuthenticator()
3233

33-
describe('NoOpAuthenticator', () => {
34-
test('authenticate returns config unchanged', async () => {
35-
const authenticator = new NoOpAuthenticator()
36-
const data = await authenticator.authenticate(config)
37-
38-
expect(data).toBe(config)
39-
})
40-
41-
test('authenticateGeneric returns empty string for token', async () => {
42-
const authenticator = new NoOpAuthenticator()
43-
const token = await authenticator.authenticateGeneric()
44-
45-
expect(token).toBe('')
46-
})
34+
expect(await authenticator.authenticate()).toStrictEqual({})
4735
})
4836

49-
describe('BearerTokenAuthenticator', () => {
50-
test('authenticate adds header with specified token', async () => {
51-
const authenticator = new BearerTokenAuthenticator('a-bearer-token')
52-
const data = await authenticator.authenticate(config)
37+
test('BearerTokenAuthenticator returns header with specified token', async () => {
38+
const authenticator = new BearerTokenAuthenticator('a-bearer-token')
5339

54-
expect(data.url).toBe(config.url)
55-
expect(data.headers?.Authorization).toBe('Bearer a-bearer-token')
56-
expect(data.headers?.Test).toBe('test')
57-
})
58-
59-
test('authenticateGeneric returns specified token', async () => {
60-
const authenticator = new BearerTokenAuthenticator('a-bearer-token')
61-
const token = await authenticator.authenticateGeneric()
62-
63-
expect(token).toBe('a-bearer-token')
64-
})
40+
expect(await authenticator.authenticate()).toStrictEqual({ Authorization: 'Bearer a-bearer-token' })
6541
})
6642

6743
describe('RefreshTokenAuthenticator', () => {
68-
test('authenticate adds header with specified token', async () => {
44+
test('authenticate returns header with specified token', async () => {
6945
const tokenStore = new TokenStore()
7046
const authenticator = new RefreshTokenAuthenticator('a-refreshable-bearer-token', tokenStore)
71-
const data = await authenticator.authenticate(config)
7247

73-
expect(data.url).toBe(config.url)
74-
expect(data.headers?.Authorization).toBe('Bearer a-refreshable-bearer-token')
48+
expect(await authenticator.authenticate()).toStrictEqual({ Authorization: 'Bearer a-refreshable-bearer-token' })
7549
})
7650

7751
test('refresh updates token', async () => {
@@ -86,7 +60,8 @@ describe('authenticators', () => {
8660
const tokenStore = new TokenStore()
8761
const authenticator = new RefreshTokenAuthenticator('a-refreshable-bearer-token', tokenStore)
8862
const endpointConfig = { urlProvider: defaultSmartThingsURLProvider, authenticator }
89-
await authenticator.refresh(config, endpointConfig)
63+
64+
expect(await authenticator.refresh(endpointConfig)).toStrictEqual({ Authorization: 'Bearer the-access-token' })
9065

9166
expect(axios.request).toHaveBeenCalledTimes(1)
9267
expect(axios.request).toHaveBeenCalledWith({
@@ -114,7 +89,7 @@ describe('authenticators', () => {
11489
const endpointConfig = { urlProvider: defaultSmartThingsURLProvider, authenticator }
11590
let message
11691
try {
117-
await authenticator.refresh(config, endpointConfig)
92+
await authenticator.refresh(endpointConfig)
11893
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11994
} catch (error: any) {
12095
message = error.message
@@ -133,10 +108,7 @@ describe('authenticators', () => {
133108
const authenticator = new SequentialRefreshTokenAuthenticator('a-bearer-token', tokenStore, mutex)
134109

135110
test('authenticate adds header with specified token', async () => {
136-
const data = await authenticator.authenticate(config)
137-
138-
expect(data.url).toBe(config.url)
139-
expect(data.headers?.Authorization).toBe('Bearer a-bearer-token')
111+
expect(await authenticator.authenticate()).toStrictEqual({ Authorization: 'Bearer a-bearer-token' })
140112
})
141113

142114
describe('refresh', () => {
@@ -151,7 +123,7 @@ describe('authenticators', () => {
151123
const endpointConfig = { urlProvider: defaultSmartThingsURLProvider, authenticator }
152124

153125
it('updates token', async () => {
154-
await authenticator.refresh(config, endpointConfig)
126+
await authenticator.refresh(endpointConfig)
155127

156128
expect(axios.request).toHaveBeenCalledTimes(1)
157129
expect(axios.request).toHaveBeenCalledWith({
@@ -169,8 +141,7 @@ describe('authenticators', () => {
169141
})
170142

171143
it('works on request with no existing headers', async () => {
172-
const configWithoutHeaders = { url: 'https://api.smartthings.com' }
173-
await authenticator.refresh(configWithoutHeaders, endpointConfig)
144+
expect(await authenticator.refresh(endpointConfig)).toStrictEqual({ Authorization: 'Bearer the-access-token' })
174145

175146
expect(axios.request).toHaveBeenCalledTimes(1)
176147
expect(axios.request).toHaveBeenCalledWith({

0 commit comments

Comments
 (0)