Skip to content

Commit c177c00

Browse files
committed
Test coverage for codesigned updates
1 parent e27ed10 commit c177c00

6 files changed

Lines changed: 177 additions & 63 deletions

File tree

expo.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ const withCodePushInfoPlist = (config, options = {}) => {
8989
if (options.ios && options.ios.CodePushServerURL) {
9090
config.modResults.CodePushServerURL = options.ios.CodePushServerURL;
9191
}
92+
if (options.ios && options.ios.CodePushPublicKey) {
93+
config.modResults.CodePushPublicKey = options.ios.CodePushPublicKey;
94+
}
9295
return config;
9396
});
9497
};
@@ -324,6 +327,9 @@ const withAndroidStrings = (config, options) => {
324327
if (options.android?.CodePushServerURL) {
325328
setString('CodePushServerUrl', options.android.CodePushServerURL);
326329
}
330+
if (options.android?.CodePushPublicKey) {
331+
setString('CodePushPublicKey', options.android.CodePushPublicKey);
332+
}
327333
return config;
328334
});
329335
};

test/codesign.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"use strict";
2+
3+
import crypto = require("crypto");
4+
import fs = require("fs");
5+
import mkdirp = require("mkdirp");
6+
import path = require("path");
7+
8+
import { Platform, ProjectManager, ServerUtil, setupUpdateScenario, TestConfig, TestUtil } from "code-push-plugin-testing-framework";
9+
10+
const CODEPUSH_METADATA_FILE_NAME = ".codepushrelease";
11+
12+
function isHashIgnored(relativePath: string): boolean {
13+
return relativePath.startsWith("__MACOSX/")
14+
|| relativePath === ".DS_Store"
15+
|| relativePath.endsWith("/.DS_Store")
16+
|| relativePath === CODEPUSH_METADATA_FILE_NAME
17+
|| relativePath.endsWith(`/${CODEPUSH_METADATA_FILE_NAME}`);
18+
}
19+
20+
/**
21+
* Computes the same content hash that the native SDKs compute over an installed update folder, so the mock server
22+
* can hand back a package_hash that will actually match what the client expects.
23+
*/
24+
export function computeUpdateContentsHash(folderPath: string): string {
25+
const manifest: string[] = [];
26+
27+
const walk = (currentPath: string, relativePrefix: string) => {
28+
for (const entryName of fs.readdirSync(currentPath)) {
29+
const entryPath = path.join(currentPath, entryName);
30+
const relativePath = relativePrefix ? `${relativePrefix}/${entryName}` : entryName;
31+
32+
if (isHashIgnored(relativePath)) {
33+
continue;
34+
}
35+
36+
if (fs.statSync(entryPath).isDirectory()) {
37+
walk(entryPath, relativePath);
38+
} else {
39+
const fileHash = crypto.createHash("sha256").update(fs.readFileSync(entryPath)).digest("hex");
40+
manifest.push(`${relativePath}:${fileHash}`);
41+
}
42+
}
43+
};
44+
45+
walk(folderPath, "");
46+
manifest.sort();
47+
48+
return crypto.createHash("sha256").update(JSON.stringify(manifest)).digest("hex");
49+
}
50+
51+
const codeSigningPrivateKey = fs.readFileSync(path.join(__dirname, "../test/fixtures/codesigning/test-private-key.pem"), "utf8");
52+
export const codeSigningPublicKey = fs.readFileSync(path.join(__dirname, "../test/fixtures/codesigning/test-public-key.pem"), "utf8").trim();
53+
54+
function base64UrlEncode(input: Buffer): string {
55+
return input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
56+
}
57+
58+
/**
59+
* Builds the RS256-signed ".codepushrelease" JWT that the native SDKs look for inside an update
60+
* archive's "CodePush/" folder.
61+
*/
62+
function signUpdateContentsHash(contentHash: string): string {
63+
const header = base64UrlEncode(Buffer.from(JSON.stringify({ alg: "RS256", typ: "JWT" })));
64+
const payload = base64UrlEncode(Buffer.from(JSON.stringify({ contentHash })));
65+
const signature = base64UrlEncode(crypto.sign("RSA-SHA256", Buffer.from(`${header}.${payload}`), codeSigningPrivateKey));
66+
return `${header}.${payload}.${signature}`;
67+
}
68+
69+
/**
70+
* Code-signs the update contents with the test key pair and records the real hash, so the mock
71+
* server hands back a package_hash that matches what the client's data-integrity check computes.
72+
*/
73+
export function signAndRecordUpdateArchive(bundleFolder: string, isDiff: boolean): void {
74+
// TODO(RA-4875): Diff updates clear it instead, since they are poorly implemented in the entire test harness.
75+
// It's going to be a bigger refactor, so for now we just skip signing/hashing to avoid using a stale value in diff tests.
76+
if (isDiff) {
77+
ServerUtil.setKnownPackageHash(undefined);
78+
return;
79+
}
80+
81+
const contentHash = computeUpdateContentsHash(bundleFolder);
82+
const signatureFolder = path.join(bundleFolder, "CodePush");
83+
mkdirp.sync(signatureFolder);
84+
fs.writeFileSync(path.join(signatureFolder, CODEPUSH_METADATA_FILE_NAME), signUpdateContentsHash(contentHash));
85+
ServerUtil.setKnownPackageHash(contentHash);
86+
}
87+
88+
export async function setupTamperedSignatureUpdateScenario(projectManager: ProjectManager, targetPlatform: Platform.IPlatform, scenarioJsPath: string, version: string): Promise<string> {
89+
const updatePath = await setupUpdateScenario(projectManager, targetPlatform, scenarioJsPath, version);
90+
91+
const bundleFolder = path.join(TestConfig.updatesDirectory, TestConfig.TestAppName, "CodePush/");
92+
const tamperedHash = "0".repeat(64);
93+
fs.writeFileSync(path.join(bundleFolder, "CodePush", CODEPUSH_METADATA_FILE_NAME), signUpdateContentsHash(tamperedHash));
94+
95+
return await TestUtil.archiveFolder(bundleFolder, "", updatePath, false);
96+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEpQIBAAKCAQEApZtuvtGcQmmeUh81n/jAjeDkktkX1QryINqRZrcoofjw+w89
3+
FdW3XxQOhsjIJLr5u0xqjHde6Hzpx9sbcUGivmW1O0HBuCsAeiVHyiTq/t+sEiKP
4+
0VZa1mbnSM3EEQl09Si3NpLs4sHK+uvttLT3KEZy+Zhx54gaM5iz7ErqavpDADaW
5+
DWplIXUK/D/pQulbM1oM+04V1XkKw5bBqEHTi6Whq/YUPWs2+7KXyLnvKyO+33YF
6+
WnHhHGbGkGLYE02sAGonYgaP220vJ4lnwLx0OcEagfdlM8YwfyOUSWn7LB0VCJaz
7+
ltDiuM1ZhL1rDA8d0rHZvtxKJDLz3uIRuppj7wIDAQABAoIBAAIEOH7+UmbEnnbl
8+
hmOiRcX0fRQErLOdZIFd5/NWO5ptS5HjB51ics8nkV22yCkaVbwgHBQFyBQQoVAb
9+
rOPeJrsmxeQo0tEJRQI3vf4KIQplctTtss6bvJNrwVkzmDWU5eWuTzzM4TGJpo0T
10+
nlta8L9+zBuZ7ZkiIR+LtnUkHGKdEHKFD4melmeZrvMCDNTGrpa2Y2bP9I11a6xZ
11+
V1XJIxvAQRftorz03vsQXFiIcscEaho65DiAObSqpn1tfFnTbPOl+z4XjKncAVF8
12+
5vmkVHvERnvUuuBmSTEgGrEcCgwmYKxtOGqWw3MpeP9DWroU5T1N/Mx5xTr3GID5
13+
6T3+bMECgYEA6Kyg43UCouS7LD/lB5z0dflK8Wu0hzdYUR58r4e2S/WhzkiyX1rZ
14+
aJNkrxyC8/39lGrYdfJHc+RNbViC1TMfH7Zao1lNFVP7vF70v2HzishCL95wSXNz
15+
KZnSY3Yac1ONVP8ZbxX20QBXh9aoTsQxtOs15+XBvOXA5kJwtaaPgTECgYEAtjWW
16+
UiLznuODzIyOQugysXN9bU0UTqUxr3QDt0yqCjA0URVlf3Ehi7OVUPerVSwgCD5m
17+
4RXltW8pqMCLY/qPLkVFH+uZJ/Oo495TEMbyk2/4GXEX4klBMW40YI5srFtP95k+
18+
HrW5+gFOoVLcpwxRHcW6JateA7RlZDReruAt7x8CgYEAgcQdqx4MTVsyRNiR3LAd
19+
61oRARpnwe4NFJjjQ2Z2NmEVUB5dVS8vB9MEmWFWa8whTFBWz1lDnpAa2rw9o7hy
20+
SFaEsIvSoO2I/aMb700q7iEIQPhXOa/o76+5lf09fUqBDYGE5t6iHCiLqNgAYIWt
21+
j1CLbP1IExk0f3dYswblDFECgYEAkib5pHiUoWYtWe2ETvahcuUIPpwNJegrqmiM
22+
coL0AagYztEy0L6WAdDSfFes/myeZP5o1zMRRi8cY1fOdyuLnbnCcJAyEXHIjr7O
23+
Mi7idJDjmMS2O7Q2rsePC8QyNy4nPpuU0F1EB9z0jUJB61xd1Fu9rGmAx8fzbCT1
24+
rZ/0OFECgYEAwQHaF2oxQMxd/WzuTHtKYuPAJDvONQj+hcEAEVFUGEVnauRoUuta
25+
rhgnWrD2rte/6JqGPnfJouW+w1Y+2Mnfp4io8/cyysiEX7VWTeZL+nUERczTserl
26+
cyujq12LNdD+YPQwJTxHguP0rbMsQ2lHlxwys1+74GGq7GqR3dIJlqc=
27+
-----END RSA PRIVATE KEY-----
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZtuvtGcQmmeUh81n/jA
3+
jeDkktkX1QryINqRZrcoofjw+w89FdW3XxQOhsjIJLr5u0xqjHde6Hzpx9sbcUGi
4+
vmW1O0HBuCsAeiVHyiTq/t+sEiKP0VZa1mbnSM3EEQl09Si3NpLs4sHK+uvttLT3
5+
KEZy+Zhx54gaM5iz7ErqavpDADaWDWplIXUK/D/pQulbM1oM+04V1XkKw5bBqEHT
6+
i6Whq/YUPWs2+7KXyLnvKyO+33YFWnHhHGbGkGLYE02sAGonYgaP220vJ4lnwLx0
7+
OcEagfdlM8YwfyOUSWn7LB0VCJazltDiuM1ZhL1rDA8d0rHZvtxKJDLz3uIRuppj
8+
7wIDAQAB
9+
-----END PUBLIC KEY-----

test/template/app.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
{
2020
"ios": {
2121
"CodePushDeploymentKey": "mock-ios-deployment-key",
22-
"CodePushServerURL": "http://127.0.0.1:3000"
22+
"CodePushServerURL": "http://127.0.0.1:3000",
23+
"CodePushPublicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZtuvtGcQmmeUh81n/jA\njeDkktkX1QryINqRZrcoofjw+w89FdW3XxQOhsjIJLr5u0xqjHde6Hzpx9sbcUGi\nvmW1O0HBuCsAeiVHyiTq/t+sEiKP0VZa1mbnSM3EEQl09Si3NpLs4sHK+uvttLT3\nKEZy+Zhx54gaM5iz7ErqavpDADaWDWplIXUK/D/pQulbM1oM+04V1XkKw5bBqEHT\ni6Whq/YUPWs2+7KXyLnvKyO+33YFWnHhHGbGkGLYE02sAGonYgaP220vJ4lnwLx0\nOcEagfdlM8YwfyOUSWn7LB0VCJazltDiuM1ZhL1rDA8d0rHZvtxKJDLz3uIRuppj\n7wIDAQAB\n-----END PUBLIC KEY-----"
2324
},
2425
"android": {
2526
"CodePushDeploymentKey": "mock-android-deployment-key",
26-
"CodePushServerURL": "http://10.0.2.2:3001"
27+
"CodePushServerURL": "http://10.0.2.2:3001",
28+
"CodePushPublicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZtuvtGcQmmeUh81n/jA\njeDkktkX1QryINqRZrcoofjw+w89FdW3XxQOhsjIJLr5u0xqjHde6Hzpx9sbcUGi\nvmW1O0HBuCsAeiVHyiTq/t+sEiKP0VZa1mbnSM3EEQl09Si3NpLs4sHK+uvttLT3\nKEZy+Zhx54gaM5iz7ErqavpDADaWDWplIXUK/D/pQulbM1oM+04V1XkKw5bBqEHT\ni6Whq/YUPWs2+7KXyLnvKyO+33YFWnHhHGbGkGLYE02sAGonYgaP220vJ4lnwLx0\nOcEagfdlM8YwfyOUSWn7LB0VCJazltDiuM1ZhL1rDA8d0rHZvtxKJDLz3uIRuppj\n7wIDAQAB\n-----END PUBLIC KEY-----"
2729
}
2830
}
2931
]

test/test.ts

Lines changed: 35 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@
22

33
import assert = require("assert");
44
import childProcess = require("child_process");
5-
import crypto = require("crypto");
65
import fs = require("fs");
76
import mkdirp = require("mkdirp");
87
import os = require("os");
98
import path = require("path");
109
import slash = require("slash");
10+
import { promisify } from "util";
1111

1212
import { Platform, PluginTestingFramework, ProjectManager, setupTestRunScenario, setupUpdateScenario, ServerUtil, TestBuilder, TestConfig, TestUtil } from "code-push-plugin-testing-framework";
1313

1414
import Q = require("q");
1515

1616
import del = require("del");
1717

18+
import { codeSigningPublicKey, signAndRecordUpdateArchive, setupTamperedSignatureUpdateScenario } from "./codesign";
19+
1820
function ensureAndroidCleartextTraffic(androidManifestPath: string): void {
1921
const androidManifestContents = fs.readFileSync(androidManifestPath, "utf8");
2022

@@ -37,6 +39,10 @@ function ensureAndroidCleartextTraffic(androidManifestPath: string): void {
3739
}
3840
}
3941

42+
async function setPlistStringValue(plistPath: string, key: string, value: string): Promise<void> {
43+
await promisify(childProcess.execFile)("plutil", ["-replace", key, "-string", value, plistPath]);
44+
}
45+
4046
/**
4147
* Returns a " --platform <ios|android>" flag for `expo prebuild` when exactly one platform is
4248
* under test in this mocha run, so prebuild only regenerates that platform's native project
@@ -69,47 +75,6 @@ function installExpoBundleTooling(projectPath: string): Q.Promise<void> {
6975
).then(() => { return null; });
7076
}
7177

72-
const CODEPUSH_METADATA_FILE_NAME = ".codepushrelease";
73-
74-
function isHashIgnored(relativePath: string): boolean {
75-
return relativePath.startsWith("__MACOSX/")
76-
|| relativePath === ".DS_Store"
77-
|| relativePath.endsWith("/.DS_Store")
78-
|| relativePath === CODEPUSH_METADATA_FILE_NAME
79-
|| relativePath.endsWith(`/${CODEPUSH_METADATA_FILE_NAME}`);
80-
}
81-
82-
/**
83-
* Computes the same content hash that the native SDKs compute over an installed update folder, so the mock server
84-
* can hand back a package_hash that will actually match what the client expects.
85-
*/
86-
function computeUpdateContentsHash(folderPath: string): string {
87-
const manifest: string[] = [];
88-
89-
const walk = (currentPath: string, relativePrefix: string) => {
90-
for (const entryName of fs.readdirSync(currentPath)) {
91-
const entryPath = path.join(currentPath, entryName);
92-
const relativePath = relativePrefix ? `${relativePrefix}/${entryName}` : entryName;
93-
94-
if (isHashIgnored(relativePath)) {
95-
continue;
96-
}
97-
98-
if (fs.statSync(entryPath).isDirectory()) {
99-
walk(entryPath, relativePath);
100-
} else {
101-
const fileHash = crypto.createHash("sha256").update(fs.readFileSync(entryPath)).digest("hex");
102-
manifest.push(`${relativePath}:${fileHash}`);
103-
}
104-
}
105-
};
106-
107-
walk(folderPath, "");
108-
manifest.sort();
109-
110-
return crypto.createHash("sha256").update(JSON.stringify(manifest)).digest("hex");
111-
}
112-
11378
//////////////////////////////////////////////////////////////////////////////////////////
11479
// Create the platforms to run the tests on.
11580

@@ -209,6 +174,7 @@ class RNAndroid extends Platform.Android implements RNPlatform {
209174
const string = path.join(innerprojectDirectory, "android", "app", "src", "main", "res", "values", "strings.xml");
210175
TestUtil.replaceString(string, TestUtil.SERVER_URL_PLACEHOLDER, this.getServerUrl());
211176
TestUtil.replaceString(string, TestUtil.ANDROID_KEY_PLACEHOLDER, this.getDefaultDeploymentKey());
177+
TestUtil.replaceString(string, "</resources>", `<string moduleConfig="true" name="CodePushPublicKey">${codeSigningPublicKey}</string>\n</resources>`);
212178
TestUtil.replaceString(AndroidManifest, "\\${usesCleartextTraffic}", "true");
213179

214180

@@ -280,14 +246,10 @@ class RNIOS extends Platform.IOS implements RNPlatform {
280246
// Install the Podfile
281247
return TestUtil.copyFile(path.join(TestConfig.templatePath, "ios", "Podfile"), podfilePath, true)
282248
.then(() => TestUtil.getProcessOutput(`pod install`, { cwd: iOSProject, noLogStdOut: true }))
283-
// Put the IOS deployment key in the Info.plist
284-
.then(TestUtil.replaceString.bind(undefined, infoPlistPath,
285-
"</dict>\n</plist>",
286-
"<key>CodePushDeploymentKey</key>\n\t<string>" + this.getDefaultDeploymentKey() + "</string>\n\t<key>CodePushServerURL</key>\n\t<string>" + this.getServerUrl() + "</string>\n\t</dict>\n</plist>"))
287-
// Set the app version to 1.0.0 instead of 1.0 in the Info.plist
288-
.then(TestUtil.replaceString.bind(undefined, infoPlistPath, "1.0", "1.0.0"))
289-
// Remove dependence of CFBundleShortVersionString from project.pbxproj
290-
.then(TestUtil.replaceString.bind(undefined, infoPlistPath, "\\$\\(MARKETING_VERSION\\)", "1.0.0"))
249+
.then(() => setPlistStringValue(infoPlistPath, "CFBundleShortVersionString", "1.0.0"))
250+
.then(() => setPlistStringValue(infoPlistPath, "CodePushDeploymentKey", this.getDefaultDeploymentKey()))
251+
.then(() => setPlistStringValue(infoPlistPath, "CodePushServerURL", this.getServerUrl()))
252+
.then(() => setPlistStringValue(infoPlistPath, "CodePushPublicKey", codeSigningPublicKey))
291253
// Fix the linker flag list in project.pbxproj (pod install adds an extra comma)
292254
.then(TestUtil.replaceString.bind(undefined, path.join(iOSProject, TestConfig.TestAppName + ".xcodeproj", "project.pbxproj"),
293255
"\"[$][(]inherited[)]\",\\s*[)];", "\"$(inherited)\"\n\t\t\t\t);"))
@@ -539,28 +501,19 @@ class RNProjectManager extends ProjectManager {
539501
.then(TestUtil.getProcessOutput.bind(undefined, "npx expo prebuild --platform " + targetPlatform.getName(), { cwd: path.join(projectDirectory, TestConfig.TestAppName), noLogStdOut: true }))
540502
.then(TestUtil.getProcessOutput.bind(undefined, "npx react-native bundle --entry-file index.js --platform " + targetPlatform.getName() + " --bundle-output " + bundlePath + " --assets-dest " + bundleFolder + " --dev false",
541503
{ cwd: path.join(projectDirectory, TestConfig.TestAppName), noLogStdOut: true }))
504+
.then(() => signAndRecordUpdateArchive(bundleFolder, isDiff))
542505
.then<string>(TestUtil.archiveFolder.bind(undefined, bundleFolder, "", path.join(projectDirectory, TestConfig.TestAppName, "update.zip"), isDiff))
543-
.then<string>(this.updateMockPackageHash.bind(this, bundleFolder, isDiff))
544506
.then((result) => { console.log(`[TIMING] createUpdateArchive(${projectDirectory}, ${targetPlatform.getName()}) took ${Date.now() - t0}ms`); return result; });
545507
} else {
546508
return deferred.promise
547509
.then(TestUtil.getProcessOutput.bind(undefined, "npx react-native bundle --entry-file index.js --platform " + targetPlatform.getName() + " --bundle-output " + bundlePath + " --assets-dest " + bundleFolder + " --dev false",
548510
{ cwd: path.join(projectDirectory, TestConfig.TestAppName), noLogStdOut: true }))
511+
.then(() => signAndRecordUpdateArchive(bundleFolder, isDiff))
549512
.then<string>(TestUtil.archiveFolder.bind(undefined, bundleFolder, "", path.join(projectDirectory, TestConfig.TestAppName, "update.zip"), isDiff))
550-
.then<string>(this.updateMockPackageHash.bind(this, bundleFolder, isDiff))
551513
.then((result) => { console.log(`[TIMING] createUpdateArchive(${projectDirectory}, ${targetPlatform.getName()}) took ${Date.now() - t0}ms`); return result; });
552514
}
553515
}
554516

555-
// Records the real hash of bundleFolder of an archive, so the mock server can hand back a
556-
// package_hash that matches what the client's verifyFolderHash integrity check will compute.
557-
private updateMockPackageHash(bundleFolder: string, isDiff: boolean, archivePath: string): string {
558-
// TODO(RA-4875): Diff updates clear it instead, since they are poorly implemented in the entire test harness.
559-
// It's going to be a bigger refactor, so for now we just clear the known package hash to avoid using a stale value in diff tests.
560-
ServerUtil.setKnownPackageHash(isDiff ? undefined : computeUpdateContentsHash(bundleFolder));
561-
return archivePath;
562-
}
563-
564517
/** JSON file containing the platforms the plugin is currently installed for.
565518
* Keys must match targetPlatform.getName()!
566519
*
@@ -1050,6 +1003,27 @@ PluginTestingFramework.initializeTests(new RNProjectManager(), supportedTargetPl
10501003
});
10511004
}, ScenarioInstall);
10521005

1006+
TestBuilder.describe("#localPackage.install.codeSigning",
1007+
() => {
1008+
TestBuilder.it("localPackage.install.codeSigning.tamperedSignature", false,
1009+
async (done: Mocha.Done) => {
1010+
try {
1011+
ServerUtil.updateResponse = { update_info: ServerUtil.createUpdateResponse(false, targetPlatform) };
1012+
1013+
/* create a normal update, then tamper with its signature after it's been signed */
1014+
const updatePath = await setupTamperedSignatureUpdateScenario(projectManager, targetPlatform, UpdateNotifyApplicationReady, "Tampered Update");
1015+
ServerUtil.updatePackagePath = updatePath;
1016+
projectManager.runApplication(TestConfig.testRunDirectory, targetPlatform);
1017+
await ServerUtil.expectTestMessages([
1018+
ServerUtil.TestMessage.CHECK_UPDATE_AVAILABLE,
1019+
ServerUtil.TestMessage.DOWNLOAD_ERROR]);
1020+
done();
1021+
} catch (e) {
1022+
done(e);
1023+
}
1024+
});
1025+
}, ScenarioInstall);
1026+
10531027
TestBuilder.describe("#localPackage.install.revert",
10541028
() => {
10551029
TestBuilder.it("localPackage.install.revert.dorevert", false,

0 commit comments

Comments
 (0)