Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@ import {selectOrganizationPrompt} from '@shopify/organizations'
import {beforeEach, describe, expect, test, vi} from 'vitest'
import {inTemporaryDirectory, readFile, writeFileSync} from '@shopify/cli-kit/node/fs'
import {joinPath} from '@shopify/cli-kit/node/path'
import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system'

vi.mock('./use.js')
vi.mock('../../../prompts/dev.js')
vi.mock('@shopify/organizations')
vi.mock('../../../prompts/config.js')
vi.mock('../../local-storage')
vi.mock('@shopify/cli-kit/node/ui')
vi.mock('@shopify/cli-kit/node/system')
vi.mock('../../dev/fetch.js')
vi.mock('../../../utilities/developer-platform-client.js')
vi.mock('../../../models/app/validation/multi-cli-warning.js')
beforeEach(async () => {
vi.mocked(terminalSupportsPrompting).mockReturnValue(true)
// Default mock for selectConfigName - tests that need a specific value can override
vi.mocked(selectConfigName).mockResolvedValue('shopify.app.toml')
})
Expand Down
50 changes: 50 additions & 0 deletions packages/app/src/cli/services/app/config/link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {joinPath} from '@shopify/cli-kit/node/path'
import {renderSuccess} from '@shopify/cli-kit/node/ui'
import {outputContent} from '@shopify/cli-kit/node/output'
import {setPathValue} from '@shopify/cli-kit/common/object'
import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system'

vi.mock('./use.js')
vi.mock('../../../prompts/config.js')
Expand All @@ -36,6 +37,7 @@ vi.mock('../../../models/app/loader.js', async () => {
})
vi.mock('../../local-storage')
vi.mock('@shopify/cli-kit/node/ui')
vi.mock('@shopify/cli-kit/node/system')
vi.mock('../../context/partner-account-info.js')
vi.mock('../../context.js')
vi.mock('../select-app.js')
Expand Down Expand Up @@ -66,6 +68,7 @@ function buildDeveloperPlatformClient(extraFields: Partial<DeveloperPlatformClie

beforeEach(async () => {
vi.mocked(fetchAppRemoteConfiguration).mockResolvedValue(DEFAULT_REMOTE_CONFIGURATION)
vi.mocked(terminalSupportsPrompting).mockReturnValue(true)
// Default mock for selectConfigName - tests that need a specific value can override
vi.mocked(selectConfigName).mockResolvedValue('shopify.app.toml')
})
Expand Down Expand Up @@ -115,6 +118,28 @@ describe('link', () => {
})
})

test('throws in non-TTY when the remote app would require prompting', async () => {
await inTemporaryDirectory(async (tmp) => {
// Given
vi.mocked(terminalSupportsPrompting).mockReturnValue(false)
const developerPlatformClient = buildDeveloperPlatformClient()
const options: LinkOptions = {
directory: tmp,
developerPlatformClient,
}
await mockLoadOpaqueAppWithApp(tmp)

// When
const result = link(options)

// Then
await expect(result).rejects.toThrow(/app config link requires additional flags/)
await expect(result).rejects.toThrow(/--client-id/)
expect(fetchOrCreateOrganizationApp).not.toHaveBeenCalled()
expect(selectConfigName).not.toHaveBeenCalled()
})
})

test('does not ask for a name when a file name is provided as a flag', async () => {
await inTemporaryDirectory(async (tmp) => {
// Given
Expand Down Expand Up @@ -877,6 +902,31 @@ describe('link', () => {
})
})

test('throws in non-TTY when the config file name would require prompting', async () => {
await inTemporaryDirectory(async (tmp) => {
// Given
vi.mocked(terminalSupportsPrompting).mockReturnValue(false)
const developerPlatformClient = buildDeveloperPlatformClient()
const remoteApp = mockRemoteApp({developerPlatformClient, apiKey: 'new-api-key'})
const options: LinkOptions = {
directory: tmp,
apiKey: remoteApp.apiKey,
developerPlatformClient,
}
writeFileSync(joinPath(tmp, 'shopify.app.toml'), 'client_id = "existing-api-key"')
await mockLoadOpaqueAppWithApp(tmp)
vi.mocked(appFromIdentifiers).mockResolvedValue(remoteApp)

// When
const result = link(options)

// Then
await expect(result).rejects.toThrow(/app config link requires additional flags/)
await expect(result).rejects.toThrow(/--file-name/)
expect(selectConfigName).not.toHaveBeenCalled()
})
})

test('uses scopes on platform if defined', async () => {
await inTemporaryDirectory(async (tmp) => {
// Given
Expand Down
13 changes: 13 additions & 0 deletions packages/app/src/cli/services/app/config/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {fileExists} from '@shopify/cli-kit/node/fs'
import {joinPath} from '@shopify/cli-kit/node/path'
import {AbortError} from '@shopify/cli-kit/node/error'
import {PackageManager} from '@shopify/cli-kit/node/node-package-manager'
import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system'

export interface LinkOptions {
directory: string
Expand Down Expand Up @@ -94,6 +95,16 @@ export default async function link(options: LinkOptions, shouldRenderSuccess = t
return {remoteApp, configFileName, configuration: mergedAppConfiguration}
}

function abortIfLinkPromptCannotRun(missingFlags: string[]) {
if (terminalSupportsPrompting() || missingFlags.length === 0) return

const flags = missingFlags.map((flag) => `--${flag}`).join(' ')
throw new AbortError(
`app config link requires additional flags in non-interactive terminal environments. Run shopify app config link ${flags} with the required values.`,
`Run shopify app config link ${flags} with the required values, or run the command in an interactive terminal.`,
)
}

/**
* Choose or create an app on the platform to link to.
*
Expand Down Expand Up @@ -125,6 +136,7 @@ async function selectOrCreateRemoteAppToLinkTo(options: LinkOptions): Promise<{
}
}

abortIfLinkPromptCannotRun(['client-id'])
const remoteApp = await fetchOrCreateOrganizationApp({...creationOptions, directory: appDirectory})

const developerPlatformClient = remoteApp.developerPlatformClient
Expand Down Expand Up @@ -322,6 +334,7 @@ async function loadConfigurationFileName(
// If no TOML files exist at all, use the default filename without prompting
if (isEmpty(existingTomls)) return 'shopify.app.toml'

abortIfLinkPromptCannotRun(['file-name'])
return selectConfigName(localAppInfo.appDirectory ?? options.directory, remoteApp.title)
}

Expand Down
Loading