From c94638e57d3b4df3b9eb256c07e2b8d9f031b3cd Mon Sep 17 00:00:00 2001 From: Quinlan Jung Date: Wed, 15 Apr 2020 20:31:46 -0700 Subject: [PATCH] [cli] better non interactive support for creds (#1881) * [cli] better non interactive support for creds * [cli] credential tests * pass in non interactive sites to constructors --- .../src/commands/build/ios/IOSBuilder.ts | 13 +- .../src/credentials/test-fixtures/mocks.ts | 77 ++++++++++++ .../src/credentials/views/IosDistCert.ts | 49 ++++++-- .../views/IosProvisioningProfile.ts | 54 ++++++--- .../credentials/views/IosPushCredentials.ts | 51 +++++--- .../src/credentials/views/SetupIosDist.ts | 21 ++-- .../views/SetupIosProvisioningProfile.ts | 14 ++- .../src/credentials/views/SetupIosPush.ts | 25 ++-- .../views/__tests__/IosDistCert-test.ts | 94 +++++++++++++++ .../__tests__/IosProvisioningProfile-test.ts | 114 ++++++++++++++++++ .../__tests__/IosPushCredentials-test.ts | 93 ++++++++++++++ .../views/__tests__/SetupIosDist-test.ts | 48 ++++++++ .../views/__tests__/SetupIosPush-test.ts | 48 ++++++++ .../SetupProvisioningProfile-test.ts | 61 ++++++++++ 14 files changed, 683 insertions(+), 79 deletions(-) create mode 100644 packages/expo-cli/src/credentials/test-fixtures/mocks.ts create mode 100644 packages/expo-cli/src/credentials/views/__tests__/IosDistCert-test.ts create mode 100644 packages/expo-cli/src/credentials/views/__tests__/IosProvisioningProfile-test.ts create mode 100644 packages/expo-cli/src/credentials/views/__tests__/IosPushCredentials-test.ts create mode 100644 packages/expo-cli/src/credentials/views/__tests__/SetupIosDist-test.ts create mode 100644 packages/expo-cli/src/credentials/views/__tests__/SetupIosPush-test.ts create mode 100644 packages/expo-cli/src/credentials/views/__tests__/SetupProvisioningProfile-test.ts diff --git a/packages/expo-cli/src/commands/build/ios/IOSBuilder.ts b/packages/expo-cli/src/commands/build/ios/IOSBuilder.ts index 83a863b9ca..59461b9142 100644 --- a/packages/expo-cli/src/commands/build/ios/IOSBuilder.ts +++ b/packages/expo-cli/src/commands/build/ios/IOSBuilder.ts @@ -1,5 +1,4 @@ import chalk from 'chalk'; -import isEmpty from 'lodash/isEmpty'; import pickBy from 'lodash/pickBy'; import get from 'lodash/get'; import { XDLError } from '@expo/xdl'; @@ -136,6 +135,7 @@ See https://docs.expo.io/versions/latest/distribution/building-standalone-apps/# } async produceCredentials(ctx: Context, experienceName: string, bundleIdentifier: string) { + const nonInteractive = this.options.parent && this.options.parent.nonInteractive; const appCredentials = await ctx.ios.getAppCredentials(experienceName, bundleIdentifier); if (ctx.hasAppleCtx()) { @@ -150,7 +150,10 @@ See https://docs.expo.io/versions/latest/distribution/building-standalone-apps/# if (distCertFromParams) { await useDistCertFromParams(ctx, appCredentials, distCertFromParams); } else { - await runCredentialsManager(ctx, new SetupIosDist({ experienceName, bundleIdentifier })); + await runCredentialsManager( + ctx, + new SetupIosDist({ experienceName, bundleIdentifier, nonInteractive }) + ); } const distributionCert = await ctx.ios.getDistCert(experienceName, bundleIdentifier); @@ -165,7 +168,10 @@ See https://docs.expo.io/versions/latest/distribution/building-standalone-apps/# if (pushKeyFromParams) { await usePushKeyFromParams(ctx, appCredentials, pushKeyFromParams); } else { - await runCredentialsManager(ctx, new SetupIosPush({ experienceName, bundleIdentifier })); + await runCredentialsManager( + ctx, + new SetupIosPush({ experienceName, bundleIdentifier, nonInteractive }) + ); } const provisioningProfileFromParams = await getProvisioningProfileFromParams(this.options); @@ -184,6 +190,7 @@ See https://docs.expo.io/versions/latest/distribution/building-standalone-apps/# experienceName, bundleIdentifier, distCert: distributionCert, + nonInteractive, }) ); } diff --git a/packages/expo-cli/src/credentials/test-fixtures/mocks.ts b/packages/expo-cli/src/credentials/test-fixtures/mocks.ts new file mode 100644 index 0000000000..44861f9d63 --- /dev/null +++ b/packages/expo-cli/src/credentials/test-fixtures/mocks.ts @@ -0,0 +1,77 @@ +const today = new Date(); +const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000); +export const testProvisioningProfile = { + provisioningProfileId: 'test-id', +}; +export const testProvisioningProfiles = [testProvisioningProfile]; +export const testProvisioningProfileFromApple = { + name: 'test-name', + status: 'Active', + expires: tomorrow, + distributionMethod: 'test', + certificates: [], + provisioningProfileId: testProvisioningProfile.provisioningProfileId, +}; +export const testProvisioningProfilesFromApple = [testProvisioningProfileFromApple]; + +export const testDistCert = { + id: 1, + type: 'dist-cert', + certP12: 'test-p12', + certPassword: 'test-password', + distCertSerialNumber: 'test-serial', + teamId: 'test-team-id', +}; +export const testDistCerts = [testDistCert]; +export const testDistCertFromApple = { + id: 'test-id', + status: 'Active', + created: today.getTime(), + expires: tomorrow.getTime(), + serialNumber: testDistCert.distCertSerialNumber, +}; +export const testDistCertsFromApple = [testDistCertFromApple]; + +export const testPushKey = { + id: 1, + type: 'push-key', + apnsKeyP8: 'test-p8', + apnsKeyId: 'test-key-id', + teamId: 'test-team-id', +}; +export const testPushKeys = [testPushKey]; +export const testPushKeyFromApple = { + id: testPushKey.apnsKeyId, + name: 'test-name', +}; +export const testPushKeysFromApple = [testPushKeyFromApple]; + +export const testAppCredentials = [{ experienceName: 'testApp', bundleIdentifier: 'test.com.app' }]; +export function getCtxMock() { + return { + ios: { + getDistCert: jest.fn(), + createDistCert: jest.fn(() => testDistCert), + useDistCert: jest.fn(), + getPushKey: jest.fn(), + createPushKey: jest.fn(() => testPushKey), + usePushKey: jest.fn(), + updateProvisioningProfile: jest.fn(), + getAppCredentials: jest.fn(() => testAppCredentials), + getProvisioningProfile: jest.fn(), + credentials: { + userCredentials: [...testDistCerts, ...testPushKeys], + appCredentials: testAppCredentials, + }, + }, + appleCtx: { + appleId: 'test-id', + appleIdPassword: 'test-password', + team: { id: 'test-team-id' }, + fastlaneSession: 'test-fastlane-session', + }, + ensureAppleCtx: jest.fn(), + user: jest.fn(), + hasAppleCtx: jest.fn(() => true), + }; +} diff --git a/packages/expo-cli/src/credentials/views/IosDistCert.ts b/packages/expo-cli/src/credentials/views/IosDistCert.ts index 8b93f008dd..417e84393a 100644 --- a/packages/expo-cli/src/credentials/views/IosDistCert.ts +++ b/packages/expo-cli/src/credentials/views/IosDistCert.ts @@ -30,12 +30,22 @@ Please revoke the old ones or reuse existing from your other apps. Please remember that Apple Distribution Certificates are not application specific! `; +type CliOptions = { + nonInteractive?: boolean; +}; + export type DistCertOptions = { experienceName: string; bundleIdentifier: string; -}; +} & CliOptions; export class CreateIosDist implements IView { + _nonInteractive: boolean; + + constructor(options: CliOptions = {}) { + this._nonInteractive = options.nonInteractive ?? false; + } + async create(ctx: Context): Promise { const newDistCert = await this.provideOrGenerate(ctx); return await ctx.ios.createDistCert(newDistCert); @@ -51,10 +61,12 @@ export class CreateIosDist implements IView { } async provideOrGenerate(ctx: Context): Promise { - const userProvided = await promptForDistCert(ctx); - if (userProvided) { - const isValid = await validateDistributionCertificate(ctx, userProvided); - return isValid ? userProvided : await this.provideOrGenerate(ctx); + if (!this._nonInteractive) { + const userProvided = await promptForDistCert(ctx); + if (userProvided) { + const isValid = await validateDistributionCertificate(ctx, userProvided); + return isValid ? userProvided : await this.provideOrGenerate(ctx); + } } return await generateDistCert(ctx); } @@ -232,11 +244,13 @@ export class UseExistingDistributionCert implements IView { export class CreateOrReuseDistributionCert implements IView { _experienceName: string; _bundleIdentifier: string; + _nonInteractive: boolean; constructor(options: DistCertOptions) { const { experienceName, bundleIdentifier } = options; this._experienceName = experienceName; this._bundleIdentifier = bundleIdentifier; + this._nonInteractive = options.nonInteractive ?? false; } async assignDistCert(ctx: Context, userCredentialsId: number) { @@ -256,7 +270,9 @@ export class CreateOrReuseDistributionCert implements IView { const existingCertificates = await getValidDistCerts(ctx.ios.credentials, ctx); if (existingCertificates.length === 0) { - const distCert = await new CreateIosDist().create(ctx); + const distCert = await new CreateIosDist({ nonInteractive: this._nonInteractive }).create( + ctx + ); await this.assignDistCert(ctx, distCert.id); return null; } @@ -274,13 +290,20 @@ export class CreateOrReuseDistributionCert implements IView { pageSize: Infinity, }; - const { confirm } = await prompt(confirmQuestion); - if (confirm) { - log(`Using Distribution Certificate: ${autoselectedCertificate.certId || '-----'}`); - await this.assignDistCert(ctx, autoselectedCertificate.id); - return null; + if (!this._nonInteractive) { + const { confirm } = await prompt(confirmQuestion); + if (!confirm) { + return await this._createOrReuse(ctx); + } } + // Use autosuggested push key + log(`Using Distribution Certificate: ${autoselectedCertificate.certId || '-----'}`); + await this.assignDistCert(ctx, autoselectedCertificate.id); + return null; + } + + async _createOrReuse(ctx: Context): Promise { const choices = [ { name: '[Choose existing certificate] (Recommended)', @@ -300,7 +323,9 @@ export class CreateOrReuseDistributionCert implements IView { const { action } = await prompt(question); if (action === 'GENERATE') { - const distCert = await new CreateIosDist().create(ctx); + const distCert = await new CreateIosDist({ nonInteractive: this._nonInteractive }).create( + ctx + ); await this.assignDistCert(ctx, distCert.id); return null; } else if (action === 'CHOOSE_EXISTING') { diff --git a/packages/expo-cli/src/credentials/views/IosProvisioningProfile.ts b/packages/expo-cli/src/credentials/views/IosProvisioningProfile.ts index e00f393da6..b14cdb8821 100644 --- a/packages/expo-cli/src/credentials/views/IosProvisioningProfile.ts +++ b/packages/expo-cli/src/credentials/views/IosProvisioningProfile.ts @@ -11,7 +11,6 @@ import { Context, IView } from '../context'; import { IosAppCredentials, IosCredentials, - IosDistCredentials, appleTeamSchema, provisioningProfileSchema, } from '../credentials'; @@ -25,11 +24,15 @@ import { ProvisioningProfileManager, } from '../../appleApi'; +type CliOptions = { + nonInteractive?: boolean; +}; + export type ProvisioningProfileOptions = { experienceName: string; bundleIdentifier: string; distCert: DistCert; -}; +} & CliOptions; export class RemoveProvisioningProfile implements IView { shouldRevoke: boolean; @@ -81,12 +84,14 @@ export class CreateProvisioningProfile implements IView { _experienceName: string; _bundleIdentifier: string; _distCert: DistCert; + _nonInteractive: boolean; constructor(options: ProvisioningProfileOptions) { const { experienceName, bundleIdentifier, distCert } = options; this._experienceName = experienceName; this._bundleIdentifier = bundleIdentifier; this._distCert = distCert; + this._nonInteractive = options.nonInteractive ?? false; } async create(ctx: Context): Promise { @@ -121,11 +126,13 @@ export class CreateProvisioningProfile implements IView { } async provideOrGenerate(ctx: Context): Promise { - const userProvided = await askForUserProvided(provisioningProfileSchema); - if (userProvided) { - // userProvided profiles don't come with ProvisioningProfileId's (only accessible from Apple Portal API) - log(chalk.yellow('Provisioning profile: Unable to validate uploaded profile.')); - return userProvided; + if (!this._nonInteractive) { + const userProvided = await askForUserProvided(provisioningProfileSchema); + if (userProvided) { + // userProvided profiles don't come with ProvisioningProfileId's (only accessible from Apple Portal API) + log(chalk.yellow('Provisioning profile: Unable to validate uploaded profile.')); + return userProvided; + } } return await generateProvisioningProfile(ctx, this._bundleIdentifier, this._distCert); } @@ -163,12 +170,14 @@ export class CreateOrReuseProvisioningProfile implements IView { _experienceName: string; _bundleIdentifier: string; _distCert: DistCert; + _nonInteractive: boolean; constructor(options: ProvisioningProfileOptions) { const { experienceName, bundleIdentifier, distCert } = options; this._experienceName = experienceName; this._bundleIdentifier = bundleIdentifier; this._distCert = distCert; + this._nonInteractive = options.nonInteractive ?? false; } choosePreferred(profiles: ProvisioningProfileInfo[]): ProvisioningProfileInfo { @@ -191,6 +200,7 @@ export class CreateOrReuseProvisioningProfile implements IView { experienceName: this._experienceName, bundleIdentifier: this._bundleIdentifier, distCert: this._distCert, + nonInteractive: this._nonInteractive, }); } @@ -202,6 +212,7 @@ export class CreateOrReuseProvisioningProfile implements IView { experienceName: this._experienceName, bundleIdentifier: this._bundleIdentifier, distCert: this._distCert, + nonInteractive: this._nonInteractive, }); } @@ -216,19 +227,25 @@ export class CreateOrReuseProvisioningProfile implements IView { pageSize: Infinity, }; - const { confirm } = await prompt(confirmQuestion); - if (confirm) { - log(`Using Provisioning Profile: ${autoselectedProfile.provisioningProfileId}`); - await configureAndUpdateProvisioningProfile( - ctx, - this._experienceName, - this._bundleIdentifier, - this._distCert, - autoselectedProfile - ); - return null; + if (!this._nonInteractive) { + const { confirm } = await prompt(confirmQuestion); + if (!confirm) { + return await this._createOrReuse(ctx); + } } + log(`Using Provisioning Profile: ${autoselectedProfile.provisioningProfileId}`); + await configureAndUpdateProvisioningProfile( + ctx, + this._experienceName, + this._bundleIdentifier, + this._distCert, + autoselectedProfile + ); + return null; + } + + async _createOrReuse(ctx: Context): Promise { const choices = [ { name: '[Choose existing provisioning profile] (Recommended)', @@ -252,6 +269,7 @@ export class CreateOrReuseProvisioningProfile implements IView { experienceName: this._experienceName, bundleIdentifier: this._bundleIdentifier, distCert: this._distCert, + nonInteractive: this._nonInteractive, }); } else if (action === 'CHOOSE_EXISTING') { return new UseExistingProvisioningProfile({ diff --git a/packages/expo-cli/src/credentials/views/IosPushCredentials.ts b/packages/expo-cli/src/credentials/views/IosPushCredentials.ts index 2b69f2b8eb..12cb65573a 100644 --- a/packages/expo-cli/src/credentials/views/IosPushCredentials.ts +++ b/packages/expo-cli/src/credentials/views/IosPushCredentials.ts @@ -5,7 +5,6 @@ import get from 'lodash/get'; import some from 'lodash/some'; import ora from 'ora'; -import inquirer from 'inquirer'; import terminalLink from 'terminal-link'; import prompt, { Question } from '../../prompt'; import log from '../../log'; @@ -19,7 +18,6 @@ import { import { CredentialSchema, askForUserProvided } from '../actions/promptForCredentials'; import { displayIosUserCredentials } from '../actions/list'; import { PushKey, PushKeyInfo, PushKeyManager, isPushKey } from '../../appleApi'; -import { CredentialsManager } from '../route'; const APPLE_KEYS_TOO_MANY_GENERATED_ERROR = ` You can have only ${chalk.underline('two')} Push Notifactions Keys on your Apple Developer account. @@ -27,12 +25,22 @@ Please revoke the old ones or reuse existing from your other apps. Please remember that Apple Keys are not application specific! `; +type CliOptions = { + nonInteractive?: boolean; +}; + export type PushKeyOptions = { experienceName: string; bundleIdentifier: string; -}; +} & CliOptions; export class CreateIosPush implements IView { + _nonInteractive: boolean; + + constructor(options: CliOptions = {}) { + this._nonInteractive = options.nonInteractive ?? false; + } + async create(ctx: Context): Promise { const newPushKey = await this.provideOrGenerate(ctx); return await ctx.ios.createPushKey(newPushKey); @@ -66,12 +74,14 @@ export class CreateIosPush implements IView { } async provideOrGenerate(ctx: Context): Promise { - const requiredQuestions = this._getRequiredQuestions(ctx); - const userProvided = await askForUserProvided(requiredQuestions); - if (userProvided) { - const pushKey = this._ensurePushKey(ctx, userProvided); - const isValid = await validatePushKey(ctx, pushKey); - return isValid ? userProvided : await this.provideOrGenerate(ctx); + if (!this._nonInteractive) { + const requiredQuestions = this._getRequiredQuestions(ctx); + const userProvided = await askForUserProvided(requiredQuestions); + if (userProvided) { + const pushKey = this._ensurePushKey(ctx, userProvided); + const isValid = await validatePushKey(ctx, pushKey); + return isValid ? userProvided : await this.provideOrGenerate(ctx); + } } return await generatePushKey(ctx); } @@ -236,11 +246,13 @@ export class UseExistingPushNotification implements IView { export class CreateOrReusePushKey implements IView { _experienceName: string; _bundleIdentifier: string; + _nonInteractive: boolean; constructor(options: PushKeyOptions) { const { experienceName, bundleIdentifier } = options; this._experienceName = experienceName; this._bundleIdentifier = bundleIdentifier; + this._nonInteractive = options.nonInteractive ?? false; } async assignPushKey(ctx: Context, userCredentialsId: number) { @@ -260,7 +272,7 @@ export class CreateOrReusePushKey implements IView { const existingPushKeys = await getValidPushKeys(ctx.ios.credentials, ctx); if (existingPushKeys.length === 0) { - const pushKey = await new CreateIosPush().create(ctx); + const pushKey = await new CreateIosPush({ nonInteractive: this._nonInteractive }).create(ctx); await this.assignPushKey(ctx, pushKey.id); return null; } @@ -278,13 +290,20 @@ export class CreateOrReusePushKey implements IView { pageSize: Infinity, }; - const { confirm } = await prompt(confirmQuestion); - if (confirm) { - log(`Using Push Key: ${autoselectedPushKey.apnsKeyId}`); - await this.assignPushKey(ctx, autoselectedPushKey.id); - return null; + if (!this._nonInteractive) { + const { confirm } = await prompt(confirmQuestion); + if (!confirm) { + return await this._createOrReuse(ctx); + } } + // Use autosuggested push key + log(`Using Push Key: ${autoselectedPushKey.apnsKeyId}`); + await this.assignPushKey(ctx, autoselectedPushKey.id); + return null; + } + + async _createOrReuse(ctx: Context): Promise { const choices = [ { name: '[Choose existing push key] (Recommended)', @@ -304,7 +323,7 @@ export class CreateOrReusePushKey implements IView { const { action } = await prompt(question); if (action === 'GENERATE') { - const pushKey = await new CreateIosPush().create(ctx); + const pushKey = await new CreateIosPush({ nonInteractive: this._nonInteractive }).create(ctx); await this.assignPushKey(ctx, pushKey.id); return null; } else if (action === 'CHOOSE_EXISTING') { diff --git a/packages/expo-cli/src/credentials/views/SetupIosDist.ts b/packages/expo-cli/src/credentials/views/SetupIosDist.ts index 0e32431c00..b7d1564b11 100644 --- a/packages/expo-cli/src/credentials/views/SetupIosDist.ts +++ b/packages/expo-cli/src/credentials/views/SetupIosDist.ts @@ -5,11 +5,13 @@ import { Context, IView } from '../context'; export class SetupIosDist implements IView { _experienceName: string; _bundleIdentifier: string; + _nonInteractive: boolean; constructor(options: iosDistView.DistCertOptions) { const { experienceName, bundleIdentifier } = options; this._experienceName = experienceName; this._bundleIdentifier = bundleIdentifier; + this._nonInteractive = options.nonInteractive ?? false; } async open(ctx: Context): Promise { @@ -22,23 +24,18 @@ export class SetupIosDist implements IView { this._bundleIdentifier ); - if (!configuredDistCert) { - return new iosDistView.CreateOrReuseDistributionCert({ - experienceName: this._experienceName, - bundleIdentifier: this._bundleIdentifier, - }); - } - - // check if valid - const isValid = await iosDistView.validateDistributionCertificate(ctx, configuredDistCert); - - if (isValid) { - return null; + if (configuredDistCert) { + // we dont need to setup if we have a valid dist cert on file + const isValid = await iosDistView.validateDistributionCertificate(ctx, configuredDistCert); + if (isValid) { + return null; + } } return new iosDistView.CreateOrReuseDistributionCert({ experienceName: this._experienceName, bundleIdentifier: this._bundleIdentifier, + nonInteractive: this._nonInteractive, }); } } diff --git a/packages/expo-cli/src/credentials/views/SetupIosProvisioningProfile.ts b/packages/expo-cli/src/credentials/views/SetupIosProvisioningProfile.ts index edb5416a17..2cc4de3ab9 100644 --- a/packages/expo-cli/src/credentials/views/SetupIosProvisioningProfile.ts +++ b/packages/expo-cli/src/credentials/views/SetupIosProvisioningProfile.ts @@ -5,21 +5,28 @@ import * as iosProfileView from './IosProvisioningProfile'; import { Context, IView } from '../context'; import { IosDistCredentials } from '../credentials'; -export type ProvisioningProfileOptions = { +type CliOptions = { + nonInteractive?: boolean; +}; + +type ProvisioningProfileOptions = { experienceName: string; bundleIdentifier: string; distCert: IosDistCredentials; -}; +} & CliOptions; + export class SetupIosProvisioningProfile implements IView { _experienceName: string; _bundleIdentifier: string; _distCert: IosDistCredentials; + _nonInteractive: boolean; constructor(options: ProvisioningProfileOptions) { const { experienceName, bundleIdentifier, distCert } = options; this._experienceName = experienceName; this._bundleIdentifier = bundleIdentifier; this._distCert = distCert; + this._nonInteractive = options.nonInteractive ?? false; } async open(ctx: Context): Promise { @@ -46,6 +53,7 @@ export class SetupIosProvisioningProfile implements IView { experienceName: this._experienceName, bundleIdentifier: this._bundleIdentifier, distCert: this._distCert, + nonInteractive: this._nonInteractive, }); } @@ -78,6 +86,7 @@ export class SetupIosProvisioningProfile implements IView { experienceName: this._experienceName, bundleIdentifier: this._bundleIdentifier, distCert: this._distCert, + nonInteractive: this._nonInteractive, }); } return null; @@ -95,6 +104,7 @@ export class SetupIosProvisioningProfile implements IView { experienceName: this._experienceName, bundleIdentifier: this._bundleIdentifier, distCert: this._distCert, + nonInteractive: this._nonInteractive, }); } diff --git a/packages/expo-cli/src/credentials/views/SetupIosPush.ts b/packages/expo-cli/src/credentials/views/SetupIosPush.ts index 6e4ff61dae..c08109c338 100644 --- a/packages/expo-cli/src/credentials/views/SetupIosPush.ts +++ b/packages/expo-cli/src/credentials/views/SetupIosPush.ts @@ -9,11 +9,13 @@ import log from '../../log'; export class SetupIosPush implements IView { _experienceName: string; _bundleIdentifier: string; + _nonInteractive: boolean; constructor(options: iosPushView.PushKeyOptions) { const { experienceName, bundleIdentifier } = options; this._experienceName = experienceName; this._bundleIdentifier = bundleIdentifier; + this._nonInteractive = options.nonInteractive ?? false; } async open(ctx: Context): Promise { @@ -49,27 +51,18 @@ export class SetupIosPush implements IView { this._bundleIdentifier ); - if (!configuredPushKey) { - return new iosPushView.CreateOrReusePushKey({ - experienceName: this._experienceName, - bundleIdentifier: this._bundleIdentifier, - }); - } - - if (!ctx.hasAppleCtx) { - throw new Error(`This workflow requires you to provide your Apple Credentials.`); - } - - // check if valid - const isValid = await iosPushView.validatePushKey(ctx, configuredPushKey); - - if (isValid) { - return null; + if (configuredPushKey) { + // we dont need to setup if we have a valid push key on file + const isValid = await iosPushView.validatePushKey(ctx, configuredPushKey); + if (isValid) { + return null; + } } return new iosPushView.CreateOrReusePushKey({ experienceName: this._experienceName, bundleIdentifier: this._bundleIdentifier, + nonInteractive: this._nonInteractive, }); } } diff --git a/packages/expo-cli/src/credentials/views/__tests__/IosDistCert-test.ts b/packages/expo-cli/src/credentials/views/__tests__/IosDistCert-test.ts new file mode 100644 index 0000000000..72c2cc2a57 --- /dev/null +++ b/packages/expo-cli/src/credentials/views/__tests__/IosDistCert-test.ts @@ -0,0 +1,94 @@ +import { CreateIosDist, CreateOrReuseDistributionCert } from '../IosDistCert'; +import { getCtxMock, testDistCert, testDistCertsFromApple } from '../../test-fixtures/mocks'; + +// these variables need to be prefixed with 'mock' if declared outside of the mock scope +const mockDistCertManagerCreate = jest.fn(() => testDistCert); +const mockDistCertManagerList = jest.fn(() => testDistCertsFromApple); +jest.mock('../../../appleApi', () => { + return { + DistCertManager: jest.fn().mockImplementation(() => ({ + create: mockDistCertManagerCreate, + list: mockDistCertManagerList, + })), + }; +}); + +jest.mock('../../actions/list'); + +const originalWarn = console.warn; +const originalLog = console.log; +beforeAll(() => { + console.warn = jest.fn(); + console.log = jest.fn(); +}); +afterAll(() => { + console.warn = originalWarn; + console.log = originalLog; +}); +beforeEach(() => { + mockDistCertManagerCreate.mockClear(); + mockDistCertManagerList.mockClear(); +}); + +describe('IosDistCert', () => { + describe('CreateIosDist', () => { + it('Basic Case - Create a Dist Cert and save it to Expo Servers', async () => { + const ctx = getCtxMock(); + const cliOptions = { + nonInteractive: true, + }; + const createIosDist = new CreateIosDist(cliOptions); + await createIosDist.open(ctx as any); + + // expect dist cert is created + expect(mockDistCertManagerCreate.mock.calls.length).toBe(1); + + // expect dist cert is saved to servers + expect(ctx.ios.createDistCert.mock.calls.length).toBe(1); + }); + }); + describe('CreateOrReuseDistributionCert', () => { + it('Reuse Autosuggested Dist Cert ', async () => { + const ctx = getCtxMock(); + const distCertOptions = { + experienceName: 'testApp', + bundleIdentifier: 'test.com.app', + nonInteractive: true, + }; + const createOrReuseIosDist = new CreateOrReuseDistributionCert(distCertOptions); + await createOrReuseIosDist.open(ctx as any); + + // expect suggested dist cert is used + expect(ctx.ios.useDistCert.mock.calls.length).toBe(1); + + // expect reuse: fail if dist cert is created + expect(mockDistCertManagerCreate.mock.calls.length).toBe(0); + + // expect reuse: fail if dist cert is saved to servers + expect(ctx.ios.createDistCert.mock.calls.length).toBe(0); + }); + it('No Autosuggested Dist Cert available, create new dist cert', async () => { + // no available certs on apple dev portal + mockDistCertManagerList.mockImplementation(() => [] as any); + + const ctx = getCtxMock(); + + const distCertOptions = { + experienceName: 'testApp', + bundleIdentifier: 'test.com.app', + nonInteractive: true, + }; + const createOrReuseIosDist = new CreateOrReuseDistributionCert(distCertOptions); + await createOrReuseIosDist.open(ctx as any); + + // expect dist cert is used + expect(ctx.ios.useDistCert.mock.calls.length).toBe(1); + + // expect dist cert is created + expect(mockDistCertManagerCreate.mock.calls.length).toBe(1); + + // expect dist cert is saved to servers + expect(ctx.ios.createDistCert.mock.calls.length).toBe(1); + }); + }); +}); diff --git a/packages/expo-cli/src/credentials/views/__tests__/IosProvisioningProfile-test.ts b/packages/expo-cli/src/credentials/views/__tests__/IosProvisioningProfile-test.ts new file mode 100644 index 0000000000..4fa9b488ec --- /dev/null +++ b/packages/expo-cli/src/credentials/views/__tests__/IosProvisioningProfile-test.ts @@ -0,0 +1,114 @@ +import { + CreateOrReuseProvisioningProfile, + CreateProvisioningProfile, +} from '../IosProvisioningProfile'; +import { + getCtxMock, + testDistCert, + testProvisioningProfiles, + testProvisioningProfilesFromApple, +} from '../../test-fixtures/mocks'; + +// these variables need to be prefixed with 'mock' if declared outside of the mock scope +const mockProvProfManagerCreate = jest.fn(() => testProvisioningProfiles); +const mockProvProfManagerUseExisting = jest.fn(); +const mockProvProfManagerList = jest.fn(() => testProvisioningProfilesFromApple); +jest.mock('../../../appleApi', () => { + return { + ProvisioningProfileManager: jest.fn().mockImplementation(() => ({ + create: mockProvProfManagerCreate, + useExisting: mockProvProfManagerUseExisting, + list: mockProvProfManagerList, + })), + }; +}); + +jest.mock('../../actions/list'); + +const originalWarn = console.warn; +const originalLog = console.log; +beforeAll(() => { + console.warn = jest.fn(); + console.log = jest.fn(); +}); +afterAll(() => { + console.warn = originalWarn; + console.log = originalLog; +}); +beforeEach(() => { + mockProvProfManagerCreate.mockClear(); + mockProvProfManagerUseExisting.mockClear(); + mockProvProfManagerList.mockClear(); +}); + +describe('IosProvisioningProfile', () => { + describe('CreateProvisioningProfile', () => { + it('Basic Case - Create a Provisioning Profile and save it to Expo Servers', async () => { + const ctx = getCtxMock(); + const provProfOptions = { + experienceName: 'testApp', + bundleIdentifier: 'test.com.app', + distCert: testDistCert, + nonInteractive: true, + }; + const createProvisioningProfile = new CreateProvisioningProfile(provProfOptions); + await createProvisioningProfile.open(ctx as any); + + // expect provisioning profile is created + expect(mockProvProfManagerCreate.mock.calls.length).toBe(1); + + // expect provisioning profile is saved to servers + expect(ctx.ios.updateProvisioningProfile.mock.calls.length).toBe(1); + }); + }); + describe('CreateOrReuseProvisioningProfile', () => { + it('Use Autosuggested Provisioning Profile ', async () => { + const ctx = getCtxMock(); + const provProfOptions = { + experienceName: 'testApp', + bundleIdentifier: 'test.com.app', + distCert: testDistCert, + nonInteractive: true, + }; + const createOrReuseProvisioningProfile = new CreateOrReuseProvisioningProfile( + provProfOptions + ); + await createOrReuseProvisioningProfile.open(ctx as any); + + // expect to use existing provisioning profile in apple developer portal + expect(mockProvProfManagerUseExisting.mock.calls.length).toBe(1); + + // expect provisioning profile is saved to servers + expect(ctx.ios.updateProvisioningProfile.mock.calls.length).toBe(1); + + // expect reuse: fail if provisioning profile is created + expect(mockProvProfManagerCreate.mock.calls.length).toBe(0); + }); + it('No Autosuggested Provisioning Profile available, create new profile', async () => { + // no available certs on apple dev portal + mockProvProfManagerList.mockImplementation(() => [] as any); + + const ctx = getCtxMock(); + const provProfOptions = { + experienceName: 'testApp', + bundleIdentifier: 'test.com.app', + distCert: testDistCert, + nonInteractive: true, + }; + const createOrReuseProvisioningProfile = new CreateOrReuseProvisioningProfile( + provProfOptions + ); + const createProvisioningProfile = await createOrReuseProvisioningProfile.open(ctx as any); + await createProvisioningProfile.open(ctx as any); + + // expect creation: fail if used existing provisioning profile in apple developer portal + expect(mockProvProfManagerUseExisting.mock.calls.length).toBe(0); + + // expect provisioning profile is saved to servers + expect(ctx.ios.updateProvisioningProfile.mock.calls.length).toBe(1); + + // expect provisioning profile is created + expect(mockProvProfManagerCreate.mock.calls.length).toBe(1); + }); + }); +}); diff --git a/packages/expo-cli/src/credentials/views/__tests__/IosPushCredentials-test.ts b/packages/expo-cli/src/credentials/views/__tests__/IosPushCredentials-test.ts new file mode 100644 index 0000000000..ff11dbad10 --- /dev/null +++ b/packages/expo-cli/src/credentials/views/__tests__/IosPushCredentials-test.ts @@ -0,0 +1,93 @@ +import { CreateIosPush, CreateOrReusePushKey } from '../IosPushCredentials'; +import { getCtxMock, testPushKey, testPushKeysFromApple } from '../../test-fixtures/mocks'; + +// these variables need to be prefixed with 'mock' if declared outside of the mock scope +const mockPushKeyManagerCreate = jest.fn(() => testPushKey); +const mockPushKeyManagerList = jest.fn(() => testPushKeysFromApple); +jest.mock('../../../appleApi', () => { + return { + PushKeyManager: jest.fn().mockImplementation(() => ({ + create: mockPushKeyManagerCreate, + list: mockPushKeyManagerList, + })), + }; +}); + +jest.mock('../../actions/list'); + +const originalWarn = console.warn; +const originalLog = console.log; +beforeAll(() => { + console.warn = jest.fn(); + console.log = jest.fn(); +}); +afterAll(() => { + console.warn = originalWarn; + console.log = originalLog; +}); +beforeEach(() => { + mockPushKeyManagerCreate.mockClear(); + mockPushKeyManagerList.mockClear(); +}); + +describe('IosPushCredentials', () => { + describe('CreateIosPush', () => { + it('Basic Case - Create a Push Key and save it to Expo Servers', async () => { + const ctx = getCtxMock(); + const cliOptions = { + nonInteractive: true, + }; + const createIosPush = new CreateIosPush(cliOptions); + await createIosPush.open(ctx as any); + + // expect push key is created + expect(mockPushKeyManagerCreate.mock.calls.length).toBe(1); + + // expect push key is saved to servers + expect(ctx.ios.createPushKey.mock.calls.length).toBe(1); + }); + }); + describe('CreateOrReusePushKey', () => { + it('Reuse Autosuggested Push Key ', async () => { + const ctx = getCtxMock(); + const pushKeyOptions = { + experienceName: 'testApp', + bundleIdentifier: 'test.com.app', + nonInteractive: true, + }; + const createOrReusePushKey = new CreateOrReusePushKey(pushKeyOptions); + await createOrReusePushKey.open(ctx as any); + + // expect suggested push key is used + expect(ctx.ios.usePushKey.mock.calls.length).toBe(1); + + // expect reuse: fail if push key is created + expect(mockPushKeyManagerCreate.mock.calls.length).toBe(0); + + // expect reuse: fail if push key is saved to servers + expect(ctx.ios.createPushKey.mock.calls.length).toBe(0); + }); + it('No Autosuggested Push Key available, create new push key', async () => { + // no available keys on apple dev portal + mockPushKeyManagerList.mockImplementation(() => [] as any); + + const ctx = getCtxMock(); + const pushKeyOptions = { + experienceName: 'testApp', + bundleIdentifier: 'test.com.app', + nonInteractive: true, + }; + const createOrReusePushKey = new CreateOrReusePushKey(pushKeyOptions); + await createOrReusePushKey.open(ctx as any); + + // expect suggested push key is used + expect(ctx.ios.usePushKey.mock.calls.length).toBe(1); + + // expect push key is created + expect(mockPushKeyManagerCreate.mock.calls.length).toBe(1); + + // expect push key is saved to servers + expect(ctx.ios.createPushKey.mock.calls.length).toBe(1); + }); + }); +}); diff --git a/packages/expo-cli/src/credentials/views/__tests__/SetupIosDist-test.ts b/packages/expo-cli/src/credentials/views/__tests__/SetupIosDist-test.ts new file mode 100644 index 0000000000..7be426a314 --- /dev/null +++ b/packages/expo-cli/src/credentials/views/__tests__/SetupIosDist-test.ts @@ -0,0 +1,48 @@ +import { SetupIosDist } from '../SetupIosDist'; +import { getCtxMock, testDistCert, testDistCertsFromApple } from '../../test-fixtures/mocks'; + +// these variables need to be prefixed with 'mock' if declared outside of the mock scope +const mockDistCertManagerCreate = jest.fn(() => testDistCert); +const mockDistCertManagerList = jest.fn(() => testDistCertsFromApple); +jest.mock('../../../appleApi', () => { + return { + DistCertManager: jest.fn().mockImplementation(() => ({ + create: mockDistCertManagerCreate, + list: mockDistCertManagerList, + })), + }; +}); + +jest.mock('../../actions/list'); + +const originalWarn = console.warn; +const originalLog = console.log; +beforeAll(() => { + console.warn = jest.fn(); + console.log = jest.fn(); +}); +afterAll(() => { + console.warn = originalWarn; + console.log = originalLog; +}); +beforeEach(() => { + mockDistCertManagerCreate.mockClear(); + mockDistCertManagerList.mockClear(); +}); + +describe('SetupIosDist', () => { + it('Basic Case - Create or Reuse', async () => { + const ctx = getCtxMock(); + const distCertOptions = { + experienceName: 'testApp', + bundleIdentifier: 'test.com.app', + nonInteractive: true, + }; + const setupIosDist = new SetupIosDist(distCertOptions); + const createOrReuse = await setupIosDist.open(ctx as any); + await createOrReuse.open(ctx as any); + + // expect suggested dist cert is used + expect(ctx.ios.useDistCert.mock.calls.length).toBe(1); + }); +}); diff --git a/packages/expo-cli/src/credentials/views/__tests__/SetupIosPush-test.ts b/packages/expo-cli/src/credentials/views/__tests__/SetupIosPush-test.ts new file mode 100644 index 0000000000..6eb77609c9 --- /dev/null +++ b/packages/expo-cli/src/credentials/views/__tests__/SetupIosPush-test.ts @@ -0,0 +1,48 @@ +import { SetupIosPush } from '../SetupIosPush'; +import { getCtxMock, testPushKey, testPushKeysFromApple } from '../../test-fixtures/mocks'; + +// these variables need to be prefixed with 'mock' if declared outside of the mock scope +const mockPushKeyManagerCreate = jest.fn(() => testPushKey); +const mockPushKeyManagerList = jest.fn(() => testPushKeysFromApple); +jest.mock('../../../appleApi', () => { + return { + PushKeyManager: jest.fn().mockImplementation(() => ({ + create: mockPushKeyManagerCreate, + list: mockPushKeyManagerList, + })), + }; +}); + +jest.mock('../../actions/list'); + +const originalWarn = console.warn; +const originalLog = console.log; +beforeAll(() => { + console.warn = jest.fn(); + console.log = jest.fn(); +}); +afterAll(() => { + console.warn = originalWarn; + console.log = originalLog; +}); +beforeEach(() => { + mockPushKeyManagerCreate.mockClear(); + mockPushKeyManagerList.mockClear(); +}); + +describe('SetupIosPush', () => { + it('Basic Case - Create or Reuse', async () => { + const ctx = getCtxMock(); + const pushKeyOptions = { + experienceName: 'testApp', + bundleIdentifier: 'test.com.app', + nonInteractive: true, + }; + const setupIosPush = new SetupIosPush(pushKeyOptions); + const createOrReuse = await setupIosPush.open(ctx as any); + await createOrReuse.open(ctx as any); + + // expect suggested push key is used + expect(ctx.ios.usePushKey.mock.calls.length).toBe(1); + }); +}); diff --git a/packages/expo-cli/src/credentials/views/__tests__/SetupProvisioningProfile-test.ts b/packages/expo-cli/src/credentials/views/__tests__/SetupProvisioningProfile-test.ts new file mode 100644 index 0000000000..e5c6270452 --- /dev/null +++ b/packages/expo-cli/src/credentials/views/__tests__/SetupProvisioningProfile-test.ts @@ -0,0 +1,61 @@ +import { + getCtxMock, + testDistCert, + testProvisioningProfiles, + testProvisioningProfilesFromApple, +} from '../../test-fixtures/mocks'; +import { SetupIosProvisioningProfile } from '../SetupIosProvisioningProfile'; +import { IosDistCredentials } from '../../credentials'; + +// these variables need to be prefixed with 'mock' if declared outside of the mock scope +const mockProvProfManagerCreate = jest.fn(() => testProvisioningProfiles); +const mockProvProfManagerUseExisting = jest.fn(); +const mockProvProfManagerList = jest.fn(() => testProvisioningProfilesFromApple); +jest.mock('../../../appleApi', () => { + return { + ProvisioningProfileManager: jest.fn().mockImplementation(() => ({ + create: mockProvProfManagerCreate, + useExisting: mockProvProfManagerUseExisting, + list: mockProvProfManagerList, + })), + }; +}); + +jest.mock('../../actions/list'); + +const originalWarn = console.warn; +const originalLog = console.log; +beforeAll(() => { + console.warn = jest.fn(); + console.log = jest.fn(); +}); +afterAll(() => { + console.warn = originalWarn; + console.log = originalLog; +}); +beforeEach(() => { + mockProvProfManagerCreate.mockClear(); + mockProvProfManagerUseExisting.mockClear(); + mockProvProfManagerList.mockClear(); +}); + +describe('SetupProvisioningProfile', () => { + it('Basic Case - Create or Reuse', async () => { + const ctx = getCtxMock(); + const provProfOptions = { + experienceName: 'testApp', + bundleIdentifier: 'test.com.app', + distCert: testDistCert as IosDistCredentials, + nonInteractive: true, + }; + const setupProvisioningProfile = new SetupIosProvisioningProfile(provProfOptions); + const createOrReuse = await setupProvisioningProfile.open(ctx as any); + await createOrReuse.open(ctx as any); + + // expect to use existing provisioning profile in apple developer portal + expect(mockProvProfManagerUseExisting.mock.calls.length).toBe(1); + + // expect provisioning profile is saved to servers + expect(ctx.ios.updateProvisioningProfile.mock.calls.length).toBe(1); + }); +});