@bravemobile/react-native-code-push 13.1.0-beta.4 → 13.1.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CodePush.podspec +5 -0
- package/README.md +15 -145
- package/android/app/src/main/java/com/microsoft/codepush/react/ArchiveAttemptLog.java +181 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/{BinaryPatchResult.java → ArchiveRestoreResult.java} +17 -7
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +20 -20
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +20 -1
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushErrorCode.java +86 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushHttpException.java +29 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushIncompleteDownloadException.java +17 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +12 -7
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +143 -43
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +31 -31
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushErrorCodeTest.java +90 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +159 -19
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +304 -15
- package/cli/commands/createHistoryCommand/createReleaseHistory.test.ts +133 -0
- package/cli/commands/createHistoryCommand/createReleaseHistory.ts +3 -11
- package/cli/commands/releaseCommand/addToReleaseHistory.ts +3 -11
- package/cli/commands/releaseCommand/release.test.ts +3 -3
- package/cli/commands/updateHistoryCommand/updateReleaseHistory.ts +3 -11
- package/cli/dist/commands/createHistoryCommand/createReleaseHistory.js +2 -8
- package/cli/dist/commands/createHistoryCommand/createReleaseHistory.test.js +95 -0
- package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +2 -8
- package/cli/dist/commands/releaseCommand/release.test.js +3 -3
- package/cli/dist/commands/updateHistoryCommand/updateReleaseHistory.js +2 -8
- package/cli/dist/functions/stageReleaseHistoryFile.js +28 -0
- package/cli/functions/stageReleaseHistoryFile.ts +38 -0
- package/ios/CodePush/CodePush.h +6 -3
- package/ios/CodePush/CodePush.mm +10 -10
- package/ios/CodePush/CodePushBinaryPatch.h +18 -8
- package/ios/CodePush/CodePushBinaryPatch.m +23 -22
- package/ios/CodePush/CodePushDownloadHandler.m +2 -1
- package/ios/CodePush/CodePushErrorUtils.m +55 -0
- package/ios/CodePush/CodePushPackage.m +242 -71
- package/package.json +2 -1
- package/src/CodePush.js +28 -18
- package/src/CodePush.test.js +110 -55
- package/src/package-mixins.js +19 -10
- package/typings/react-native-code-push.d.ts +110 -29
- package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +0 -92
|
@@ -2,9 +2,11 @@ package com.microsoft.codepush.react;
|
|
|
2
2
|
|
|
3
3
|
import static org.junit.Assert.assertArrayEquals;
|
|
4
4
|
import static org.junit.Assert.assertEquals;
|
|
5
|
+
import static org.junit.Assert.fail;
|
|
5
6
|
import static org.junit.Assert.assertFalse;
|
|
6
7
|
import static org.junit.Assert.assertTrue;
|
|
7
8
|
|
|
9
|
+
import org.json.JSONException;
|
|
8
10
|
import org.json.JSONObject;
|
|
9
11
|
import org.junit.After;
|
|
10
12
|
import org.junit.Before;
|
|
@@ -90,7 +92,7 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
@Test
|
|
93
|
-
public void installsAnUpdateFromItsBinaryPatchArchive() throws IOException {
|
|
95
|
+
public void installsAnUpdateFromItsBinaryPatchArchive() throws IOException, JSONException {
|
|
94
96
|
String patchUrl = serve("/patch.zip", zipOf(patchArchiveContents()));
|
|
95
97
|
String fullUrl = serve("/full.zip", zipOf(fullArchiveContents()));
|
|
96
98
|
|
|
@@ -102,7 +104,12 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
102
104
|
assertInstalledContents();
|
|
103
105
|
assertEquals("applied", patchResult.optString("status", null));
|
|
104
106
|
assertFalse("an applied patch has nothing to report a reason for", patchResult.has("fallbackReason"));
|
|
105
|
-
assertTrue("the
|
|
107
|
+
assertTrue("the whole path is timed", patchResult.optLong("totalDurationMs", -1) >= 0);
|
|
108
|
+
assertFalse("the result no longer carries a top level apply time", patchResult.has("applyDurationMs"));
|
|
109
|
+
|
|
110
|
+
JSONObject appliedAttempt = patchResult.getJSONArray("attempts").getJSONObject(0);
|
|
111
|
+
assertTrue("an applied attempt carries its apply time", appliedAttempt.has("applyDurationMs"));
|
|
112
|
+
assertTrue(appliedAttempt.optLong("applyDurationMs", -1) >= 0);
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
@Test
|
|
@@ -116,22 +123,73 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
116
123
|
|
|
117
124
|
assertEquals(Arrays.asList("/patch.zip", "/full.zip"), mServer.requestedPaths());
|
|
118
125
|
assertInstalledContents();
|
|
119
|
-
assertFallbackResult(patchResult,
|
|
126
|
+
assertFallbackResult(patchResult, ArchiveRestoreResult.REASON_INVALID_MANIFEST);
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
@Test
|
|
123
130
|
public void reportsAnInvalidManifestWhenThePatchUrlDoesNotServeAnArchive() throws IOException {
|
|
124
131
|
String patchUrl = serve("/patch.zip", ERROR_PAGE);
|
|
125
132
|
|
|
126
|
-
|
|
133
|
+
ArchiveRestoreResult result = updateManager(applierWriting(TARGET_BUNDLE)).downloadAndInstallPackage(
|
|
127
134
|
updatePackage("https://example.test/unused.zip", patchUrl), BUNDLE_FILE_NAME, ignoreProgress(),
|
|
128
|
-
patchUrl, true, new
|
|
135
|
+
patchUrl, true, new ArchiveAttemptLog());
|
|
129
136
|
|
|
130
137
|
assertFalse(result.succeeded());
|
|
131
|
-
assertEquals(
|
|
138
|
+
assertEquals(ArchiveRestoreResult.REASON_INVALID_MANIFEST, result.getFailureReason());
|
|
132
139
|
assertFalse("bytes that are not an update must not reach the package folder", mPackageFolder.exists());
|
|
133
140
|
}
|
|
134
141
|
|
|
142
|
+
@Test
|
|
143
|
+
public void failsTheDownloadWhenTheServerAnswersTheArchiveWithAnErrorStatus() throws IOException {
|
|
144
|
+
// Nothing is served at this path, so the server answers it with a 404.
|
|
145
|
+
String fullUrl = mServer.urlOf("/missing-full.zip");
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
updateManager(applierWriting(TARGET_BUNDLE)).downloadPackage(
|
|
149
|
+
fullUpdatePackage(mPackageHash, fullUrl), BUNDLE_FILE_NAME, ignoreProgress());
|
|
150
|
+
fail("a download the server refused must not be reported as installed");
|
|
151
|
+
} catch (CodePushHttpException e) {
|
|
152
|
+
assertEquals(404, e.getStatusCode());
|
|
153
|
+
assertTrue("the message names the status the server answered with",
|
|
154
|
+
e.getMessage().contains("404"));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
assertFalse("nothing the server refused reaches the package folder", mPackageFolder.exists());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@Test
|
|
161
|
+
public void givesUpTheDownloadWhenThePatchArchiveArrivesShort() throws IOException {
|
|
162
|
+
// The connection carried part of the body and stopped. The full archive is behind
|
|
163
|
+
// the same connection and is larger, so there is nothing to fall back to.
|
|
164
|
+
byte[] patchArchive = zipOf(patchArchiveContents());
|
|
165
|
+
String patchUrl = mServer.serveClaimingLength("/patch.zip", patchArchive, patchArchive.length + 1024);
|
|
166
|
+
String fullUrl = serve("/full.zip", zipOf(fullArchiveContents()));
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
updateManager(applierWriting(TARGET_BUNDLE))
|
|
170
|
+
.downloadPackage(updatePackage(fullUrl, patchUrl), BUNDLE_FILE_NAME, ignoreProgress());
|
|
171
|
+
fail("a download that stopped short must not be reported as installed");
|
|
172
|
+
} catch (IOException e) {
|
|
173
|
+
assertEquals(CodePushErrorCode.NETWORK, CodePushErrorCode.of(e));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
assertEquals("the full archive is behind the connection that just stopped",
|
|
177
|
+
Arrays.asList("/patch.zip"), mServer.requestedPaths());
|
|
178
|
+
assertFalse("nothing that arrived short reaches the package folder", mPackageFolder.exists());
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@Test
|
|
182
|
+
public void installsAnUpdateTheServerSentWithoutDeclaringItsLength() throws IOException {
|
|
183
|
+
// `getContentLength()` answers -1 for a body sent with no `Content-Length`, which no
|
|
184
|
+
// read total matches - so a download checked against it anyway could never arrive.
|
|
185
|
+
String fullUrl = mServer.serveWithoutContentLength("/full.zip", zipOf(fullArchiveContents()));
|
|
186
|
+
|
|
187
|
+
updateManager(applierWriting(TARGET_BUNDLE)).downloadPackage(
|
|
188
|
+
fullUpdatePackage(mPackageHash, fullUrl), BUNDLE_FILE_NAME, ignoreProgress());
|
|
189
|
+
|
|
190
|
+
assertInstalledContents();
|
|
191
|
+
}
|
|
192
|
+
|
|
135
193
|
@Test
|
|
136
194
|
public void fallsBackToTheFullArchiveWhenApplyingThePatchRunsOutOfMemory() throws IOException {
|
|
137
195
|
String patchUrl = serve("/patch.zip", zipOf(patchArchiveContents()));
|
|
@@ -167,7 +225,7 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
167
225
|
|
|
168
226
|
assertEquals(Arrays.asList("/patch.zip", "/full.zip"), mServer.requestedPaths());
|
|
169
227
|
assertInstalledContents();
|
|
170
|
-
assertFallbackResult(patchResult,
|
|
228
|
+
assertFallbackResult(patchResult, ArchiveRestoreResult.REASON_PACKAGE_VERIFICATION_FAILED);
|
|
171
229
|
}
|
|
172
230
|
|
|
173
231
|
@Test
|
|
@@ -204,7 +262,7 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
204
262
|
.downloadPackage(updatePackage(updateHash, fullUrl, diffUrl), BUNDLE_FILE_NAME, ignoreProgress());
|
|
205
263
|
|
|
206
264
|
assertEquals(Arrays.asList("/installed.zip", "/diff.zip", "/full.zip"), mServer.requestedPaths());
|
|
207
|
-
assertFallbackResult(patchResult,
|
|
265
|
+
assertFallbackResult(patchResult, ArchiveRestoreResult.REASON_PACKAGE_VERIFICATION_FAILED);
|
|
208
266
|
assertInstalledContents(updateHash, updateContents);
|
|
209
267
|
}
|
|
210
268
|
|
|
@@ -221,10 +279,177 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
221
279
|
.downloadPackage(updatePackage(updateHash, fullUrl, diffUrl), BUNDLE_FILE_NAME, ignoreProgress());
|
|
222
280
|
|
|
223
281
|
assertEquals(Arrays.asList("/diff.zip", "/full.zip"), mServer.requestedPaths());
|
|
224
|
-
assertFallbackResult(patchResult,
|
|
282
|
+
assertFallbackResult(patchResult, ArchiveRestoreResult.REASON_PACKAGE_VERIFICATION_FAILED);
|
|
283
|
+
assertInstalledContents(updateHash, updateContents);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@Test
|
|
287
|
+
public void fallsBackToThePatchArchiveWhenTheInstalledPackageIsGoneFromDisk() throws IOException {
|
|
288
|
+
// The metadata still names the installed update, but its files are gone: the merge
|
|
289
|
+
// has nothing to read, which is a failure of the merge itself rather than of its
|
|
290
|
+
// result - and the one failure the patch archive, carrying every asset, is not
|
|
291
|
+
// implicated in.
|
|
292
|
+
installBaseUpdate();
|
|
293
|
+
FileUtils.deleteDirectoryAtPath(new File(
|
|
294
|
+
new File(mDocumentsDirectory, CodePushConstants.CODE_PUSH_FOLDER_PREFIX),
|
|
295
|
+
packageHashOf(installedArchiveContents())).getAbsolutePath());
|
|
296
|
+
|
|
297
|
+
Map<String, byte[]> updateContents = assetDiffTargetContents();
|
|
298
|
+
String updateHash = packageHashOf(updateContents);
|
|
299
|
+
String diffUrl = serve("/diff.zip", zipOf(assetDiffArchiveContents(DROPPED_ASSET_PATH)));
|
|
300
|
+
String patchUrl = serve("/patch.zip", zipOf(patchArchiveContentsForAssetDiffTarget()));
|
|
301
|
+
String fullUrl = serve("/full.zip", zipOf(updateContents));
|
|
302
|
+
|
|
303
|
+
JSONObject patchResult = updateManager(applierWriting(TARGET_BUNDLE)).downloadPackage(
|
|
304
|
+
updatePackageWithAssetDiff(updateHash, fullUrl, patchUrl, diffUrl), BUNDLE_FILE_NAME, ignoreProgress());
|
|
305
|
+
|
|
306
|
+
assertEquals(Arrays.asList("/installed.zip", "/diff.zip", "/patch.zip"), mServer.requestedPaths());
|
|
307
|
+
assertEquals("applied", patchResult.optString("status", null));
|
|
308
|
+
assertEquals("binary-patch", patchResult.optString("archive", null));
|
|
309
|
+
assertEquals(ArchiveRestoreResult.REASON_ASSET_MERGE_FAILED,
|
|
310
|
+
patchResult.optJSONArray("attempts").optJSONObject(0).optString("fallbackReason", null));
|
|
311
|
+
assertInstalledContents(updateHash, updateContents);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
@Test
|
|
315
|
+
public void fallsBackToThePatchArchiveWhenTheAssetDiffManifestCannotBeParsed() throws IOException {
|
|
316
|
+
// Bytes that are not JSON leave nothing to read the deletions out of, so the merge
|
|
317
|
+
// has no way to know what it was supposed to delete - a failure of the merge itself.
|
|
318
|
+
installBaseUpdate();
|
|
319
|
+
Map<String, byte[]> updateContents = assetDiffTargetContents();
|
|
320
|
+
String updateHash = packageHashOf(updateContents);
|
|
321
|
+
String diffUrl = serve("/diff.zip",
|
|
322
|
+
zipOf(assetDiffArchiveContentsWithManifest(bytes("{\"deletedFiles\":"))));
|
|
323
|
+
String patchUrl = serve("/patch.zip", zipOf(patchArchiveContentsForAssetDiffTarget()));
|
|
324
|
+
String fullUrl = serve("/full.zip", zipOf(updateContents));
|
|
325
|
+
|
|
326
|
+
JSONObject patchResult = updateManager(applierWriting(TARGET_BUNDLE)).downloadPackage(
|
|
327
|
+
updatePackageWithAssetDiff(updateHash, fullUrl, patchUrl, diffUrl), BUNDLE_FILE_NAME, ignoreProgress());
|
|
328
|
+
|
|
329
|
+
assertEquals(Arrays.asList("/installed.zip", "/diff.zip", "/patch.zip"), mServer.requestedPaths());
|
|
330
|
+
assertEquals("applied", patchResult.optString("status", null));
|
|
331
|
+
assertEquals("binary-patch", patchResult.optString("archive", null));
|
|
332
|
+
assertEquals(ArchiveRestoreResult.REASON_ASSET_MERGE_FAILED,
|
|
333
|
+
patchResult.optJSONArray("attempts").optJSONObject(0).optString("fallbackReason", null));
|
|
334
|
+
assertInstalledContents(updateHash, updateContents);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
@Test
|
|
338
|
+
public void fallsBackToThePatchArchiveWhenTheAssetDiffManifestDoesNotNameTheFilesToDelete() throws IOException {
|
|
339
|
+
// A manifest without the key is not a manifest with nothing to delete: the CLI always
|
|
340
|
+
// writes it, an empty list included, so its absence says the manifest is not the one
|
|
341
|
+
// the release published - and merging past it would leave behind files the update
|
|
342
|
+
// dropped.
|
|
343
|
+
installBaseUpdate();
|
|
344
|
+
Map<String, byte[]> updateContents = assetDiffTargetContents();
|
|
345
|
+
String updateHash = packageHashOf(updateContents);
|
|
346
|
+
String diffUrl = serve("/diff.zip", zipOf(assetDiffArchiveContentsWithManifest(bytes("{}"))));
|
|
347
|
+
String patchUrl = serve("/patch.zip", zipOf(patchArchiveContentsForAssetDiffTarget()));
|
|
348
|
+
String fullUrl = serve("/full.zip", zipOf(updateContents));
|
|
349
|
+
|
|
350
|
+
JSONObject patchResult = updateManager(applierWriting(TARGET_BUNDLE)).downloadPackage(
|
|
351
|
+
updatePackageWithAssetDiff(updateHash, fullUrl, patchUrl, diffUrl), BUNDLE_FILE_NAME, ignoreProgress());
|
|
352
|
+
|
|
353
|
+
assertEquals(Arrays.asList("/installed.zip", "/diff.zip", "/patch.zip"), mServer.requestedPaths());
|
|
354
|
+
assertEquals("applied", patchResult.optString("status", null));
|
|
355
|
+
assertEquals("binary-patch", patchResult.optString("archive", null));
|
|
356
|
+
assertEquals(ArchiveRestoreResult.REASON_ASSET_MERGE_FAILED,
|
|
357
|
+
patchResult.optJSONArray("attempts").optJSONObject(0).optString("fallbackReason", null));
|
|
358
|
+
assertInstalledContents(updateHash, updateContents);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
@Test
|
|
362
|
+
public void triesThePatchArchiveWhenTheServerDoesNotServeTheAssetDiff() throws IOException {
|
|
363
|
+
// A 404 is a verdict on the URL it was asked of. Diffs are published one per recent
|
|
364
|
+
// version and are the first thing a retention policy clears out, while the patch
|
|
365
|
+
// archive at its own URL stays - so nothing about a diff that is gone says the patch
|
|
366
|
+
// archive is.
|
|
367
|
+
Map<String, byte[]> updateContents = assetDiffTargetContents();
|
|
368
|
+
String updateHash = packageHashOf(updateContents);
|
|
369
|
+
String diffUrl = mServer.urlOf("/missing-diff.zip");
|
|
370
|
+
String patchUrl = serve("/patch.zip", zipOf(patchArchiveContentsForAssetDiffTarget()));
|
|
371
|
+
String fullUrl = serve("/full.zip", zipOf(updateContents));
|
|
372
|
+
|
|
373
|
+
JSONObject patchResult = updateManager(applierWriting(TARGET_BUNDLE)).downloadPackage(
|
|
374
|
+
updatePackageWithAssetDiff(updateHash, fullUrl, patchUrl, diffUrl), BUNDLE_FILE_NAME, ignoreProgress());
|
|
375
|
+
|
|
376
|
+
assertEquals("the full archive is not downloaded when the patch archive installs",
|
|
377
|
+
Arrays.asList("/missing-diff.zip", "/patch.zip"), mServer.requestedPaths());
|
|
378
|
+
assertEquals("applied", patchResult.optString("status", null));
|
|
379
|
+
assertEquals("binary-patch", patchResult.optString("archive", null));
|
|
380
|
+
assertEquals(2, patchResult.optJSONArray("attempts").length());
|
|
381
|
+
assertInstalledContents(updateHash, updateContents);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@Test
|
|
385
|
+
public void skipsThePatchArchiveWhenTheAssetDiffFailsInItsBundlePatch() throws IOException {
|
|
386
|
+
// Both archives carry that patch byte for byte, so an applier that refused it here
|
|
387
|
+
// would refuse it there - and trying it would put a second doomed download in front
|
|
388
|
+
// of the full one.
|
|
389
|
+
Map<String, byte[]> updateContents = assetDiffTargetContents();
|
|
390
|
+
String updateHash = packageHashOf(updateContents);
|
|
391
|
+
String diffUrl = serve("/diff.zip", zipOf(assetDiffArchiveContents(DROPPED_ASSET_PATH)));
|
|
392
|
+
String patchUrl = serve("/patch.zip", zipOf(patchArchiveContentsForAssetDiffTarget()));
|
|
393
|
+
String fullUrl = serve("/full.zip", zipOf(updateContents));
|
|
394
|
+
|
|
395
|
+
JSONObject patchResult = updateManager(applierRefusingThePatch()).downloadPackage(
|
|
396
|
+
updatePackageWithAssetDiff(updateHash, fullUrl, patchUrl, diffUrl), BUNDLE_FILE_NAME, ignoreProgress());
|
|
397
|
+
|
|
398
|
+
assertEquals(Arrays.asList("/diff.zip", "/full.zip"), mServer.requestedPaths());
|
|
399
|
+
assertEquals(1, patchResult.optJSONArray("attempts").length());
|
|
225
400
|
assertInstalledContents(updateHash, updateContents);
|
|
226
401
|
}
|
|
227
402
|
|
|
403
|
+
@Test
|
|
404
|
+
public void givesUpTheDownloadWhenTheNetworkCannotCarryTheAssetDiff() throws IOException {
|
|
405
|
+
// A refused connection is the network failing rather than a verdict on the archive,
|
|
406
|
+
// and the archives behind it are behind the same network - the full one only larger
|
|
407
|
+
// and started over from nothing.
|
|
408
|
+
String unreachableDiffUrl = "http://127.0.0.1:" + portNothingListensOn() + "/diff.zip";
|
|
409
|
+
String patchUrl = serve("/patch.zip", zipOf(patchArchiveContents()));
|
|
410
|
+
String fullUrl = serve("/full.zip", zipOf(fullArchiveContents()));
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
updateManager(applierWriting(TARGET_BUNDLE)).downloadPackage(
|
|
414
|
+
updatePackageWithAssetDiff(mPackageHash, fullUrl, patchUrl, unreachableDiffUrl),
|
|
415
|
+
BUNDLE_FILE_NAME, ignoreProgress());
|
|
416
|
+
fail("a network that carried nothing must not be reported as an installed update");
|
|
417
|
+
} catch (IOException e) {
|
|
418
|
+
assertEquals(CodePushErrorCode.NETWORK, CodePushErrorCode.of(e));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
assertTrue("no archive behind the same network is asked for", mServer.requestedPaths().isEmpty());
|
|
422
|
+
assertFalse("nothing that never arrived reaches the package folder", mPackageFolder.exists());
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
/** A loopback port that is opened only to be closed, so connecting to it is refused. */
|
|
427
|
+
private static int portNothingListensOn() throws IOException {
|
|
428
|
+
ServerSocket socket = new ServerSocket(0, 0, InetAddress.getByName("127.0.0.1"));
|
|
429
|
+
int port = socket.getLocalPort();
|
|
430
|
+
socket.close();
|
|
431
|
+
return port;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
@Test
|
|
435
|
+
public void installsFromThePatchArchiveWhenTheAssetDiffUrlIsAnEmptyString() throws IOException {
|
|
436
|
+
// An empty slot is not an archive on offer. Attempting it would fail for want of a
|
|
437
|
+
// URL and leave no verdict behind, which is what skips the patch archive - so the
|
|
438
|
+
// update would be downloaded in full with a perfectly good patch archive untried.
|
|
439
|
+
String patchUrl = serve("/patch.zip", zipOf(patchArchiveContents()));
|
|
440
|
+
String fullUrl = serve("/full.zip", zipOf(fullArchiveContents()));
|
|
441
|
+
|
|
442
|
+
JSONObject patchResult = updateManager(applierWriting(TARGET_BUNDLE)).downloadPackage(
|
|
443
|
+
updatePackageWithAssetDiff(mPackageHash, fullUrl, patchUrl, ""), BUNDLE_FILE_NAME, ignoreProgress());
|
|
444
|
+
|
|
445
|
+
assertEquals("the empty diff slot is not downloaded and does not cost the patch archive its try",
|
|
446
|
+
Arrays.asList("/patch.zip"), mServer.requestedPaths());
|
|
447
|
+
assertEquals("applied", patchResult.optString("status", null));
|
|
448
|
+
assertEquals("binary-patch", patchResult.optString("archive", null));
|
|
449
|
+
assertEquals(1, patchResult.optJSONArray("attempts").length());
|
|
450
|
+
assertInstalledContents();
|
|
451
|
+
}
|
|
452
|
+
|
|
228
453
|
@Test
|
|
229
454
|
public void leavesAFileOutsideThePackageFolderAloneWhenTheAssetDiffManifestNamesIt() throws IOException {
|
|
230
455
|
// A manifest entry that climbs out of the package folder: the client is not the one
|
|
@@ -287,7 +512,8 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
287
512
|
private static void assertFallbackResult(JSONObject patchResult, String expectedReason) {
|
|
288
513
|
assertEquals("fallback", patchResult.optString("status", null));
|
|
289
514
|
assertEquals(expectedReason, patchResult.optString("fallbackReason", null));
|
|
290
|
-
assertTrue("the
|
|
515
|
+
assertTrue("the whole path is timed", patchResult.optLong("totalDurationMs", -1) >= 0);
|
|
516
|
+
assertFalse("the result no longer carries a top level apply time", patchResult.has("applyDurationMs"));
|
|
291
517
|
}
|
|
292
518
|
|
|
293
519
|
/** The installed update is the full archive's contents, whichever archive it came from. */
|
|
@@ -307,7 +533,7 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
307
533
|
// How the update was downloaded says nothing about the update, so it is not part of
|
|
308
534
|
// what the update is installed from and outlives the download in no file.
|
|
309
535
|
assertFalse("the patch attempt reached the stored metadata",
|
|
310
|
-
metadata.has(CodePushConstants.
|
|
536
|
+
metadata.has(CodePushConstants.UPDATE_ARCHIVE_RESULT_KEY));
|
|
311
537
|
|
|
312
538
|
File binaryPatchFolder = new File(
|
|
313
539
|
new File(mDocumentsDirectory, CodePushConstants.CODE_PUSH_FOLDER_PREFIX),
|
|
@@ -378,6 +604,16 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
378
604
|
return new CodePushUpdateManager(mDocumentsDirectory, binaryPatch);
|
|
379
605
|
}
|
|
380
606
|
|
|
607
|
+
/** An applier that refuses the bundle patch, which every archive of a release carries. */
|
|
608
|
+
private static CodePushBinaryPatch.PatchApplier applierRefusingThePatch() {
|
|
609
|
+
return new CodePushBinaryPatch.PatchApplier() {
|
|
610
|
+
@Override
|
|
611
|
+
public int apply(byte[] base, byte[] patch, String outputPath, long expectedTargetSize) {
|
|
612
|
+
return RESULT_APPLY_FAILED;
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
381
617
|
private static CodePushBinaryPatch.PatchApplier applierWriting(final byte[] restoredBundle) {
|
|
382
618
|
return new CodePushBinaryPatch.PatchApplier() {
|
|
383
619
|
@Override
|
|
@@ -437,11 +673,16 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
437
673
|
* holds unchanged is not shipped at all - the client copies it over.
|
|
438
674
|
*/
|
|
439
675
|
private Map<String, byte[]> assetDiffArchiveContents(String deletedAssetPath) {
|
|
676
|
+
return assetDiffArchiveContentsWithManifest(
|
|
677
|
+
bytes("{\"deletedFiles\":[\"" + CONTENTS_DIR_NAME + "/" + deletedAssetPath + "\"]}"));
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** The same archive carrying a manifest of its own, which is how one the CLI did not write arrives. */
|
|
681
|
+
private Map<String, byte[]> assetDiffArchiveContentsWithManifest(byte[] manifest) {
|
|
440
682
|
Map<String, byte[]> contents = patchArchiveContents();
|
|
441
683
|
contents.remove(CONTENTS_DIR_NAME + "/" + ASSET_PATH);
|
|
442
684
|
contents.put(CONTENTS_DIR_NAME + "/" + ADDED_ASSET_PATH, ADDED_ASSET);
|
|
443
|
-
contents.put(CodePushConstants.DIFF_MANIFEST_FILE_NAME,
|
|
444
|
-
bytes("{\"deletedFiles\":[\"" + CONTENTS_DIR_NAME + "/" + deletedAssetPath + "\"]}"));
|
|
685
|
+
contents.put(CodePushConstants.DIFF_MANIFEST_FILE_NAME, manifest);
|
|
445
686
|
return contents;
|
|
446
687
|
}
|
|
447
688
|
|
|
@@ -453,6 +694,13 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
453
694
|
return contents;
|
|
454
695
|
}
|
|
455
696
|
|
|
697
|
+
/** The patch archive of the release the asset diff belongs to: every asset, no merge. */
|
|
698
|
+
private Map<String, byte[]> patchArchiveContentsForAssetDiffTarget() {
|
|
699
|
+
Map<String, byte[]> contents = patchArchiveContents();
|
|
700
|
+
contents.put(CONTENTS_DIR_NAME + "/" + ADDED_ASSET_PATH, ADDED_ASSET);
|
|
701
|
+
return contents;
|
|
702
|
+
}
|
|
703
|
+
|
|
456
704
|
/** What an asset diff has to add up to: the contents of the update's full archive. */
|
|
457
705
|
private Map<String, byte[]> assetDiffTargetContents() {
|
|
458
706
|
Map<String, byte[]> contents = fullArchiveContents();
|
|
@@ -470,6 +718,14 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
470
718
|
return updatePackage;
|
|
471
719
|
}
|
|
472
720
|
|
|
721
|
+
/** A release offering all three archives, the diff in the slot of its own the JS fills. */
|
|
722
|
+
private JSONObject updatePackageWithAssetDiff(String packageHash, String downloadUrl,
|
|
723
|
+
String binaryPatchDownloadUrl, String assetDiffDownloadUrl) {
|
|
724
|
+
JSONObject updatePackage = updatePackage(packageHash, downloadUrl, binaryPatchDownloadUrl);
|
|
725
|
+
CodePushUtils.setJSONValueForKey(updatePackage, CodePushConstants.ASSET_DIFF_DOWNLOAD_URL_KEY, assetDiffDownloadUrl);
|
|
726
|
+
return updatePackage;
|
|
727
|
+
}
|
|
728
|
+
|
|
473
729
|
/** A release with no second archive to try, so it is downloaded in full. */
|
|
474
730
|
private JSONObject fullUpdatePackage(String packageHash, String downloadUrl) {
|
|
475
731
|
JSONObject updatePackage = new JSONObject();
|
|
@@ -491,7 +747,10 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
491
747
|
private static class TestArchiveServer {
|
|
492
748
|
|
|
493
749
|
private final ServerSocket mSocket;
|
|
750
|
+
private static final long NO_CONTENT_LENGTH = -1;
|
|
751
|
+
|
|
494
752
|
private final Map<String, byte[]> mBodies = new HashMap<>();
|
|
753
|
+
private final Map<String, Long> mDeclaredLengths = new HashMap<>();
|
|
495
754
|
private final List<String> mRequestedPaths = Collections.synchronizedList(new ArrayList<String>());
|
|
496
755
|
|
|
497
756
|
TestArchiveServer() throws IOException {
|
|
@@ -508,6 +767,23 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
508
767
|
|
|
509
768
|
synchronized String serve(String path, byte[] body) {
|
|
510
769
|
mBodies.put(path, body);
|
|
770
|
+
return urlOf(path);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** Serves a body under a `Content-Length` of the server's choosing rather than its own. */
|
|
774
|
+
synchronized String serveClaimingLength(String path, byte[] body, long declaredLength) {
|
|
775
|
+
mBodies.put(path, body);
|
|
776
|
+
mDeclaredLengths.put(path, declaredLength);
|
|
777
|
+
return urlOf(path);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/** Serves a body with no `Content-Length` at all, which the client reads until it closes. */
|
|
781
|
+
synchronized String serveWithoutContentLength(String path, byte[] body) {
|
|
782
|
+
return serveClaimingLength(path, body, NO_CONTENT_LENGTH);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** The URL of a path this server answers - with a 404, when nothing is served there. */
|
|
786
|
+
String urlOf(String path) {
|
|
511
787
|
return "http://127.0.0.1:" + mSocket.getLocalPort() + path;
|
|
512
788
|
}
|
|
513
789
|
|
|
@@ -557,8 +833,17 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
557
833
|
if (body == null) {
|
|
558
834
|
response.write(bytes("HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"));
|
|
559
835
|
} else {
|
|
560
|
-
|
|
561
|
-
|
|
836
|
+
Long declaredLength = declaredLengthFor(path);
|
|
837
|
+
if (declaredLength == null) {
|
|
838
|
+
response.write(bytes("HTTP/1.1 200 OK\r\nContent-Length: " + body.length
|
|
839
|
+
+ "\r\nConnection: close\r\n\r\n"));
|
|
840
|
+
} else if (declaredLength == NO_CONTENT_LENGTH) {
|
|
841
|
+
response.write(bytes("HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n"));
|
|
842
|
+
} else {
|
|
843
|
+
response.write(bytes("HTTP/1.1 200 OK\r\nContent-Length: " + declaredLength
|
|
844
|
+
+ "\r\nConnection: close\r\n\r\n"));
|
|
845
|
+
}
|
|
846
|
+
|
|
562
847
|
response.write(body);
|
|
563
848
|
}
|
|
564
849
|
response.flush();
|
|
@@ -573,6 +858,10 @@ public class CodePushUpdateManagerDownloadTest {
|
|
|
573
858
|
private synchronized byte[] bodyFor(String path) {
|
|
574
859
|
return mBodies.get(path);
|
|
575
860
|
}
|
|
861
|
+
|
|
862
|
+
private synchronized Long declaredLengthFor(String path) {
|
|
863
|
+
return mDeclaredLengths.get(path);
|
|
864
|
+
}
|
|
576
865
|
}
|
|
577
866
|
|
|
578
867
|
private static byte[] zipOf(Map<String, byte[]> contents) throws IOException {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
5
|
+
import { createReleaseHistory } from "./createReleaseHistory.js";
|
|
6
|
+
import { updateReleaseHistory } from "../updateHistoryCommand/updateReleaseHistory.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Covers where the release history is staged before the config is handed it.
|
|
10
|
+
*
|
|
11
|
+
* The config receives a path rather than the history itself, so the command has to put the
|
|
12
|
+
* file somewhere first. Two releases of one app for different platforms are the same binary
|
|
13
|
+
* version, so a staging path derived from that version alone is the same path for both, and
|
|
14
|
+
* they overwrite each other's contents before either has read its own.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const BINARY_VERSION = "1.0.0";
|
|
18
|
+
|
|
19
|
+
let workingDir: string;
|
|
20
|
+
let previousCwd: string;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
previousCwd = process.cwd();
|
|
24
|
+
workingDir = fs.mkdtempSync(path.join(os.tmpdir(), "codepush-history-staging-"));
|
|
25
|
+
process.chdir(workingDir);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
process.chdir(previousCwd);
|
|
30
|
+
fs.rmSync(workingDir, { recursive: true, force: true });
|
|
31
|
+
jest.restoreAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function stagedPath(platform: "ios" | "android"): string {
|
|
35
|
+
return path.join(workingDir, "codepush-release-history", platform, `${BINARY_VERSION}.json`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Turns the command's own exit into a throw, so a staging failure fails the test instead
|
|
40
|
+
* of taking the worker down with it.
|
|
41
|
+
*/
|
|
42
|
+
function failOnExit(): void {
|
|
43
|
+
jest.spyOn(process, "exit").mockImplementation((code) => {
|
|
44
|
+
throw new Error(`the command exited with code ${String(code)}`);
|
|
45
|
+
});
|
|
46
|
+
jest.spyOn(console, "error").mockImplementation(() => undefined);
|
|
47
|
+
jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("staging the release history a config is handed", () => {
|
|
51
|
+
it("gives each platform writing at the same time a file of its own", async () => {
|
|
52
|
+
failOnExit();
|
|
53
|
+
|
|
54
|
+
const staged: Record<string, { jsonFilePath: string; contents: string }> = {};
|
|
55
|
+
const setReleaseHistory = async (
|
|
56
|
+
_binaryVersion: string,
|
|
57
|
+
jsonFilePath: string,
|
|
58
|
+
_releaseInfo: unknown,
|
|
59
|
+
platform: "ios" | "android",
|
|
60
|
+
): Promise<void> => {
|
|
61
|
+
// Yields once, which is all it takes for the other platform to reach its own
|
|
62
|
+
// write and its own clean-up while this one still holds only a path.
|
|
63
|
+
await Promise.resolve();
|
|
64
|
+
staged[platform] = { jsonFilePath, contents: fs.readFileSync(jsonFilePath, "utf8") };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
await Promise.all([
|
|
68
|
+
createReleaseHistory(BINARY_VERSION, setReleaseHistory, "ios", "RN0840"),
|
|
69
|
+
createReleaseHistory(BINARY_VERSION, setReleaseHistory, "android", "RN0840"),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
expect(staged.ios.jsonFilePath).not.toBe(staged.android.jsonFilePath);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("hands each platform the history that platform released", async () => {
|
|
76
|
+
failOnExit();
|
|
77
|
+
|
|
78
|
+
const released: Record<string, string> = {
|
|
79
|
+
ios: JSON.stringify({ [BINARY_VERSION]: { enabled: true, mandatory: false, downloadUrl: "", packageHash: "" }, "1.0.1": { enabled: true, mandatory: true, downloadUrl: "ios-url", packageHash: "ios-hash" } }),
|
|
80
|
+
android: JSON.stringify({ [BINARY_VERSION]: { enabled: true, mandatory: false, downloadUrl: "", packageHash: "" }, "1.0.1": { enabled: true, mandatory: true, downloadUrl: "android-url", packageHash: "android-hash" } }),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const staged: Record<string, string> = {};
|
|
84
|
+
const getReleaseHistory = async (
|
|
85
|
+
_binaryVersion: string,
|
|
86
|
+
platform: "ios" | "android",
|
|
87
|
+
) => JSON.parse(released[platform]);
|
|
88
|
+
const setReleaseHistory = async (
|
|
89
|
+
_binaryVersion: string,
|
|
90
|
+
jsonFilePath: string,
|
|
91
|
+
_releaseInfo: unknown,
|
|
92
|
+
platform: "ios" | "android",
|
|
93
|
+
): Promise<void> => {
|
|
94
|
+
await Promise.resolve();
|
|
95
|
+
staged[platform] = fs.readFileSync(jsonFilePath, "utf8");
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await Promise.all([
|
|
99
|
+
updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "ios", "RN0840", undefined, false, undefined),
|
|
100
|
+
updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "android", "RN0840", undefined, false, undefined),
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
expect(staged.ios).toContain("ios-url");
|
|
104
|
+
expect(staged.ios).not.toContain("android-url");
|
|
105
|
+
expect(staged.android).toContain("android-url");
|
|
106
|
+
expect(staged.android).not.toContain("ios-url");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("takes the file away once the config has stored the history", async () => {
|
|
110
|
+
failOnExit();
|
|
111
|
+
|
|
112
|
+
const setReleaseHistory = async (): Promise<void> => undefined;
|
|
113
|
+
await createReleaseHistory(BINARY_VERSION, setReleaseHistory, "ios", "RN0840");
|
|
114
|
+
|
|
115
|
+
expect(fs.existsSync(stagedPath("ios"))).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("leaves the file behind when the config could not store the history", async () => {
|
|
119
|
+
failOnExit();
|
|
120
|
+
|
|
121
|
+
const setReleaseHistory = async (): Promise<void> => {
|
|
122
|
+
throw new Error("the storage backend rejected the history");
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
await expect(createReleaseHistory(BINARY_VERSION, setReleaseHistory, "ios", "RN0840")).rejects.toThrow(
|
|
126
|
+
"the command exited with code 1",
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// What the release tried to store is worth reading back, so a failed release keeps
|
|
130
|
+
// its history at a path that is the same on every run.
|
|
131
|
+
expect(Object.keys(JSON.parse(fs.readFileSync(stagedPath("ios"), "utf8")))).toEqual([BINARY_VERSION]);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
1
|
import type { CliConfigInterface, ReleaseHistoryInterface, ReleaseInfo } from "../../../typings/react-native-code-push.d.ts";
|
|
2
|
+
import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
|
|
4
3
|
|
|
5
4
|
export async function createReleaseHistory(
|
|
6
5
|
targetVersion: string,
|
|
@@ -20,15 +19,8 @@ export async function createReleaseHistory(
|
|
|
20
19
|
};
|
|
21
20
|
|
|
22
21
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(INITIAL_HISTORY, null, 2));
|
|
27
|
-
fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(INITIAL_HISTORY));
|
|
28
|
-
|
|
29
|
-
await setReleaseHistory(targetVersion, JSON_FILE_PATH, INITIAL_HISTORY, platform, identifier)
|
|
30
|
-
|
|
31
|
-
fs.unlinkSync(JSON_FILE_PATH);
|
|
22
|
+
await stageReleaseHistoryFile(targetVersion, INITIAL_HISTORY, platform, (jsonFilePath) =>
|
|
23
|
+
setReleaseHistory(targetVersion, jsonFilePath, INITIAL_HISTORY, platform, identifier));
|
|
32
24
|
} catch (error) {
|
|
33
25
|
console.error('Error occurred while creating new history:', error);
|
|
34
26
|
process.exit(1)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs";
|
|
3
1
|
import type { CliConfigInterface } from "../../../typings/react-native-code-push.d.ts";
|
|
2
|
+
import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
|
|
4
3
|
|
|
5
4
|
export async function addToReleaseHistory(
|
|
6
5
|
appVersion: string,
|
|
@@ -51,15 +50,8 @@ export async function addToReleaseHistory(
|
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(newReleaseHistory, null, 2));
|
|
58
|
-
fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(newReleaseHistory));
|
|
59
|
-
|
|
60
|
-
await setReleaseHistory(binaryVersion, JSON_FILE_PATH, newReleaseHistory, platform, identifier)
|
|
61
|
-
|
|
62
|
-
fs.unlinkSync(JSON_FILE_PATH);
|
|
53
|
+
await stageReleaseHistoryFile(binaryVersion, newReleaseHistory, platform, (jsonFilePath) =>
|
|
54
|
+
setReleaseHistory(binaryVersion, jsonFilePath, newReleaseHistory, platform, identifier));
|
|
63
55
|
} catch (error) {
|
|
64
56
|
console.error('Error occurred while updating history:', error);
|
|
65
57
|
process.exit(1)
|
|
@@ -206,9 +206,9 @@ beforeEach(() => {
|
|
|
206
206
|
|
|
207
207
|
afterEach(() => {
|
|
208
208
|
jest.restoreAllMocks();
|
|
209
|
-
// addToReleaseHistory writes its JSON
|
|
210
|
-
// when the history was stored successfully.
|
|
211
|
-
fs.rmSync(path.resolve(process.cwd(),
|
|
209
|
+
// addToReleaseHistory writes its JSON under the directory it was invoked in, and only
|
|
210
|
+
// removes it when the history was stored successfully.
|
|
211
|
+
fs.rmSync(path.resolve(process.cwd(), "codepush-release-history"), { recursive: true, force: true });
|
|
212
212
|
});
|
|
213
213
|
|
|
214
214
|
describe("release without --binary-bundle-path", () => {
|