1
1
import {
2
2
exchangeAccessForApplicationTokens ,
3
3
exchangeCustomPartnerToken ,
4
+ exchangeCliTokenForAppManagementAccessToken ,
5
+ exchangeCliTokenForBusinessPlatformAccessToken ,
4
6
InvalidGrantError ,
5
7
InvalidRequestError ,
6
8
refreshAccessToken ,
@@ -9,7 +11,7 @@ import {applicationId, clientId} from './identity.js'
9
11
import { IdentityToken } from './schema.js'
10
12
import { shopifyFetch } from '../../../public/node/http.js'
11
13
import { identityFqdn } from '../../../public/node/context/fqdn.js'
12
- import { getLastSeenUserIdAfterAuth } from '../session.js'
14
+ import { getLastSeenUserIdAfterAuth , getLastSeenAuthMethod } from '../session.js'
13
15
import { describe , test , expect , vi , afterAll , beforeEach } from 'vitest'
14
16
import { Response } from 'node-fetch'
15
17
import { AbortError } from '@shopify/cli-kit/node/error'
@@ -197,30 +199,93 @@ describe('refresh access tokens', () => {
197
199
} )
198
200
} )
199
201
200
- describe ( 'exchangeCustomPartnerToken' , ( ) => {
201
- const token = 'customToken'
202
-
203
- // Generated from `customToken` using `nonRandomUUID()`
204
- const userId = 'eab16ac4-0690-5fed-9d00-71bd202a3c2b37259a8f'
205
-
206
- test ( 'returns access token and user ID for a valid token' , async ( ) => {
207
- // Given
208
- const data = {
209
- access_token : 'access_token' ,
210
- expires_in : 300 ,
211
- scope : 'scope,scope2' ,
212
- }
213
- // Given
214
- const response = new Response ( JSON . stringify ( data ) )
215
-
216
- // Need to do it 3 times because a Response can only be used once
217
- vi . mocked ( shopifyFetch ) . mockResolvedValue ( response )
218
-
219
- // When
220
- const result = await exchangeCustomPartnerToken ( token )
221
-
222
- // Then
223
- expect ( result ) . toEqual ( { accessToken : 'access_token' , userId} )
224
- await expect ( getLastSeenUserIdAfterAuth ( ) ) . resolves . toBe ( userId )
225
- } )
226
- } )
202
+ const tokenExchangeMethods = [
203
+ {
204
+ tokenExchangeMethod : exchangeCustomPartnerToken ,
205
+ expectedScopes : [ 'https://api.shopify.com/auth/partners.app.cli.access' ] ,
206
+ expectedApi : 'partners' ,
207
+ expectedErrorName : 'Partners' ,
208
+ } ,
209
+ {
210
+ tokenExchangeMethod : exchangeCliTokenForAppManagementAccessToken ,
211
+ expectedScopes : [ 'https://api.shopify.com/auth/organization.apps.manage' ] ,
212
+ expectedApi : 'app-management' ,
213
+ expectedErrorName : 'App Management' ,
214
+ } ,
215
+ {
216
+ tokenExchangeMethod : exchangeCliTokenForBusinessPlatformAccessToken ,
217
+ expectedScopes : [
218
+ 'https://api.shopify.com/auth/destinations.readonly' ,
219
+ 'https://api.shopify.com/auth/organization.store-management' ,
220
+ ] ,
221
+ expectedApi : 'business-platform' ,
222
+ expectedErrorName : 'Business Platform' ,
223
+ } ,
224
+ ]
225
+
226
+ describe . each ( tokenExchangeMethods ) (
227
+ 'Token exchange: %s' ,
228
+ ( { tokenExchangeMethod, expectedScopes, expectedApi, expectedErrorName} ) => {
229
+ const cliToken = 'customToken'
230
+ // Generated from `customToken` using `nonRandomUUID()`
231
+ const userId = 'eab16ac4-0690-5fed-9d00-71bd202a3c2b37259a8f'
232
+
233
+ const grantType = 'urn:ietf:params:oauth:grant-type:token-exchange'
234
+ const accessTokenType = 'urn:ietf:params:oauth:token-type:access_token'
235
+
236
+ test ( `Executing ${ tokenExchangeMethod . name } returns access token and user ID for a valid CLI token` , async ( ) => {
237
+ // Given
238
+ let capturedUrl = ''
239
+ vi . mocked ( shopifyFetch ) . mockImplementation ( async ( url , options ) => {
240
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
241
+ capturedUrl = url . toString ( )
242
+ return Promise . resolve (
243
+ new Response (
244
+ JSON . stringify ( {
245
+ access_token : 'expected_access_token' ,
246
+ expires_in : 300 ,
247
+ scope : 'scope,scope2' ,
248
+ } ) ,
249
+ ) ,
250
+ )
251
+ } )
252
+
253
+ // When
254
+ const result = await tokenExchangeMethod ( cliToken )
255
+
256
+ // Then
257
+ expect ( result ) . toEqual ( { accessToken : 'expected_access_token' , userId} )
258
+ await expect ( getLastSeenUserIdAfterAuth ( ) ) . resolves . toBe ( userId )
259
+ await expect ( getLastSeenAuthMethod ( ) ) . resolves . toBe ( 'partners_token' )
260
+
261
+ // Assert token exchange parameters are correct
262
+ const actualUrl = new URL ( capturedUrl )
263
+ expect ( actualUrl ) . toBeDefined ( )
264
+ expect ( actualUrl . href ) . toContain ( 'https://fqdn.com/oauth/token' )
265
+
266
+ const params = actualUrl . searchParams
267
+ expect ( params . get ( 'grant_type' ) ) . toBe ( grantType )
268
+ expect ( params . get ( 'requested_token_type' ) ) . toBe ( accessTokenType )
269
+ expect ( params . get ( 'subject_token_type' ) ) . toBe ( accessTokenType )
270
+ expect ( params . get ( 'client_id' ) ) . toBe ( 'clientId' )
271
+ expect ( params . get ( 'audience' ) ) . toBe ( expectedApi )
272
+ expect ( params . get ( 'scope' ) ) . toBe ( expectedScopes . join ( ' ' ) )
273
+ expect ( params . get ( 'subject_token' ) ) . toBe ( cliToken )
274
+ } )
275
+
276
+ test ( `Executing ${ tokenExchangeMethod . name } throws AbortError if an error is caught` , async ( ) => {
277
+ const expectedErrorMessage = `The custom token provided can't be used for the ${ expectedErrorName } API.`
278
+ vi . mocked ( shopifyFetch ) . mockImplementation ( async ( ) => {
279
+ throw new Error ( 'BAD ERROR' )
280
+ } )
281
+
282
+ try {
283
+ await tokenExchangeMethod ( cliToken )
284
+ // eslint-disable-next-line no-catch-all/no-catch-all
285
+ } catch ( error ) {
286
+ expect ( error ) . toBeInstanceOf ( AbortError )
287
+ expect ( error . message ) . toBe ( expectedErrorMessage )
288
+ }
289
+ } )
290
+ } ,
291
+ )
0 commit comments