Skip to content
Open
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
56 changes: 47 additions & 9 deletions packages/store/src/cli/commands/store/create/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import StoreCreateDev from './dev.js'
import {createDevStore} from '../../../services/store/create/dev.js'
import {storeNamePrompt, storePlanPrompt} from '../../../prompts/store.js'
import {selectOrg} from '@shopify/organizations'
import {reportAnalyticsEvent} from '@shopify/cli-kit/node/analytics'
import {AbortError} from '@shopify/cli-kit/node/error'
import {outputResult} from '@shopify/cli-kit/node/output'
import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system'
import {describe, expect, test, vi, beforeEach} from 'vitest'
import {beforeEach, describe, expect, test, vi} from 'vitest'

vi.mock('../../../services/store/create/dev.js')
vi.mock('../../../prompts/store.js')
vi.mock('@shopify/cli-kit/node/system')
vi.mock('@shopify/cli-kit/node/analytics', () => ({
reportAnalyticsEvent: vi.fn(),
}))
Comment thread
dmerand marked this conversation as resolved.

vi.mock('@shopify/organizations', () => ({
selectOrg: vi.fn(),
Expand All @@ -31,6 +35,10 @@ beforeEach(() => {
})

describe('store create dev command', () => {
test('requires synchronous analytics', () => {
expect(StoreCreateDev.requiresSyncAnalytics).toBe(true)
})

test('resolves the organization and passes parsed flags through to the service', async () => {
await StoreCreateDev.run(['--name', 'my-test-store', '--plan', 'plus', '--organization-id', '12345'])

Expand Down Expand Up @@ -190,24 +198,54 @@ describe('store create dev command', () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => {
throw new Error('process.exit')
}) as never)
const mockCommandCatch = vi.spyOn(StoreCreateDev.prototype, 'catch').mockImplementation(async (error) => {
throw error
})

await expect(
StoreCreateDev.run(['--name', 'my-test-store', '--plan', 'plus', '--organization-id', '12345', '--json']),
).rejects.toThrow('process.exit')

const call = vi.mocked(outputResult).mock.calls[0]![0] as string
const parsed = JSON.parse(call)
expect(parsed).toEqual({
error: true,
message: 'Something went wrong',
nextSteps: [],
exitCode: 1,
})
expect(outputResult).toHaveBeenCalledTimes(1)
expect(outputResult).toHaveBeenCalledWith(
JSON.stringify(
{
error: true,
message: 'Something went wrong',
nextSteps: [],
exitCode: 1,
},
null,
2,
),
)
expect(mockExit).toHaveBeenCalledTimes(1)
expect(mockExit).toHaveBeenCalledWith(1)
expect(reportAnalyticsEvent).toHaveBeenCalledTimes(1)
expect(reportAnalyticsEvent).toHaveBeenCalledWith({
config: expect.anything(),
errorMessage: 'Something went wrong',
exitMode: 'expected_error',
})

mockCommandCatch.mockRestore()
mockExit.mockRestore()
})

test('leaves organization selection errors to global handling when --json is active', async () => {
vi.mocked(selectOrg).mockRejectedValueOnce(new AbortError('Could not select organization'))
vi.spyOn(StoreCreateDev.prototype, 'catch').mockImplementation(async (error) => {
throw error
})

await expect(
StoreCreateDev.run(['--name', 'my-test-store', '--plan', 'plus', '--organization-id', '12345', '--json']),
).rejects.toThrow('Could not select organization')

expect(outputResult).not.toHaveBeenCalled()
expect(reportAnalyticsEvent).not.toHaveBeenCalled()
})

test('does not output JSON for non-AbortError even when --json is active', async () => {
vi.mocked(createDevStore).mockRejectedValueOnce(new Error('unexpected'))

Expand Down
6 changes: 6 additions & 0 deletions packages/store/src/cli/commands/store/create/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ import {countryFlag, storeFlags} from '../../../flags.js'
import {selectOrg} from '@shopify/organizations'
import Command from '@shopify/cli-kit/node/base-command'
import {globalFlags, jsonFlag, requiredIfNonInteractive} from '@shopify/cli-kit/node/cli'
import {reportAnalyticsEvent} from '@shopify/cli-kit/node/analytics'
import {AbortError} from '@shopify/cli-kit/node/error'
import {outputResult} from '@shopify/cli-kit/node/output'
import {Flags} from '@oclif/core'

export default class StoreCreateDev extends Command {
static hidden = true

public static get requiresSyncAnalytics(): boolean {
return true
}

static summary = 'Create a new development store.'

static descriptionWithMarkdown = 'Creates a new app development store in your organization.'
Expand Down Expand Up @@ -66,6 +71,7 @@ export default class StoreCreateDev extends Command {
})
} catch (error) {
if (flags.json && error instanceof AbortError) {
await reportAnalyticsEvent({config: this.config, errorMessage: error.message, exitMode: 'expected_error'})
outputResult(
JSON.stringify(
{
Expand Down
112 changes: 87 additions & 25 deletions packages/store/src/cli/commands/store/delete.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import StoreDelete from './delete.js'
import {deleteDevStore} from '../../services/store/delete/dev.js'
import {resolveOrganizationForStore} from '../../utilities/store-lookup/organization.js'
import {recordStoreFqdnMetadata} from '../../services/store/attribution.js'
import {reportAnalyticsEvent} from '@shopify/cli-kit/node/analytics'
import {AbortError} from '@shopify/cli-kit/node/error'
import {outputResult} from '@shopify/cli-kit/node/output'
import {isTTY, renderDangerousConfirmationPrompt} from '@shopify/cli-kit/node/ui'
import {describe, expect, test, vi, beforeEach} from 'vitest'
import {beforeEach, describe, expect, test, vi} from 'vitest'

vi.mock('../../services/store/delete/dev.js')
vi.mock('../../services/store/attribution.js')
vi.mock('../../utilities/store-lookup/organization.js')
vi.mock('@shopify/cli-kit/node/analytics', () => ({
reportAnalyticsEvent: vi.fn(),
}))

vi.mock('@shopify/cli-kit/node/output', async (importOriginal) => {
const actual: Record<string, unknown> = await importOriginal()
Expand Down Expand Up @@ -35,6 +41,10 @@ beforeEach(() => {
})

describe('store delete command', () => {
test('requires synchronous analytics', () => {
expect(StoreDelete.requiresSyncAnalytics).toBe(true)
})

test('resolves the organization and passes parsed flags through to the service', async () => {
await StoreDelete.run(['--store', 'my-store.myshopify.com', '--organization-id', '12345'])

Expand Down Expand Up @@ -91,6 +101,7 @@ describe('store delete command', () => {

await expect(StoreDelete.run(['--store', 'my-store.myshopify.com', '--organization-id', '12345'])).rejects.toThrow()
expect(deleteDevStore).not.toHaveBeenCalled()
expect(reportAnalyticsEvent).not.toHaveBeenCalled()
})

test('skips the confirmation prompt when --force is passed', async () => {
Expand Down Expand Up @@ -123,22 +134,39 @@ describe('store delete command', () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => {
throw new Error('process.exit')
}) as never)
const mockCommandCatch = vi.spyOn(StoreDelete.prototype, 'catch').mockImplementation(async (error) => {
throw error
})

await expect(
StoreDelete.run(['--store', 'my-store.myshopify.com', '--organization-id', '12345', '--json']),
).rejects.toThrow('process.exit')

const call = vi.mocked(outputResult).mock.calls[0]![0] as string
const parsed = JSON.parse(call)
expect(parsed).toEqual({
error: true,
message: 'Deleting the development store my-store.myshopify.com requires confirmation.',
nextSteps: ['Use the `--force` flag to skip confirmation when running non-interactively.'],
exitCode: 1,
})
expect(outputResult).toHaveBeenCalledTimes(1)
expect(outputResult).toHaveBeenCalledWith(
JSON.stringify(
{
error: true,
message: 'Deleting the development store my-store.myshopify.com requires confirmation.',
nextSteps: ['Use the `--force` flag to skip confirmation when running non-interactively.'],
exitCode: 1,
},
null,
2,
),
)
expect(deleteDevStore).not.toHaveBeenCalled()
expect(mockExit).toHaveBeenCalledTimes(1)
expect(mockExit).toHaveBeenCalledWith(1)
expect(recordStoreFqdnMetadata).toHaveBeenCalledWith('my-store.myshopify.com', false)
expect(reportAnalyticsEvent).toHaveBeenCalledTimes(1)
expect(reportAnalyticsEvent).toHaveBeenCalledWith({
config: expect.anything(),
errorMessage: 'Deleting the development store my-store.myshopify.com requires confirmation.',
exitMode: 'expected_error',
})

mockCommandCatch.mockRestore()
mockExit.mockRestore()
})

Expand All @@ -147,21 +175,38 @@ describe('store delete command', () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => {
throw new Error('process.exit')
}) as never)
const mockCommandCatch = vi.spyOn(StoreDelete.prototype, 'catch').mockImplementation(async (error) => {
throw error
})

await expect(
StoreDelete.run(['--store', 'my-store.myshopify.com', '--organization-id', '12345', '--json']),
).rejects.toThrow('process.exit')

const call = vi.mocked(outputResult).mock.calls[0]![0] as string
const parsed = JSON.parse(call)
expect(parsed).toEqual({
error: true,
message: 'Something went wrong',
nextSteps: [],
exitCode: 1,
})
expect(outputResult).toHaveBeenCalledTimes(1)
expect(outputResult).toHaveBeenCalledWith(
JSON.stringify(
{
error: true,
message: 'Something went wrong',
nextSteps: [],
exitCode: 1,
},
null,
2,
),
)
expect(mockExit).toHaveBeenCalledTimes(1)
expect(mockExit).toHaveBeenCalledWith(1)
expect(recordStoreFqdnMetadata).toHaveBeenCalledWith('my-store.myshopify.com', false)
expect(reportAnalyticsEvent).toHaveBeenCalledTimes(1)
expect(reportAnalyticsEvent).toHaveBeenCalledWith({
config: expect.anything(),
errorMessage: 'Something went wrong',
exitMode: 'expected_error',
})

mockCommandCatch.mockRestore()
mockExit.mockRestore()
})

Expand All @@ -170,20 +215,37 @@ describe('store delete command', () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => {
throw new Error('process.exit')
}) as never)
const mockCommandCatch = vi.spyOn(StoreDelete.prototype, 'catch').mockImplementation(async (error) => {
throw error
})

await expect(StoreDelete.run(['--store', 'my-store.myshopify.com', '--json'])).rejects.toThrow('process.exit')

const call = vi.mocked(outputResult).mock.calls[0]![0] as string
const parsed = JSON.parse(call)
expect(parsed).toEqual({
error: true,
message: 'Could not resolve organization',
nextSteps: [],
exitCode: 1,
})
expect(outputResult).toHaveBeenCalledTimes(1)
expect(outputResult).toHaveBeenCalledWith(
JSON.stringify(
{
error: true,
message: 'Could not resolve organization',
nextSteps: [],
exitCode: 1,
},
null,
2,
),
)
expect(deleteDevStore).not.toHaveBeenCalled()
expect(mockExit).toHaveBeenCalledTimes(1)
expect(mockExit).toHaveBeenCalledWith(1)
expect(recordStoreFqdnMetadata).toHaveBeenCalledWith('my-store.myshopify.com', false)
expect(reportAnalyticsEvent).toHaveBeenCalledTimes(1)
expect(reportAnalyticsEvent).toHaveBeenCalledWith({
config: expect.anything(),
errorMessage: 'Could not resolve organization',
exitMode: 'expected_error',
})

mockCommandCatch.mockRestore()
mockExit.mockRestore()
})

Expand Down
8 changes: 8 additions & 0 deletions packages/store/src/cli/commands/store/delete.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {deleteDevStore} from '../../services/store/delete/dev.js'
import {storeFlags} from '../../flags.js'
import {resolveOrganizationForStore} from '../../utilities/store-lookup/organization.js'
import {recordStoreFqdnMetadata} from '../../services/store/attribution.js'
import Command from '@shopify/cli-kit/node/base-command'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
import {reportAnalyticsEvent} from '@shopify/cli-kit/node/analytics'
import {AbortError, AbortSilentError} from '@shopify/cli-kit/node/error'
import {outputResult} from '@shopify/cli-kit/node/output'
import {isTTY, renderDangerousConfirmationPrompt} from '@shopify/cli-kit/node/ui'
Expand All @@ -11,6 +13,10 @@ import {Flags} from '@oclif/core'
export default class StoreDelete extends Command {
static hidden = true

public static get requiresSyncAnalytics(): boolean {
return true
}

static summary = 'Delete a development store.'

static descriptionWithMarkdown = 'Deletes a development store from your organization.'
Expand Down Expand Up @@ -38,6 +44,7 @@ export default class StoreDelete extends Command {

async run(): Promise<void> {
const {flags} = await this.parse(StoreDelete)
await recordStoreFqdnMetadata(flags.store, false)

try {
// Deleting a store is irreversible: in non-interactive runs (CI, agents, piped
Expand Down Expand Up @@ -67,6 +74,7 @@ export default class StoreDelete extends Command {
// Only expected failures (AbortError) are rendered as JSON. Unexpected errors rethrow to the
// global error handler so they keep their stack traces and get reported as CLI bugs.
if (flags.json && error instanceof AbortError) {
await reportAnalyticsEvent({config: this.config, errorMessage: error.message, exitMode: 'expected_error'})
outputResult(
JSON.stringify(
{
Expand Down
Loading
Loading