@@ -11,9 +11,10 @@ import {
1111 type AppSecuritySelectionDependencies ,
1212} from './app-security-selection.js'
1313import { validAppConfiguration } from './app-security-selection.test-data.js'
14- import { getCachedAppInfo } from './local-storage.js'
14+ import { getCachedAppInfo , setCachedAppInfo } from './local-storage.js'
1515import { appCreationDefaults } from './app/config/link.js'
1616import { fetchOrCreateOrganizationApp } from './context.js'
17+ import use from './app/config/use.js'
1718import { AbortError } from '@shopify/cli-kit/node/error'
1819import { fileRealPath , inTemporaryDirectory , mkdir , writeFile } from '@shopify/cli-kit/node/fs'
1920import { joinPath } from '@shopify/cli-kit/node/path'
@@ -25,7 +26,9 @@ import type {OrganizationApp} from '../models/organization.js'
2526vi . mock ( './local-storage.js' , async ( importOriginal ) => ( {
2627 ...( await importOriginal < typeof import ( './local-storage.js' ) > ( ) ) ,
2728 getCachedAppInfo : vi . fn ( ) ,
29+ setCachedAppInfo : vi . fn ( ) ,
2830} ) )
31+ vi . mock ( './app/config/use.js' , ( ) => ( { default : vi . fn ( ) } ) )
2932vi . mock ( './context.js' , async ( importOriginal ) => ( {
3033 ...( await importOriginal < typeof import ( './context.js' ) > ( ) ) ,
3134 fetchOrCreateOrganizationApp : vi . fn ( ) ,
@@ -51,6 +54,7 @@ function promptDependencies(overrides: Partial<AppSecuritySelectionDependencies>
5154 return {
5255 confirmScanWithoutAppConfig : vi . fn ( async ( _directory : string ) => true ) ,
5356 pickClientId : vi . fn ( async ( _appDirectory : string ) => 'picked-client-id' ) ,
57+ pickConfigFile : vi . fn ( async ( _appDirectory : string ) => 'shopify.app.staging.toml' ) ,
5458 ...overrides ,
5559 }
5660}
@@ -212,6 +216,116 @@ describe('resolveAppSecuritySelection with an app configuration', () => {
212216 } )
213217} )
214218
219+ describe ( 'resolveAppSecuritySelection with several TOMLs and none selected' , ( ) => {
220+ async function writeProductionAndStaging ( directory : string ) : Promise < void > {
221+ await writeConfiguration ( directory , 'production-client-id' , 'shopify.app.production.toml' )
222+ await writeConfiguration ( directory , 'staging-client-id' , 'shopify.app.staging.toml' )
223+ }
224+
225+ test ( 'asks which TOML to scan, without saving the answer' , async ( ) => {
226+ await inTemporaryDirectory ( async ( directory ) => {
227+ await writeProductionAndStaging ( directory )
228+ const dependencies = promptDependencies ( )
229+
230+ const selection = await resolveAppSecuritySelection ( { path : directory , allowPrompts : true } , dependencies )
231+
232+ expect ( selection ) . toMatchObject ( {
233+ kind : 'config' ,
234+ appConfigFilePath : joinPath ( await fileRealPath ( directory ) , 'shopify.app.staging.toml' ) ,
235+ configClientId : 'staging-client-id' ,
236+ appConfigFilePicked : true ,
237+ } )
238+ expect ( dependencies . pickConfigFile ) . toHaveBeenCalledOnce ( )
239+ expect ( setCachedAppInfo ) . not . toHaveBeenCalled ( )
240+ } )
241+ } )
242+
243+ test ( 'asks when the `app config use` choice no longer exists' , async ( ) => {
244+ await inTemporaryDirectory ( async ( directory ) => {
245+ await writeProductionAndStaging ( directory )
246+ vi . mocked ( getCachedAppInfo ) . mockReturnValue ( { directory, configFile : 'shopify.app.deleted.toml' } )
247+ const dependencies = promptDependencies ( )
248+
249+ const selection = await resolveAppSecuritySelection ( { path : directory , allowPrompts : true } , dependencies )
250+
251+ expect ( selection ) . toMatchObject ( { configClientId : 'staging-client-id' , appConfigFilePicked : true } )
252+ expect ( setCachedAppInfo ) . not . toHaveBeenCalled ( )
253+ } )
254+ } )
255+
256+ test ( 'scans shopify.app.toml without asking or saving a choice when the `app config use` choice no longer exists' , async ( ) => {
257+ await inTemporaryDirectory ( async ( directory ) => {
258+ await writeProductionAndStaging ( directory )
259+ await writeConfiguration ( directory , 'default-client-id' )
260+ vi . mocked ( getCachedAppInfo ) . mockReturnValue ( { directory, configFile : 'shopify.app.deleted.toml' } )
261+ const dependencies = promptDependencies ( )
262+
263+ const selection = await resolveAppSecuritySelection ( { path : directory , allowPrompts : true } , dependencies )
264+
265+ expect ( selection ) . toMatchObject ( { configClientId : 'default-client-id' , appConfigFilePicked : false } )
266+ expect ( dependencies . pickConfigFile ) . not . toHaveBeenCalled ( )
267+ expect ( use ) . not . toHaveBeenCalled ( )
268+ expect ( setCachedAppInfo ) . not . toHaveBeenCalled ( )
269+ } )
270+ } )
271+
272+ test ( 'scans shopify.app.toml without asking when it exists' , async ( ) => {
273+ await inTemporaryDirectory ( async ( directory ) => {
274+ await writeProductionAndStaging ( directory )
275+ await writeConfiguration ( directory , 'default-client-id' )
276+ const dependencies = promptDependencies ( )
277+
278+ const selection = await resolveAppSecuritySelection ( { path : directory , allowPrompts : true } , dependencies )
279+
280+ expect ( selection ) . toMatchObject ( { configClientId : 'default-client-id' , appConfigFilePicked : undefined } )
281+ expect ( dependencies . pickConfigFile ) . not . toHaveBeenCalled ( )
282+ } )
283+ } )
284+
285+ test ( 'scans the only TOML without asking' , async ( ) => {
286+ await inTemporaryDirectory ( async ( directory ) => {
287+ await writeConfiguration ( directory , 'staging-client-id' , 'shopify.app.staging.toml' )
288+ const dependencies = promptDependencies ( )
289+
290+ const selection = await resolveAppSecuritySelection ( { path : directory , allowPrompts : false } , dependencies )
291+
292+ expect ( selection ) . toMatchObject ( { configClientId : 'staging-client-id' , appConfigFilePicked : false } )
293+ expect ( dependencies . pickConfigFile ) . not . toHaveBeenCalled ( )
294+ } )
295+ } )
296+
297+ test ( 'aborts with the TOMLs to choose from when it cannot ask' , async ( ) => {
298+ await inTemporaryDirectory ( async ( directory ) => {
299+ await writeProductionAndStaging ( directory )
300+ const dependencies = promptDependencies ( )
301+
302+ const error = await selectionError (
303+ resolveAppSecuritySelection ( { path : directory , allowPrompts : false } , dependencies ) ,
304+ )
305+
306+ expect ( error . message ) . toBe ( `2 app configurations found in ${ directory } , and none is selected.` )
307+ expect ( error . tryMessage ) . toBe ( 'Pass `--config` with one of: production, staging.' )
308+ expect ( dependencies . pickConfigFile ) . not . toHaveBeenCalled ( )
309+ } )
310+ } )
311+
312+ test ( 'aborts instead of asking with --client-id, which --config cannot repeat' , async ( ) => {
313+ await inTemporaryDirectory ( async ( directory ) => {
314+ await writeProductionAndStaging ( directory )
315+ const dependencies = promptDependencies ( )
316+
317+ const error = await selectionError (
318+ resolveAppSecuritySelection ( { path : directory , clientId : 'flag-client-id' , allowPrompts : true } , dependencies ) ,
319+ )
320+
321+ expect ( error . tryMessage ) . toBe (
322+ "`--client-id` can't be combined with `--config`, so first select one with `shopify app config use <config>`: production, staging." ,
323+ )
324+ expect ( dependencies . pickConfigFile ) . not . toHaveBeenCalled ( )
325+ } )
326+ } )
327+ } )
328+
215329describe ( 'resolveAppSecuritySelection with --without-app-config' , ( ) => {
216330 test ( 'aborts without --client-id' , async ( ) => {
217331 await inTemporaryDirectory ( async ( directory ) => {
@@ -486,6 +600,34 @@ describe('resolveAppDirectory', () => {
486600 } )
487601 } )
488602
603+ test ( 'finds the app directory when there are several TOMLs and none is selected' , async ( ) => {
604+ await inTemporaryDirectory ( async ( directory ) => {
605+ await writeConfiguration ( directory , 'production-client-id' , 'shopify.app.production.toml' )
606+ await writeConfiguration ( directory , 'staging-client-id' , 'shopify.app.staging.toml' )
607+
608+ await expect ( resolveAppDirectory ( { path : directory } ) ) . resolves . toBe ( await fileRealPath ( directory ) )
609+ } )
610+ } )
611+
612+ test ( 'finds the app directory without validating its TOML' , async ( ) => {
613+ await inTemporaryDirectory ( async ( directory ) => {
614+ await writeFile ( joinPath ( directory , 'shopify.app.toml' ) , 'name = [' )
615+
616+ await expect ( resolveAppDirectory ( { path : directory } ) ) . resolves . toBe ( await fileRealPath ( directory ) )
617+ } )
618+ } )
619+
620+ test ( 'aborts when --path does not exist instead of walking up to the app above it' , async ( ) => {
621+ await inTemporaryDirectory ( async ( directory ) => {
622+ await writeConfiguration ( directory , 'default-client-id' )
623+ const missing = joinPath ( directory , 'missing-app' )
624+
625+ await expect ( resolveAppDirectory ( { path : missing } ) ) . rejects . toMatchObject ( {
626+ message : `--path ${ missing } : not a directory.` ,
627+ } )
628+ } )
629+ } )
630+
489631 test ( 'aborts when no TOML is found, without prompting' , async ( ) => {
490632 await inTemporaryDirectory ( async ( directory ) => {
491633 await expect ( resolveAppDirectory ( { path : directory } ) ) . rejects . toMatchObject ( {
0 commit comments