Compare commits

..
Author SHA1 Message Date
Quinlan Jung a4eb9ff37b functions start 2022-05-10 17:27:48 -07:00
Quinlan Jung ddfe541af2 put deps into eascli 2022-05-10 17:27:48 -07:00
Quinlan Jung cde1ab36d2 last log is not always done 2022-05-10 17:27:48 -07:00
Quinlan Jung 087074d831 extra debug 2022-05-10 17:27:48 -07:00
Quinlan Jung 365e2d7e07 tmp 2022-05-10 17:27:48 -07:00
Quinlan Jung 1691f3271f tmp 2022-05-10 17:27:47 -07:00
Will Schurman c4adf7056e [eas-cli] Replace asMock with jest.mocked (#1089) 2022-05-02 12:56:16 -07:00
Expo CI 0e7f326de2 update CHANGELOG.md 2022-04-26 13:10:13 +00:00
72 changed files with 2412 additions and 449 deletions

No files matched your search

+6 -2
View File
@@ -8,12 +8,16 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Support target specific entitlements. ([#1078](https://github.com/expo/eas-cli/pull/1078) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
### 🧹 Chores
## [0.52.0](https://github.com/expo/eas-cli/releases/tag/v0.52.0) - 2022-04-26
### 🎉 New features
- Support target specific entitlements. ([#1078](https://github.com/expo/eas-cli/pull/1078) by [@wkozyra95](https://github.com/wkozyra95))
## [0.51.0](https://github.com/expo/eas-cli/releases/tag/v0.51.0) - 2022-04-19
### 🎉 New features
@@ -0,0 +1,7 @@
{
"extends": "@tsconfig/node16/tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"strict": true
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
overwrite: true
schema: 'https://staging-api.expo.dev/graphql'
schema: 'http://localhost:3000/graphql'
documents:
- 'src/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
+347
View File
@@ -2162,6 +2162,39 @@
"name": "AccountUsageMetrics",
"description": null,
"fields": [
{
"name": "byBillingPeriod",
"description": null,
"args": [
{
"name": "date",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "UsageMetricTotal",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "metricsForServiceMetric",
"description": null,
@@ -13755,6 +13788,33 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeployFunctionResult",
"description": null,
"fields": [
{
"name": "success",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Deployment",
@@ -14564,6 +14624,210 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "FunctionMutation",
"description": null,
"fields": [
{
"name": "createFunction",
"description": null,
"args": [
{
"name": "appId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "bucketKey",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DeployFunctionResult",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "getFunctionStatus",
"description": null,
"args": [
{
"name": "appId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "FunctionStatusResult",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "getSignedFunctionUploadSpecification",
"description": null,
"args": [
{
"name": "appId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GetFunctionUploadUrlResult",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "FunctionStatusResult",
"description": null,
"fields": [
{
"name": "logUrls",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "status",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GetFunctionUploadUrlResult",
"description": null,
"fields": [
{
"name": "specification",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GetSignedAssetUploadSpecificationsResult",
@@ -19657,6 +19921,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "function",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "FunctionMutation",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "googleServiceAccountKey",
"description": "Mutations that modify a Google Service Account Key",
@@ -22979,6 +23259,73 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "UsageMetricTotal",
"description": null,
"fields": [
{
"name": "billingPeriod",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "BillingPeriod",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "metrics",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "AccountUsageMetric",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "total",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Float",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "UsageMetricType",
+11
View File
@@ -17,6 +17,7 @@
"@expo/eas-json": "0.52.0",
"@expo/json-file": "8.2.34",
"@expo/multipart-body-parser": "1.1.0",
"@expo/package-manager": "0.0.50",
"@expo/pkcs12": "0.0.7",
"@expo/plist": "0.0.17",
"@expo/plugin-warn-if-update-available": "2.1.0",
@@ -30,6 +31,7 @@
"@oclif/plugin-help": "5.1.12",
"@urql/core": "2.4.3",
"@urql/exchange-retry": "0.3.2",
"adm-zip": "0.5.9",
"chalk": "4.1.2",
"cli-progress": "3.10.0",
"cli-table3": "0.6.1",
@@ -42,6 +44,7 @@
"form-data": "4.0.0",
"fs-extra": "10.0.1",
"getenv": "1.0.0",
"got": "^11.1.4",
"gradle-to-js": "2.0.0",
"graphql": "16.3.0",
"graphql-tag": "2.12.6",
@@ -49,6 +52,7 @@
"ignore": "5.2.0",
"indent-string": "4.0.0",
"jks-js": "1.0.2",
"js-yaml": "^3.13.1",
"joi": "17.6.0",
"keychain": "1.3.0",
"log-symbols": "4.1.0",
@@ -62,16 +66,20 @@
"pkg-dir": "4.2.0",
"prompts": "2.4.2",
"qrcode-terminal": "0.12.0",
"pnp-webpack-plugin": "1.7.0",
"react-dev-utils": "12.0.0",
"resolve-from": "5.0.0",
"semver": "7.3.5",
"slash": "3.0.0",
"strip-ansi": "6.0.0",
"tar": "6.1.11",
"terminal-link": "2.1.1",
"ts-loader": "^9.2.8",
"tslib": "2.3.1",
"turndown": "7.1.1",
"untildify": "4.0.0",
"uuid": "8.3.2",
"webpack": "5.72.0",
"wrap-ansi": "7.0.0"
},
"devDependencies": {
@@ -80,10 +88,12 @@
"@graphql-codegen/typescript": "2.4.8",
"@graphql-codegen/typescript-operations": "2.3.5",
"@types/cli-progress": "3.9.2",
"@types/adm-zip": "0.4.34",
"@types/dateformat": "3.0.1",
"@types/envinfo": "7.8.1",
"@types/fs-extra": "9.0.13",
"@types/getenv": "^1.0.0",
"@types/js-yaml": "^3.12.2",
"@types/mime": "2.0.3",
"@types/node-fetch": "^2.5.7",
"@types/node-forge": "1.0.1",
@@ -92,6 +102,7 @@
"@types/tar": "6.1.1",
"@types/tough-cookie": "4.0.1",
"@types/uuid": "8.3.4",
"@types/webpack": "5.28.0",
"@types/wrap-ansi": "3.0.0",
"axios": "0.26.1",
"eslint-plugin-graphql": "4.0.0",
-3
View File
@@ -1,3 +0,0 @@
export function asMock(fn: any): jest.Mock {
return fn as jest.Mock;
}
@@ -1,3 +1,4 @@
import admzip from 'adm-zip';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
@@ -122,6 +123,63 @@ export async function makeProjectTarballAsync(): Promise<{ path: string; size: n
return { size, path: tarPath };
}
export async function makeZipAsync(pathToZip: string): Promise<{ path: string; size: number }> {
const spinner = ora('Compressing project files');
await fs.mkdirp(getTmpDirectory());
const shallowClonePath = path.join(getTmpDirectory(), `${uuidv4()}-shallow-clone`);
try {
process.chdir(pathToZip);
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
process.chdir('..');
const zipPath = path.join(getTmpDirectory(), `${uuidv4()}.zip`);
// If the compression takes longer then a second, show the spinner.
// This can happen when the user has a lot of resources or doesn't ignore their CocoaPods.
// A basic project on a Mac can compress in roughly ~40ms.
// A fairly complex project without CocoaPods ignored can take up to 30s.
const timer = setTimeout(
() => {
spinner.start();
},
Log.isDebug ? 1 : 1000
);
// TODO: Possibly warn after more time about unoptimized assets.
const compressTimerLabel = 'makeProjectZipAsync';
startTimer(compressTimerLabel);
const zip = new admzip();
zip.addLocalFolder(shallowClonePath);
await zipAsync(zip, zipPath);
clearTimeout(timer);
const { size } = await fs.stat(zipPath);
const duration = endTimer(compressTimerLabel);
if (spinner.isSpinning) {
const prettyTime = formatMilliseconds(duration);
spinner.succeed(
`Compressed project files ${chalk.dim(`${prettyTime} (${formatBytes(size)})`)}`
);
}
return { size, path: zipPath };
} catch (err) {
if (spinner.isSpinning) {
spinner.fail();
}
throw err;
} finally {
await fs.remove(shallowClonePath);
}
}
async function zipAsync(zip: admzip, targetFileName?: string): Promise<void> {
return new Promise((resolve, reject) => {
zip.writeZip(targetFileName, (error: Error | null) => (error ? reject(error) : resolve()));
});
}
enum ShouldCommitChanges {
Yes,
ShowDiffFirst,
@@ -1,12 +1,8 @@
import { asMock } from '../../__tests__/utils';
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
import { getUserAsync } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions';
import EasCommand from '../EasCommand';
import TestEasCommand from './TestEasCommand';
TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
jest.mock('../../user/User');
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
@@ -32,17 +28,30 @@ afterAll(() => {
});
beforeEach(() => {
asMock(getUserAsync)
jest
.mocked(getUserAsync)
.mockReset()
.mockImplementation(() => mockJester);
asMock(ensureLoggedInAsync)
.mockImplementation(async () => mockJester);
jest
.mocked(ensureLoggedInAsync)
.mockReset()
.mockImplementation(() => mockJester);
asMock(initAsync).mockReset();
asMock(flushAsync).mockReset();
asMock(logEvent).mockReset();
.mockImplementation(async () => mockJester);
jest.mocked(initAsync).mockReset();
jest.mocked(flushAsync).mockReset();
jest.mocked(logEvent).mockReset();
});
const createTestEasCommand = (authValue: boolean): typeof EasCommand => {
class TestEasCommand extends EasCommand {
requiresAuthentication = authValue;
async runAsync(): Promise<void> {}
}
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
return TestEasCommand;
};
describe(EasCommand.name, () => {
describe('without exceptions', () => {
// The first test in this suite should have an increased timeout
@@ -53,32 +62,36 @@ describe(EasCommand.name, () => {
// See https://github.com/oclif/command/blob/master/src/command.ts#L80
// and look for "Config.load"
it('ensures the user is logged in', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(ensureLoggedInAsync).toHaveBeenCalled();
}, 15_000);
it('ensures the user data is read from cache', async () => {
asMock(TestEasCommand.prototype.authValue).mockImplementationOnce(() => false);
const TestEasCommand = createTestEasCommand(false);
await TestEasCommand.run();
expect(getUserAsync).toHaveReturnedWith(mockJester);
expect(jest.mocked(getUserAsync)).toBeCalledTimes(1);
expect(jest.mocked(ensureLoggedInAsync)).not.toBeCalled();
});
it('initializes analytics', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(initAsync).toHaveBeenCalled();
});
it('flushes analytics', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(flushAsync).toHaveBeenCalled();
});
it('logs events', async () => {
const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run();
expect(logEvent).toHaveBeenCalledWith('action', {
@@ -89,6 +102,7 @@ describe(EasCommand.name, () => {
describe('after exceptions', () => {
it('flushes analytics', async () => {
const TestEasCommand = createTestEasCommand(true);
try {
await TestEasCommand.run().then(() => {
throw new Error('foo');
@@ -1,13 +0,0 @@
import EasCommand from '../EasCommand';
export default class TestEasCommand extends EasCommand {
requiresAuthentication = this.authValue();
authValue(): boolean {
return false;
}
async runAsync(): Promise<void> {}
}
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
@@ -0,0 +1,151 @@
import { getConfig } from '@expo/config';
import spawnAsync, { SpawnOptions, SpawnResult } from '@expo/spawn-async';
import admzip from 'adm-zip';
import fs from 'fs';
import fsPromise from 'fs/promises';
import got from 'got';
import fetch from 'node-fetch';
import path from 'path';
import { pipeline } from 'stream/promises';
import { makeZipAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { bundleFunctionsAsync, prepareFunctionEntriesAsync } from '../../functions/webpack';
import { FunctionMutation } from '../../graphql/mutations/FunctionMutation';
import { PresignedPost } from '../../graphql/mutations/UploadSessionMutation';
import Log from '../../log';
import { ora } from '../../ora';
import {
findProjectRootAsync,
getProjectAccountName,
getProjectIdAsync,
} from '../../project/projectUtils';
import { uploadWithPresignedPostAsync } from '../../uploads';
import { ensureLoggedInAsync } from '../../user/actions';
import { expoCommandAsync } from '../../utils/expoCli';
import { sleepAsync } from '../../utils/promise';
export default class FunctionsCreate extends EasCommand {
static description = 'create a function';
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
const projectId = await getProjectIdAsync(exp);
const downloadSpinner = ora('Downloading functions template...').start();
const functionsTemplateUrl =
'https://github.com/quinlanj/functions-entry-point/archive/refs/heads/main.zip';
const functionsBuild = 'functions-build';
const functionsBuildZip = `${functionsBuild}.zip`;
const originalUnzippedDirectory = 'functions-entry-point-main';
// ensure empty build directory
await fsPromise.rm(path.join(projectDir, functionsBuild), { force: true, recursive: true });
// download and unzip the functions template
const downloadStream = got.stream(functionsTemplateUrl);
await pipeline(downloadStream, fs.createWriteStream(path.join(projectDir, functionsBuildZip)));
const zip = new admzip(functionsBuildZip);
zip.extractAllTo(/*target path*/ path.join(projectDir), /*overwrite*/ true);
// rename to desired dir, remove zip
await Promise.all([
fsPromise.rename(
path.join(projectDir, originalUnzippedDirectory),
path.join(projectDir, functionsBuild)
),
fsPromise.rm(path.join(projectDir, functionsBuildZip)),
]);
downloadSpinner.succeed('Downloaded functions template');
const permissionSpinner = ora('Modifying permissions...').start();
const promises = [];
for (const file of await fsPromise.readdir(path.join(projectDir, functionsBuild, 'scripts'))) {
promises.push(fsPromise.chmod(path.join(projectDir, functionsBuild, 'scripts', file), 0o777));
}
await Promise.all(promises);
permissionSpinner.succeed('Modified permissions');
const gitSpinner = ora('Initializing inner git...').start();
process.chdir(path.join(projectDir, functionsBuild));
const { status, stderr } = await spawnAsync('git', ['init']);
if (status !== 0) {
throw new Error('Failed to initialize git: ' + stderr);
}
process.chdir('..');
gitSpinner.succeed('Initialized inner git');
Log.log('Bundling functions...');
const functionDirectory = path.join(projectDir, 'functions');
const functionEntries = await prepareFunctionEntriesAsync(functionDirectory);
const outputDirectory = path.join(projectDir, functionsBuild, 'functions');
await bundleFunctionsAsync(projectDir, functionEntries, outputDirectory);
Log.log('Successfully bundled functions...');
Log.log('Doing web build...');
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
const functionName = `${accountName}-${exp.slug}`; // TODO: set up a Cloudflare enterprise domain so we don't have to do this
const region = 'us-east1';
const cloudFunctionProject = 'eas-functions';
const publicUrl = `https://${region}-${cloudFunctionProject}.cloudfunctions.net/${functionName}`;
const webOutputDirectory = path.join(projectDir, functionsBuild, 'web-build');
await expoCommandAsync(projectDir, ['build:web'], {
env: { WEBPACK_BUILD_OUTPUT_PATH: webOutputDirectory, WEB_PUBLIC_URL: publicUrl },
});
Log.log('Finished web build...');
const { path: compressedProjectPath } = await makeZipAsync(
path.join(projectDir, functionsBuild)
);
const uploadSpinner = ora('Uploading project...').start();
let bucketKey;
try {
const specification = await FunctionMutation.uploadAsync(projectId);
const presignedPost: PresignedPost = JSON.parse(specification);
await uploadWithPresignedPostAsync(compressedProjectPath, presignedPost);
bucketKey = presignedPost.fields.key;
uploadSpinner.succeed(`Successfully uploaded project to bucket: ${bucketKey}`);
} catch (err) {
uploadSpinner.fail('Failed to upload project');
throw err;
}
const spinner = ora('Creating a function...').start();
try {
const success = await FunctionMutation.createAsync(bucketKey, projectId);
spinner.succeed(`Successfully created function: ${success}`);
} catch (err) {
spinner.fail('Failed to create a function');
throw err;
}
Log.log('Streaming build logs....');
const logsPrinted = new Set<string>();
let sawLastLog = false;
while (true) {
const { status, logUrls } = await FunctionMutation.getStatusAsync(projectId);
Log.log(`Function status: ${status}`);
const logsUrlsToPrint = logUrls.filter(url => !logsPrinted.has(url)).sort();
const logResponses = await Promise.all(logsUrlsToPrint.map(url => fetch(url)));
const logBodies = await Promise.all(logResponses.map(body => body.text()));
const logPayloads = logBodies.map(body => JSON.parse(body)).map(body => body.textPayload);
logPayloads.forEach(response => Log.log(response));
logsUrlsToPrint.forEach(logUrl => logsPrinted.add(logUrl));
if (!sawLastLog) {
sawLastLog = logPayloads.some(log => log === 'DONE');
}
if (sawLastLog && status === 'ACTIVE') {
Log.log(`🎉🎉🎉 Hooray! Your function is complete!`);
Log.log(`🎉🎉🎉 Your web-app is available at: ${publicUrl}`);
for (const submodule of Object.keys(functionEntries)) {
Log.log(`🎉🎉🎉 ${submodule} is available at: ${publicUrl}/functions/${submodule}`);
}
break;
} else if (status !== 'ACTIVE' && status !== 'DEPLOY_IN_PROGRESS') {
Log.log(`🤔 Uh oh...your function is ${status}`);
break;
}
await sleepAsync(6000);
}
}
}
@@ -0,0 +1,25 @@
import path from 'path';
import EasCommand from '../../commandUtils/EasCommand';
import { installNodeDependenciesAsync } from '../../functions/CreateApp';
import { YarnPackageManager } from '../../functions/PackageManager';
import Log from '../../log';
import { ora } from '../../ora';
import { findProjectRootAsync } from '../../project/projectUtils';
export default class FunctionsCreate extends EasCommand {
static description = 'start a function';
async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync();
const functionsBuild = 'functions-build';
const installSpinner = ora('Installing dependencies...').start();
await installNodeDependenciesAsync(path.join(projectDir, functionsBuild), 'yarn');
installSpinner.succeed('Modified permissions');
Log.log('serving up the function...');
const yarn = new YarnPackageManager({ cwd: path.join(projectDir, functionsBuild) });
await yarn.runAsync('dev');
}
}
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync, promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -17,7 +16,7 @@ import {
jest.mock('../../../../ora');
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe('BuildCredentialsUtils', () => {
describe(canCopyLegacyCredentialsAsync, () => {
@@ -84,7 +83,9 @@ describe('BuildCredentialsUtils', () => {
});
describe(promptUserAndCopyLegacyCredentialsAsync, () => {
it('copies all legacy credentials to EAS if the user is eligible', async () => {
asMock(promptAsync).mockImplementation(() => ({ providedName: 'test-provided-name' }));
jest
.mocked(promptAsync)
.mockImplementation(async () => ({ providedName: 'test-provided-name' }));
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -1,11 +1,10 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { CreateFcm } from '../CreateFcm';
jest.mock('../../../../prompts');
asMock(promptAsync).mockImplementation(() => ({ fcmApiKey: 'blah' }));
jest.mocked(promptAsync).mockImplementation(async () => ({ fcmApiKey: 'blah' }));
describe(CreateFcm, () => {
it('creates an fcm api key in Interactive Mode', async () => {
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
@@ -8,7 +7,7 @@ import { CreateGoogleServiceAccountKey } from '../CreateGoogleServiceAccountKey'
jest.mock('../../../../prompts');
jest.mock('fs');
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
filePath: '/google-service-account-key.json',
}));
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { jester as mockJester } from '../../../../credentials/__tests__/fixtures-constants';
import { testKeystore } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -15,7 +14,7 @@ jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn(
describe('CreateKeystore', () => {
beforeEach(() => {
asMock(generateRandomKeystoreAsync).mockReset();
jest.mocked(generateRandomKeystoreAsync).mockReset();
});
it('creates a keystore in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
@@ -28,7 +27,7 @@ describe('CreateKeystore', () => {
expect(generateRandomKeystoreAsync).toHaveBeenCalled();
});
it('creates a keystore by uploading', async () => {
asMock(askForUserProvidedAsync).mockImplementationOnce(() => testKeystore);
jest.mocked(askForUserProvidedAsync).mockImplementationOnce(async () => testKeystore);
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const createKeystoreAction = new CreateKeystore(appLookupParams.account);
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -9,7 +8,7 @@ import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { RemoveFcm } from '../RemoveFcm';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(RemoveFcm, () => {
it('removes an FCM API Key', async () => {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -8,7 +7,7 @@ import { createCtxMock } from '../../../__tests__/fixtures-context';
import { RemoveGoogleServiceAccountKey } from '../RemoveGoogleServiceAccountKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(RemoveGoogleServiceAccountKey, () => {
it('removes an Google Service Account Key', async () => {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import { testAndroidBuildCredentialsFragment } from '../../../__tests__/fixtures-android';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -7,7 +6,7 @@ import { RemoveKeystore } from '../RemoveKeystore';
jest.mock('fs-extra');
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(RemoveKeystore, () => {
it('removes a keystore', async () => {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import {
getNewAndroidApiMock,
testAndroidBuildCredentialsFragment,
@@ -19,7 +18,7 @@ jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn(
describe('SetUpBuildCredentials', () => {
beforeEach(() => {
asMock(generateRandomKeystoreAsync).mockReset();
jest.mocked(generateRandomKeystoreAsync).mockReset();
});
it('skips setup when there are prior credentials', async () => {
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { AndroidKeystoreType } from '../../../../graphql/generated';
import { confirmAsync } from '../../../../prompts';
import {
@@ -13,13 +12,14 @@ import {
SelectAndroidBuildCredentials,
SelectAndroidBuildCredentialsResultType,
} from '../../../manager/SelectAndroidBuildCredentials';
import { AppLookupParams } from '../../api/GraphqlClient';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetUpBuildCredentialsFromCredentialsJson } from '../SetUpBuildCredentialsFromCredentialsJson';
jest.mock('fs');
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
jest.mock('../../../manager/SelectAndroidBuildCredentials');
beforeEach(() => {
@@ -28,7 +28,7 @@ beforeEach(() => {
describe(SetUpBuildCredentialsFromCredentialsJson, () => {
it('sets up a new build configuration from credentials.json upon user request', async () => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
jest.mocked(SelectAndroidBuildCredentials).mockImplementation((() => {
return {
runAsync: () => {
return {
@@ -37,7 +37,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
};
},
};
});
}) as any as (app: AppLookupParams) => SelectAndroidBuildCredentials);
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -78,7 +78,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
expect(ctx.android.createAndroidAppBuildCredentialsAsync).toHaveBeenCalledTimes(1);
});
it('uses an existing build configuration upon user request', async () => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
jest.mocked(SelectAndroidBuildCredentials).mockImplementation((() => {
return {
runAsync: () => {
return {
@@ -87,7 +87,7 @@ describe(SetUpBuildCredentialsFromCredentialsJson, () => {
};
},
};
});
}) as any as (app: AppLookupParams) => SelectAndroidBuildCredentials);
const ctx = createCtxMock({
nonInteractive: false,
android: {
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -14,7 +13,7 @@ import { SetUpGoogleServiceAccountKey } from '../SetUpGoogleServiceAccountKey';
jest.mock('../../../../prompts');
jest.mock('fs');
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
filePath: '/google-service-account-key.json',
}));
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import {
getNewAndroidApiMock,
@@ -9,7 +8,9 @@ import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { UseExistingGoogleServiceAccountKey } from '../UseExistingGoogleServiceAccountKey';
jest.mock('../../../../prompts');
asMock(promptAsync).mockImplementation(() => ({ chosenKey: testGoogleServiceAccountKeyFragment }));
jest
.mocked(promptAsync)
.mockImplementation(async () => ({ chosenKey: testGoogleServiceAccountKeyFragment }));
describe(UseExistingGoogleServiceAccountKey, () => {
it('uses an existing Google Service Account Key in Interactive Mode', async () => {
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { detectGoogleServiceAccountKeyPathAsync } from '../googleServiceAccountKey';
@@ -25,13 +24,13 @@ afterAll(() => {
});
afterEach(() => {
asMock(promptAsync).mockClear();
asMock(confirmAsync).mockClear();
jest.mocked(promptAsync).mockClear();
jest.mocked(confirmAsync).mockClear();
});
describe('Google Service Account Key path detection', () => {
it('detects a single google-services file and prompts for confirmation', async () => {
asMock(confirmAsync).mockResolvedValueOnce(true);
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir/subdir');
expect(confirmAsync).toHaveBeenCalledWith(
expect.objectContaining({ message: 'Would you like to use this file?' })
@@ -46,7 +45,7 @@ describe('Google Service Account Key path detection', () => {
});
it('returns null, when user rejects to use detected file', async () => {
asMock(confirmAsync).mockResolvedValueOnce(false);
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir/subdir');
expect(confirmAsync).toHaveBeenCalledTimes(1);
expect(confirmAsync).toHaveBeenCalledWith(
@@ -56,7 +55,7 @@ describe('Google Service Account Key path detection', () => {
});
it('displays a chooser, when multiple files are found', async () => {
asMock(promptAsync).mockResolvedValueOnce({
jest.mocked(promptAsync).mockResolvedValueOnce({
selectedPath: '/project_dir/another-service-account.json',
});
const serviceAccountPath = await detectGoogleServiceAccountKeyPathAsync('/project_dir'); // should find 2 files here
@@ -76,7 +75,7 @@ describe('Google Service Account Key path detection', () => {
});
it('returns null, when user selects the "None of above"', async () => {
asMock(promptAsync).mockResolvedValueOnce({
jest.mocked(promptAsync).mockResolvedValueOnce({
selectedPath: false,
});
@@ -1,6 +1,5 @@
import mockdate from 'mockdate';
import { asMock } from '../../../../__tests__/utils';
import Log from '../../../../log';
import {
testAndroidAppCredentialsFragment,
@@ -25,7 +24,9 @@ describe('print credentials', () => {
appLookupParams,
appCredentials: testAndroidAppCredentialsFragment,
});
const loggedSoFar = asMock(Log.log).mock.calls.reduce((acc, mockValue) => acc + mockValue);
const loggedSoFar = jest
.mocked(Log.log)
.mock.calls.reduce((acc, mockValue) => acc + mockValue.toString(), '');
expect(loggedSoFar).toMatchSnapshot();
});
});
@@ -2,7 +2,6 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import prompts from 'prompts';
import { asMock } from '../../../__tests__/utils';
import { IosDistributionType } from '../../../graphql/generated';
import {
testKeystore,
@@ -25,8 +24,8 @@ jest.mock('prompts');
describe('update credentials.json', () => {
beforeEach(() => {
vol.reset();
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
jest.mocked(prompts).mockReset();
jest.mocked(prompts).mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
});
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -18,8 +17,8 @@ function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
}
afterEach(() => {
asMock(promptAsync).mockClear();
asMock(getCredentialsFromUserAsync).mockClear();
jest.mocked(promptAsync).mockClear();
jest.mocked(getCredentialsFromUserAsync).mockClear();
});
describe(getAscApiKeyName, () => {
@@ -35,10 +34,10 @@ describe(getAscApiKeyName, () => {
describe(promptForAscApiKeyPathAsync, () => {
it('prompts for keyId, keyP8Path and issuerId when user is not authenticated to Apple', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
jest.mocked(getCredentialsFromUserAsync).mockImplementation(async () => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
@@ -62,10 +61,10 @@ describe(promptForAscApiKeyPathAsync, () => {
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(2); // keyId, issuerId
});
it('prompts for keyId, keyP8Path and detects issuerId when user is authenticated to Apple', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
jest.mocked(getCredentialsFromUserAsync).mockImplementation(async () => ({
keyId: 'test-key-id',
}));
const ctx = createCtxMock({
@@ -85,6 +84,6 @@ describe(promptForAscApiKeyPathAsync, () => {
});
expect(promptAsync).toHaveBeenCalledTimes(1); // keyP8Path
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(1); // keyId
expect(asMock(ctx.appStore.getAscApiKeyAsync).mock.calls.length).toBe(1); // issuerId
expect(jest.mocked(ctx.appStore.getAscApiKeyAsync).mock.calls.length).toBe(1); // issuerId
});
});
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -15,7 +14,7 @@ import { ConfigureProvisioningProfile } from '../ConfigureProvisioningProfile';
jest.mock('../../../../ora');
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe('ConfigureProvisioningProfile', () => {
it('configures a Provisioning Profile in Interactive Mode', async () => {
@@ -43,9 +42,9 @@ describe('ConfigureProvisioningProfile', () => {
await provProfConfigurator.runAsync(ctx);
// expect provisioning profile not to be updated on expo servers
expect(asMock(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(1);
// expect provisioning profile not to be updated on apple portal
expect(asMock(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -62,8 +61,8 @@ describe('ConfigureProvisioningProfile', () => {
);
// expect provisioning profile not to be updated on expo servers
expect(asMock(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.updateProvisioningProfileAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be updated on apple portal
expect(asMock(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.appStore.useExistingProvisioningProfileAsync).mock.calls.length).toBe(0);
});
});
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -9,7 +8,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreateAscApiKey } from '../CreateAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(CreateAscApiKey, () => {
it('creates a App Store API Key in Interactive Mode', async () => {
@@ -27,9 +26,9 @@ describe(CreateAscApiKey, () => {
await createAscApiKeyAction.runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
// expect api key to be created on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
// expect api key to be created on apple portal
expect(asMock(ctx.appStore.createAscApiKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.createAscApiKeyAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -13,7 +12,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreateProvisioningProfile } from '../CreateProvisioningProfile';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe('CreateProvisioningProfile', () => {
it('creates a Provisioning Profile in Interactive Mode', async () => {
@@ -34,9 +33,9 @@ describe('CreateProvisioningProfile', () => {
await createProvProfAction.runAsync(ctx);
// expect provisioning profile to be created on expo servers
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be created on apple portal
expect(asMock(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -52,8 +51,8 @@ describe('CreateProvisioningProfile', () => {
);
// expect provisioning profile not to be created on expo servers
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be created on apple portal
expect(asMock(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.appStore.createProvisioningProfileAsync).mock.calls.length).toBe(0);
});
});
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -8,7 +7,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreatePushKey } from '../CreatePushKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(CreatePushKey, () => {
it('creates a Push Key in Interactive Mode', async () => {
@@ -25,9 +24,9 @@ describe(CreatePushKey, () => {
await createPushKeyAction.runAsync(ctx);
// expect push key to be created on expo servers
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(1);
// expect push key to be created on apple portal
expect(asMock(ctx.appStore.createPushKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.createPushKeyAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,11 +1,10 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKeyFragment } from '../../../__tests__/fixtures-ios';
import { RemoveAscApiKey } from '../RemoveAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(RemoveAscApiKey, () => {
it('removes an Asc API Key', async () => {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -11,7 +10,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { RemoveDistributionCertificate } from '../RemoveDistributionCertificate';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe('RemoveDistributionCertificate', () => {
it('deletes the distribution certificate on Expo and Apple servers when there are no App Dependencies in Interactive Mode', async () => {
@@ -24,11 +23,11 @@ describe('RemoveDistributionCertificate', () => {
await removeDistCertAction.runAsync(ctx);
// expect dist cert to be deleted on expo servers
expect(asMock(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect dist cert to be deleted on apple portal
expect(asMock(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile deletion to be skipped because there arent any associated with the dist cert
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('deletes the distribution certificate on Expo servers when there are no App Dependencies in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -40,11 +39,11 @@ describe('RemoveDistributionCertificate', () => {
await removeDistCertAction.runAsync(ctx);
// expect dist cert to be deleted on expo servers
expect(asMock(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
// not supported in non-interactive mode
expect(asMock(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(0);
// expect provisioning profile deletion to be skipped because there arent any associated with the dist cert
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('deletes the distribution certificate and its provisioning profile on Expo and Apple servers when there are App Dependencies in Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: false });
@@ -56,11 +55,11 @@ describe('RemoveDistributionCertificate', () => {
await removeDistCertAction.runAsync(ctx);
// expect dist cert to be deleted on expo servers
expect(asMock(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.deleteDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect dist cert to be deleted on apple portal
expect(asMock(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.revokeDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile deletion to be invoked on expo servers
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
});
it('errors when the distribution certificate has App Dependencies in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testTargets } from '../../../__tests__/fixtures-ios';
@@ -20,6 +19,6 @@ describe('RemoveProvisioningProfile', () => {
await removeProfilesAction.runAsync(ctx);
// expect provisioning profile to be deleted from servers
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
});
});
@@ -1,11 +1,10 @@
import { asMock } from '../../../../__tests__/utils';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testPushKey } from '../../../__tests__/fixtures-ios';
import { RemovePushKey } from '../RemovePushKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(RemovePushKey, () => {
it('deletes the push key on Expo and Apple servers in Interactive Mode', async () => {
@@ -14,9 +13,9 @@ describe(RemovePushKey, () => {
await removePushKeyAction.runAsync(ctx);
// expect push key to be deleted on expo servers
expect(asMock(ctx.ios.deletePushKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.deletePushKeyAsync).mock.calls.length).toBe(1);
// expect push key to be revoked on apple portal
expect(asMock(ctx.appStore.revokePushKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.appStore.revokePushKeyAsync).mock.calls.length).toBe(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -15,7 +14,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetUpAscApiKey, SetupAscApiKeyChoice } from '../SetUpAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(SetUpAscApiKey, () => {
it('skips setting up a ASC API Key if it is already configured', async () => {
@@ -36,9 +35,9 @@ describe(SetUpAscApiKey, () => {
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration not to be updated with a new ASC API Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
});
it('sets up a ASC API Key, creating a new one if there are no existing ASC API Keys', async () => {
const ctx = createCtxMock({
@@ -61,9 +60,9 @@ describe(SetUpAscApiKey, () => {
await setupAscApiKeyAction.runAsync(ctx);
// expect to create a new ASC API Key on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
// expect configuration to be updated with a new ASC API Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a ASC API Key, use autoselected ASC API Key when there are existing keys', async () => {
const ctx = createCtxMock({
@@ -87,9 +86,9 @@ describe(SetUpAscApiKey, () => {
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers, we are using the existing one
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration to be updated with a new ASC API Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a ASC API Key, allowing user to choose existing keys', async () => {
const ctx = createCtxMock({
@@ -99,7 +98,7 @@ describe(SetUpAscApiKey, () => {
getAscApiKeysForAccountAsync: jest.fn(() => [testAscApiKeyFragment]),
},
});
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
choice: SetupAscApiKeyChoice.USE_EXISTING,
chosenAscApiKey: testAscApiKeyFragment,
}));
@@ -111,9 +110,9 @@ describe(SetUpAscApiKey, () => {
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC API Key on expo servers, we are using the existing one
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration to be updated with a new ASC API Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('works in Non Interactive Mode if ASC Key is configured', async () => {
const ctx = createCtxMock({
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { IosAppBuildCredentialsFragment, IosDistributionType } from '../../../../graphql/generated';
import { promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -14,10 +13,10 @@ jest.mock('../SetUpProvisioningProfile');
jest.mock('../BuildCredentialsUtils', () => ({ getAllBuildCredentialsAsync: jest.fn() }));
beforeEach(() => {
asMock(promptAsync).mockReset();
jest.mocked(promptAsync).mockReset();
asMock(getAllBuildCredentialsAsync).mockReset();
asMock(getAllBuildCredentialsAsync).mockImplementation(() => {
jest.mocked(getAllBuildCredentialsAsync).mockReset();
jest.mocked(getAllBuildCredentialsAsync).mockImplementation(() => {
throw new Error(
`unhandled getAllBuildCredentialsAsync call - this shouldn't happen - fix tests!`
);
@@ -37,7 +36,7 @@ describe(SetUpInternalProvisioningProfile, () => {
describe('interactive mode', () => {
describe('when authenticated with apple', () => {
it('runs the SetUpAdhocProvisioningProfile action for non-enterprise team', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
@@ -64,10 +63,10 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('asks the user for an action to run when they have access to an enterprise team', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
distributionType: IosDistributionType.Enterprise,
}));
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
@@ -99,10 +98,10 @@ describe(SetUpInternalProvisioningProfile, () => {
});
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(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
distributionType: IosDistributionType.Enterprise,
}));
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [
testAdhocBuildCredentials,
testEnterpriseBuildCredentials,
@@ -133,10 +132,10 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('runs the SetUpAdhocProvisioningProfile action when adhoc credentials exist', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
distributionType: IosDistributionType.Enterprise,
}));
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [testAdhocBuildCredentials];
return buildCredentials;
});
@@ -164,10 +163,10 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('runs the SetUpProvisioningProfile action when enterprise credentials exist', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
distributionType: IosDistributionType.Enterprise,
}));
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [
testEnterpriseBuildCredentials,
];
@@ -197,10 +196,10 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('forces the apple authentication when neither adhoc nor enterprise credentials exist', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
distributionType: IosDistributionType.Enterprise,
}));
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
@@ -228,7 +227,7 @@ describe(SetUpInternalProvisioningProfile, () => {
describe('non-interactive mode', () => {
it('throws an error when both adhoc and enterprise credentials are set up', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [
testAdhocBuildCredentials,
testEnterpriseBuildCredentials,
@@ -249,7 +248,7 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('throws an error when neither adhoc nor enterprise credentials are set up', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
@@ -265,7 +264,7 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('runs the SetUpAdhocProvisioningProfile action when adhoc credentials exist', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [testAdhocBuildCredentials];
return buildCredentials;
});
@@ -287,7 +286,7 @@ describe(SetUpInternalProvisioningProfile, () => {
});
it('runs the SetUpProvisioningProfile action when enterprise credentials exist', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
jest.mocked(getAllBuildCredentialsAsync).mockImplementationOnce(async () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [testEnterpriseBuildCredentials];
return buildCredentials;
});
@@ -1,6 +1,5 @@
import nullthrows from 'nullthrows';
import { asMock } from '../../../../__tests__/utils';
import { IosDistributionType } from '../../../../graphql/generated';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
@@ -19,7 +18,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetUpProvisioningProfile } from '../SetUpProvisioningProfile';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
jest.mock('../SetUpDistributionCertificate');
jest.mock('../ConfigureProvisioningProfile');
jest.mock('../CreateProvisioningProfile');
@@ -27,7 +26,7 @@ jest.mock('../../validators/validateProvisioningProfile');
describe('SetUpProvisioningProfile', () => {
it('repairs existing Provisioning Profile with bad build credentials in Interactive Mode', async () => {
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -61,12 +60,14 @@ describe('SetUpProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile not to be deleted on expo servers
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('sets up a new Provisioning Profile with bad build credentials in Interactive Mode', async () => {
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -91,12 +92,14 @@ describe('SetUpProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile to be deleted on expo servers
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(1);
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 Interactive Mode', async () => {
asMock(validateProvisioningProfileAsync).mockImplementation(() => true);
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => true);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -121,12 +124,14 @@ describe('SetUpProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
0
);
// expect provisioning profile not to be deleted on expo servers
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('sets up a Provisioning Profile with no prior build credentials configured in Interactive Mode', async () => {
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
@@ -147,12 +152,14 @@ describe('SetUpProvisioningProfile', () => {
await setupProvisioningProfileAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect provisioning profile not to be deleted on expo servers
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
it('errors in Non Interactive Mode', async () => {
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
jest.mocked(validateProvisioningProfileAsync).mockImplementation(async () => false);
const ctx = createCtxMock({
nonInteractive: true,
});
@@ -166,8 +173,10 @@ describe('SetUpProvisioningProfile', () => {
);
// expect build credentials not to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
0
);
// expect provisioning profile not to be deleted on expo servers
expect(asMock(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.deleteProvisioningProfilesAsync).mock.calls.length).toBe(0);
});
});
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
@@ -8,7 +7,7 @@ import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetUpPushKey } from '../SetUpPushKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(SetUpPushKey, () => {
it('skips setting up a Push Key if it is already configured', async () => {
@@ -24,9 +23,9 @@ describe(SetUpPushKey, () => {
await setupPushKeyAction.runAsync(ctx);
// expect not to create a new push key on expo servers
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(0);
// expect configuration not to be updated with a new push key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
});
it('sets up a Push Key, creating a new one if there are no existing push keys', async () => {
const ctx = createCtxMock({
@@ -42,9 +41,9 @@ describe(SetUpPushKey, () => {
await setupPushKeyAction.runAsync(ctx);
// expect to create a new push key on expo servers
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(1);
// expect configuration to be updated with a new push key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a Push Key, use autoselected push key when there are existing keys', async () => {
const ctx = createCtxMock({
@@ -66,9 +65,9 @@ describe(SetUpPushKey, () => {
await setupPushKeyAction.runAsync(ctx);
// expect not to create a new push key on expo servers, we are using the existing one
expect(asMock(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createPushKeyAsync).mock.calls.length).toBe(0);
// expect configuration not to be updated with a new push key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
@@ -15,15 +14,16 @@ import {
} from '../SetUpSubmissionCredentials';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
describe(SetUpSubmissionCredentials, () => {
it('allows user to enter an App Specific Password', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({
choice: PROMPT_FOR_APP_SPECIFIC_PASSWORD,
}))
.mockImplementationOnce(() => ({
.mockImplementationOnce(async () => ({
appSpecificPassword: 'super secret',
}));
const ctx = createCtxMock({
@@ -37,7 +37,7 @@ describe(SetUpSubmissionCredentials, () => {
const asp = await setupAscApiKeyAction.runAsync(ctx);
// prompt to choose ASP, then prompt to input ASP
expect(asMock(promptAsync).mock.calls.length).toBe(2);
expect(jest.mocked(promptAsync).mock.calls.length).toBe(2);
expect(asp).toBe('super secret');
});
it('returns an ASC API key', async () => {
@@ -1,4 +1,3 @@
import { asMock } from '../../../../__tests__/utils';
import { IosDistributionType } from '../../../../graphql/generated';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
@@ -21,8 +20,8 @@ jest.mock('../../../../prompts');
jest.mock('../../../credentialsJson/read');
beforeEach(() => {
asMock(confirmAsync).mockReset();
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockReset();
jest.mocked(confirmAsync).mockImplementation(async () => true);
});
describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
@@ -61,11 +60,13 @@ describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials not to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
0
);
// expect distribution certificate not to be uploaded on expo servers
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(0);
// expect provisioning profile not to be uploaded on expo servers
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(0);
});
it('sets up build credentials with no prior configuration in Interactive Mode', async () => {
const ctx = createCtxMock({
@@ -90,11 +91,13 @@ describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect distribution certificate to be uploaded on expo servers
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be uploaded on expo servers
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('sets up build credentials with different prior configuration in Interactive Mode', async () => {
// create deep clone the quick and dirty way
@@ -123,11 +126,13 @@ describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect build credentials to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect distribution certificate to be uploaded on expo servers
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be uploaded on expo servers
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
it('works in Non Interactive Mode - sets up build credentials with different prior configuration', async () => {
// create deep clone the quick and dirty way
@@ -157,12 +162,14 @@ describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
await setupBuildCredentialsFromCredentialsJsonAction.runAsync(ctx);
// expect no prompts to be called
expect(asMock(confirmAsync).mock.calls.length).toBe(0);
expect(jest.mocked(confirmAsync).mock.calls.length).toBe(0);
// expect build credentials to be created or updated on expo servers
expect(asMock(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createOrUpdateIosAppBuildCredentialsAsync).mock.calls.length).toBe(
1
);
// expect distribution certificate to be uploaded on expo servers
expect(asMock(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createDistributionCertificateAsync).mock.calls.length).toBe(1);
// expect provisioning profile to be uploaded on expo servers
expect(asMock(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
expect(jest.mocked(ctx.ios.createProvisioningProfileAsync).mock.calls.length).toBe(1);
});
});
@@ -1,6 +1,5 @@
import { ApiKey, ApiKeyType, UserRole } from '@expo/apple-utils';
import { asMock } from '../../../../__tests__/utils';
import {
createAscApiKeyAsync,
getAscApiKeyAsync,
@@ -57,7 +56,7 @@ beforeEach(() => {
test(`listAscApiKeysAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'getAsync').mockImplementation(async () => []);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await listAscApiKeysAsync(mockAuthCtx);
expect(ApiKey.getAsync).toHaveBeenLastCalledWith(mockRequestContext);
expect(result).toEqual([]);
@@ -66,7 +65,7 @@ test(`listAscApiKeysAsync`, async () => {
test(`getAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'infoAsync').mockImplementation(async () => mockApiKey);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await getAscApiKeyAsync(mockAuthCtx, 'test-key-id');
expect(ApiKey.infoAsync).toHaveBeenLastCalledWith(mockRequestContext, { id: 'test-key-id' });
expect(result).toEqual(mockAscApiKeyInfo);
@@ -75,7 +74,7 @@ test(`getAscApiKeyAsync`, async () => {
test(`createAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'createAsync').mockImplementation(async () => mockApiKey);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await createAscApiKeyAsync(mockAuthCtx, { nickname: 'test-name' });
expect(ApiKey.createAsync).toHaveBeenLastCalledWith(mockRequestContext, {
nickname: 'test-name',
@@ -89,7 +88,7 @@ test(`createAscApiKeyAsync`, async () => {
test(`revokeAscApiKeyAsync`, async () => {
const mockRequestContext = {};
jest.spyOn(ApiKey, 'infoAsync').mockImplementation(async () => mockApiKey);
asMock(getRequestContext).mockImplementation(() => mockRequestContext);
jest.mocked(getRequestContext).mockImplementation(() => mockRequestContext);
const result = await revokeAscApiKeyAsync(mockAuthCtx, 'test-key-id');
expect(ApiKey.infoAsync).toHaveBeenLastCalledWith(mockRequestContext, {
id: 'test-key-id',
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { AppleTeamType } from '../authenticateTypes';
import { resolveAppleTeamAsync, resolveAscApiKeyAsync } from '../resolveCredentials';
@@ -17,7 +16,7 @@ const testAscApiKey = {
beforeEach(() => {
vol.reset();
process.env = {};
asMock(promptAsync).mockReset();
jest.mocked(promptAsync).mockReset();
});
describe(resolveAscApiKeyAsync, () => {
@@ -44,7 +43,7 @@ describe(resolveAscApiKeyAsync, () => {
vol.fromJSON({
'/test-asc-key.p8': testAscApiKey.keyP8,
});
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
ascApiKeyPath: '/test-asc-key.p8',
ascIssuerId: testAscApiKey.issuerId,
ascApiKeyId: testAscApiKey.keyId,
@@ -78,7 +77,7 @@ describe(resolveAppleTeamAsync, () => {
});
});
it(`prompts the user if it can't find anything else`, async () => {
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
appleTeamId: testTeam.teamId,
appleTeamType: testTeam.teamType,
}));
@@ -1,8 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`print credentials prints the IosAppCredentials map 1`] = `
Array [
"iOS Credentials
"iOS Credentials
Project @quinlanj/test52
Bundle Identifier com.quinlanj.test52
Apple Team test-apple-identifier
@@ -51,6 +50,5 @@ Status active
Expiration Wed, 13 Oct 2021 18:28:44 UTC
Apple Team test-apple-identifier
Updated 5 months ago
",
]
"
`;
@@ -1,7 +1,6 @@
import mockdate from 'mockdate';
import nullthrows from 'nullthrows';
import { asMock } from '../../../../__tests__/utils';
import Log from '../../../../log';
import { IosAppCredentialsQuery } from '../../api/graphql/queries/IosAppCredentialsQuery';
import { App, Target } from '../../types';
@@ -33,7 +32,9 @@ describe('print credentials', () => {
{ targetName: 'test52', bundleIdentifier: 'com.quinlanj.test52', entitlements: {} },
];
displayIosCredentials(app, appCredentials, targets);
const loggedSoFar = asMock(Log.log).mock.calls.reduce((acc, mockValue) => acc + mockValue);
const loggedSoFar = jest
.mocked(Log.log)
.mock.calls.reduce((acc, mockValue) => acc + mockValue, '');
expect(loggedSoFar).toMatchSnapshot();
});
});
@@ -1,4 +1,3 @@
import { asMock } from '../../../__tests__/utils';
import { promptAsync } from '../../../prompts';
import {
getNewAndroidApiMock,
@@ -15,12 +14,12 @@ const TEST_STRING = 'TEST_STRING';
jest.mock('../../../prompts');
beforeEach(() => {
asMock(promptAsync).mockReset();
jest.mocked(promptAsync).mockReset();
});
describe(SelectAndroidBuildCredentials, () => {
it('returns a request to make default build credentials when there are no credentials', async () => {
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
providedName: TEST_STRING,
}));
const ctx = createCtxMock({
@@ -38,7 +37,7 @@ describe(SelectAndroidBuildCredentials, () => {
});
});
it('returns a request to make build credentials when the user chooses to make a new one', async () => {
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
buildCredentialsResultOrRequestToCreateNew:
SelectAndroidBuildCredentialsResultType.CREATE_REQUEST,
providedName: TEST_STRING,
@@ -60,7 +59,7 @@ describe(SelectAndroidBuildCredentials, () => {
});
});
it('returns a request to make default build credentials when the user chooses to make a new one, and if they have no existing credentials', async () => {
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
buildCredentialsResultOrRequestToCreateNew:
SelectAndroidBuildCredentialsResultType.CREATE_REQUEST,
providedName: TEST_STRING,
@@ -80,7 +79,7 @@ describe(SelectAndroidBuildCredentials, () => {
});
});
it('returns buildCredentials of the users choice', async () => {
asMock(promptAsync).mockImplementation(() => ({
jest.mocked(promptAsync).mockImplementation(async () => ({
buildCredentialsResultOrRequestToCreateNew: testAndroidBuildCredentialsFragment,
}));
const ctx = createCtxMock({
@@ -1,6 +1,5 @@
import prompts from 'prompts';
import { asMock } from '../../__tests__/utils';
import Log from '../../log';
import { Actor } from '../../user/User';
import { AccountResolver } from '../manager';
@@ -15,8 +14,8 @@ jest.mock('../../project/projectUtils', () => {
jest.mock('../../credentials/ios/api/graphql/queries/AppleTeamQuery');
beforeEach(() => {
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
jest.mocked(prompts).mockReset();
jest.mocked(prompts).mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
});
@@ -38,7 +37,7 @@ describe(AccountResolver, () => {
const exp = {} as any;
it('returns the account defined in app.json/app.config.js if user confirms', async () => {
asMock(prompts).mockImplementationOnce(() => ({
jest.mocked(prompts).mockImplementationOnce(async () => ({
value: true,
}));
@@ -48,10 +47,10 @@ describe(AccountResolver, () => {
});
it('asks the user to choose the account from his account list if he rejects to use the one defined in app.json / app.config.js', async () => {
asMock(prompts).mockImplementationOnce(() => ({
jest.mocked(prompts).mockImplementationOnce(async () => ({
useProjectAccount: false,
}));
asMock(prompts).mockImplementationOnce(() => ({
jest.mocked(prompts).mockImplementationOnce(async () => ({
account: user.accounts[0],
}));
@@ -61,7 +60,7 @@ describe(AccountResolver, () => {
});
it(`asks the user to choose the account from his account list if he doesn't have access to the account defined in app.json / app.config.js`, async () => {
asMock(prompts).mockImplementationOnce(() => ({
jest.mocked(prompts).mockImplementationOnce(async () => ({
account: user.accounts[0],
}));
@@ -80,7 +79,7 @@ describe(AccountResolver, () => {
describe('when outside project dir', () => {
it('asks the user to choose the account from his account list', async () => {
asMock(prompts).mockImplementationOnce(() => ({
jest.mocked(prompts).mockImplementationOnce(async () => ({
account: user.accounts[0],
}));
@@ -1,6 +1,5 @@
import prompts from 'prompts';
import { asMock } from '../../../../__tests__/utils';
import { Account } from '../../../../user/Account';
import DeviceCreateAction, { RegistrationMethod } from '../action';
import { runInputMethodAsync } from '../inputMethod';
@@ -11,19 +10,19 @@ jest.mock('../registrationUrlMethod');
jest.mock('../inputMethod');
beforeEach(() => {
const promptsMock = asMock(prompts);
const promptsMock = jest.mocked(prompts);
promptsMock.mockReset();
promptsMock.mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
asMock(runRegistrationUrlMethodAsync).mockClear();
asMock(runInputMethodAsync).mockClear();
jest.mocked(runRegistrationUrlMethodAsync).mockClear();
jest.mocked(runInputMethodAsync).mockClear();
});
describe(DeviceCreateAction, () => {
describe('#runAsync', () => {
it('calls runRegistrationUrlMethodAsync if user chooses the website method', async () => {
asMock(prompts).mockImplementationOnce(() => ({
jest.mocked(prompts).mockImplementationOnce(async () => ({
method: RegistrationMethod.WEBSITE,
}));
@@ -43,7 +42,7 @@ describe(DeviceCreateAction, () => {
});
it('calls runInputMethodAsync if user chooses the input method', async () => {
asMock(prompts).mockImplementationOnce(() => ({
jest.mocked(prompts).mockImplementationOnce(async () => ({
method: RegistrationMethod.INPUT,
}));
@@ -1,6 +1,5 @@
import prompts from 'prompts';
import { asMock } from '../../../../__tests__/utils';
import { AppleDeviceMutation } from '../../../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
import { AppleDeviceClass, AppleTeam } from '../../../../graphql/generated';
import { runInputMethodAsync } from '../inputMethod';
@@ -9,19 +8,19 @@ jest.mock('../../../../credentials/ios/api/graphql/mutations/AppleDeviceMutation
jest.mock('../../../../ora');
beforeEach(() => {
asMock(prompts).mockReset();
asMock(prompts).mockImplementation(() => {
jest.mocked(prompts).mockReset();
jest.mocked(prompts).mockImplementation(() => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
asMock(AppleDeviceMutation.createAppleDeviceAsync).mockClear();
jest.mocked(AppleDeviceMutation.createAppleDeviceAsync).mockClear();
});
describe(runInputMethodAsync, () => {
it('should allow for multiple device registration', async () => {
mockDeviceData('00001111-001122334455662E', 'my iPhone', AppleDeviceClass.Iphone);
asMock(prompts).mockImplementationOnce(() => ({ value: true }));
jest.mocked(prompts).mockImplementationOnce(async () => ({ value: true }));
mockDeviceData('b12cba9856d89c932ab7a4b813c4d932534e1679', 'my iPad', AppleDeviceClass.Ipad);
asMock(prompts).mockImplementationOnce(() => ({ value: false }));
jest.mocked(prompts).mockImplementationOnce(async () => ({ value: false }));
const accountId = 'account-id';
// @ts-expect-error appleTeam is missing properties of AppleTeam GraphQL type
@@ -38,8 +37,8 @@ describe(runInputMethodAsync, () => {
});
function mockDeviceData(udid: string, name: string, deviceClass: AppleDeviceClass): void {
asMock(prompts).mockImplementationOnce(() => ({ udid }));
asMock(prompts).mockImplementationOnce(() => ({ name }));
asMock(prompts).mockImplementationOnce(() => ({ deviceClass }));
asMock(prompts).mockImplementationOnce(() => ({ value: true }));
jest.mocked(prompts).mockImplementationOnce(async () => ({ udid }));
jest.mocked(prompts).mockImplementationOnce(async () => ({ name }));
jest.mocked(prompts).mockImplementationOnce(async () => ({ deviceClass }));
jest.mocked(prompts).mockImplementationOnce(async () => ({ value: true }));
}
@@ -0,0 +1,41 @@
import * as PackageManager from '@expo/package-manager';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import * as path from 'path';
import semver from 'semver';
import Log from '../log';
export async function installNodeDependenciesAsync(
projectRoot: string,
packageManager: 'yarn' | 'npm'
): Promise<void> {
const options = { cwd: projectRoot };
if (packageManager === 'yarn') {
const yarn = new PackageManager.YarnPackageManager(options);
const version = await yarn.versionAsync();
const nodeLinker = await yarn.getConfigAsync('nodeLinker');
if (semver.satisfies(version, '>=2.0.0-rc.24') && nodeLinker !== 'node-modules') {
const yarnRc = path.join(projectRoot, '.yarnrc.yml');
let yamlString = '';
try {
yamlString = await fs.readFile(yarnRc, 'utf8');
} catch (error: any) {
if (error.code !== 'ENOENT') {
throw error;
}
}
const config: any = yamlString ? yaml.safeLoad(yamlString) : {};
config.nodeLinker = 'node-modules';
Log.warn(
`Yarn v${version} detected, enabling experimental Yarn v2 support using the node-modules plugin.`
);
Log.log(`Writing ${yarnRc}...`);
await fs.writeFile(yarnRc, yaml.safeDump(config));
}
await yarn.installAsync();
} else {
await new PackageManager.NpmPackageManager(options).installAsync();
}
}
@@ -0,0 +1,66 @@
import spawnAsync, { SpawnOptions, SpawnResult } from '@expo/spawn-async';
import { Transform } from 'stream';
import Log from '../log';
function ansiRegex({ onlyFirst = false } = {}): RegExp {
const pattern = [
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))',
].join('|');
return new RegExp(pattern, onlyFirst ? undefined : 'g');
}
const ansi = `(?:${ansiRegex().source})*`;
const yarnPeerDependencyWarningPattern = new RegExp(
`${ansi}warning${ansi} "[^"]+" has (?:unmet|incorrect) peer dependency "[^"]+"\\.\n`,
'g'
);
class YarnStderrTransform extends Transform {
_transform(
chunk: Buffer,
_encoding: string,
callback: (error?: Error | null, data?: any) => void
): void {
this.push(chunk.toString().replace(yarnPeerDependencyWarningPattern, ''));
callback();
}
}
export class YarnPackageManager {
options: SpawnOptions;
constructor({ cwd, silent }: { cwd: string; silent?: boolean }) {
this.options = {
env: {
...process.env,
},
cwd,
...(silent
? { ignoreStdio: true }
: {
stdio: ['inherit', 'inherit', 'pipe'],
}),
};
}
async runAsync(command: string): Promise<void> {
await this.runPrivateAsync([command]);
}
// Private
private async runPrivateAsync(args: string[]): Promise<SpawnResult> {
if (!this.options.ignoreStdio) {
Log.log(`> yarn ${args.join(' ')}`);
}
// Have spawnAsync consume stdio but we don't actually do anything with it if it's ignored
const promise = spawnAsync('yarnpkg', args, { ...this.options, ignoreStdio: false });
if (promise.child.stderr && !this.options.ignoreStdio) {
promise.child.stderr.pipe(new YarnStderrTransform()).pipe(process.stderr);
}
return promise;
}
}
@@ -0,0 +1,123 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const friendlySyntaxErrorLabel = 'Syntax error:';
function isLikelyASyntaxError(message: string | string[]): boolean {
return message.indexOf(friendlySyntaxErrorLabel) !== -1;
}
// Cleans up webpack error messages.
function formatMessage(message: any): any {
let lines = [];
if (typeof message === 'string') {
lines = message.split('\n');
} else if ('message' in message) {
lines = message['message'].split('\n');
} else if (Array.isArray(message)) {
message.forEach(message => {
if ('message' in message) {
lines = message['message'].split('\n');
}
});
}
// Strip webpack-added headers off errors/warnings
// https://github.com/webpack/webpack/blob/master/lib/ModuleError.js
lines = lines.filter((line: string) => !/Module [A-z ]+\(from/.test(line));
// Transform parsing error into syntax error
// TODO: move this to our ESLint formatter?
lines = lines.map((line: string) => {
const parsingError = /Line (\d+):(?:(\d+):)?\s*Parsing error: (.+)$/.exec(line);
if (!parsingError) {
return line;
}
const [, errorLine, errorColumn, errorMessage] = parsingError;
return `${friendlySyntaxErrorLabel} ${errorMessage} (${errorLine}:${errorColumn})`;
});
message = lines.join('\n');
// Smoosh syntax errors (commonly found in CSS)
message = message.replace(
/SyntaxError\s+\((\d+):(\d+)\)\s*(.+?)\n/g,
`${friendlySyntaxErrorLabel} $3 ($1:$2)\n`
);
// Clean up export errors
message = message.replace(
/^.*export '(.+?)' was not found in '(.+?)'.*$/gm,
`Attempted import error: '$1' is not exported from '$2'.`
);
message = message.replace(
/^.*export 'default' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
`Attempted import error: '$2' does not contain a default export (imported as '$1').`
);
message = message.replace(
/^.*export '(.+?)' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
`Attempted import error: '$1' is not exported from '$3' (imported as '$2').`
);
lines = message.split('\n');
// Remove leading newline
if (lines.length > 2 && lines[1].trim() === '') {
lines.splice(1, 1);
}
// Clean up file name
lines[0] = lines[0].replace(/^(.*) \d+:\d+-\d+$/, '$1');
// Cleans up verbose "module not found" messages for files and packages.
if (lines[1] && lines[1].indexOf('Module not found: ') === 0) {
lines = [
lines[0],
lines[1]
.replace('Error: ', '')
.replace('Module not found: Cannot find file:', 'Cannot find file:'),
];
}
// Add helpful message for users trying to use Sass for the first time
if (lines[1] && lines[1].match(/Cannot find module.+sass/)) {
lines[1] = 'To import Sass files, you first need to install sass.\n';
lines[1] += 'Run `npm install sass` or `yarn add sass` inside your workspace.';
}
message = lines.join('\n');
// Internal stacks are generally useless so we strip them... with the
// exception of stacks containing `webpack:` because they're normally
// from user code generated by webpack. For more information see
// https://github.com/facebook/create-react-app/pull/1050
message = message.replace(/^\s*at\s((?!webpack:).)*:\d+:\d+[\s)]*(\n|$)/gm, ''); // at ... ...:x:y
message = message.replace(/^\s*at\s<anonymous>(\n|$)/gm, ''); // at <anonymous>
lines = message.split('\n');
// Remove duplicated newlines
lines = lines.filter(
(line: string, index: number, arr: string[]) =>
index === 0 || line.trim() !== '' || line.trim() !== arr[index - 1].trim()
);
// Reassemble the message
message = lines.join('\n');
return message.trim();
}
export function formatWebpackMessages(json: any): {
errors: any[];
warnings: any[];
} {
const formattedErrors = json.errors.map(formatMessage);
const formattedWarnings = json.warnings.map(formatMessage);
const result = { errors: formattedErrors, warnings: formattedWarnings };
if (result.errors.some(isLikelyASyntaxError)) {
// If there are any syntax errors, show just them.
result.errors = result.errors.filter(isLikelyASyntaxError);
}
return result;
}
+132
View File
@@ -0,0 +1,132 @@
import chalk from 'chalk';
import fs from 'fs/promises';
import path from 'path';
import PnpWebpackPlugin from 'pnp-webpack-plugin';
import { Compiler, Configuration, EntryObject, webpack } from 'webpack';
import Log from '../log';
import { formatWebpackMessages } from './formatWebpackMessages';
async function getWebpackFunctionConfigurationAsync(
projectDir: string,
entry: EntryObject,
outputPath: string
): Promise<Configuration> {
const webpackConfig = {
entry,
target: 'node',
mode: 'development' as 'development',
// Disable file info logs.
//stats: 'none',
// https://webpack.js.org/configuration/other-options/#bail
// Fail out on the first error instead of tolerating it.
bail: true,
module: {
rules: [
{
test: /\.tsx?$/,
use: {
loader: require.resolve('ts-loader'),
options: {
configFile: require.resolve('../../functions.tsconfig.webpack.json'),
context: projectDir,
},
},
exclude: /node_modules/,
},
],
},
resolve: {
plugins: [PnpWebpackPlugin],
extensions: ['.tsx', '.ts', '.js'],
},
resolveLoader: {
plugins: [PnpWebpackPlugin.moduleLoader(module)],
},
output: {
filename: '[name].js',
path: outputPath,
library: {
type: 'commonjs',
},
},
};
return webpackConfig;
}
async function compileWebAppAsync(compiler: Compiler): Promise<any> {
// We generate the stats.json file in the webpack-config
const { warnings } = await new Promise((resolve, reject) =>
compiler.run((error, stats) => {
let messages;
if (error) {
if (!error.message) {
return reject(error);
}
messages = formatWebpackMessages({
errors: [error.message],
warnings: [],
_showErrors: true,
_showWarnings: true,
});
} else {
messages = formatWebpackMessages(
stats?.toJson({ all: false, warnings: true, errors: true })
);
}
if (messages.errors.length) {
// Only keep the first error. Others are often indicative
// of the same problem, but confuse the reader with noise.
if (messages.errors.length > 1) {
messages.errors.length = 1;
}
return reject(messages.errors.join('\n\n'));
}
resolve({
warnings: messages.warnings,
});
})
);
return { warnings };
}
export async function bundleFunctionsAsync(
projectDir: string,
entry: EntryObject,
outputPath: string
): Promise<void> {
const config = await getWebpackFunctionConfigurationAsync(projectDir, entry, outputPath);
const compiler = webpack(config);
try {
const { warnings } = await compileWebAppAsync(compiler);
if (warnings.length) {
Log.warn(chalk.yellow('Compiled with warnings.\n'));
Log.warn(warnings.join('\n\n'));
} else {
Log.log(chalk.green('Compiled successfully.\n'));
}
} catch (error) {
Log.error(chalk.red('Failed to compile.\n'));
Log.error(error);
throw error;
}
}
export async function prepareFunctionEntriesAsync(functionsPath: string): Promise<EntryObject> {
const files = await fs.readdir(functionsPath);
if (files.length === 0) {
throw new Error(`No functions found in ${functionsPath}`);
}
const entries = {} as EntryObject;
for (const file of files) {
const filenameNoExtension = path.parse(file).name;
entries[filenameNoExtension] = path.join(functionsPath, file);
}
return entries;
}
+75
View File
@@ -401,10 +401,16 @@ export type AccountUsageMetric = {
export type AccountUsageMetrics = {
__typename?: 'AccountUsageMetrics';
byBillingPeriod: UsageMetricTotal;
metricsForServiceMetric: Array<AccountUsageMetric>;
};
export type AccountUsageMetricsByBillingPeriodArgs = {
date: Scalars['DateTime'];
};
export type AccountUsageMetricsMetricsForServiceMetricArgs = {
granularity: UsageMetricsGranularity;
metric: EasServiceMetric;
@@ -2019,6 +2025,11 @@ export type DeleteWebhookResult = {
id: Scalars['ID'];
};
export type DeployFunctionResult = {
__typename?: 'DeployFunctionResult';
success: Scalars['Boolean'];
};
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type Deployment = {
__typename?: 'Deployment';
@@ -2149,6 +2160,40 @@ export enum Feature {
Teams = 'TEAMS'
}
export type FunctionMutation = {
__typename?: 'FunctionMutation';
createFunction: DeployFunctionResult;
getFunctionStatus: FunctionStatusResult;
getSignedFunctionUploadSpecification: GetFunctionUploadUrlResult;
};
export type FunctionMutationCreateFunctionArgs = {
appId: Scalars['String'];
bucketKey: Scalars['String'];
};
export type FunctionMutationGetFunctionStatusArgs = {
appId: Scalars['String'];
};
export type FunctionMutationGetSignedFunctionUploadSpecificationArgs = {
appId: Scalars['String'];
};
export type FunctionStatusResult = {
__typename?: 'FunctionStatusResult';
logUrls: Array<Scalars['String']>;
status: Scalars['String'];
};
export type GetFunctionUploadUrlResult = {
__typename?: 'GetFunctionUploadUrlResult';
specification: Scalars['String'];
};
export type GetSignedAssetUploadSpecificationsResult = {
__typename?: 'GetSignedAssetUploadSpecificationsResult';
specifications: Array<Scalars['String']>;
@@ -2866,6 +2911,7 @@ export type RootMutation = {
emailSubscription: EmailSubscriptionMutation;
/** Mutations that create and delete EnvironmentSecrets */
environmentSecret: EnvironmentSecretMutation;
function: FunctionMutation;
/** Mutations that modify a Google Service Account Key */
googleServiceAccountKey: GoogleServiceAccountKeyMutation;
/** Mutations that modify the build credentials for an iOS app */
@@ -3395,6 +3441,13 @@ export enum UploadSessionType {
EasSubmitAppArchive = 'EAS_SUBMIT_APP_ARCHIVE'
}
export type UsageMetricTotal = {
__typename?: 'UsageMetricTotal';
billingPeriod: BillingPeriod;
metrics: Array<AccountUsageMetric>;
total: Scalars['Float'];
};
export enum UsageMetricType {
Request = 'REQUEST'
}
@@ -4291,6 +4344,28 @@ export type DeleteEnvironmentSecretMutationVariables = Exact<{
export type DeleteEnvironmentSecretMutation = { __typename?: 'RootMutation', environmentSecret: { __typename?: 'EnvironmentSecretMutation', deleteEnvironmentSecret: { __typename?: 'DeleteEnvironmentSecretResult', id: string } } };
export type GetSignedUploadFunctionMutationVariables = Exact<{
appId: Scalars['String'];
}>;
export type GetSignedUploadFunctionMutation = { __typename?: 'RootMutation', function: { __typename?: 'FunctionMutation', getSignedFunctionUploadSpecification: { __typename?: 'GetFunctionUploadUrlResult', specification: string } } };
export type CreateFunctionMutationVariables = Exact<{
bucketKey: Scalars['String'];
appId: Scalars['String'];
}>;
export type CreateFunctionMutation = { __typename?: 'RootMutation', function: { __typename?: 'FunctionMutation', createFunction: { __typename?: 'DeployFunctionResult', success: boolean } } };
export type GetFunctionStatusMutationVariables = Exact<{
appId: Scalars['String'];
}>;
export type GetFunctionStatusMutation = { __typename?: 'RootMutation', function: { __typename?: 'FunctionMutation', getFunctionStatus: { __typename?: 'FunctionStatusResult', status: string, logUrls: Array<string> } } };
export type CreateKeystoreGenerationUrlMutationVariables = Exact<{ [key: string]: never; }>;
@@ -0,0 +1,79 @@
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import {
CreateFunctionMutation,
CreateFunctionMutationVariables,
GetFunctionStatusMutation,
GetFunctionStatusMutationVariables,
GetSignedUploadFunctionMutation,
GetSignedUploadFunctionMutationVariables,
} from '../generated';
export const FunctionMutation = {
async uploadAsync(appId: string): Promise<string> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<GetSignedUploadFunctionMutation, GetSignedUploadFunctionMutationVariables>(
gql`
mutation GetSignedUploadFunctionMutation($appId: String!) {
function {
getSignedFunctionUploadSpecification(appId: $appId) {
specification
}
}
}
`,
{
appId,
}
)
.toPromise()
);
return data.function.getSignedFunctionUploadSpecification.specification;
},
async createAsync(bucketKey: string, appId: string): Promise<boolean> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateFunctionMutation, CreateFunctionMutationVariables>(
gql`
mutation CreateFunctionMutation($bucketKey: String!, $appId: String!) {
function {
createFunction(bucketKey: $bucketKey, appId: $appId) {
success
}
}
}
`,
{
bucketKey,
appId,
}
)
.toPromise()
);
return data.function.createFunction.success;
},
async getStatusAsync(appId: string): Promise<{ status: string; logUrls: string[] }> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<GetFunctionStatusMutation, GetFunctionStatusMutationVariables>(
gql`
mutation GetFunctionStatusMutation($appId: String!) {
function {
getFunctionStatus(appId: $appId) {
status
logUrls
}
}
}
`,
{
appId,
}
)
.toPromise()
);
return data.function.getFunctionStatus;
},
};
@@ -1,6 +1,5 @@
import { vol } from 'memfs';
import { asMock } from '../../__tests__/utils';
import { confirmAsync } from '../../prompts';
import { Actor, getUserAsync } from '../../user/User';
import {
@@ -123,52 +122,58 @@ describe(getProjectAccountNameAsync, () => {
const expWithoutOwner: any = {};
beforeEach(() => {
asMock(getUserAsync).mockReset();
jest.mocked(getUserAsync).mockReset();
});
it(`returns the owner field's value from app.json / app.config.js`, async () => {
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'notnotbrent',
accounts: [
{ id: 'account_id_1', name: 'notnotbrent' },
{ id: 'account_id_2', name: 'dominik' },
],
isExpoAdmin: false,
}));
jest.mocked(getUserAsync).mockImplementation(
async (): Promise<Actor> => ({
__typename: 'User',
id: 'user_id',
username: 'notnotbrent',
accounts: [
{ id: 'account_id_1', name: 'notnotbrent' },
{ id: 'account_id_2', name: 'dominik' },
],
isExpoAdmin: false,
})
);
const projectAccountName = await getProjectAccountNameAsync(expWithOwner);
expect(projectAccountName).toBe('dominik');
});
it(`returns the username if owner field is not set in app.json / app.config.js`, async () => {
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'notnotbrent',
accounts: [
{ id: 'account_id_1', name: 'notnotbrent' },
{ id: 'account_id_2', name: 'dominik' },
],
isExpoAdmin: false,
}));
jest.mocked(getUserAsync).mockImplementation(
async (): Promise<Actor> => ({
__typename: 'User',
id: 'user_id',
username: 'notnotbrent',
accounts: [
{ id: 'account_id_1', name: 'notnotbrent' },
{ id: 'account_id_2', name: 'dominik' },
],
isExpoAdmin: false,
})
);
const projectAccountName = await getProjectAccountNameAsync(expWithoutOwner);
expect(projectAccountName).toBe('notnotbrent');
});
it(`throws when project owner is undefined for robot actors`, async () => {
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'Robot',
id: 'user_id',
firstName: 'GLaDOS',
accounts: [
{ id: 'account_id_1', name: 'notnotbrent' },
{ id: 'account_id_2', name: 'dominik' },
],
isExpoAdmin: false,
}));
jest.mocked(getUserAsync).mockImplementation(
async (): Promise<Actor> => ({
__typename: 'Robot',
id: 'user_id',
firstName: 'GLaDOS',
accounts: [
{ id: 'account_id_1', name: 'notnotbrent' },
{ id: 'account_id_2', name: 'dominik' },
],
isExpoAdmin: false,
})
);
await expect(getProjectAccountNameAsync(expWithoutOwner)).rejects.toThrow(
'manifest property is required'
);
@@ -177,57 +182,67 @@ describe(getProjectAccountNameAsync, () => {
describe(promptToCreateProjectIfNotExistsAsync, () => {
beforeEach(() => {
asMock(getUserAsync).mockReset();
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockReset();
jest.mocked(getUserAsync).mockReset();
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockReset();
});
it('returns the existing project', async () => {
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
}));
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockImplementation(
() => 'already-existing-project-id'
jest.mocked(getUserAsync).mockImplementation(
async (): Promise<Actor> => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
})
);
jest
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
.mockImplementation(async () => 'already-existing-project-id');
const exp: any = {};
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
expect(projectId).toBe('already-existing-project-id');
});
it('makes a new project if none exists', async () => {
jest.mock('../../prompts');
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
}));
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockImplementation(() => null);
asMock(ensureProjectExistsAsync).mockImplementation(() => 'new-project-id');
jest.mocked(getUserAsync).mockImplementation(
async (): Promise<Actor> => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
})
);
jest
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
.mockImplementation(async () => null);
jest.mocked(ensureProjectExistsAsync).mockImplementation(async () => 'new-project-id');
// user agrees to make new project
asMock(confirmAsync).mockImplementation(() => true);
jest.mocked(confirmAsync).mockImplementation(async () => true);
const exp: any = {};
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
expect(projectId).toBe('new-project-id');
});
it('aborts making a new project if the user declines', async () => {
jest.mock('../../prompts');
asMock(getUserAsync).mockImplementation((): Actor | undefined => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
}));
asMock(findProjectIdByAccountNameAndSlugNullableAsync).mockImplementation(() => null);
asMock(ensureProjectExistsAsync).mockImplementation(() => 'new-project-id');
jest.mocked(getUserAsync).mockImplementation(
async (): Promise<Actor> => ({
__typename: 'User',
id: 'user_id',
username: 'quin',
accounts: [{ id: 'account_id_1', name: 'quin' }],
isExpoAdmin: false,
})
);
jest
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
.mockImplementation(async () => null);
jest.mocked(ensureProjectExistsAsync).mockImplementation(async () => 'new-project-id');
// user doesnt agree to make new project
asMock(confirmAsync).mockImplementation(() => false);
jest.mocked(confirmAsync).mockImplementation(async () => false);
const exp: any = {};
const projectId = await promptToCreateProjectIfNotExistsAsync(exp);
expect(projectId).toBe(null);
@@ -2,7 +2,6 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { promptAsync } from '../../../prompts';
import {
@@ -21,7 +20,7 @@ beforeEach(async () => {
// this fixes a weird error with tempy in @expo/image-utils
await fs.mkdirp(os.tmpdir());
asMock(promptAsync).mockReset();
jest.mocked(promptAsync).mockReset();
});
describe(getApplicationIdAsync, () => {
@@ -122,7 +121,7 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
'/app'
);
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
packageName: 'com.expo.notdominik',
}));
@@ -139,7 +138,7 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
'/app'
);
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
packageName: 'com.expo.notdominik',
}));
@@ -2,7 +2,6 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { promptAsync } from '../../../prompts';
import { resolveGradleBuildContextAsync } from '../gradle';
@@ -17,7 +16,7 @@ beforeEach(async () => {
// this fixes a weird error with tempy in @expo/image-utils
await fs.mkdirp(os.tmpdir());
asMock(promptAsync).mockReset();
jest.mocked(promptAsync).mockReset();
});
describe(resolveGradleBuildContextAsync, () => {
@@ -3,7 +3,6 @@ import { vol } from 'memfs';
import os from 'os';
import path from 'path';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { promptAsync } from '../../../prompts';
import {
@@ -23,7 +22,7 @@ beforeEach(async () => {
// this fixes a weird error with tempy in @expo/image-utils
await fs.mkdirp(os.tmpdir());
asMock(promptAsync).mockReset();
jest.mocked(promptAsync).mockReset();
});
const originalFs = jest.requireActual('fs') as typeof fs;
@@ -105,7 +104,7 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
'/app'
);
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
bundleIdentifier: 'com.expo.notdominik',
}));
@@ -122,7 +121,7 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
'/app'
);
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
bundleIdentifier: 'com.expo.notdominik',
}));
@@ -2,7 +2,6 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { asMock } from '../../../__tests__/utils';
import { promptAsync } from '../../../prompts';
import { selectSchemeAsync } from '../scheme';
@@ -16,7 +15,7 @@ beforeEach(async () => {
// this fixes a weird error with tempy in @expo/image-utils
await fs.mkdirp(os.tmpdir());
asMock(promptAsync).mockReset();
jest.mocked(promptAsync).mockReset();
});
describe(selectSchemeAsync, () => {
@@ -58,7 +57,7 @@ describe(selectSchemeAsync, () => {
},
projectDir
);
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
selectedScheme: 'scheme3',
}));
@@ -3,7 +3,6 @@ import fs from 'fs-extra';
import { vol } from 'memfs';
import { v4 as uuidv4 } from 'uuid';
import { asMock } from '../../__tests__/utils';
import { AppPlatform, BuildFragment, UploadSessionType } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
@@ -51,13 +50,13 @@ describe(getArchiveAsync, () => {
beforeEach(() => {
vol.reset();
asMock(promptAsync).mockReset();
asMock(promptAsync).mockImplementation(() => {
jest.mocked(promptAsync).mockReset();
jest.mocked(promptAsync).mockImplementation(async () => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
asMock(confirmAsync).mockReset();
asMock(confirmAsync).mockImplementation(() => {
jest.mocked(confirmAsync).mockReset();
jest.mocked(confirmAsync).mockImplementation(async () => {
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
});
});
@@ -73,7 +72,8 @@ describe(getArchiveAsync, () => {
});
it('prompts again if provided URL is invalid', async () => {
asMock(promptAsync)
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
@@ -88,8 +88,8 @@ describe(getArchiveAsync, () => {
});
it('asks the user if use build id instead of build details page url', async () => {
asMock(confirmAsync).mockResolvedValueOnce(true);
asMock(BuildQuery.byIdAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT);
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
jest.mocked(BuildQuery.byIdAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT as BuildFragment);
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
@@ -103,7 +103,8 @@ describe(getArchiveAsync, () => {
});
it('handles prompt source', async () => {
asMock(promptAsync)
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
@@ -117,7 +118,7 @@ describe(getArchiveAsync, () => {
it('handles Build ID source', async () => {
const buildId = uuidv4();
asMock(BuildQuery.byIdAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT);
jest.mocked(BuildQuery.byIdAsync).mockResolvedValueOnce(MOCK_BUILD_FRAGMENT as BuildFragment);
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
@@ -130,8 +131,9 @@ describe(getArchiveAsync, () => {
});
it('prompts again if build with provided ID doesnt exist', async () => {
asMock(BuildQuery.byIdAsync).mockRejectedValue(new Error('Build doesnt exist'));
asMock(promptAsync)
jest.mocked(BuildQuery.byIdAsync).mockRejectedValue(new Error('Build doesnt exist'));
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
@@ -146,7 +148,9 @@ describe(getArchiveAsync, () => {
it('handles latest build source', async () => {
const projectId = uuidv4();
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([MOCK_BUILD_FRAGMENT]);
jest
.mocked(getRecentBuildsForSubmissionAsync)
.mockResolvedValueOnce([MOCK_BUILD_FRAGMENT as BuildFragment]);
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
@@ -162,8 +166,9 @@ describe(getArchiveAsync, () => {
});
it('prompts again if no builds exists when selected latest', async () => {
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([]);
asMock(promptAsync)
jest.mocked(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([]);
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
@@ -177,8 +182,10 @@ describe(getArchiveAsync, () => {
it('handles build-list-select source', async () => {
const projectId = uuidv4();
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([MOCK_BUILD_FRAGMENT]);
asMock(promptAsync).mockResolvedValueOnce({ selectedBuild: MOCK_BUILD_FRAGMENT });
jest
.mocked(getRecentBuildsForSubmissionAsync)
.mockResolvedValueOnce([MOCK_BUILD_FRAGMENT as BuildFragment]);
jest.mocked(promptAsync).mockResolvedValueOnce({ selectedBuild: MOCK_BUILD_FRAGMENT });
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
@@ -195,13 +202,14 @@ describe(getArchiveAsync, () => {
});
it('prompts again if all builds have expired', async () => {
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([
jest.mocked(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([
{
...MOCK_BUILD_FRAGMENT,
updatedAt: new Date(Date.now() - 31 * 24 * 3600 * 1000),
},
} as BuildFragment,
]);
asMock(promptAsync)
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
@@ -214,8 +222,11 @@ describe(getArchiveAsync, () => {
});
it('falls back to prompt if user selected "None of the above"', async () => {
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([MOCK_BUILD_FRAGMENT]);
asMock(promptAsync)
jest
.mocked(getRecentBuildsForSubmissionAsync)
.mockResolvedValueOnce([MOCK_BUILD_FRAGMENT as BuildFragment]);
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ selectedBuild: null })
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
@@ -231,7 +242,7 @@ describe(getArchiveAsync, () => {
it('handles path source', async () => {
const path = '/archive.apk';
await fs.writeFile(path, 'some content');
asMock(uploadAsync).mockResolvedValueOnce({ url: ARCHIVE_URL });
jest.mocked(uploadAsync).mockResolvedValueOnce({ url: ARCHIVE_URL, bucketKey: 'wat' });
const archive = await getArchiveAsync({
...SOURCE_STUB_INPUT,
@@ -248,7 +259,8 @@ describe(getArchiveAsync, () => {
});
it('prompts again if provided path doesnt exist', async () => {
asMock(promptAsync)
jest
.mocked(promptAsync)
.mockResolvedValueOnce({ sourceType: ArchiveSourceType.url })
.mockResolvedValueOnce({ url: ARCHIVE_URL });
@@ -3,7 +3,6 @@ import { AndroidReleaseStatus, AndroidReleaseTrack } from '@expo/eas-json';
import { vol } from 'memfs';
import { v4 as uuidv4 } from 'uuid';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import {
AppPlatform,
@@ -74,7 +73,7 @@ describe(AndroidSubmitCommand, () => {
});
afterEach(() => {
asMock(getProjectIdAsync).mockClear();
jest.mocked(getProjectIdAsync).mockClear();
});
describe('non-interactive mode', () => {
@@ -136,7 +135,9 @@ describe(AndroidSubmitCommand, () => {
it('assigns the build ID to submission', async () => {
const projectId = uuidv4();
asMock(getRecentBuildsForSubmissionAsync).mockResolvedValueOnce([fakeBuildFragment]);
jest
.mocked(getRecentBuildsForSubmissionAsync)
.mockResolvedValueOnce([fakeBuildFragment as BuildFragment]);
const ctx = await createSubmissionContextAsync({
platform: Platform.ANDROID,
@@ -3,7 +3,6 @@ import { AndroidReleaseStatus, AndroidReleaseTrack } from '@expo/eas-json';
import { vol } from 'memfs';
import { v4 as uuidv4 } from 'uuid';
import { asMock } from '../../../__tests__/utils';
import { testAndroidAppCredentialsFragment } from '../../../credentials/__tests__/fixtures-android';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { SetUpGoogleServiceAccountKey } from '../../../credentials/android/actions/SetUpGoogleServiceAccountKey';
@@ -60,14 +59,14 @@ afterAll(() => {
});
afterEach(() => {
asMock(promptAsync).mockClear();
jest.mocked(promptAsync).mockClear();
jest.restoreAllMocks();
});
describe(getServiceAccountKeyPathAsync, () => {
describe('when source is ServiceAccountSourceType.path', () => {
it("prompts for path if the provided file doesn't exist", async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
filePath: '/google-service-account.json',
}));
const source: ServiceAccountSource = {
@@ -100,7 +99,7 @@ describe(getServiceAccountKeyPathAsync, () => {
describe('when source is ServiceAccountSourceType.prompt', () => {
it('prompts for path', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
filePath: '/google-service-account.json',
}));
const source: ServiceAccountSource = {
@@ -112,14 +111,15 @@ describe(getServiceAccountKeyPathAsync, () => {
});
it('prompts for path until the user provides an existing file', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({
filePath: '/doesnt-exist.json',
}))
.mockImplementationOnce(() => ({
.mockImplementationOnce(async () => ({
filePath: '/googl-service-account.json',
}))
.mockImplementationOnce(() => ({
.mockImplementationOnce(async () => ({
filePath: '/google-service-account.json',
}));
const source: ServiceAccountSource = {
@@ -166,7 +166,7 @@ describe(getServiceAccountKeyResultAsync, () => {
});
it('returns a local Service Account Key file with a ServiceAccountSourceType.prompt source', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
filePath: '/project_dir/subdir/service-account.json',
}));
const ctx = await createSubmissionContextAsync({
@@ -2,7 +2,6 @@ import { Platform } from '@expo/eas-build-job';
import { vol } from 'memfs';
import { v4 as uuidv4 } from 'uuid';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { getCredentialsFromUserAsync } from '../../../credentials/utils/promptForCredentials';
import { createTestProject } from '../../../project/__tests__/project-utils';
@@ -63,16 +62,16 @@ afterAll(() => {
});
afterEach(() => {
asMock(promptAsync).mockClear();
jest.mocked(promptAsync).mockClear();
});
describe(getAscApiKeyPathAsync, () => {
describe('when source is AscApiKeySourceType.path', () => {
it("prompts for path if the provided file doesn't exist", async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
jest.mocked(getCredentialsFromUserAsync).mockImplementation(async () => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
@@ -117,10 +116,10 @@ describe(getAscApiKeyPathAsync, () => {
describe('when source is AscApiKeySourceType.prompt', () => {
it('prompts for path', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
jest.mocked(getCredentialsFromUserAsync).mockImplementation(async () => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
@@ -138,17 +137,18 @@ describe(getAscApiKeyPathAsync, () => {
});
it('prompts for path until the user provides an existing file', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({
keyP8Path: '/doesnt-exist.p8',
}))
.mockImplementationOnce(() => ({
.mockImplementationOnce(async () => ({
keyP8Path: '/blah.p8',
}))
.mockImplementationOnce(() => ({
.mockImplementationOnce(async () => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
jest.mocked(getCredentialsFromUserAsync).mockImplementation(async () => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
@@ -190,10 +190,10 @@ describe(getAscApiKeyLocallyAsync, () => {
});
it('returns a local Asc API Key file with a AscApiKeySourceType.prompt source', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
jest.mocked(promptAsync).mockImplementationOnce(async () => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementationOnce(() => ({
jest.mocked(getCredentialsFromUserAsync).mockImplementationOnce(async () => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
@@ -1,7 +1,6 @@
import { Platform } from '@expo/eas-build-job';
import { v4 as uuidv4 } from 'uuid';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { testCommonIosAppCredentialsFragment } from '../../../credentials/__tests__/fixtures-ios';
import { SetUpSubmissionCredentials } from '../../../credentials/ios/actions/SetUpSubmissionCredentials';
@@ -57,8 +56,8 @@ describe(getFromCredentialsServiceAsync, () => {
jest
.spyOn(SetUpSubmissionCredentials.prototype, 'runAsync')
.mockImplementation(async _ctx => 'super secret');
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
asMock(promptAsync).mockImplementationOnce(() => ({ appleId: '[email protected]' }));
jest.mocked(getBundleIdentifierAsync).mockImplementation(async () => 'com.hello.world');
jest.mocked(promptAsync).mockImplementationOnce(async () => ({ appleId: '[email protected]' }));
const result = await getFromCredentialsServiceAsync(ctx, {
sourceType: CREDENTIALS_SERVICE_SOURCE,
@@ -84,7 +83,7 @@ describe(getFromCredentialsServiceAsync, () => {
jest
.spyOn(SetUpSubmissionCredentials.prototype, 'runAsync')
.mockImplementation(async _ctx => testCommonIosAppCredentialsFragment);
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
jest.mocked(getBundleIdentifierAsync).mockImplementation(async () => 'com.hello.world');
const result = await getFromCredentialsServiceAsync(ctx, {
sourceType: CREDENTIALS_SERVICE_SOURCE,
@@ -2,7 +2,6 @@ import { Platform } from '@expo/eas-build-job';
import { vol } from 'memfs';
import { v4 as uuidv4 } from 'uuid';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
import { createTestProject } from '../../../project/__tests__/project-utils';
@@ -50,7 +49,7 @@ describe(IosSubmitCommand, () => {
});
afterEach(() => {
asMock(getProjectIdAsync).mockClear();
jest.mocked(getProjectIdAsync).mockClear();
});
describe('non-interactive mode', () => {
@@ -1,4 +1,3 @@
import { asMock } from '../../__tests__/utils';
import { ApiV2Error } from '../../api';
import { promptAsync } from '../../prompts';
import { Actor, loginAsync } from '../User';
@@ -16,12 +15,12 @@ jest.mock('../User', () => ({
}));
beforeEach(() => {
asMock(promptAsync).mockReset();
asMock(promptAsync).mockImplementation(() => {
jest.mocked(promptAsync).mockReset();
jest.mocked(promptAsync).mockImplementation(async () => {
throw new Error('Should not be called');
});
asMock(loginAsync).mockReset();
jest.mocked(loginAsync).mockReset();
});
const userStub: Actor = {
@@ -52,13 +51,15 @@ describe('ensureActorHasUsername', () => {
describe(showLoginPromptAsync, () => {
it('prompts for OTP when 2FA is enabled', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({ username: 'hello', password: 'world' }))
.mockImplementationOnce(() => ({ otp: '123456' }))
.mockImplementation(() => {
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({ username: 'hello', password: 'world' }))
.mockImplementationOnce(async () => ({ otp: '123456' }))
.mockImplementation(async () => {
throw new Error("shouldn't happen");
});
asMock(loginAsync)
jest
.mocked(loginAsync)
.mockImplementationOnce(async () => {
throw new ApiV2Error({
message: 'An OTP is required',
@@ -76,7 +77,7 @@ describe(showLoginPromptAsync, () => {
},
});
})
.mockImplementation(() => {});
.mockImplementation(async () => {});
await showLoginPromptAsync();
+52 -40
View File
@@ -1,4 +1,3 @@
import { asMock } from '../../__tests__/utils';
import { api } from '../../api';
import Log from '../../log';
import { promptAsync, selectAsync } from '../../prompts';
@@ -13,24 +12,25 @@ jest.mock('../User', () => ({
jest.mock('../../log');
beforeEach(() => {
asMock(promptAsync).mockReset();
asMock(promptAsync).mockImplementation(() => {
jest.mocked(promptAsync).mockReset();
jest.mocked(promptAsync).mockImplementation(async () => {
throw new Error('Should not be called');
});
asMock(selectAsync).mockReset();
asMock(selectAsync).mockImplementation(() => {
jest.mocked(selectAsync).mockReset();
jest.mocked(selectAsync).mockImplementation(() => {
throw new Error('Should not be called');
});
asMock(loginAsync).mockReset();
asMock(Log.log).mockReset();
jest.mocked(loginAsync).mockReset();
jest.mocked(Log.log).mockReset();
});
describe(retryUsernamePasswordAuthWithOTPAsync, () => {
it('shows SMS OTP prompt when SMS is primary and code was automatically sent', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({ otp: 'hello' }))
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({ otp: 'hello' }))
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
@@ -51,12 +51,13 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
'One-time password was sent to the phone number ending in testphone.'
);
expect(asMock(loginAsync)).toHaveBeenCalledTimes(1);
expect(jest.mocked(loginAsync)).toHaveBeenCalledTimes(1);
});
it('shows authenticator OTP prompt when authenticator is primary', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({ otp: 'hello' }))
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({ otp: 'hello' }))
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
@@ -74,19 +75,21 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
});
expect(Log.log).toHaveBeenCalledWith('One-time password from authenticator required.');
expect(asMock(loginAsync)).toHaveBeenCalledTimes(1);
expect(jest.mocked(loginAsync)).toHaveBeenCalledTimes(1);
});
it('shows menu when user bails on primary', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({ otp: null }))
.mockImplementationOnce(() => ({ otp: 'hello' })) // second time it is prompted after selecting backup code
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({ otp: null }))
.mockImplementationOnce(async () => ({ otp: 'hello' })) // second time it is prompted after selecting backup code
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
asMock(selectAsync)
.mockImplementationOnce(() => -1)
jest
.mocked(selectAsync)
.mockImplementationOnce(async () => -1)
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
@@ -109,13 +112,14 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
smsAutomaticallySent: false,
});
expect(asMock(selectAsync).mock.calls.length).toEqual(1);
expect(asMock(loginAsync)).toHaveBeenCalledTimes(1);
expect(jest.mocked(selectAsync).mock.calls.length).toEqual(1);
expect(jest.mocked(loginAsync)).toHaveBeenCalledTimes(1);
});
it('shows a warning when when user bails on primary and does not have any secondary set up', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({ otp: null }))
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({ otp: null }))
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
@@ -138,15 +142,17 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
});
it('prompts for authenticator OTP when user selects authenticator secondary', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({ otp: null }))
.mockImplementationOnce(() => ({ otp: 'hello' })) // second time it is prompted after selecting backup code
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({ otp: null }))
.mockImplementationOnce(async () => ({ otp: 'hello' })) // second time it is prompted after selecting backup code
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
asMock(selectAsync)
.mockImplementationOnce(() => -1)
jest
.mocked(selectAsync)
.mockImplementationOnce(async () => -1)
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
@@ -169,24 +175,28 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
smsAutomaticallySent: false,
});
expect(asMock(promptAsync).mock.calls.length).toBe(2); // first OTP, second OTP
expect(jest.mocked(promptAsync).mock.calls.length).toBe(2); // first OTP, second OTP
});
it('requests SMS OTP and prompts for SMS OTP when user selects SMS secondary', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({ otp: null }))
.mockImplementationOnce(() => ({ otp: 'hello' })) // second time it is prompted after selecting backup code
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({ otp: null }))
.mockImplementationOnce(async () => ({ otp: 'hello' })) // second time it is prompted after selecting backup code
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
asMock(selectAsync)
.mockImplementationOnce(() => 0)
jest
.mocked(selectAsync)
.mockImplementationOnce(async () => 0)
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
asMock(api.postAsync).mockReturnValueOnce(Promise.resolve({ sessionSecret: 'SESSION_SECRET' }));
jest
.mocked(api.postAsync)
.mockReturnValueOnce(Promise.resolve({ sessionSecret: 'SESSION_SECRET' }));
await retryUsernamePasswordAuthWithOTPAsync('blah', 'blah', {
secondFactorDevices: [
@@ -206,8 +216,8 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
smsAutomaticallySent: false,
});
expect(asMock(promptAsync).mock.calls.length).toBe(2); // first OTP, second OTP
expect(asMock(api.postAsync).mock.calls[0]).toEqual([
expect(jest.mocked(promptAsync).mock.calls.length).toBe(2); // first OTP, second OTP
expect(jest.mocked(api.postAsync).mock.calls[0]).toEqual([
'auth/send-sms-otp',
{
body: {
@@ -220,14 +230,16 @@ describe(retryUsernamePasswordAuthWithOTPAsync, () => {
});
it('exits when user bails on primary and backup', async () => {
asMock(promptAsync)
.mockImplementationOnce(() => ({ otp: null }))
jest
.mocked(promptAsync)
.mockImplementationOnce(async () => ({ otp: null }))
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
asMock(selectAsync)
.mockImplementationOnce(() => -2)
jest
.mocked(selectAsync)
.mockImplementationOnce(async () => -2)
.mockImplementation(() => {
throw new Error("shouldn't happen");
});
@@ -1,7 +1,6 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJsonReader, errors } from '@expo/eas-json';
import { asMock } from '../../__tests__/utils';
import { selectAsync } from '../../prompts';
import { getProfilesAsync } from '../profiles';
@@ -27,7 +26,7 @@ describe(getProfilesAsync, () => {
afterEach(() => {
getBuildProfileAsync.mockReset();
getBuildProfileNamesAsync.mockReset();
asMock(selectAsync).mockReset();
jest.mocked(selectAsync).mockReset();
});
it('defaults to production profile', async () => {
@@ -76,7 +75,7 @@ describe(getProfilesAsync, () => {
throw new errors.MissingProfileError();
});
getBuildProfileNamesAsync.mockImplementation(() => Promise.resolve(['foo', 'bar']));
asMock(selectAsync).mockImplementation(() => 'foo');
jest.mocked(selectAsync).mockImplementation(async () => 'foo');
const result = await getProfilesAsync({
projectDir: '/fake',
+5 -1
View File
@@ -7,11 +7,15 @@ import Log from '../log';
export async function expoCommandAsync(
projectDir: string,
args: string[],
{ silent = false }: { silent?: boolean } = {}
{ silent = false, env }: { silent?: boolean; env?: { [key: string]: string } } = {}
): Promise<void> {
const expoCliPath = resolveFrom(projectDir, 'expo/bin/cli.js');
const spawnPromise = spawnAsync(expoCliPath, args, {
stdio: ['inherit', 'pipe', 'pipe'], // inherit stdin so user can install a missing expo-cli from inside this command
env: {
...process.env,
...env,
},
});
const {
child: { stdout, stderr },
+4 -3
View File
@@ -78,11 +78,12 @@ export async function makeShallowCopyAsync(src: string, dst: string): Promise<vo
const ignore = new Ignore(src);
await ignore.initIgnoreAsync();
await fs.copy(src, dst, {
filter: (srcFilePath: string) => {
if (srcFilePath === src) {
filter: (_srcFilePath: string) => {
return true; // TODO: handle ignored build folder
/* if (srcFilePath === src) {
return true;
}
return !ignore.ignores(path.relative(src, srcFilePath));
return !ignore.ignores(path.relative(src, srcFilePath)); */
},
});
}
+5
View File
@@ -0,0 +1,5 @@
declare module 'pnp-webpack-plugin' {
function apply(resolver: any /* EnhancedResolve.Resolver */): void;
function moduleLoader(module: NodeModule): any;
function forkTsCheckerOptions(config: any): any;
}
Generated
+831 -19
View File
File diff suppressed because it is too large. Load diff