Compare commits

...
Author SHA1 Message Date
Stanisław Chmiela 9b943a351c wip 2023-06-07 10:14:17 +02:00
8 changed files with 219 additions and 47 deletions

No files matched your search

@@ -14,6 +14,7 @@ import {
} from '../../__tests__/fixtures-ios';
import IosCredentialsProvider from '../IosCredentialsProvider';
import { getAppLookupParamsFromContextAsync } from '../actions/BuildCredentialsUtils';
import { MissingCredentialsNonInteractiveError } from '../../errors';
jest.mock('fs');
jest.mock('../validators/validateProvisioningProfile', () => ({
@@ -76,7 +77,7 @@ describe(IosCredentialsProvider, () => {
distribution: 'store',
});
await expect(provider.getCredentialsAsync(CredentialsSource.REMOTE)).rejects.toThrowError(
/Credentials are not set up/
MissingCredentialsNonInteractiveError
);
});
@@ -23,6 +23,7 @@ export class CreateProvisioningProfile {
) {}
async runAsync(ctx: CredentialsContext): Promise<AppleProvisioningProfileMutationResult> {
// TODO: Remove this
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Creating Provisioning Profiles is only supported in interactive mode.'
@@ -2,11 +2,13 @@ import assert from 'assert';
import chalk from 'chalk';
import dateformat from 'dateformat';
import { formatAppleTeam } from './AppleTeamFormatting';
import { AccountFragment, AppleDistributionCertificateFragment } from '../../../graphql/generated';
import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import { fromNow } from '../../../utils/date';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import {
@@ -17,7 +19,6 @@ import { filterRevokedDistributionCertsFromEasServers } from '../appstore/Creden
import { AppleTooManyCertsError } from '../appstore/distributionCertificate';
import { distributionCertificateSchema } from '../credentials';
import { validateDistributionCertificateAsync } from '../validators/validateDistributionCertificate';
import { formatAppleTeam } from './AppleTeamFormatting';
export function formatDistributionCertificate(
distributionCertificate: AppleDistributionCertificateFragment,
@@ -212,7 +213,7 @@ async function generateDistributionCertificateAsync(
Log.warn(APPLE_DIST_CERTS_TOO_MANY_GENERATED_ERROR);
if (ctx.nonInteractive) {
throw new Error(
throw new MissingCredentialsNonInteractiveError(
"Start the CLI without the '--non-interactive' flag to revoke existing certificates."
);
}
@@ -48,14 +48,14 @@ export class SetUpDistributionCertificate {
}
private async runNonInteractiveAsync(
_ctx: CredentialsContext,
ctx: CredentialsContext,
currentCertificate: AppleDistributionCertificateFragment | null
): Promise<AppleDistributionCertificateFragment> {
// TODO: implement validation
Log.addNewLineIfNone();
Log.warn('Distribution Certificate is not validated for non-interactive builds.');
if (!currentCertificate) {
throw new MissingCredentialsNonInteractiveError();
return await this.createNewDistCertAsync(ctx);
}
return currentCertificate;
}
@@ -1,19 +1,5 @@
import nullthrows from 'nullthrows';
import {
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
IosAppBuildCredentialsFragment,
IosDistributionType,
} from '../../../graphql/generated';
import { getApplePlatformFromTarget } from '../../../project/ios/target';
import { confirmAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { ProvisioningProfileStoreInfo } from '../appstore/Credentials.types';
import { Target } from '../types';
import { validateProvisioningProfileAsync } from '../validators/validateProvisioningProfile';
import {
assignBuildCredentialsAsync,
getBuildCredentialsAsync,
@@ -23,6 +9,20 @@ import { ConfigureProvisioningProfile } from './ConfigureProvisioningProfile';
import { CreateProvisioningProfile } from './CreateProvisioningProfile';
import { formatProvisioningProfileFromApple } from './ProvisioningProfileUtils';
import { SetUpDistributionCertificate } from './SetUpDistributionCertificate';
import {
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
AppleTeamFragment,
IosAppBuildCredentialsFragment,
IosDistributionType,
} from '../../../graphql/generated';
import { getApplePlatformFromTarget } from '../../../project/ios/target';
import { confirmAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { ProvisioningProfileStoreInfo } from '../appstore/Credentials.types';
import { Target } from '../types';
import { validateProvisioningProfileAsync } from '../validators/validateProvisioningProfile';
/**
* Sets up either APP_STORE or ENTERPRISE provisioning profiles
@@ -47,6 +47,7 @@ export class SetUpProvisioningProfile {
const buildCredentials = await this.createAndAssignProfileAsync(ctx, distCert);
// delete 'currentProfile' since its no longer valid
await ctx.ios.deleteProvisioningProfilesAsync(ctx.graphqlClient, [currentProfile.id]);
await ctx.appStore.revokeProvisioningProfileAsync(this.app.bundleIdentifier, currentProfile.id);
return buildCredentials;
}
@@ -59,12 +60,17 @@ export class SetUpProvisioningProfile {
this.target,
distCert
).runAsync(ctx);
const appleTeam: AppleTeamFragment = nullthrows(
distCert.appleTeam ?? provisioningProfile.appleTeam ?? null
);
return await assignBuildCredentialsAsync(
ctx,
this.app,
this.distributionType,
distCert,
provisioningProfile
provisioningProfile,
appleTeam ?? undefined
);
}
@@ -102,11 +108,6 @@ export class SetUpProvisioningProfile {
if (areBuildCredentialsSetup) {
return nullthrows(await getBuildCredentialsAsync(ctx, this.app, this.distributionType));
}
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Provisioning profile is not configured correctly. Run this command again in interactive mode.'
);
}
const currentProfile = await getProvisioningProfileAsync(ctx, this.app, this.distributionType);
if (!currentProfile) {
@@ -114,7 +115,8 @@ export class SetUpProvisioningProfile {
}
// See if the profile we have exists on the Apple Servers
const applePlatform = await getApplePlatformFromTarget(this.target);
const applePlatform = getApplePlatformFromTarget(this.target);
// TODO: Could it ask for login in non-interactive mode?
const existingProfiles = await ctx.appStore.listProvisioningProfilesAsync(
this.app.bundleIdentifier,
applePlatform
@@ -127,6 +129,11 @@ export class SetUpProvisioningProfile {
return await this.assignNewAndDeleteOldProfileAsync(ctx, distCert, currentProfile);
}
// TODO: Should we instead reuse original profile?
if (ctx.nonInteractive) {
return await this.assignNewAndDeleteOldProfileAsync(ctx, distCert, currentProfile);
}
const confirm = await confirmAsync({
message: `${formatProvisioningProfileFromApple(
currentProfileFromServer
@@ -12,14 +12,24 @@ import {
testAppleAppIdentifierFragment,
testCommonIosAppCredentialsFragment,
testIosAppBuildCredentialsFragment,
testProvisioningProfileFragment,
testTarget,
testTargets,
} from '../../../__tests__/fixtures-ios';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
// import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { validateProvisioningProfileAsync } from '../../validators/validateProvisioningProfile';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import * as BuildCredentialsUtils from '../BuildCredentialsUtils';
import {
getAppLookupParamsFromContextAsync,
// getProvisioningProfileAsync,
} from '../BuildCredentialsUtils';
import { SetUpProvisioningProfile } from '../SetUpProvisioningProfile';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
const getProvisioningProfileAsync = jest.spyOn(
BuildCredentialsUtils,
'getProvisioningProfileAsync'
);
jest.mock('../../../../prompts');
jest.mocked(confirmAsync).mockImplementation(async () => true);
jest.mock('../SetUpDistributionCertificate');
@@ -181,10 +191,172 @@ describe('SetUpProvisioningProfile', () => {
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('errors in Non Interactive Mode', async () => {
it('repairs existing Provisioning Profile with bad build credentials in Non Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: true,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
listProvisioningProfilesAsync: jest.fn(() => [
{
provisioningProfileId: nullthrows(
testCommonIosAppCredentialsFragment.iosAppBuildCredentialsList[0].provisioningProfile
).developerPortalIdentifier,
},
]),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
createOrUpdateIosAppBuildCredentialsAsync: jest.fn(
() => testIosAppBuildCredentialsFragment
),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile to be deleted from expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
});
it('sets up a new Provisioning Profile with bad build credentials in Non Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: true,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
listProvisioningProfilesAsync: jest.fn(() => []),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
});
it('skips setting up a Provisioning Profile with prior build credentials configured properly in Non Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => true);
const ctx = createCtxMock({
nonInteractive: true,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
//listProvisioningProfilesAsync: jest.fn(() => []),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
0
);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('sets up a Provisioning Profile with no prior build credentials configured in Non Interactive Mode', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: true,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
},
ios: {
...getNewIosApiMock(),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
findApplicationTarget(testTargets)
);
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
testTarget,
IosDistributionType.AppStore
);
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it.only('errors in Non Interactive Mode if not authenticated with App Store', async () => {
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
jest
.mocked(getProvisioningProfileAsync)
.mockImplementation(async () => testProvisioningProfileFragment);
const ctx = createCtxMock({
nonInteractive: true,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => null),
authCtx: null,
},
ios: {
...getNewIosApiMock(),
createOrGetExistingAppleAppIdentifierAsync: jest.fn(() => testAppleAppIdentifierFragment),
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(
ctx,
@@ -198,12 +370,5 @@ describe('SetUpProvisioningProfile', () => {
await expect(setupProvisioningProfileAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
);
// expect build credentials not to be created or updated on expo servers
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
0
);
// expect provisioning profile not to be deleted on expo servers
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
});
@@ -160,11 +160,10 @@ export default class AppStoreApi {
public async revokeProvisioningProfileAsync(
bundleIdentifier: string,
applePlatform: ApplePlatform,
profileClass?: ProfileClass
profileId: string
): Promise<void> {
const ctx = await this.ensureAuthenticatedAsync();
return await revokeProvisioningProfileAsync(ctx, bundleIdentifier, applePlatform, profileClass);
return await revokeProvisioningProfileAsync(ctx, bundleIdentifier, profileId);
}
public async createOrReuseAdhocProvisioningProfileAsync(
@@ -243,20 +243,18 @@ export async function createProvisioningProfileAsync(
export async function revokeProvisioningProfileAsync(
authCtx: AuthCtx,
bundleIdentifier: string,
applePlatform: ApplePlatform,
profileClass: ProfileClass = ProfileClass.General
profileId: string
): Promise<void> {
const spinner = ora(`Revoking Apple provisioning profile`).start();
try {
const context = getRequestContext(authCtx);
const profiles = await getProfilesForBundleIdAsync(context, bundleIdentifier);
const profileType = resolveProfileType(applePlatform, profileClass, authCtx.team.inHouse);
await Promise.all(
profiles
.filter(profile => profile.attributes.profileType === profileType)
.map(profile => Profile.deleteAsync(context, { id: profile.id }))
);
const profile = profiles.find(profile => profile.id === profileId);
// TODO: See if we can just deleteAsync
if (profile) {
await Profile.deleteAsync(context, { id: profile.id });
}
spinner.succeed('Revoked Apple provisioning profile');
} catch (error) {
spinner.fail('Failed to revoke Apple provisioning profile');