Skip to content

Commit 8420572

Browse files
test: add coverage for installCount in deploy prompt and identifiers
- deploy-release: verify installCount is passed to danger prompt when > 0 - deploy-release: verify installCount is omitted when 0 - identifiers: verify appInstallCount called with remoteApp.id (not appId) - identifiers: verify appInstallCount skipped when --force - identifiers: verify appInstallCount skipped when --allow-updates + --allow-deletes - identifiers: verify graceful degradation when appInstallCount throws
1 parent 040a430 commit 8420572

4 files changed

Lines changed: 220 additions & 3 deletions

File tree

packages/app/src/cli/prompts/deploy-release.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,60 @@ describe('deployOrReleaseConfirmationPrompt', () => {
206206
expect(result).toBe(true)
207207
})
208208

209+
test('and no force with deleted extensions and installCount should pass installCount to danger prompt', async () => {
210+
// Given
211+
const breakdownInfo = buildCompleteBreakdownInfo()
212+
const renderDangerousConfirmationPromptSpyOn = vi
213+
.spyOn(ui, 'renderDangerousConfirmationPrompt')
214+
.mockResolvedValue(true)
215+
vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async () => {})
216+
const appTitle = 'app title'
217+
218+
// When
219+
const result = await deployOrReleaseConfirmationPrompt({
220+
...breakdownInfo,
221+
appTitle,
222+
release: true,
223+
force: false,
224+
installCount: 1243,
225+
})
226+
227+
// Then
228+
expect(renderDangerousConfirmationPromptSpyOn).toHaveBeenCalledWith(
229+
expect.objectContaining({
230+
installCount: 1243,
231+
}),
232+
)
233+
expect(result).toBe(true)
234+
})
235+
236+
test('and no force with deleted extensions but installCount 0 should not pass installCount to danger prompt', async () => {
237+
// Given
238+
const breakdownInfo = buildCompleteBreakdownInfo()
239+
const renderDangerousConfirmationPromptSpyOn = vi
240+
.spyOn(ui, 'renderDangerousConfirmationPrompt')
241+
.mockResolvedValue(true)
242+
vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async () => {})
243+
const appTitle = 'app title'
244+
245+
// When
246+
const result = await deployOrReleaseConfirmationPrompt({
247+
...breakdownInfo,
248+
appTitle,
249+
release: true,
250+
force: false,
251+
installCount: 0,
252+
})
253+
254+
// Then
255+
expect(renderDangerousConfirmationPromptSpyOn).toHaveBeenCalledWith(
256+
expect.not.objectContaining({
257+
installCount: expect.anything(),
258+
}),
259+
)
260+
expect(result).toBe(true)
261+
})
262+
209263
test('and no force with deleted extensions but without app title should display the complete confirmation prompt', async () => {
210264
// Given
211265
const breakdownInfo = buildCompleteBreakdownInfo()

packages/app/src/cli/prompts/deploy-release.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,11 @@ async function deployConfirmationPrompt({
162162
confirmation: appTitle,
163163
...(showInstallCountWarning
164164
? {
165-
body: `This ${release ? 'release' : 'version'} removes extensions and related data from ${installCount} app installations.\nUse caution as this may include production data on live stores.`,
165+
warningItem: [
166+
'This release removes extensions and related data from',
167+
{error: installCount.toString()},
168+
'app installations.\nUse caution as this may include production data on live stores.',
169+
],
166170
}
167171
: {}),
168172
})

packages/app/src/cli/services/context/identifiers.test.ts

Lines changed: 153 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import {configExtensionsIdentifiersBreakdown, extensionsIdentifiersDeployBreakdown} from './breakdown-extensions.js'
1+
import {
2+
buildExtensionBreakdownInfo,
3+
configExtensionsIdentifiersBreakdown,
4+
ExtensionIdentifierBreakdownInfo,
5+
extensionsIdentifiersDeployBreakdown,
6+
} from './breakdown-extensions.js'
27
import {ensureDeploymentIdsPresence} from './identifiers.js'
38
import {deployConfirmed} from './identifiers-extensions.js'
49
import {deployOrReleaseConfirmationPrompt} from '../../prompts/deploy-release.js'
@@ -34,6 +39,152 @@ describe('ensureDeploymentIdsPresence', () => {
3439
await expect(ensureDeploymentIdsPresence(params)).rejects.toThrow(AbortSilentError)
3540
})
3641

42+
test('when there are remote-only extensions and not forced, appInstallCount is called with remoteApp.id', async () => {
43+
// Given
44+
const breakdown = buildExtensionsBreakdown()
45+
breakdown.extensionIdentifiersBreakdown.onlyRemote = [buildExtensionBreakdownInfo('removed', 'uuid-1')]
46+
vi.mocked(extensionsIdentifiersDeployBreakdown).mockResolvedValue(breakdown)
47+
vi.mocked(configExtensionsIdentifiersBreakdown).mockResolvedValue(buildConfigBreakdown())
48+
vi.mocked(deployOrReleaseConfirmationPrompt).mockResolvedValue(false)
49+
50+
const remoteApp = testOrganizationApp({id: 'real-app-id', apiKey: 'api-key-different'})
51+
const client = testDeveloperPlatformClient({
52+
appInstallCount: vi.fn().mockResolvedValue(42),
53+
})
54+
55+
const params = {
56+
app: testApp(),
57+
developerPlatformClient: client,
58+
appId: 'api-key-different',
59+
appName: 'appName',
60+
remoteApp,
61+
envIdentifiers: {},
62+
force: false,
63+
release: true,
64+
}
65+
66+
// When
67+
await expect(ensureDeploymentIdsPresence(params)).rejects.toThrow()
68+
69+
// Then
70+
expect(client.appInstallCount).toHaveBeenCalledWith({
71+
id: 'real-app-id',
72+
apiKey: 'api-key-different',
73+
organizationId: remoteApp.organizationId,
74+
})
75+
})
76+
77+
test('when force is true, appInstallCount is not called even with remote-only extensions', async () => {
78+
// Given
79+
const breakdown = buildExtensionsBreakdown()
80+
breakdown.extensionIdentifiersBreakdown.onlyRemote = [buildExtensionBreakdownInfo('removed', 'uuid-1')]
81+
vi.mocked(extensionsIdentifiersDeployBreakdown).mockResolvedValue(breakdown)
82+
vi.mocked(configExtensionsIdentifiersBreakdown).mockResolvedValue(buildConfigBreakdown())
83+
vi.mocked(deployOrReleaseConfirmationPrompt).mockResolvedValue(true)
84+
vi.mocked(deployConfirmed).mockResolvedValue({
85+
extensions: {},
86+
extensionIds: {},
87+
extensionsNonUuidManaged: {},
88+
})
89+
90+
const client = testDeveloperPlatformClient({
91+
appInstallCount: vi.fn().mockResolvedValue(42),
92+
})
93+
94+
const params = {
95+
app: testApp(),
96+
developerPlatformClient: client,
97+
appId: 'appId',
98+
appName: 'appName',
99+
remoteApp: testOrganizationApp(),
100+
envIdentifiers: {},
101+
force: true,
102+
release: true,
103+
}
104+
105+
// When
106+
await ensureDeploymentIdsPresence(params)
107+
108+
// Then
109+
expect(client.appInstallCount).not.toHaveBeenCalled()
110+
})
111+
112+
test('when allowUpdates and allowDeletes are both true, appInstallCount is not called', async () => {
113+
// Given
114+
const breakdown = buildExtensionsBreakdown()
115+
breakdown.extensionIdentifiersBreakdown.onlyRemote = [buildExtensionBreakdownInfo('removed', 'uuid-1')]
116+
vi.mocked(extensionsIdentifiersDeployBreakdown).mockResolvedValue(breakdown)
117+
vi.mocked(configExtensionsIdentifiersBreakdown).mockResolvedValue(buildConfigBreakdown())
118+
vi.mocked(deployOrReleaseConfirmationPrompt).mockResolvedValue(true)
119+
vi.mocked(deployConfirmed).mockResolvedValue({
120+
extensions: {},
121+
extensionIds: {},
122+
extensionsNonUuidManaged: {},
123+
})
124+
125+
const client = testDeveloperPlatformClient({
126+
appInstallCount: vi.fn().mockResolvedValue(42),
127+
})
128+
129+
const params = {
130+
app: testApp(),
131+
developerPlatformClient: client,
132+
appId: 'appId',
133+
appName: 'appName',
134+
remoteApp: testOrganizationApp(),
135+
envIdentifiers: {},
136+
force: false,
137+
allowUpdates: true,
138+
allowDeletes: true,
139+
release: true,
140+
}
141+
142+
// When
143+
await ensureDeploymentIdsPresence(params)
144+
145+
// Then
146+
expect(client.appInstallCount).not.toHaveBeenCalled()
147+
})
148+
149+
test('when appInstallCount throws, installCount is undefined and deploy proceeds', async () => {
150+
// Given
151+
const breakdown = buildExtensionsBreakdown()
152+
breakdown.extensionIdentifiersBreakdown.onlyRemote = [buildExtensionBreakdownInfo('removed', 'uuid-1')]
153+
vi.mocked(extensionsIdentifiersDeployBreakdown).mockResolvedValue(breakdown)
154+
vi.mocked(configExtensionsIdentifiersBreakdown).mockResolvedValue(buildConfigBreakdown())
155+
vi.mocked(deployOrReleaseConfirmationPrompt).mockResolvedValue(true)
156+
vi.mocked(deployConfirmed).mockResolvedValue({
157+
extensions: {},
158+
extensionIds: {},
159+
extensionsNonUuidManaged: {},
160+
})
161+
162+
const client = testDeveloperPlatformClient({
163+
appInstallCount: vi.fn().mockRejectedValue(new Error('API error')),
164+
})
165+
166+
const params = {
167+
app: testApp(),
168+
developerPlatformClient: client,
169+
appId: 'appId',
170+
appName: 'appName',
171+
remoteApp: testOrganizationApp(),
172+
envIdentifiers: {},
173+
force: false,
174+
release: true,
175+
}
176+
177+
// When
178+
await ensureDeploymentIdsPresence(params)
179+
180+
// Then - installCount should be undefined in the prompt call
181+
expect(deployOrReleaseConfirmationPrompt).toHaveBeenCalledWith(
182+
expect.objectContaining({
183+
installCount: undefined,
184+
}),
185+
)
186+
})
187+
37188
test('when the prompt is confirmed post-confirmation actions as run and the result is returned', async () => {
38189
// Given
39190
vi.mocked(extensionsIdentifiersDeployBreakdown).mockResolvedValue(buildExtensionsBreakdown())
@@ -75,7 +226,7 @@ describe('ensureDeploymentIdsPresence', () => {
75226
function buildExtensionsBreakdown() {
76227
return {
77228
extensionIdentifiersBreakdown: {
78-
onlyRemote: [],
229+
onlyRemote: [] as ExtensionIdentifierBreakdownInfo[],
79230
toCreate: [],
80231
toUpdate: [],
81232
fromDashboard: [],

packages/cli-kit/src/private/node/ui/components/DangerousConfirmationPrompt.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface DangerousConfirmationPromptProps {
1616
message: string
1717
confirmation: string
1818
infoTable?: InfoTableProps['table']
19+
warningItem?: TokenItem<InlineToken>
1920
onSubmit: (value: boolean) => void
2021
abortSignal?: AbortSignal
2122
}
@@ -24,6 +25,7 @@ const DangerousConfirmationPrompt: FunctionComponent<DangerousConfirmationPrompt
2425
message,
2526
confirmation,
2627
infoTable,
28+
warningItem,
2729
onSubmit,
2830
abortSignal,
2931
}) => {
@@ -103,6 +105,12 @@ const DangerousConfirmationPrompt: FunctionComponent<DangerousConfirmationPrompt
103105
<InfoTable table={infoTable} />
104106
</Box>
105107
) : null}
108+
{warningItem ? (
109+
<Box flexDirection="column">
110+
<Text color="red">{figures.warning} WARNING</Text>
111+
<TokenizedText item={warningItem} />
112+
</Box>
113+
) : null}
106114
<Box>
107115
<TokenizedText item={['Type', {userInput: confirmation}, 'to confirm, or press Escape to cancel.']} />
108116
</Box>

0 commit comments

Comments
 (0)