diff --git a/CHANGELOG.md b/CHANGELOG.md index c8d71a5ba8..f3171c0cde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ This is the log of notable changes to EAS CLI and related packages. ### 🎉 New features +- Implement offline distribution certificate validation when running a build in non-interactive mode. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal)) +- Add support for building internal distribution apps for Apple Enterprise Teams. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal)) + ### 🐛 Bug fixes - Display descriptive error message when API for EAS Build changes. ([#359](https://github.com/expo/eas-cli/pull/359) by [@wkozyra95](https://github.com/wkozyra95)) diff --git a/packages/eas-cli/src/credentials/__tests__/fixtures-context.ts b/packages/eas-cli/src/credentials/__tests__/fixtures-context.ts index 59359b9dba..fa1b82c721 100644 --- a/packages/eas-cli/src/credentials/__tests__/fixtures-context.ts +++ b/packages/eas-cli/src/credentials/__tests__/fixtures-context.ts @@ -14,6 +14,7 @@ export function createCtxMock(mockOverride: Record = {}): Context { newIos: getNewIosApiMockWithoutCredentials(), android: getAndroidApiMockWithoutCredentials(), appStore: getAppstoreMock(), + bestEffortAppStoreAuthenticateAsync: jest.fn(), ensureAppleCtx: jest.fn(), ensureProjectContext: jest.fn(), user: { diff --git a/packages/eas-cli/src/credentials/ios/IosCredentialsProvider.ts b/packages/eas-cli/src/credentials/ios/IosCredentialsProvider.ts index f7074bb26a..ce53e7be14 100644 --- a/packages/eas-cli/src/credentials/ios/IosCredentialsProvider.ts +++ b/packages/eas-cli/src/credentials/ios/IosCredentialsProvider.ts @@ -1,17 +1,11 @@ import { Platform } from '@expo/eas-build-job'; import { CredentialsSource, IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json'; -import { IosAppBuildCredentialsFragment } from '../../graphql/generated'; -import Log from '../../log'; import { CredentialsManager } from '../CredentialsManager'; import { Context } from '../context'; import * as credentialsJsonReader from '../credentialsJson/read'; import type { IosCredentials } from '../credentialsJson/read'; import { SetupBuildCredentials } from './actions/SetupBuildCredentials'; -import { - getBuildCredentialsAsync, - resolveDistributionType, -} from './actions/new/BuildCredentialsUtils'; import { AppLookupParams } from './api/GraphqlClient'; import { isAdHocProfile } from './utils/provisioningProfile'; @@ -32,11 +26,10 @@ export default class IosCredentialsProvider { public async getCredentialsAsync( src: CredentialsSource.LOCAL | CredentialsSource.REMOTE ): Promise { - switch (src) { - case CredentialsSource.LOCAL: - return await this.getLocalAsync(); - case CredentialsSource.REMOTE: - return await this.getRemoteAsync(); + if (src === CredentialsSource.LOCAL) { + return await this.getLocalAsync(); + } else { + return await this.getRemoteAsync(); } } @@ -70,54 +63,18 @@ export default class IosCredentialsProvider { } private async getRemoteAsync(): Promise { - if (this.options.skipCredentialsCheck) { - Log.log('Skipping credentials check'); - } else { - await new CredentialsManager(this.ctx).runActionAsync( - new SetupBuildCredentials({ - app: this.options.app, - distribution: this.options.distribution, - enterpriseProvisioning: this.options.enterpriseProvisioning, - }) - ); - } - - const buildCredentials = await this.fetchRemoteAsync(); - if ( - !buildCredentials?.distributionCertificate?.certificateP12 || - !buildCredentials.distributionCertificate?.certificatePassword - ) { - if (this.options.skipCredentialsCheck) { - throw new Error( - 'Distribution certificate is missing and credentials check was skipped. Run without --skip-credentials-check to set it up.' - ); - } else { - throw new Error('Distribution certificate is missing'); - } - } - if (!buildCredentials.provisioningProfile?.provisioningProfile) { - if (this.options.skipCredentialsCheck) { - throw new Error( - 'Provisioning profile is missing and credentials check was skipped. Run without --skip-credentials-check to set it up.' - ); - } else { - throw new Error('Provisioning profile is missing'); - } - } + const manager = new CredentialsManager(this.ctx); + const { provisioningProfile, distributionCertificate } = await new SetupBuildCredentials({ + app: this.options.app, + distribution: this.options.distribution, + enterpriseProvisioning: this.options.enterpriseProvisioning, + }).runAsync(manager, this.ctx); return { - provisioningProfile: buildCredentials.provisioningProfile.provisioningProfile, + provisioningProfile, distributionCertificate: { - certP12: buildCredentials.distributionCertificate.certificateP12, - certPassword: buildCredentials.distributionCertificate.certificatePassword, + certP12: distributionCertificate.certificateP12, + certPassword: distributionCertificate.certificatePassword, }, }; } - - private async fetchRemoteAsync(): Promise { - const distributionType = resolveDistributionType( - this.options.distribution, - this.options.enterpriseProvisioning - ); - return await getBuildCredentialsAsync(this.ctx, this.options.app, distributionType); - } } diff --git a/packages/eas-cli/src/credentials/ios/__tests__/IosCredentialsProvider-test.ts b/packages/eas-cli/src/credentials/ios/__tests__/IosCredentialsProvider-test.ts index 3329f45a3f..92ad908191 100644 --- a/packages/eas-cli/src/credentials/ios/__tests__/IosCredentialsProvider-test.ts +++ b/packages/eas-cli/src/credentials/ios/__tests__/IosCredentialsProvider-test.ts @@ -1,6 +1,7 @@ import { CredentialsSource } from '@expo/eas-json'; import { vol } from 'memfs'; +import { IosAppBuildCredentialsFragment } from '../../../graphql/generated'; import { getAppstoreMock } from '../../__tests__/fixtures-appstore'; import { createCtxMock } from '../../__tests__/fixtures-context'; import { testIosAppCredentialsWithBuildCredentialsQueryResult } from '../../__tests__/fixtures-ios'; @@ -9,13 +10,32 @@ import IosCredentialsProvider from '../IosCredentialsProvider'; import { getAppLookupParamsFromContext } from '../actions/new/BuildCredentialsUtils'; jest.mock('fs'); +jest.mock('../validators/validateProvisioningProfile', () => ({ + validateProvisioningProfileAsync: async ( + _ctx: any, + _app: any, + buildCredentials: Partial | null + ): Promise => { + return !!( + buildCredentials && + buildCredentials.distributionCertificate && + buildCredentials.provisioningProfile + ); + }, +})); const originalConsoleLog = console.log; +const originalConsoleError = console.error; +const originalConsoleWarn = console.warn; beforeAll(() => { console.log = jest.fn(); + console.error = jest.fn(); + console.warn = jest.fn(); }); afterAll(() => { console.log = originalConsoleLog; + console.error = originalConsoleError; + console.warn = originalConsoleWarn; }); beforeEach(() => { @@ -27,7 +47,7 @@ describe(IosCredentialsProvider, () => { describe('remote credentials', () => { it('throws an error is credentials do not exist', async () => { const ctx = createCtxMock({ - nonInteractive: false, + nonInteractive: true, appStore: getAppstoreMock(), projectDir: '/app', newIos: { @@ -46,15 +66,16 @@ describe(IosCredentialsProvider, () => { projectName: appLookupParams.projectName, }, distribution: 'store', - skipCredentialsCheck: true, }); - await expect(provider.getCredentialsAsync(CredentialsSource.REMOTE)).rejects.toThrowError(); + await expect(provider.getCredentialsAsync(CredentialsSource.REMOTE)).rejects.toThrowError( + /Credentials are not set up/ + ); }); it('returns credentials if they exist', async () => { const ctx = createCtxMock({ - nonInteractive: false, + nonInteractive: true, appStore: getAppstoreMock(), projectDir: '/app', newIos: { @@ -62,6 +83,11 @@ describe(IosCredentialsProvider, () => { getIosAppCredentialsWithBuildCredentialsAsync: jest.fn( () => testIosAppCredentialsWithBuildCredentialsQueryResult ), + getDistributionCertificateForAppAsync: jest.fn( + () => + testIosAppCredentialsWithBuildCredentialsQueryResult.iosAppBuildCredentialsArray[0] + .distributionCertificate + ), }, }); const appLookupParams = getAppLookupParamsFromContext(ctx); @@ -75,7 +101,6 @@ describe(IosCredentialsProvider, () => { projectName: appLookupParams.projectName, }, distribution: 'store', - skipCredentialsCheck: true, }); const buildCredentials = diff --git a/packages/eas-cli/src/credentials/ios/actions/SetupBuildCredentials.ts b/packages/eas-cli/src/credentials/ios/actions/SetupBuildCredentials.ts index 8788671851..a787f5f789 100644 --- a/packages/eas-cli/src/credentials/ios/actions/SetupBuildCredentials.ts +++ b/packages/eas-cli/src/credentials/ios/actions/SetupBuildCredentials.ts @@ -1,5 +1,6 @@ import { IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json'; import chalk from 'chalk'; +import nullthrows from 'nullthrows'; import { IosDistributionType as GraphQLIosDistributionType, @@ -26,10 +27,19 @@ interface Options { skipCredentialsCheck?: boolean; } -export class SetupBuildCredentials implements Action { +interface IosAppBuildCredentials { + iosDistributionType: GraphQLIosDistributionType; + provisioningProfile: string; + distributionCertificate: { + certificateP12: string; + certificatePassword: string; + }; +} + +export class SetupBuildCredentials implements Action { constructor(private options: Options) {} - async runAsync(manager: CredentialsManager, ctx: Context): Promise { + async runAsync(manager: CredentialsManager, ctx: Context): Promise { const { app } = this.options; await ctx.bestEffortAppStoreAuthenticateAsync(); @@ -46,12 +56,21 @@ export class SetupBuildCredentials implements Action { } try { const buildCredentials = await this.setupBuildCredentials(ctx); - const appInfo = `@${app.account.name}/${app.projectName} (${app.bundleIdentifier})`; displayProjectCredentials(app, buildCredentials); Log.newLine(); Log.log(chalk.green(`All credentials are ready to build ${appInfo}`)); Log.newLine(); + return { + iosDistributionType: buildCredentials.iosDistributionType, + provisioningProfile: nullthrows(buildCredentials.provisioningProfile?.provisioningProfile), + distributionCertificate: { + certificateP12: nullthrows(buildCredentials.distributionCertificate?.certificateP12), + certificatePassword: nullthrows( + buildCredentials.distributionCertificate?.certificatePassword + ), + }, + }; } catch (error) { Log.error('Failed to setup credentials.'); throw error; diff --git a/packages/eas-cli/src/credentials/ios/actions/new/BuildCredentialsUtils.ts b/packages/eas-cli/src/credentials/ios/actions/new/BuildCredentialsUtils.ts index 1313bb45ac..beffe75f80 100644 --- a/packages/eas-cli/src/credentials/ios/actions/new/BuildCredentialsUtils.ts +++ b/packages/eas-cli/src/credentials/ios/actions/new/BuildCredentialsUtils.ts @@ -1,4 +1,3 @@ -import { IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json'; import nullthrows from 'nullthrows'; import { @@ -17,6 +16,17 @@ import { Context } from '../../../context'; import { AppLookupParams } from '../../api/GraphqlClient'; import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils'; +export async function getAllBuildCredentialsAsync( + ctx: Context, + app: AppLookupParams +): Promise { + const appCredentials = await ctx.newIos.getIosAppCredentialsWithBuildCredentialsAsync(app, {}); + if (!appCredentials) { + return []; + } + return appCredentials.iosAppBuildCredentialsArray; +} + export async function getBuildCredentialsAsync( ctx: Context, app: AppLookupParams, @@ -94,20 +104,3 @@ export function getAppLookupParamsFromContext(ctx: Context): AppLookupParams { return { account, projectName, bundleIdentifier }; } - -export function resolveDistributionType( - distribution: IosDistributionType, - enterpriseProvisioning?: IosEnterpriseProvisioning -): GraphQLIosDistributionType { - if (distribution === 'internal') { - if (enterpriseProvisioning === 'adhoc') { - return GraphQLIosDistributionType.AdHoc; - } else if (enterpriseProvisioning === 'universal') { - return GraphQLIosDistributionType.Enterprise; - } else { - return GraphQLIosDistributionType.AdHoc; - } - } else { - return GraphQLIosDistributionType.AppStore; - } -} diff --git a/packages/eas-cli/src/credentials/ios/actions/new/SetupDistributionCertificate.ts b/packages/eas-cli/src/credentials/ios/actions/new/SetupDistributionCertificate.ts index b3da94005c..4878c7e59e 100644 --- a/packages/eas-cli/src/credentials/ios/actions/new/SetupDistributionCertificate.ts +++ b/packages/eas-cli/src/credentials/ios/actions/new/SetupDistributionCertificate.ts @@ -80,6 +80,22 @@ export class SetupDistributionCertificate { if (!currentCertificate) { return false; } + + const now = new Date(); + if ( + now < new Date(currentCertificate.validityNotBefore) || + now > new Date(currentCertificate.validityNotAfter) + ) { + return false; + } + + if (!ctx.appStore.authCtx) { + Log.warn( + "Skipping Distribution Certificate validation on Apple Servers because we aren't authenticated." + ); + return true; + } + const validCertSerialNumbers = (await this.getValidDistCertsAsync(ctx)).map( i => i.serialNumber ); diff --git a/packages/eas-cli/src/credentials/ios/actions/new/SetupInternalProvisioningProfile.ts b/packages/eas-cli/src/credentials/ios/actions/new/SetupInternalProvisioningProfile.ts index f5b1bde21d..301599a4b0 100644 --- a/packages/eas-cli/src/credentials/ios/actions/new/SetupInternalProvisioningProfile.ts +++ b/packages/eas-cli/src/credentials/ios/actions/new/SetupInternalProvisioningProfile.ts @@ -1,12 +1,115 @@ -import { IosAppBuildCredentialsFragment } from '../../../../graphql/generated'; +import { IosAppBuildCredentialsFragment, IosDistributionType } from '../../../../graphql/generated'; +import Log from '../../../../log'; +import { promptAsync } from '../../../../prompts'; import { Context } from '../../../context'; import { AppLookupParams } from '../../api/GraphqlClient'; +import { getAllBuildCredentialsAsync } from './BuildCredentialsUtils'; import { SetupAdhocProvisioningProfile } from './SetupAdhocProvisioningProfile'; +import { SetupProvisioningProfile } from './SetupProvisioningProfile'; +/** + * It's used when setting up credentials for internal distribution but `enterpriseProvisioning` is not set. + * + * TLDR: If the user authenticates with an account with Apple Developer Enterprise Program membership we ask them + * to choose if they want to set up an adhoc or universal distribution provisioning profile. Otherwise, always + * set up an adhoc provisioning profile. + */ export class SetupInternalProvisioningProfile { constructor(private app: AppLookupParams) {} async runAsync(ctx: Context): Promise { + const buildCredentials = await getAllBuildCredentialsAsync(ctx, this.app); + + const adhocBuildCredentialsExist = + buildCredentials.filter( + ({ iosDistributionType }) => iosDistributionType === IosDistributionType.AdHoc + ).length > 0; + const enterpriseBuildCredentialsExist = + buildCredentials.filter( + ({ iosDistributionType }) => iosDistributionType === IosDistributionType.Enterprise + ).length > 0; + + if (!ctx.nonInteractive) { + if (ctx.appStore.authCtx) { + if (ctx.appStore.authCtx.team.inHouse) { + return await this.askForDistributionTypeAndSetupAsync( + ctx, + 'Which credentials would you like to set up?' + ); + } else { + return await this.setupAdhocProvisioningProfileAsync(ctx); + } + } else { + if (adhocBuildCredentialsExist && enterpriseBuildCredentialsExist) { + Log.log('You have set up both adhoc and universal distribution credentials.'); + return await this.askForDistributionTypeAndSetupAsync( + ctx, + 'Which credentials would you like to use?' + ); + } else if (adhocBuildCredentialsExist) { + return await this.setupAdhocProvisioningProfileAsync(ctx); + } else if (enterpriseBuildCredentialsExist) { + return await this.setupUniversalProvisioningProfileAsync(ctx); + } else { + const { team } = await ctx.appStore.ensureAuthenticatedAsync(); + if (team.inHouse) { + return await this.askForDistributionTypeAndSetupAsync( + ctx, + 'Which credentials would you like to set up?' + ); + } else { + return await this.setupAdhocProvisioningProfileAsync(ctx); + } + } + } + } else { + if (adhocBuildCredentialsExist && enterpriseBuildCredentialsExist) { + throw new Error( + `You're in non-interactive mode. You have set up both adhoc and universal distribution credentials. Please set the 'enterpriseProvisioning' property (to 'adhoc' or 'universal') in eas.json to choose the credentials to use.` + ); + } else if (adhocBuildCredentialsExist) { + return await this.setupAdhocProvisioningProfileAsync(ctx); + } else if (enterpriseBuildCredentialsExist) { + return await this.setupUniversalProvisioningProfileAsync(ctx); + } else { + throw new Error( + `You're in non-interactive mode. EAS CLI couldn't find any credentials suitable for internal distribution. Please run again in interactive mode.` + ); + } + } + } + + private async setupAdhocProvisioningProfileAsync( + ctx: Context + ): Promise { return await new SetupAdhocProvisioningProfile(this.app).runAsync(ctx); } + + private async setupUniversalProvisioningProfileAsync( + ctx: Context + ): Promise { + return await new SetupProvisioningProfile(this.app, IosDistributionType.Enterprise).runAsync( + ctx + ); + } + + private async askForDistributionTypeAndSetupAsync( + ctx: Context, + message: string + ): Promise { + const { distributionType } = await promptAsync({ + type: 'select', + name: 'distributionType', + message, + choices: [ + { title: 'Universal Distribution', value: IosDistributionType.Enterprise }, + { title: 'Adhoc Distribution', value: IosDistributionType.AdHoc }, + ], + }); + if (distributionType === IosDistributionType.Enterprise) { + return await this.setupUniversalProvisioningProfileAsync(ctx); + } else { + return await this.setupAdhocProvisioningProfileAsync(ctx); + } + } } diff --git a/packages/eas-cli/src/credentials/ios/actions/new/__tests__/SetupInternalProvisioningProfile-test.ts b/packages/eas-cli/src/credentials/ios/actions/new/__tests__/SetupInternalProvisioningProfile-test.ts new file mode 100644 index 0000000000..0529462eac --- /dev/null +++ b/packages/eas-cli/src/credentials/ios/actions/new/__tests__/SetupInternalProvisioningProfile-test.ts @@ -0,0 +1,314 @@ +import { asMock } from '../../../../../__tests__/utils'; +import { + IosAppBuildCredentialsFragment, + IosDistributionType, +} from '../../../../../graphql/generated'; +import { promptAsync } from '../../../../../prompts'; +import { getAppstoreMock, testAuthCtx } from '../../../../__tests__/fixtures-appstore'; +import { createCtxMock } from '../../../../__tests__/fixtures-context'; +import { getAllBuildCredentialsAsync } from '../BuildCredentialsUtils'; +import { SetupAdhocProvisioningProfile } from '../SetupAdhocProvisioningProfile'; +import { SetupInternalProvisioningProfile } from '../SetupInternalProvisioningProfile'; +import { SetupProvisioningProfile } from '../SetupProvisioningProfile'; + +jest.mock('../../../../../prompts'); +jest.mock('../SetupAdhocProvisioningProfile'); +jest.mock('../SetupProvisioningProfile'); +jest.mock('../BuildCredentialsUtils', () => ({ getAllBuildCredentialsAsync: jest.fn() })); + +beforeEach(() => { + asMock(promptAsync).mockReset(); + + asMock(getAllBuildCredentialsAsync).mockReset(); + asMock(getAllBuildCredentialsAsync).mockImplementation(() => { + throw new Error( + `unhandled getAllBuildCredentialsAsync call - this shouldn't happen - fix tests!` + ); + }); +}); + +const testAdhocBuildCredentials: IosAppBuildCredentialsFragment = { + id: 'test-app-build-credentials-id-1', + iosDistributionType: IosDistributionType.AdHoc, +}; +const testEnterpriseBuildCredentials: IosAppBuildCredentialsFragment = { + id: 'test-app-build-credentials-id-2', + iosDistributionType: IosDistributionType.Enterprise, +}; + +describe(SetupInternalProvisioningProfile, () => { + describe('interactive mode', () => { + describe('when authenticated with apple', () => { + it('runs the SetupAdhocProvisioningProfile action for non-enterprise team', async () => { + asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => { + const buildCredentials: IosAppBuildCredentialsFragment[] = []; + return buildCredentials; + }); + const action = new SetupInternalProvisioningProfile({ + account: { id: 'account-id', name: 'account-name' }, + bundleIdentifier: 'com.expo.test', + projectName: 'testproject', + }); + const ctx = createCtxMock({ + nonInteractive: false, + appStore: { + ...getAppstoreMock(), + ensureAuthenticatedAsync: jest.fn(() => testAuthCtx), + authCtx: testAuthCtx, + }, + }); + + const runAsync = jest.fn(); + SetupAdhocProvisioningProfile.prototype.runAsync = runAsync; + + await action.runAsync(ctx); + + expect(runAsync).toHaveBeenCalled(); + }); + + it('asks the user for an action to run when they have access to an enterprise team', async () => { + asMock(promptAsync).mockImplementationOnce(() => ({ + distributionType: IosDistributionType.Enterprise, + })); + asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => { + const buildCredentials: IosAppBuildCredentialsFragment[] = []; + return buildCredentials; + }); + + const action = new SetupInternalProvisioningProfile({ + account: { id: 'account-id', name: 'account-name' }, + bundleIdentifier: 'com.expo.test', + projectName: 'testproject', + }); + const ctx = createCtxMock({ + nonInteractive: false, + appStore: { + ...getAppstoreMock(), + ensureAuthenticatedAsync: jest.fn(() => ({ + ...testAuthCtx, + team: { ...testAuthCtx.team, inHouse: true }, + })), + authCtx: { ...testAuthCtx, team: { ...testAuthCtx.team, inHouse: true } }, + }, + }); + + const runAsync = jest.fn(); + SetupProvisioningProfile.prototype.runAsync = runAsync; + + await action.runAsync(ctx); + + expect(runAsync).toHaveBeenCalled(); + }); + }); + describe('when not authenticated with apple', () => { + it('asks the user for an action to run when both adhoc and universal distribution credentials exist', async () => { + asMock(promptAsync).mockImplementationOnce(() => ({ + distributionType: IosDistributionType.Enterprise, + })); + asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => { + const buildCredentials: IosAppBuildCredentialsFragment[] = [ + testAdhocBuildCredentials, + testEnterpriseBuildCredentials, + ]; + return buildCredentials; + }); + + const action = new SetupInternalProvisioningProfile({ + account: { id: 'account-id', name: 'account-name' }, + bundleIdentifier: 'com.expo.test', + projectName: 'testproject', + }); + const ctx = createCtxMock({ + nonInteractive: false, + appStore: { + ...getAppstoreMock(), + ensureAuthenticatedAsync: jest.fn(() => null), + authCtx: null, + }, + }); + + const runAsync = jest.fn(); + SetupProvisioningProfile.prototype.runAsync = runAsync; + + await action.runAsync(ctx); + + expect(runAsync).toHaveBeenCalled(); + }); + + it('runs the SetupAdhocProvisioningProfile action when adhoc credentials exist', async () => { + asMock(promptAsync).mockImplementationOnce(() => ({ + distributionType: IosDistributionType.Enterprise, + })); + asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => { + const buildCredentials: IosAppBuildCredentialsFragment[] = [testAdhocBuildCredentials]; + return buildCredentials; + }); + + const action = new SetupInternalProvisioningProfile({ + account: { id: 'account-id', name: 'account-name' }, + bundleIdentifier: 'com.expo.test', + projectName: 'testproject', + }); + const ctx = createCtxMock({ + nonInteractive: false, + appStore: { + ...getAppstoreMock(), + ensureAuthenticatedAsync: jest.fn(() => null), + authCtx: null, + }, + }); + + const runAsync = jest.fn(); + SetupAdhocProvisioningProfile.prototype.runAsync = runAsync; + + await action.runAsync(ctx); + + expect(runAsync).toHaveBeenCalled(); + }); + + it('runs the SetupProvisioningProfile action when enterprise credentials exist', async () => { + asMock(promptAsync).mockImplementationOnce(() => ({ + distributionType: IosDistributionType.Enterprise, + })); + asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => { + const buildCredentials: IosAppBuildCredentialsFragment[] = [ + testEnterpriseBuildCredentials, + ]; + return buildCredentials; + }); + + const action = new SetupInternalProvisioningProfile({ + account: { id: 'account-id', name: 'account-name' }, + bundleIdentifier: 'com.expo.test', + projectName: 'testproject', + }); + const ctx = createCtxMock({ + nonInteractive: false, + appStore: { + ...getAppstoreMock(), + ensureAuthenticatedAsync: jest.fn(() => null), + authCtx: null, + }, + }); + + const runAsync = jest.fn(); + SetupProvisioningProfile.prototype.runAsync = runAsync; + + await action.runAsync(ctx); + + expect(runAsync).toHaveBeenCalled(); + }); + + it('forces the apple authentication when neither adhoc nor enterprise credentials exist', async () => { + asMock(promptAsync).mockImplementationOnce(() => ({ + distributionType: IosDistributionType.Enterprise, + })); + asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => { + const buildCredentials: IosAppBuildCredentialsFragment[] = []; + return buildCredentials; + }); + + const action = new SetupInternalProvisioningProfile({ + account: { id: 'account-id', name: 'account-name' }, + bundleIdentifier: 'com.expo.test', + projectName: 'testproject', + }); + const ctx = createCtxMock({ + nonInteractive: false, + appStore: { + ...getAppstoreMock(), + ensureAuthenticatedAsync: jest.fn(() => testAuthCtx), + authCtx: null, + }, + }); + + await action.runAsync(ctx); + + expect(ctx.appStore.ensureAuthenticatedAsync).toHaveBeenCalled(); + }); + }); + }); + + describe('non-interactive mode', () => { + it('throws an error when both adhoc and enterprise credentials are set up', async () => { + asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => { + const buildCredentials: IosAppBuildCredentialsFragment[] = [ + testAdhocBuildCredentials, + testEnterpriseBuildCredentials, + ]; + return buildCredentials; + }); + const action = new SetupInternalProvisioningProfile({ + account: { id: 'account-id', name: 'account-name' }, + bundleIdentifier: 'com.expo.test', + projectName: 'testproject', + }); + const ctx = createCtxMock({ + nonInteractive: true, + }); + await expect(action.runAsync(ctx)).rejects.toThrow( + /You have set up both adhoc and universal distribution credentials/ + ); + }); + + it('throws an error when neither adhoc nor enterprise credentials are set up', async () => { + asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => { + const buildCredentials: IosAppBuildCredentialsFragment[] = []; + return buildCredentials; + }); + const action = new SetupInternalProvisioningProfile({ + account: { id: 'account-id', name: 'account-name' }, + bundleIdentifier: 'com.expo.test', + projectName: 'testproject', + }); + const ctx = createCtxMock({ + nonInteractive: true, + }); + await expect(action.runAsync(ctx)).rejects.toThrow(/couldn't find any credentials/); + }); + + it('runs the SetupAdhocProvisioningProfile action when adhoc credentials exist', async () => { + asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => { + const buildCredentials: IosAppBuildCredentialsFragment[] = [testAdhocBuildCredentials]; + return buildCredentials; + }); + const action = new SetupInternalProvisioningProfile({ + account: { id: 'account-id', name: 'account-name' }, + bundleIdentifier: 'com.expo.test', + projectName: 'testproject', + }); + const ctx = createCtxMock({ + nonInteractive: true, + }); + + const runAsync = jest.fn(); + SetupAdhocProvisioningProfile.prototype.runAsync = runAsync; + + await action.runAsync(ctx); + + expect(runAsync).toHaveBeenCalled(); + }); + + it('runs the SetupProvisioningProfile action when enterprise credentials exist', async () => { + asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => { + const buildCredentials: IosAppBuildCredentialsFragment[] = [testEnterpriseBuildCredentials]; + return buildCredentials; + }); + const action = new SetupInternalProvisioningProfile({ + account: { id: 'account-id', name: 'account-name' }, + bundleIdentifier: 'com.expo.test', + projectName: 'testproject', + }); + const ctx = createCtxMock({ + nonInteractive: true, + }); + + const runAsync = jest.fn(); + SetupProvisioningProfile.prototype.runAsync = runAsync; + + await action.runAsync(ctx); + + expect(runAsync).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/eas-cli/src/credentials/ios/api/GraphqlClient.ts b/packages/eas-cli/src/credentials/ios/api/GraphqlClient.ts index cf29bc2a3f..dbef5a7942 100644 --- a/packages/eas-cli/src/credentials/ios/api/GraphqlClient.ts +++ b/packages/eas-cli/src/credentials/ios/api/GraphqlClient.ts @@ -98,7 +98,7 @@ export async function createOrUpdateIosAppBuildCredentialsAsync( export async function getIosAppCredentialsWithBuildCredentialsAsync( appLookupParams: AppLookupParams, - { iosDistributionType }: { iosDistributionType: IosDistributionType } + { iosDistributionType }: { iosDistributionType?: IosDistributionType } ): Promise { const { account, bundleIdentifier } = appLookupParams; const appleAppIdentifier = await AppleAppIdentifierQuery.byBundleIdentifierAsync( diff --git a/packages/eas-cli/src/credentials/ios/api/graphql/queries/IosAppCredentialsQuery.ts b/packages/eas-cli/src/credentials/ios/api/graphql/queries/IosAppCredentialsQuery.ts index d7b375aee2..c9172b603d 100644 --- a/packages/eas-cli/src/credentials/ios/api/graphql/queries/IosAppCredentialsQuery.ts +++ b/packages/eas-cli/src/credentials/ios/api/graphql/queries/IosAppCredentialsQuery.ts @@ -66,7 +66,7 @@ const IosAppCredentialsQuery = { iosDistributionType, }: { appleAppIdentifierId: string; - iosDistributionType: IosDistributionType; + iosDistributionType?: IosDistributionType; } ): Promise { const data = await withErrorHandlingAsync( @@ -76,7 +76,7 @@ const IosAppCredentialsQuery = { query IosAppCredentialsWithBuildCredentialsByAppIdentifierIdQuery( $projectFullName: String! $appleAppIdentifierId: String! - $iosDistributionType: IosDistributionType! + $iosDistributionType: IosDistributionType ) { app { byFullName(fullName: $projectFullName) { diff --git a/packages/eas-cli/src/credentials/manager/ManageIos.ts b/packages/eas-cli/src/credentials/manager/ManageIos.ts index dd745d1883..e6d1039c52 100644 --- a/packages/eas-cli/src/credentials/manager/ManageIos.ts +++ b/packages/eas-cli/src/credentials/manager/ManageIos.ts @@ -92,7 +92,7 @@ export class ManageIos implements Action { }); try { - await manager.runActionAsync(this.getAction(manager, ctx, accountName, action)); + await this.getAction(ctx, accountName, action).runAsync(manager, ctx); } catch (err) { Log.error(err); } @@ -115,12 +115,7 @@ export class ManageIos implements Action { return { accountName, projectName, bundleIdentifier }; } - private getAction( - manager: CredentialsManager, - ctx: Context, - accountName: string, - action: ActionType - ): Action { + private getAction(ctx: Context, accountName: string, action: ActionType): Action { switch (action) { case ActionType.CreateDistributionCertificate: return new CreateDistributionCertificateStandaloneManager(accountName); diff --git a/packages/eas-cli/src/credentials/manager/ManageIosBeta.ts b/packages/eas-cli/src/credentials/manager/ManageIosBeta.ts index f4199ed368..d2afad9ad9 100644 --- a/packages/eas-cli/src/credentials/manager/ManageIosBeta.ts +++ b/packages/eas-cli/src/credentials/manager/ManageIosBeta.ts @@ -127,10 +127,11 @@ export class ManageIosBeta implements Action { if (!iosDistributionTypeEasConfig) { throw new Error(`The distributionType field is required in your iOS build profile`); } - return await new SetupBuildCredentials({ + await new SetupBuildCredentials({ app: appLookupParams, distribution: iosDistributionTypeEasConfig, }).runAsync(manager, ctx); + return; } case ActionType.SetupBuildCredentialsFromCredentialsJson: { const iosAppCredentials = await ctx.newIos.getIosAppCredentialsWithCommonFieldsAsync( diff --git a/packages/eas-cli/src/graphql/generated.ts b/packages/eas-cli/src/graphql/generated.ts index 93e262cafa..9b9035663a 100644 --- a/packages/eas-cli/src/graphql/generated.ts +++ b/packages/eas-cli/src/graphql/generated.ts @@ -4058,7 +4058,7 @@ export type IosAppCredentialsByAppIdentifierIdQuery = ( export type IosAppCredentialsWithBuildCredentialsByAppIdentifierIdQueryVariables = Exact<{ projectFullName: Scalars['String']; appleAppIdentifierId: Scalars['String']; - iosDistributionType: IosDistributionType; + iosDistributionType?: Maybe; }>;