@capawesome/cli 4.22.0 → 4.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/commands/apps/automations/update.js +1 -1
- package/dist/commands/apps/builds/create.js +44 -9
- package/dist/commands/apps/builds/create.test.js +85 -0
- package/dist/commands/apps/deployments/create.js +12 -0
- package/dist/commands/apps/deployments/create.test.js +70 -0
- package/dist/commands/apps/destinations/create.js +217 -58
- package/dist/commands/apps/destinations/create.test.js +291 -0
- package/dist/commands/apps/destinations/update.js +47 -1
- package/dist/commands/apps/destinations/update.test.js +138 -0
- package/dist/commands/apps/import.js +2 -4
- package/dist/utils/appflow-export.js +27 -5
- package/dist/utils/appflow-export.test.js +33 -0
- package/dist/utils/cli-options.js +7 -0
- package/dist/utils/cli-options.test.js +16 -0
- package/dist/utils/release-notes.js +55 -0
- package/dist/utils/release-notes.test.js +74 -0
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import appDestinationsService from '../../../services/app-destinations.js';
|
|
|
3
3
|
import appGoogleServiceAccountKeysService from '../../../services/app-google-service-account-keys.js';
|
|
4
4
|
import appsService from '../../../services/apps.js';
|
|
5
5
|
import { withAuth } from '../../../utils/auth.js';
|
|
6
|
+
import { parseListOption } from '../../../utils/cli-options.js';
|
|
6
7
|
import { isInteractive } from '../../../utils/environment.js';
|
|
7
8
|
import { isReadable } from '../../../utils/file.js';
|
|
8
9
|
import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
|
|
@@ -18,12 +19,42 @@ export default defineCommand({
|
|
|
18
19
|
json: z.boolean().optional().describe('Output in JSON format.'),
|
|
19
20
|
name: z.string().optional().describe('Name of the destination.'),
|
|
20
21
|
platform: z.enum(['android', 'ios']).optional().describe('Platform of the destination (android, ios).'),
|
|
22
|
+
type: z
|
|
23
|
+
.enum([
|
|
24
|
+
'app-store-connect',
|
|
25
|
+
'apple-app-store-connect',
|
|
26
|
+
'firebase-app-distribution',
|
|
27
|
+
'google-play',
|
|
28
|
+
'huawei-appgallery',
|
|
29
|
+
])
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('Type of the destination (apple-app-store-connect, firebase-app-distribution, google-play, huawei-appgallery). Defaults to `google-play` for android and `apple-app-store-connect` for ios in non-interactive environments.'),
|
|
21
32
|
appleId: z.string().optional().describe('Apple ID for the destination.'),
|
|
22
33
|
appleAppId: z.string().optional().describe('Apple App ID for the destination.'),
|
|
23
34
|
appleTeamId: z.string().optional().describe('Apple Team ID for the destination.'),
|
|
24
35
|
appleAppPassword: z.string().optional().describe('Apple app-specific password for the destination.'),
|
|
25
36
|
appleApiKeyFile: z.string().optional().describe('Path to the Apple API key (.p8) file.'),
|
|
26
37
|
appleIssuerId: z.string().optional().describe('Apple Issuer ID for the destination.'),
|
|
38
|
+
appleBetaGroup: z
|
|
39
|
+
.array(z.string())
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('Name of an external TestFlight beta group to distribute to. Can be specified multiple times or comma-separated.'),
|
|
42
|
+
appleSubmitForReview: z
|
|
43
|
+
.union([z.boolean(), z.stringbool()], {
|
|
44
|
+
message: 'Apple submit for review must be either `true` or `false`.',
|
|
45
|
+
})
|
|
46
|
+
.optional()
|
|
47
|
+
.describe('Submit the build for App Review after the upload (App Store Connect only, requires an API key).'),
|
|
48
|
+
appleReleaseType: z
|
|
49
|
+
.enum(['after-approval', 'manual'])
|
|
50
|
+
.optional()
|
|
51
|
+
.describe('Release type of the App Store version after approval (App Store Connect only).'),
|
|
52
|
+
appleRejectIfPossible: z
|
|
53
|
+
.union([z.boolean(), z.stringbool()], {
|
|
54
|
+
message: 'Apple reject if possible must be either `true` or `false`.',
|
|
55
|
+
})
|
|
56
|
+
.optional()
|
|
57
|
+
.describe('Cancel a submission waiting for review before submitting the new build (App Store Connect only).'),
|
|
27
58
|
androidPackageName: z.string().optional().describe('Android package name for the destination.'),
|
|
28
59
|
androidBuildArtifactType: z.enum(['aab', 'apk']).optional().describe('Android build artifact type (aab, apk).'),
|
|
29
60
|
androidReleaseStatus: z
|
|
@@ -32,9 +63,28 @@ export default defineCommand({
|
|
|
32
63
|
.describe('Android release status (completed, draft).'),
|
|
33
64
|
googleServiceAccountKeyFile: z.string().optional().describe('Path to the Google service account key JSON file.'),
|
|
34
65
|
googlePlayTrack: z.string().optional().describe('Google Play track for the destination.'),
|
|
66
|
+
firebaseAppId: z.string().optional().describe('Firebase app ID for the destination.'),
|
|
67
|
+
firebaseTesterGroup: z
|
|
68
|
+
.array(z.string())
|
|
69
|
+
.optional()
|
|
70
|
+
.describe('The alias of a Firebase tester group to distribute to. Can be specified multiple times or comma-separated.'),
|
|
71
|
+
huaweiAppId: z.string().optional().describe('Huawei AppGallery app ID for the destination.'),
|
|
72
|
+
huaweiClientId: z.string().optional().describe('Huawei AppGallery Connect API client ID for the destination.'),
|
|
73
|
+
huaweiClientSecret: z
|
|
74
|
+
.string()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe('Huawei AppGallery Connect API client secret for the destination.'),
|
|
77
|
+
defaultLanguage: z
|
|
78
|
+
.string()
|
|
79
|
+
.optional()
|
|
80
|
+
.describe('Language of the default release notes text, e.g. `en-US` (Google Play and Huawei AppGallery only).'),
|
|
35
81
|
})),
|
|
36
82
|
action: withAuth(async (options, args) => {
|
|
37
|
-
let { appId, json, name, platform, appleId, appleAppId, appleTeamId, appleAppPassword, appleApiKeyFile, appleIssuerId, androidPackageName, androidBuildArtifactType, androidReleaseStatus, googleServiceAccountKeyFile, googlePlayTrack, } = options;
|
|
83
|
+
let { appId, json, name, platform, type, appleId, appleAppId, appleTeamId, appleAppPassword, appleApiKeyFile, appleIssuerId, appleBetaGroup, appleSubmitForReview, appleReleaseType, appleRejectIfPossible, androidPackageName, androidBuildArtifactType, androidReleaseStatus, googleServiceAccountKeyFile, googlePlayTrack, firebaseAppId, firebaseTesterGroup, huaweiAppId, huaweiClientId, huaweiClientSecret, defaultLanguage, } = options;
|
|
84
|
+
if (type === 'app-store-connect') {
|
|
85
|
+
consola.warn('The destination type `app-store-connect` is deprecated. Use `apple-app-store-connect` instead.');
|
|
86
|
+
type = 'apple-app-store-connect';
|
|
87
|
+
}
|
|
38
88
|
let appleApiKeyId;
|
|
39
89
|
let appAppleApiKeyId;
|
|
40
90
|
let appGoogleServiceAccountKeyId;
|
|
@@ -85,7 +135,29 @@ export default defineCommand({
|
|
|
85
135
|
process.exit(1);
|
|
86
136
|
}
|
|
87
137
|
}
|
|
88
|
-
|
|
138
|
+
// Select destination type
|
|
139
|
+
if (type) {
|
|
140
|
+
const supportedTypes = destinationTypeOptionsByPlatform[platform].map((option) => option.value);
|
|
141
|
+
if (!supportedTypes.includes(type)) {
|
|
142
|
+
consola.error(`The destination type \`${type}\` is not supported for the ${platform} platform. Supported types: ${supportedTypes.join(', ')}.`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else if (isInteractive()) {
|
|
147
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
148
|
+
type = await prompt('Select the destination type:', {
|
|
149
|
+
type: 'select',
|
|
150
|
+
options: destinationTypeOptionsByPlatform[platform],
|
|
151
|
+
});
|
|
152
|
+
if (!type) {
|
|
153
|
+
consola.error('You must select a destination type.');
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
type = defaultDestinationTypeByPlatform[platform];
|
|
159
|
+
}
|
|
160
|
+
if (type === 'google-play') {
|
|
89
161
|
// 4. Ask for track
|
|
90
162
|
if (!googlePlayTrack) {
|
|
91
163
|
if (!isInteractive()) {
|
|
@@ -112,72 +184,54 @@ export default defineCommand({
|
|
|
112
184
|
}
|
|
113
185
|
// 6. Ask for publishing format
|
|
114
186
|
if (!androidBuildArtifactType) {
|
|
115
|
-
|
|
116
|
-
consola.error('You must provide the Android build artifact type when running in non-interactive environment.');
|
|
117
|
-
process.exit(1);
|
|
118
|
-
}
|
|
119
|
-
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
120
|
-
androidBuildArtifactType = await prompt('Select the publishing format:', {
|
|
121
|
-
type: 'select',
|
|
122
|
-
options: [
|
|
123
|
-
{ label: 'AAB', value: 'aab' },
|
|
124
|
-
{ label: 'APK', value: 'apk' },
|
|
125
|
-
],
|
|
126
|
-
});
|
|
127
|
-
if (!androidBuildArtifactType) {
|
|
128
|
-
consola.error('You must select a publishing format.');
|
|
129
|
-
process.exit(1);
|
|
130
|
-
}
|
|
187
|
+
androidBuildArtifactType = await promptAndroidBuildArtifactType();
|
|
131
188
|
}
|
|
132
189
|
// 7. Ask for release status
|
|
133
190
|
if (!androidReleaseStatus) {
|
|
134
|
-
|
|
135
|
-
consola.error('You must provide the Android release status when running in non-interactive environment.');
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}
|
|
138
|
-
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
139
|
-
androidReleaseStatus = await prompt('Select the release status:', {
|
|
140
|
-
type: 'select',
|
|
141
|
-
options: [
|
|
142
|
-
{ label: 'Draft', value: 'draft' },
|
|
143
|
-
{ label: 'Completed', value: 'completed' },
|
|
144
|
-
],
|
|
145
|
-
});
|
|
146
|
-
if (!androidReleaseStatus) {
|
|
147
|
-
consola.error('You must select a release status.');
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
191
|
+
androidReleaseStatus = await promptAndroidReleaseStatus();
|
|
150
192
|
}
|
|
151
193
|
// 8. Ask for JSON key path
|
|
152
194
|
if (!googleServiceAccountKeyFile) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
195
|
+
googleServiceAccountKeyFile = await promptGoogleServiceAccountKeyFile();
|
|
196
|
+
}
|
|
197
|
+
appGoogleServiceAccountKeyId = await uploadGoogleServiceAccountKeyFile(appId, googleServiceAccountKeyFile);
|
|
198
|
+
}
|
|
199
|
+
if (type === 'huawei-appgallery') {
|
|
200
|
+
if (!huaweiAppId) {
|
|
201
|
+
huaweiAppId = await promptRequiredText('Huawei AppGallery app ID');
|
|
202
|
+
}
|
|
203
|
+
if (!huaweiClientId) {
|
|
204
|
+
huaweiClientId = await promptRequiredText('Huawei AppGallery client ID');
|
|
205
|
+
}
|
|
206
|
+
if (!huaweiClientSecret) {
|
|
207
|
+
huaweiClientSecret = await promptRequiredText('Huawei AppGallery client secret');
|
|
208
|
+
}
|
|
209
|
+
if (!androidBuildArtifactType) {
|
|
210
|
+
androidBuildArtifactType = await promptAndroidBuildArtifactType();
|
|
211
|
+
}
|
|
212
|
+
if (!androidReleaseStatus) {
|
|
213
|
+
androidReleaseStatus = await promptAndroidReleaseStatus();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (type === 'firebase-app-distribution') {
|
|
217
|
+
if (!firebaseAppId) {
|
|
218
|
+
firebaseAppId = await promptRequiredText('Firebase app ID');
|
|
219
|
+
}
|
|
220
|
+
if (!firebaseTesterGroup && isInteractive()) {
|
|
221
|
+
const firebaseTesterGroupInput = await prompt('Enter the Firebase tester group aliases, comma-separated (leave empty to skip):', { type: 'text' });
|
|
222
|
+
if (firebaseTesterGroupInput) {
|
|
223
|
+
firebaseTesterGroup = [firebaseTesterGroupInput];
|
|
163
224
|
}
|
|
164
225
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (!googleServiceAccountKeyFileReadable) {
|
|
168
|
-
consola.error(`The Google service account key file does not exist or is not accessible: ${googleServiceAccountKeyFile}`);
|
|
169
|
-
process.exit(1);
|
|
226
|
+
if (platform === 'android' && !androidBuildArtifactType) {
|
|
227
|
+
androidBuildArtifactType = await promptAndroidBuildArtifactType();
|
|
170
228
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
buffer,
|
|
176
|
-
fileName,
|
|
177
|
-
});
|
|
178
|
-
appGoogleServiceAccountKeyId = key.id;
|
|
229
|
+
if (!googleServiceAccountKeyFile) {
|
|
230
|
+
googleServiceAccountKeyFile = await promptGoogleServiceAccountKeyFile();
|
|
231
|
+
}
|
|
232
|
+
appGoogleServiceAccountKeyId = await uploadGoogleServiceAccountKeyFile(appId, googleServiceAccountKeyFile);
|
|
179
233
|
}
|
|
180
|
-
if (
|
|
234
|
+
if (type === 'apple-app-store-connect') {
|
|
181
235
|
// 9. Ask for authentication method
|
|
182
236
|
let authMethod;
|
|
183
237
|
if (appleApiKeyFile || appleIssuerId) {
|
|
@@ -313,6 +367,7 @@ export default defineCommand({
|
|
|
313
367
|
appId,
|
|
314
368
|
name,
|
|
315
369
|
platform: platform,
|
|
370
|
+
type,
|
|
316
371
|
appleId,
|
|
317
372
|
appleAppId,
|
|
318
373
|
appleTeamId,
|
|
@@ -320,11 +375,21 @@ export default defineCommand({
|
|
|
320
375
|
appleApiKeyId,
|
|
321
376
|
appleIssuerId,
|
|
322
377
|
appAppleApiKeyId,
|
|
378
|
+
appleBetaGroups: parseListOption(appleBetaGroup),
|
|
379
|
+
appleRejectIfPossible,
|
|
380
|
+
appleReleaseType,
|
|
381
|
+
appleSubmitForReview,
|
|
323
382
|
androidPackageName,
|
|
324
383
|
androidBuildArtifactType,
|
|
325
384
|
androidReleaseStatus,
|
|
326
385
|
appGoogleServiceAccountKeyId,
|
|
327
386
|
googlePlayTrack,
|
|
387
|
+
firebaseAppId,
|
|
388
|
+
firebaseTesterGroups: parseListOption(firebaseTesterGroup),
|
|
389
|
+
huaweiAppId,
|
|
390
|
+
huaweiClientId,
|
|
391
|
+
huaweiClientSecret,
|
|
392
|
+
defaultLanguage,
|
|
328
393
|
});
|
|
329
394
|
if (json) {
|
|
330
395
|
console.log(JSON.stringify({ id: response.id }, null, 2));
|
|
@@ -335,3 +400,97 @@ export default defineCommand({
|
|
|
335
400
|
}
|
|
336
401
|
}),
|
|
337
402
|
});
|
|
403
|
+
const destinationTypeOptionsByPlatform = {
|
|
404
|
+
android: [
|
|
405
|
+
{ label: 'Google Play', value: 'google-play' },
|
|
406
|
+
{ label: 'Firebase App Distribution', value: 'firebase-app-distribution' },
|
|
407
|
+
{ label: 'Huawei AppGallery', value: 'huawei-appgallery' },
|
|
408
|
+
],
|
|
409
|
+
ios: [
|
|
410
|
+
{ label: 'App Store Connect', value: 'apple-app-store-connect' },
|
|
411
|
+
{ label: 'Firebase App Distribution', value: 'firebase-app-distribution' },
|
|
412
|
+
],
|
|
413
|
+
};
|
|
414
|
+
const defaultDestinationTypeByPlatform = {
|
|
415
|
+
android: 'google-play',
|
|
416
|
+
ios: 'apple-app-store-connect',
|
|
417
|
+
};
|
|
418
|
+
const promptRequiredText = async (label) => {
|
|
419
|
+
if (!isInteractive()) {
|
|
420
|
+
consola.error(`You must provide the ${label} when running in non-interactive environment.`);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
const value = await prompt(`Enter the ${label}:`, { type: 'text' });
|
|
424
|
+
if (!value) {
|
|
425
|
+
consola.error(`You must provide the ${label}.`);
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
return value;
|
|
429
|
+
};
|
|
430
|
+
const promptAndroidBuildArtifactType = async () => {
|
|
431
|
+
if (!isInteractive()) {
|
|
432
|
+
consola.error('You must provide the Android build artifact type when running in non-interactive environment.');
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
436
|
+
const androidBuildArtifactType = await prompt('Select the publishing format:', {
|
|
437
|
+
type: 'select',
|
|
438
|
+
options: [
|
|
439
|
+
{ label: 'AAB', value: 'aab' },
|
|
440
|
+
{ label: 'APK', value: 'apk' },
|
|
441
|
+
],
|
|
442
|
+
});
|
|
443
|
+
if (!androidBuildArtifactType) {
|
|
444
|
+
consola.error('You must select a publishing format.');
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
return androidBuildArtifactType;
|
|
448
|
+
};
|
|
449
|
+
const promptAndroidReleaseStatus = async () => {
|
|
450
|
+
if (!isInteractive()) {
|
|
451
|
+
consola.error('You must provide the Android release status when running in non-interactive environment.');
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
455
|
+
const androidReleaseStatus = await prompt('Select the release status:', {
|
|
456
|
+
type: 'select',
|
|
457
|
+
options: [
|
|
458
|
+
{ label: 'Draft', value: 'draft' },
|
|
459
|
+
{ label: 'Completed', value: 'completed' },
|
|
460
|
+
],
|
|
461
|
+
});
|
|
462
|
+
if (!androidReleaseStatus) {
|
|
463
|
+
consola.error('You must select a release status.');
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
return androidReleaseStatus;
|
|
467
|
+
};
|
|
468
|
+
const promptGoogleServiceAccountKeyFile = async () => {
|
|
469
|
+
if (!isInteractive()) {
|
|
470
|
+
consola.error('You must provide the Google service account key file when running in non-interactive environment.');
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
const googleServiceAccountKeyFile = await prompt('Enter the path to the Google service account key JSON file:', {
|
|
474
|
+
type: 'text',
|
|
475
|
+
});
|
|
476
|
+
if (!googleServiceAccountKeyFile) {
|
|
477
|
+
consola.error('You must provide a Google service account key file path.');
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
return googleServiceAccountKeyFile;
|
|
481
|
+
};
|
|
482
|
+
const uploadGoogleServiceAccountKeyFile = async (appId, googleServiceAccountKeyFile) => {
|
|
483
|
+
const googleServiceAccountKeyFileReadable = await isReadable(googleServiceAccountKeyFile);
|
|
484
|
+
if (!googleServiceAccountKeyFileReadable) {
|
|
485
|
+
consola.error(`The Google service account key file does not exist or is not accessible: ${googleServiceAccountKeyFile}`);
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
const buffer = fs.readFileSync(googleServiceAccountKeyFile);
|
|
489
|
+
const fileName = path.basename(googleServiceAccountKeyFile);
|
|
490
|
+
const key = await appGoogleServiceAccountKeysService.create({
|
|
491
|
+
appId,
|
|
492
|
+
buffer,
|
|
493
|
+
fileName,
|
|
494
|
+
});
|
|
495
|
+
return key.id;
|
|
496
|
+
};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
|
|
2
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
3
|
+
import { isInteractive } from '../../../utils/environment.js';
|
|
4
|
+
import { prompt } from '../../../utils/prompt.js';
|
|
5
|
+
import userConfig from '../../../utils/user-config.js';
|
|
6
|
+
import consola from 'consola';
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import nock from 'nock';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import createDestinationCommand from './create.js';
|
|
13
|
+
// Mock dependencies
|
|
14
|
+
vi.mock('@/utils/user-config.js');
|
|
15
|
+
vi.mock('@/utils/prompt.js');
|
|
16
|
+
vi.mock('@/services/authorization-service.js');
|
|
17
|
+
vi.mock('@/utils/environment.js');
|
|
18
|
+
vi.mock('consola');
|
|
19
|
+
describe('apps-destinations-create', () => {
|
|
20
|
+
const mockUserConfig = vi.mocked(userConfig);
|
|
21
|
+
const mockPrompt = vi.mocked(prompt);
|
|
22
|
+
const mockIsInteractive = vi.mocked(isInteractive);
|
|
23
|
+
const mockConsola = vi.mocked(consola);
|
|
24
|
+
const mockAuthorizationService = vi.mocked(authorizationService);
|
|
25
|
+
const appId = 'app-123';
|
|
26
|
+
const destinationId = 'destination-456';
|
|
27
|
+
const googleServiceAccountKeyId = 'key-789';
|
|
28
|
+
const appleApiKeyId = 'apple-key-012';
|
|
29
|
+
const testToken = 'test-token';
|
|
30
|
+
let tempDirectory;
|
|
31
|
+
let googleServiceAccountKeyFile;
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
mockUserConfig.read.mockReturnValue({ token: testToken });
|
|
35
|
+
mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue(testToken);
|
|
36
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
|
|
37
|
+
mockIsInteractive.mockReturnValue(false);
|
|
38
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
39
|
+
throw new Error(`Process exited with code ${code}`);
|
|
40
|
+
});
|
|
41
|
+
tempDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'capawesome-cli-destinations-create-'));
|
|
42
|
+
googleServiceAccountKeyFile = path.join(tempDirectory, 'service-account.json');
|
|
43
|
+
await fs.writeFile(googleServiceAccountKeyFile, '{}');
|
|
44
|
+
});
|
|
45
|
+
afterEach(async () => {
|
|
46
|
+
nock.cleanAll();
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
await fs.rm(tempDirectory, { force: true, recursive: true });
|
|
49
|
+
});
|
|
50
|
+
it('should create a Huawei AppGallery destination', async () => {
|
|
51
|
+
const options = {
|
|
52
|
+
appId,
|
|
53
|
+
name: 'Huawei',
|
|
54
|
+
platform: 'android',
|
|
55
|
+
type: 'huawei-appgallery',
|
|
56
|
+
huaweiAppId: '112233445',
|
|
57
|
+
huaweiClientId: 'client-id',
|
|
58
|
+
huaweiClientSecret: 'client-secret',
|
|
59
|
+
androidBuildArtifactType: 'aab',
|
|
60
|
+
androidReleaseStatus: 'draft',
|
|
61
|
+
defaultLanguage: 'en-US',
|
|
62
|
+
};
|
|
63
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
64
|
+
.post(`/v1/apps/${appId}/destinations`, {
|
|
65
|
+
appId,
|
|
66
|
+
name: 'Huawei',
|
|
67
|
+
platform: 'android',
|
|
68
|
+
type: 'huawei-appgallery',
|
|
69
|
+
androidBuildArtifactType: 'aab',
|
|
70
|
+
androidReleaseStatus: 'draft',
|
|
71
|
+
huaweiAppId: '112233445',
|
|
72
|
+
huaweiClientId: 'client-id',
|
|
73
|
+
huaweiClientSecret: 'client-secret',
|
|
74
|
+
defaultLanguage: 'en-US',
|
|
75
|
+
})
|
|
76
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
77
|
+
.reply(201, { id: destinationId });
|
|
78
|
+
await createDestinationCommand.action(options, undefined);
|
|
79
|
+
expect(scope.isDone()).toBe(true);
|
|
80
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Destination created successfully.');
|
|
81
|
+
});
|
|
82
|
+
it('should create an App Store Connect destination with the Apple submission settings', async () => {
|
|
83
|
+
mockIsInteractive.mockReturnValue(true);
|
|
84
|
+
mockPrompt.mockResolvedValueOnce('ABC123DEFG');
|
|
85
|
+
const appleApiKeyFile = path.join(tempDirectory, 'AuthKey.p8');
|
|
86
|
+
await fs.writeFile(appleApiKeyFile, 'private-key');
|
|
87
|
+
const options = {
|
|
88
|
+
appId,
|
|
89
|
+
name: 'App Store',
|
|
90
|
+
platform: 'ios',
|
|
91
|
+
type: 'apple-app-store-connect',
|
|
92
|
+
appleApiKeyFile,
|
|
93
|
+
appleIssuerId: 'issuer-id',
|
|
94
|
+
appleTeamId: 'team-id',
|
|
95
|
+
appleBetaGroup: ['External Testers, QA'],
|
|
96
|
+
appleSubmitForReview: true,
|
|
97
|
+
appleReleaseType: 'after-approval',
|
|
98
|
+
appleRejectIfPossible: false,
|
|
99
|
+
};
|
|
100
|
+
const keyScope = nock(DEFAULT_API_BASE_URL)
|
|
101
|
+
.post(`/v1/apps/${appId}/apple-api-keys`)
|
|
102
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
103
|
+
.reply(201, { id: appleApiKeyId });
|
|
104
|
+
const destinationScope = nock(DEFAULT_API_BASE_URL)
|
|
105
|
+
.post(`/v1/apps/${appId}/destinations`, {
|
|
106
|
+
appId,
|
|
107
|
+
name: 'App Store',
|
|
108
|
+
platform: 'ios',
|
|
109
|
+
type: 'apple-app-store-connect',
|
|
110
|
+
appleTeamId: 'team-id',
|
|
111
|
+
appleApiKeyId: 'ABC123DEFG',
|
|
112
|
+
appleIssuerId: 'issuer-id',
|
|
113
|
+
appAppleApiKeyId: appleApiKeyId,
|
|
114
|
+
appleBetaGroups: ['External Testers', 'QA'],
|
|
115
|
+
appleRejectIfPossible: false,
|
|
116
|
+
appleReleaseType: 'after-approval',
|
|
117
|
+
appleSubmitForReview: true,
|
|
118
|
+
})
|
|
119
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
120
|
+
.reply(201, { id: destinationId });
|
|
121
|
+
await createDestinationCommand.action(options, undefined);
|
|
122
|
+
expect(keyScope.isDone()).toBe(true);
|
|
123
|
+
expect(destinationScope.isDone()).toBe(true);
|
|
124
|
+
expect(mockPrompt).toHaveBeenCalledTimes(1);
|
|
125
|
+
});
|
|
126
|
+
it('should map the deprecated destination type app-store-connect to apple-app-store-connect', async () => {
|
|
127
|
+
mockIsInteractive.mockReturnValue(true);
|
|
128
|
+
mockPrompt.mockResolvedValueOnce('ABC123DEFG');
|
|
129
|
+
const appleApiKeyFile = path.join(tempDirectory, 'AuthKey.p8');
|
|
130
|
+
await fs.writeFile(appleApiKeyFile, 'private-key');
|
|
131
|
+
const options = {
|
|
132
|
+
appId,
|
|
133
|
+
name: 'App Store',
|
|
134
|
+
platform: 'ios',
|
|
135
|
+
type: 'app-store-connect',
|
|
136
|
+
appleApiKeyFile,
|
|
137
|
+
appleIssuerId: 'issuer-id',
|
|
138
|
+
appleTeamId: 'team-id',
|
|
139
|
+
appleBetaGroup: ['External Testers, QA'],
|
|
140
|
+
appleSubmitForReview: true,
|
|
141
|
+
appleReleaseType: 'after-approval',
|
|
142
|
+
appleRejectIfPossible: false,
|
|
143
|
+
};
|
|
144
|
+
const keyScope = nock(DEFAULT_API_BASE_URL)
|
|
145
|
+
.post(`/v1/apps/${appId}/apple-api-keys`)
|
|
146
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
147
|
+
.reply(201, { id: appleApiKeyId });
|
|
148
|
+
const destinationScope = nock(DEFAULT_API_BASE_URL)
|
|
149
|
+
.post(`/v1/apps/${appId}/destinations`, {
|
|
150
|
+
appId,
|
|
151
|
+
name: 'App Store',
|
|
152
|
+
platform: 'ios',
|
|
153
|
+
type: 'apple-app-store-connect',
|
|
154
|
+
appleTeamId: 'team-id',
|
|
155
|
+
appleApiKeyId: 'ABC123DEFG',
|
|
156
|
+
appleIssuerId: 'issuer-id',
|
|
157
|
+
appAppleApiKeyId: appleApiKeyId,
|
|
158
|
+
appleBetaGroups: ['External Testers', 'QA'],
|
|
159
|
+
appleRejectIfPossible: false,
|
|
160
|
+
appleReleaseType: 'after-approval',
|
|
161
|
+
appleSubmitForReview: true,
|
|
162
|
+
})
|
|
163
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
164
|
+
.reply(201, { id: destinationId });
|
|
165
|
+
await createDestinationCommand.action(options, undefined);
|
|
166
|
+
expect(keyScope.isDone()).toBe(true);
|
|
167
|
+
expect(destinationScope.isDone()).toBe(true);
|
|
168
|
+
expect(mockPrompt).toHaveBeenCalledTimes(1);
|
|
169
|
+
expect(mockConsola.warn).toHaveBeenCalledWith('The destination type `app-store-connect` is deprecated. Use `apple-app-store-connect` instead.');
|
|
170
|
+
});
|
|
171
|
+
it('should create a Firebase App Distribution destination for iOS', async () => {
|
|
172
|
+
const options = {
|
|
173
|
+
appId,
|
|
174
|
+
name: 'Firebase',
|
|
175
|
+
platform: 'ios',
|
|
176
|
+
type: 'firebase-app-distribution',
|
|
177
|
+
firebaseAppId: '1:123456789012:ios:0a1b2c3d4e5f67890',
|
|
178
|
+
firebaseTesterGroup: ['qa-team, beta-testers', 'internal'],
|
|
179
|
+
googleServiceAccountKeyFile,
|
|
180
|
+
};
|
|
181
|
+
const keyScope = nock(DEFAULT_API_BASE_URL)
|
|
182
|
+
.post(`/v1/apps/${appId}/google-service-account-keys`)
|
|
183
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
184
|
+
.reply(201, { id: googleServiceAccountKeyId });
|
|
185
|
+
const destinationScope = nock(DEFAULT_API_BASE_URL)
|
|
186
|
+
.post(`/v1/apps/${appId}/destinations`, {
|
|
187
|
+
appId,
|
|
188
|
+
name: 'Firebase',
|
|
189
|
+
platform: 'ios',
|
|
190
|
+
type: 'firebase-app-distribution',
|
|
191
|
+
appGoogleServiceAccountKeyId: googleServiceAccountKeyId,
|
|
192
|
+
firebaseAppId: '1:123456789012:ios:0a1b2c3d4e5f67890',
|
|
193
|
+
firebaseTesterGroups: ['qa-team', 'beta-testers', 'internal'],
|
|
194
|
+
})
|
|
195
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
196
|
+
.reply(201, { id: destinationId });
|
|
197
|
+
await createDestinationCommand.action(options, undefined);
|
|
198
|
+
expect(keyScope.isDone()).toBe(true);
|
|
199
|
+
expect(destinationScope.isDone()).toBe(true);
|
|
200
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Destination created successfully.');
|
|
201
|
+
});
|
|
202
|
+
it('should require the publishing format for a Firebase App Distribution destination for Android', async () => {
|
|
203
|
+
const options = {
|
|
204
|
+
appId,
|
|
205
|
+
name: 'Firebase',
|
|
206
|
+
platform: 'android',
|
|
207
|
+
type: 'firebase-app-distribution',
|
|
208
|
+
firebaseAppId: '1:123456789012:android:0a1b2c3d4e5f67890',
|
|
209
|
+
googleServiceAccountKeyFile,
|
|
210
|
+
};
|
|
211
|
+
await expect(createDestinationCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
212
|
+
expect(mockConsola.error).toHaveBeenCalledWith('You must provide the Android build artifact type when running in non-interactive environment.');
|
|
213
|
+
});
|
|
214
|
+
it('should reject a type that is not supported for the platform', async () => {
|
|
215
|
+
const options = {
|
|
216
|
+
appId,
|
|
217
|
+
name: 'Huawei',
|
|
218
|
+
platform: 'ios',
|
|
219
|
+
type: 'huawei-appgallery',
|
|
220
|
+
};
|
|
221
|
+
await expect(createDestinationCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
222
|
+
expect(mockConsola.error).toHaveBeenCalledWith('The destination type `huawei-appgallery` is not supported for the ios platform. Supported types: apple-app-store-connect, firebase-app-distribution.');
|
|
223
|
+
});
|
|
224
|
+
it('should default to the Google Play type for Android when running non-interactively', async () => {
|
|
225
|
+
const options = {
|
|
226
|
+
appId,
|
|
227
|
+
name: 'Google Play',
|
|
228
|
+
platform: 'android',
|
|
229
|
+
androidPackageName: 'com.example.app',
|
|
230
|
+
androidBuildArtifactType: 'aab',
|
|
231
|
+
androidReleaseStatus: 'completed',
|
|
232
|
+
googlePlayTrack: 'internal',
|
|
233
|
+
googleServiceAccountKeyFile,
|
|
234
|
+
};
|
|
235
|
+
const keyScope = nock(DEFAULT_API_BASE_URL)
|
|
236
|
+
.post(`/v1/apps/${appId}/google-service-account-keys`)
|
|
237
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
238
|
+
.reply(201, { id: googleServiceAccountKeyId });
|
|
239
|
+
const destinationScope = nock(DEFAULT_API_BASE_URL)
|
|
240
|
+
.post(`/v1/apps/${appId}/destinations`, {
|
|
241
|
+
appId,
|
|
242
|
+
name: 'Google Play',
|
|
243
|
+
platform: 'android',
|
|
244
|
+
type: 'google-play',
|
|
245
|
+
androidPackageName: 'com.example.app',
|
|
246
|
+
androidBuildArtifactType: 'aab',
|
|
247
|
+
androidReleaseStatus: 'completed',
|
|
248
|
+
appGoogleServiceAccountKeyId: googleServiceAccountKeyId,
|
|
249
|
+
googlePlayTrack: 'internal',
|
|
250
|
+
})
|
|
251
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
252
|
+
.reply(201, { id: destinationId });
|
|
253
|
+
await createDestinationCommand.action(options, undefined);
|
|
254
|
+
expect(keyScope.isDone()).toBe(true);
|
|
255
|
+
expect(destinationScope.isDone()).toBe(true);
|
|
256
|
+
expect(mockPrompt).not.toHaveBeenCalled();
|
|
257
|
+
});
|
|
258
|
+
it('should prompt for the type with the options of the platform when running interactively', async () => {
|
|
259
|
+
mockIsInteractive.mockReturnValue(true);
|
|
260
|
+
mockPrompt.mockResolvedValueOnce('firebase-app-distribution').mockResolvedValueOnce('');
|
|
261
|
+
const options = {
|
|
262
|
+
appId,
|
|
263
|
+
name: 'Firebase',
|
|
264
|
+
platform: 'ios',
|
|
265
|
+
firebaseAppId: '1:123456789012:ios:0a1b2c3d4e5f67890',
|
|
266
|
+
googleServiceAccountKeyFile,
|
|
267
|
+
};
|
|
268
|
+
nock(DEFAULT_API_BASE_URL)
|
|
269
|
+
.post(`/v1/apps/${appId}/google-service-account-keys`)
|
|
270
|
+
.reply(201, { id: googleServiceAccountKeyId });
|
|
271
|
+
const destinationScope = nock(DEFAULT_API_BASE_URL)
|
|
272
|
+
.post(`/v1/apps/${appId}/destinations`, {
|
|
273
|
+
appId,
|
|
274
|
+
name: 'Firebase',
|
|
275
|
+
platform: 'ios',
|
|
276
|
+
type: 'firebase-app-distribution',
|
|
277
|
+
appGoogleServiceAccountKeyId: googleServiceAccountKeyId,
|
|
278
|
+
firebaseAppId: '1:123456789012:ios:0a1b2c3d4e5f67890',
|
|
279
|
+
})
|
|
280
|
+
.reply(201, { id: destinationId });
|
|
281
|
+
await createDestinationCommand.action(options, undefined);
|
|
282
|
+
expect(destinationScope.isDone()).toBe(true);
|
|
283
|
+
expect(mockPrompt).toHaveBeenCalledWith('Select the destination type:', {
|
|
284
|
+
type: 'select',
|
|
285
|
+
options: [
|
|
286
|
+
{ label: 'App Store Connect', value: 'apple-app-store-connect' },
|
|
287
|
+
{ label: 'Firebase App Distribution', value: 'firebase-app-distribution' },
|
|
288
|
+
],
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
});
|