@capgo/cordova-updater 8.1.1

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.
Files changed (76) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +82 -0
  3. package/dist/esm/definitions.d.ts +2684 -0
  4. package/dist/esm/definitions.js +109 -0
  5. package/dist/esm/definitions.js.map +1 -0
  6. package/dist/esm/exec.d.ts +4 -0
  7. package/dist/esm/exec.js +25 -0
  8. package/dist/esm/exec.js.map +1 -0
  9. package/dist/esm/history.d.ts +1 -0
  10. package/dist/esm/history.js +283 -0
  11. package/dist/esm/history.js.map +1 -0
  12. package/dist/esm/index.d.ts +57 -0
  13. package/dist/esm/index.js +68 -0
  14. package/dist/esm/index.js.map +1 -0
  15. package/dist/esm/plugin.d.ts +57 -0
  16. package/dist/esm/plugin.js +15 -0
  17. package/dist/esm/plugin.js.map +1 -0
  18. package/dist/plugin.cjs.js +217 -0
  19. package/dist/plugin.cjs.js.map +1 -0
  20. package/dist/plugin.js +222 -0
  21. package/dist/plugin.js.map +1 -0
  22. package/package.json +96 -0
  23. package/plugin.xml +207 -0
  24. package/src/android/CordovaUpdaterPlugin.java +5662 -0
  25. package/src/android/UpdaterPathHandler.java +99 -0
  26. package/src/android/app/capgo/cordova/updater/AndroidAppExitReporter.java +92 -0
  27. package/src/android/app/capgo/cordova/updater/AppLifecycleObserver.java +115 -0
  28. package/src/android/app/capgo/cordova/updater/BundleInfo.java +246 -0
  29. package/src/android/app/capgo/cordova/updater/BundleStatus.java +45 -0
  30. package/src/android/app/capgo/cordova/updater/Callback.java +13 -0
  31. package/src/android/app/capgo/cordova/updater/CapgoUpdater.java +3404 -0
  32. package/src/android/app/capgo/cordova/updater/CordovaUpdaterConfig.java +56 -0
  33. package/src/android/app/capgo/cordova/updater/CryptoCipher.java +462 -0
  34. package/src/android/app/capgo/cordova/updater/DataManager.java +44 -0
  35. package/src/android/app/capgo/cordova/updater/DelayCondition.java +56 -0
  36. package/src/android/app/capgo/cordova/updater/DelayUntilNext.java +14 -0
  37. package/src/android/app/capgo/cordova/updater/DelayUpdateUtils.java +307 -0
  38. package/src/android/app/capgo/cordova/updater/DeviceIdHelper.java +230 -0
  39. package/src/android/app/capgo/cordova/updater/DownloadService.java +1219 -0
  40. package/src/android/app/capgo/cordova/updater/DownloadWorkerManager.java +260 -0
  41. package/src/android/app/capgo/cordova/updater/InternalUtils.java +54 -0
  42. package/src/android/app/capgo/cordova/updater/Logger.java +345 -0
  43. package/src/android/app/capgo/cordova/updater/ShakeDetector.java +72 -0
  44. package/src/android/app/capgo/cordova/updater/ShakeMenu.java +39 -0
  45. package/src/android/app/capgo/cordova/updater/ThreeFingerPinchDetector.java +323 -0
  46. package/src/android/app/capgo/cordova/updater/compat/JSArray.java +9 -0
  47. package/src/android/app/capgo/cordova/updater/compat/JSObject.java +83 -0
  48. package/src/android/app/capgo/cordova/updater/compat/PluginCall.java +165 -0
  49. package/src/android/capgo-cordova-updater.gradle +19 -0
  50. package/src/android/capgo-updater.xml +3 -0
  51. package/src/android/test/app/capgo/cordova/updater/CapacitorUpdaterUnitTest.java.skip +3039 -0
  52. package/src/android/test/app/capgo/cordova/updater/DelayUpdateUtilsTest.java +177 -0
  53. package/src/android/test/app/capgo/cordova/updater/NativeContractTest.java +105 -0
  54. package/src/ios/AES.swift +216 -0
  55. package/src/ios/AppHealthTracker.swift +82 -0
  56. package/src/ios/BundleInfo.swift +185 -0
  57. package/src/ios/BundleStatus.swift +127 -0
  58. package/src/ios/CapgoRawRsa.swift +36 -0
  59. package/src/ios/CapgoUpdater.swift +3544 -0
  60. package/src/ios/CordovaCompat.swift +11 -0
  61. package/src/ios/CordovaPluginCall.swift +190 -0
  62. package/src/ios/CordovaUpdaterCommands.swift +270 -0
  63. package/src/ios/CordovaUpdaterPlugin.swift +4706 -0
  64. package/src/ios/CryptoCipher.swift +326 -0
  65. package/src/ios/DelayCondition.swift +74 -0
  66. package/src/ios/DelayUntilNext.swift +30 -0
  67. package/src/ios/DelayUpdateUtils.swift +257 -0
  68. package/src/ios/DeviceIdHelper.swift +120 -0
  69. package/src/ios/Info.plist +28 -0
  70. package/src/ios/InternalUtils.swift +471 -0
  71. package/src/ios/Logger.swift +283 -0
  72. package/src/ios/RSA.swift +96 -0
  73. package/src/ios/ShakeMenu.swift +52 -0
  74. package/src/ios/UpdaterSchemeHandler.swift +81 -0
  75. package/src/ios/UserDefaultsExtension.swift +46 -0
  76. package/src/ios/WebViewStatsReporter.swift +303 -0
@@ -0,0 +1,3039 @@
1
+ package app.capgo.cordova.updater;
2
+
3
+ import static org.junit.Assert.*;
4
+ import static org.mockito.Mockito.*;
5
+
6
+ import android.app.ApplicationExitInfo;
7
+ import android.content.SharedPreferences;
8
+ import android.content.pm.PackageInfo;
9
+ import android.os.Handler;
10
+ import android.os.Looper;
11
+ import android.webkit.WebView;
12
+ import androidx.appcompat.app.AppCompatActivity;
13
+ import com.getcapacitor.Bridge;
14
+ import com.getcapacitor.CapConfig;
15
+ import com.getcapacitor.JSArray;
16
+ import com.getcapacitor.JSObject;
17
+ import com.getcapacitor.PluginCall;
18
+ import com.getcapacitor.PluginConfig;
19
+ import com.getcapacitor.PluginHandle;
20
+ import io.github.g00fy2.versioncompare.Version;
21
+ import java.io.File;
22
+ import java.io.IOException;
23
+ import java.lang.reflect.Field;
24
+ import java.lang.reflect.InvocationTargetException;
25
+ import java.lang.reflect.Method;
26
+ import java.nio.charset.StandardCharsets;
27
+ import java.nio.file.Files;
28
+ import java.nio.file.Path;
29
+ import java.util.ArrayList;
30
+ import java.util.Date;
31
+ import java.util.HashMap;
32
+ import java.util.List;
33
+ import java.util.Map;
34
+ import java.util.concurrent.Phaser;
35
+ import java.util.concurrent.TimeUnit;
36
+ import java.util.function.BooleanSupplier;
37
+ import java.util.zip.ZipEntry;
38
+ import java.util.zip.ZipOutputStream;
39
+ import org.json.JSONArray;
40
+ import org.json.JSONException;
41
+ import org.json.JSONObject;
42
+ import org.junit.Test;
43
+ import org.mockito.ArgumentCaptor;
44
+ import org.mockito.MockedConstruction;
45
+ import org.mockito.MockedStatic;
46
+
47
+ public class CapacitorUpdaterUnitTest {
48
+
49
+ private static class TestableCordovaUpdaterPlugin extends CordovaUpdaterPlugin {
50
+
51
+ private final ArrayList<String> notifiedEventNames = new ArrayList<>();
52
+ private final Map<String, JSObject> notifiedEventPayloads = new HashMap<>();
53
+
54
+ @Override
55
+ public void notifyListeners(String eventName, JSObject data) {
56
+ this.notifiedEventNames.add(eventName);
57
+ this.notifiedEventPayloads.put(eventName, data);
58
+ }
59
+
60
+ @Override
61
+ public void notifyListeners(String eventName, JSObject data, boolean retainUntilConsumed) {
62
+ this.notifiedEventNames.add(eventName);
63
+ this.notifiedEventPayloads.put(eventName, data);
64
+ }
65
+
66
+ boolean hasNotifiedEvent(final String eventName) {
67
+ return this.notifiedEventNames.contains(eventName);
68
+ }
69
+
70
+ JSObject getNotifiedEventPayload(final String eventName) {
71
+ return this.notifiedEventPayloads.get(eventName);
72
+ }
73
+ }
74
+
75
+ private static final class ImmediateThreadCordovaUpdaterPlugin extends TestableCordovaUpdaterPlugin {
76
+
77
+ private boolean versionDownloadInProgress = false;
78
+
79
+ @Override
80
+ public Thread startNewThread(final Runnable function, Number waitTime) {
81
+ function.run();
82
+ return new Thread();
83
+ }
84
+
85
+ @Override
86
+ public Thread startNewThread(final Runnable function) {
87
+ function.run();
88
+ return new Thread();
89
+ }
90
+
91
+ @Override
92
+ boolean isVersionDownloadInProgress(final String version) {
93
+ return this.versionDownloadInProgress;
94
+ }
95
+ }
96
+
97
+ private static final class DirectUpdateDispatchPlugin extends TestableCordovaUpdaterPlugin {
98
+
99
+ private final AppCompatActivity activity = mock(AppCompatActivity.class);
100
+ private boolean startNewThreadCalled = false;
101
+ private boolean reloadCalled = false;
102
+
103
+ @Override
104
+ public AppCompatActivity getActivity() {
105
+ return this.activity;
106
+ }
107
+
108
+ @Override
109
+ public Thread startNewThread(final Runnable function, Number waitTime) {
110
+ this.startNewThreadCalled = true;
111
+ function.run();
112
+ return new Thread();
113
+ }
114
+
115
+ @Override
116
+ public Thread startNewThread(final Runnable function) {
117
+ this.startNewThreadCalled = true;
118
+ function.run();
119
+ return new Thread();
120
+ }
121
+
122
+ @Override
123
+ protected boolean _reload() {
124
+ this.reloadCalled = true;
125
+ return true;
126
+ }
127
+ }
128
+
129
+ private static final class InstallNextDispatchPlugin extends TestableCordovaUpdaterPlugin {
130
+
131
+ private boolean startNewThreadCalled = false;
132
+ private boolean reloadCalled = false;
133
+
134
+ @Override
135
+ public Thread startNewThread(final Runnable function, Number waitTime) {
136
+ return this.startNewThread(function);
137
+ }
138
+
139
+ @Override
140
+ public Thread startNewThread(final Runnable function) {
141
+ this.startNewThreadCalled = true;
142
+ function.run();
143
+ return new Thread();
144
+ }
145
+
146
+ @Override
147
+ protected boolean _reload() {
148
+ this.reloadCalled = true;
149
+ return true;
150
+ }
151
+ }
152
+
153
+ private static final class ReloadDispatchPlugin extends TestableCordovaUpdaterPlugin {
154
+
155
+ private boolean startNewThreadCalled = false;
156
+ private boolean reloadCalled = false;
157
+
158
+ @Override
159
+ public Thread startNewThread(final Runnable function, Number waitTime) {
160
+ return this.startNewThread(function);
161
+ }
162
+
163
+ @Override
164
+ public Thread startNewThread(final Runnable function) {
165
+ this.startNewThreadCalled = true;
166
+ function.run();
167
+ return new Thread();
168
+ }
169
+
170
+ @Override
171
+ protected boolean _reload() {
172
+ this.reloadCalled = true;
173
+ return true;
174
+ }
175
+ }
176
+
177
+ private static final class InstallNextCapgoUpdater extends CapgoUpdater {
178
+
179
+ private BundleInfo currentBundle = new BundleInfo(
180
+ BundleInfo.ID_BUILTIN,
181
+ "builtin",
182
+ BundleStatus.SUCCESS,
183
+ BundleInfo.DOWNLOADED_BUILTIN,
184
+ "builtin"
185
+ );
186
+ private BundleInfo nextBundle = new BundleInfo("next-bundle-id", "2.0.0", BundleStatus.PENDING, new Date(), "abc123");
187
+ private int setCalls = 0;
188
+ private String lastSetNextBundleId = "next-bundle-id";
189
+
190
+ InstallNextCapgoUpdater() {
191
+ super(null);
192
+ }
193
+
194
+ @Override
195
+ public BundleInfo getCurrentBundle() {
196
+ return this.currentBundle;
197
+ }
198
+
199
+ @Override
200
+ public BundleInfo getNextBundle() {
201
+ return this.nextBundle;
202
+ }
203
+
204
+ @Override
205
+ public Boolean set(final BundleInfo bundle) {
206
+ this.setCalls++;
207
+ this.currentBundle = bundle;
208
+ return true;
209
+ }
210
+
211
+ @Override
212
+ public boolean setNextBundle(final String next) {
213
+ this.lastSetNextBundleId = next;
214
+ this.nextBundle = next == null ? null : this.nextBundle;
215
+ return true;
216
+ }
217
+ }
218
+
219
+ private static final class ReloadCapgoUpdater extends CapgoUpdater {
220
+
221
+ private final BundleInfo currentBundle = new BundleInfo("current-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc123");
222
+
223
+ ReloadCapgoUpdater() {
224
+ super(null);
225
+ }
226
+
227
+ @Override
228
+ public BundleInfo getCurrentBundle() {
229
+ return this.currentBundle;
230
+ }
231
+
232
+ @Override
233
+ public BundleInfo getNextBundle() {
234
+ return null;
235
+ }
236
+ }
237
+
238
+ private static final class StatsIgnoringCapgoUpdater extends CapgoUpdater {
239
+
240
+ StatsIgnoringCapgoUpdater() {
241
+ super(mock(Logger.class));
242
+ }
243
+
244
+ @Override
245
+ public void sendStats(final String action) {}
246
+ }
247
+
248
+ private static final class FreshDownloadCapgoUpdater extends CapgoUpdater {
249
+
250
+ private final BundleInfo currentBundle = new BundleInfo("current-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc123");
251
+ private BundleInfo existingLatestBundle;
252
+ private BooleanSupplier consumedStateSupplier = () -> false;
253
+ private BooleanSupplier directUpdateStateSupplier = () -> false;
254
+ private boolean downloadBackgroundCalled = false;
255
+ private boolean downloadBackgroundSetNext = true;
256
+ private boolean consumedWhenDownloadStarted = false;
257
+ private boolean directUpdateWhenDownloadStarted = false;
258
+ private boolean setNextBundleCalled = false;
259
+ private String lastSetNextBundleId;
260
+ private java.util.function.Consumer<String> updateAvailableNotifier = (version) -> {};
261
+
262
+ FreshDownloadCapgoUpdater() {
263
+ super(null);
264
+ }
265
+
266
+ @Override
267
+ public void getLatest(final String updateUrl, final String channel, final Callback callback) {
268
+ final Map<String, Object> response = new HashMap<>();
269
+ response.put("version", "2.0.0");
270
+ response.put("url", "https://example.com/update.zip");
271
+ callback.callback(response);
272
+ }
273
+
274
+ @Override
275
+ public BundleInfo getCurrentBundle() {
276
+ return this.currentBundle;
277
+ }
278
+
279
+ @Override
280
+ public BundleInfo getBundleInfoByName(final String version) {
281
+ return this.existingLatestBundle;
282
+ }
283
+
284
+ @Override
285
+ public void downloadBackground(
286
+ final String url,
287
+ final String version,
288
+ final String sessionKey,
289
+ final String checksum,
290
+ final JSONArray manifest
291
+ ) {
292
+ this.downloadBackground(url, version, sessionKey, checksum, manifest, true);
293
+ }
294
+
295
+ @Override
296
+ public void downloadBackground(
297
+ final String url,
298
+ final String version,
299
+ final String sessionKey,
300
+ final String checksum,
301
+ final JSONArray manifest,
302
+ final boolean setNext
303
+ ) {
304
+ this.downloadBackgroundCalled = true;
305
+ this.downloadBackgroundSetNext = setNext;
306
+ this.consumedWhenDownloadStarted = this.consumedStateSupplier.getAsBoolean();
307
+ this.directUpdateWhenDownloadStarted = this.directUpdateStateSupplier.getAsBoolean();
308
+ this.updateAvailableNotifier.accept(version);
309
+ }
310
+
311
+ @Override
312
+ public boolean setNextBundle(final String next) {
313
+ this.setNextBundleCalled = true;
314
+ this.lastSetNextBundleId = next;
315
+ return true;
316
+ }
317
+ }
318
+
319
+ private static final class BuiltinLatestCapgoUpdater extends CapgoUpdater {
320
+
321
+ private final BundleInfo currentBundle;
322
+ private final BundleInfo builtinBundle = new BundleInfo(
323
+ BundleInfo.ID_BUILTIN,
324
+ "builtin",
325
+ BundleStatus.SUCCESS,
326
+ BundleInfo.DOWNLOADED_BUILTIN,
327
+ "builtin"
328
+ );
329
+ private boolean setNextBundleCalled = false;
330
+ private String lastSetNextBundleId;
331
+
332
+ BuiltinLatestCapgoUpdater() {
333
+ this(new BundleInfo("current-id", "2.0.0", BundleStatus.SUCCESS, new Date(), "abc123"));
334
+ }
335
+
336
+ BuiltinLatestCapgoUpdater(final BundleInfo currentBundle) {
337
+ super(null);
338
+ this.currentBundle = currentBundle;
339
+ }
340
+
341
+ @Override
342
+ public void getLatest(final String updateUrl, final String channel, final Callback callback) {
343
+ final Map<String, Object> response = new HashMap<>();
344
+ response.put("version", "builtin");
345
+ callback.callback(response);
346
+ }
347
+
348
+ @Override
349
+ public BundleInfo getCurrentBundle() {
350
+ return this.currentBundle;
351
+ }
352
+
353
+ @Override
354
+ public BundleInfo getBundleInfo(final String id) {
355
+ if (BundleInfo.ID_BUILTIN.equals(id)) {
356
+ return this.builtinBundle;
357
+ }
358
+ return this.currentBundle;
359
+ }
360
+
361
+ @Override
362
+ public boolean setNextBundle(final String next) {
363
+ this.setNextBundleCalled = true;
364
+ this.lastSetNextBundleId = next;
365
+ return true;
366
+ }
367
+ }
368
+
369
+ private static final class NoNewVersionCapgoUpdater extends CapgoUpdater {
370
+
371
+ private final BundleInfo currentBundle = new BundleInfo("current-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc123");
372
+ private boolean sendStatsCalled = false;
373
+ private final boolean includeKind;
374
+
375
+ NoNewVersionCapgoUpdater() {
376
+ this(true);
377
+ }
378
+
379
+ NoNewVersionCapgoUpdater(final boolean includeKind) {
380
+ super(null);
381
+ this.includeKind = includeKind;
382
+ }
383
+
384
+ @Override
385
+ public void getLatest(final String updateUrl, final String channel, final Callback callback) {
386
+ final Map<String, Object> response = new HashMap<>();
387
+ response.put("error", "no_new_version_available");
388
+ if (this.includeKind) {
389
+ response.put("kind", "up_to_date");
390
+ }
391
+ response.put("message", "No new version available");
392
+ response.put("statusCode", 200);
393
+ callback.callback(response);
394
+ }
395
+
396
+ @Override
397
+ public BundleInfo getCurrentBundle() {
398
+ return this.currentBundle;
399
+ }
400
+
401
+ @Override
402
+ public void sendStats(final String action, final String versionName, final String oldVersionName) {
403
+ this.sendStatsCalled = true;
404
+ }
405
+ }
406
+
407
+ private static final class BlockedUpdateCapgoUpdater extends CapgoUpdater {
408
+
409
+ private final BundleInfo currentBundle = new BundleInfo("current-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc123");
410
+ private boolean sendStatsCalled = false;
411
+
412
+ BlockedUpdateCapgoUpdater() {
413
+ super(null);
414
+ }
415
+
416
+ @Override
417
+ public void getLatest(final String updateUrl, final String channel, final Callback callback) {
418
+ final Map<String, Object> response = new HashMap<>();
419
+ response.put("error", "disable_auto_update_to_major");
420
+ response.put("kind", "blocked");
421
+ response.put("message", "Cannot upgrade major version");
422
+ response.put("statusCode", 200);
423
+ callback.callback(response);
424
+ }
425
+
426
+ @Override
427
+ public BundleInfo getCurrentBundle() {
428
+ return this.currentBundle;
429
+ }
430
+
431
+ @Override
432
+ public void sendStats(final String action, final String versionName, final String oldVersionName) {
433
+ this.sendStatsCalled = true;
434
+ }
435
+ }
436
+
437
+ private static final class BreakingNoUrlCapgoUpdater extends CapgoUpdater {
438
+
439
+ private final BundleInfo currentBundle = new BundleInfo("current-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc123");
440
+ private boolean downloadFailStatsCalled = false;
441
+
442
+ BreakingNoUrlCapgoUpdater() {
443
+ super(null);
444
+ }
445
+
446
+ @Override
447
+ public void getLatest(final String updateUrl, final String channel, final Callback callback) {
448
+ final Map<String, Object> response = new HashMap<>();
449
+ response.put("version", "1.0.17");
450
+ response.put("breaking", true);
451
+ response.put("message", "store_update_required");
452
+ response.put("statusCode", 200);
453
+ callback.callback(response);
454
+ }
455
+
456
+ @Override
457
+ public BundleInfo getCurrentBundle() {
458
+ return this.currentBundle;
459
+ }
460
+
461
+ @Override
462
+ public void sendStats(final String action, final String versionName, final String oldVersionName) {
463
+ this.downloadFailStatsCalled = "download_fail".equals(action);
464
+ }
465
+ }
466
+
467
+ private static final class FailedUpdateCapgoUpdater extends CapgoUpdater {
468
+
469
+ private final BundleInfo currentBundle = new BundleInfo("current-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc123");
470
+ private boolean downloadFailStatsCalled = false;
471
+
472
+ FailedUpdateCapgoUpdater() {
473
+ super(null);
474
+ }
475
+
476
+ @Override
477
+ public void getLatest(final String updateUrl, final String channel, final Callback callback) {
478
+ final Map<String, Object> response = new HashMap<>();
479
+ response.put("error", "response_error");
480
+ response.put("kind", "failed");
481
+ response.put("message", "Error getting Latest");
482
+ response.put("statusCode", 500);
483
+ callback.callback(response);
484
+ }
485
+
486
+ @Override
487
+ public BundleInfo getCurrentBundle() {
488
+ return this.currentBundle;
489
+ }
490
+
491
+ @Override
492
+ public void sendStats(final String action, final String versionName, final String oldVersionName) {
493
+ this.downloadFailStatsCalled = "download_fail".equals(action);
494
+ }
495
+ }
496
+
497
+ private static final class ResetTrackingCapgoUpdater extends CapgoUpdater {
498
+
499
+ private BundleInfo currentBundle = new BundleInfo("current-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc123");
500
+ private final List<BundleInfo> listedBundles = new ArrayList<>();
501
+ private BundleInfo fallbackBundle = new BundleInfo(
502
+ BundleInfo.ID_BUILTIN,
503
+ "builtin",
504
+ BundleStatus.SUCCESS,
505
+ BundleInfo.DOWNLOADED_BUILTIN,
506
+ "builtin"
507
+ );
508
+ private BundleInfo nextBundle;
509
+ private BundleInfo previewFallbackBundle;
510
+ private BundleInfo stagedPreviewFallbackBundle;
511
+ private boolean resetCalled = false;
512
+ private boolean prepareResetStateForTransitionCalled = false;
513
+ private int prepareResetStateForTransitionCalls = 0;
514
+ private boolean finalizeResetTransitionCalled = false;
515
+ private int finalizeResetTransitionCalls = 0;
516
+ private String finalizeResetTransitionPreviousBundleName;
517
+ private boolean finalizeResetTransitionInternal = true;
518
+ private boolean canSetResult = true;
519
+ private boolean setResult = true;
520
+ private int canSetCalls = 0;
521
+ private int setCalls = 0;
522
+ private boolean stagePendingReloadResult = true;
523
+ private int stagePendingReloadCalls = 0;
524
+ private boolean stagePreviewFallbackReloadResult = true;
525
+ private int stagePreviewFallbackReloadCalls = 0;
526
+ private int finalizePendingReloadCalls = 0;
527
+ private BundleInfo finalizedPendingReloadBundle;
528
+ private String finalizePendingReloadPreviousBundleName;
529
+ private int setPreviewFallbackBundleCalls = 0;
530
+ private String lastPreviewFallbackBundle;
531
+ private int restoreResetStateCalls = 0;
532
+ private final ResetState capturedState = new ResetState("/stored/current", "fallback-id", "next-id");
533
+ private ResetState restoredState;
534
+
535
+ ResetTrackingCapgoUpdater() {
536
+ super(null);
537
+ }
538
+
539
+ @Override
540
+ public BundleInfo getCurrentBundle() {
541
+ return this.currentBundle;
542
+ }
543
+
544
+ @Override
545
+ public BundleInfo getFallbackBundle() {
546
+ return this.fallbackBundle;
547
+ }
548
+
549
+ @Override
550
+ public BundleInfo getNextBundle() {
551
+ return this.nextBundle;
552
+ }
553
+
554
+ @Override
555
+ public List<BundleInfo> list(final boolean rawList) {
556
+ return this.listedBundles;
557
+ }
558
+
559
+ @Override
560
+ public BundleInfo getPreviewFallbackBundle() {
561
+ return this.previewFallbackBundle;
562
+ }
563
+
564
+ @Override
565
+ public BundleInfo getBundleInfo(final String id) {
566
+ if (BundleInfo.ID_BUILTIN.equals(id)) {
567
+ return new BundleInfo(BundleInfo.ID_BUILTIN, "builtin", BundleStatus.SUCCESS, BundleInfo.DOWNLOADED_BUILTIN, "builtin");
568
+ }
569
+ if (this.currentBundle != null && this.currentBundle.getId().equals(id)) {
570
+ return this.currentBundle;
571
+ }
572
+ if (this.fallbackBundle != null && this.fallbackBundle.getId().equals(id)) {
573
+ return this.fallbackBundle;
574
+ }
575
+ if (this.previewFallbackBundle != null && this.previewFallbackBundle.getId().equals(id)) {
576
+ return this.previewFallbackBundle;
577
+ }
578
+ return new BundleInfo(id, id, BundleStatus.PENDING, new Date(), "");
579
+ }
580
+
581
+ @Override
582
+ boolean canSet(final BundleInfo bundle) {
583
+ this.canSetCalls++;
584
+ return this.canSetResult;
585
+ }
586
+
587
+ @Override
588
+ ResetState captureResetState() {
589
+ return this.capturedState;
590
+ }
591
+
592
+ @Override
593
+ void restoreResetState(final ResetState state) {
594
+ this.restoreResetStateCalls++;
595
+ this.restoredState = state;
596
+ }
597
+
598
+ @Override
599
+ public Boolean set(final BundleInfo bundle) {
600
+ this.setCalls++;
601
+ return this.setResult;
602
+ }
603
+
604
+ @Override
605
+ public boolean setNextBundle(final String next) {
606
+ return true;
607
+ }
608
+
609
+ @Override
610
+ boolean stagePendingReload(final BundleInfo bundle) {
611
+ this.stagePendingReloadCalls++;
612
+ return this.stagePendingReloadResult;
613
+ }
614
+
615
+ @Override
616
+ boolean stagePreviewFallbackReload(final BundleInfo bundle) {
617
+ this.stagePreviewFallbackReloadCalls++;
618
+ this.stagedPreviewFallbackBundle = bundle;
619
+ return this.stagePreviewFallbackReloadResult;
620
+ }
621
+
622
+ @Override
623
+ public boolean setPreviewFallbackBundle(final String fallback) {
624
+ this.setPreviewFallbackBundleCalls++;
625
+ this.lastPreviewFallbackBundle = fallback;
626
+ if (fallback == null) {
627
+ this.previewFallbackBundle = null;
628
+ }
629
+ return true;
630
+ }
631
+
632
+ @Override
633
+ public Boolean delete(final String id, final Boolean removeInfo) {
634
+ return true;
635
+ }
636
+
637
+ @Override
638
+ void finalizePendingReload(final BundleInfo bundle, final String previousBundleName) {
639
+ this.finalizePendingReloadCalls++;
640
+ this.finalizedPendingReloadBundle = bundle;
641
+ this.finalizePendingReloadPreviousBundleName = previousBundleName;
642
+ }
643
+
644
+ @Override
645
+ public void reset(final boolean internal) {
646
+ this.resetCalled = true;
647
+ }
648
+
649
+ @Override
650
+ void prepareResetStateForTransition() {
651
+ this.prepareResetStateForTransitionCalled = true;
652
+ this.prepareResetStateForTransitionCalls++;
653
+ }
654
+
655
+ @Override
656
+ void finalizeResetTransition(final String previousBundleName, final boolean internal) {
657
+ this.finalizeResetTransitionCalled = true;
658
+ this.finalizeResetTransitionCalls++;
659
+ this.finalizeResetTransitionPreviousBundleName = previousBundleName;
660
+ this.finalizeResetTransitionInternal = internal;
661
+ }
662
+ }
663
+
664
+ private static final class ReloadBypassCordovaUpdaterPlugin extends TestableCordovaUpdaterPlugin {
665
+
666
+ @Override
667
+ public Thread startNewThread(final Runnable function, Number waitTime) {
668
+ return this.startNewThread(function);
669
+ }
670
+
671
+ @Override
672
+ public Thread startNewThread(final Runnable function) {
673
+ function.run();
674
+ return new Thread();
675
+ }
676
+
677
+ @Override
678
+ protected boolean _reload() {
679
+ return true;
680
+ }
681
+
682
+ @Override
683
+ protected boolean reloadWithoutWaitingForAppReady() {
684
+ return true;
685
+ }
686
+ }
687
+
688
+ private static final class ReloadFailureCordovaUpdaterPlugin extends TestableCordovaUpdaterPlugin {
689
+
690
+ private int restoreLiveBundleStateAfterFailedReloadCalls = 0;
691
+
692
+ @Override
693
+ public Thread startNewThread(final Runnable function, Number waitTime) {
694
+ return this.startNewThread(function);
695
+ }
696
+
697
+ @Override
698
+ public Thread startNewThread(final Runnable function) {
699
+ function.run();
700
+ return new Thread();
701
+ }
702
+
703
+ @Override
704
+ protected boolean _reload() {
705
+ return false;
706
+ }
707
+
708
+ @Override
709
+ protected boolean reloadWithoutWaitingForAppReady() {
710
+ return false;
711
+ }
712
+
713
+ @Override
714
+ protected void restoreLiveBundleStateAfterFailedReload() {
715
+ this.restoreLiveBundleStateAfterFailedReloadCalls++;
716
+ }
717
+ }
718
+
719
+ private static final class SequenceReloadCordovaUpdaterPlugin extends TestableCordovaUpdaterPlugin {
720
+
721
+ private final boolean[] reloadResults;
722
+ private int reloadCallCount = 0;
723
+ private int restoreLiveBundleStateAfterFailedReloadCalls = 0;
724
+
725
+ SequenceReloadCordovaUpdaterPlugin(final boolean... reloadResults) {
726
+ this.reloadResults = reloadResults;
727
+ }
728
+
729
+ @Override
730
+ protected boolean _reload() {
731
+ final int resultIndex = Math.min(this.reloadCallCount, this.reloadResults.length - 1);
732
+ this.reloadCallCount++;
733
+ return this.reloadResults[resultIndex];
734
+ }
735
+
736
+ @Override
737
+ protected boolean reloadWithoutWaitingForAppReady() {
738
+ final int resultIndex = Math.min(this.reloadCallCount, this.reloadResults.length - 1);
739
+ this.reloadCallCount++;
740
+ return this.reloadResults[resultIndex];
741
+ }
742
+
743
+ @Override
744
+ protected void restoreLiveBundleStateAfterFailedReload() {
745
+ this.restoreLiveBundleStateAfterFailedReloadCalls++;
746
+ }
747
+ }
748
+
749
+ private static final class PendingReloadFinalizeCapgoUpdater extends CapgoUpdater {
750
+
751
+ private final Map<String, BundleInfo> bundleInfos = new HashMap<>();
752
+ private final BundleInfo currentBundle = new BundleInfo("current-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc123");
753
+ private String lastStatsAction;
754
+ private String lastStatsVersionName;
755
+ private final List<String> sentStatsActions = new ArrayList<>();
756
+ private final List<Map<String, String>> sentStatsMetadata = new ArrayList<>();
757
+ private String lastStatsOldVersionName;
758
+ private Map<String, String> lastStatsMetadata;
759
+ private boolean acknowledgeStats = true;
760
+
761
+ PendingReloadFinalizeCapgoUpdater() {
762
+ super(null);
763
+ }
764
+
765
+ @Override
766
+ public BundleInfo getCurrentBundle() {
767
+ return this.currentBundle;
768
+ }
769
+
770
+ @Override
771
+ public BundleInfo getBundleInfo(final String id) {
772
+ return this.bundleInfos.get(id);
773
+ }
774
+
775
+ @Override
776
+ public void saveBundleInfo(final String id, final BundleInfo info) {
777
+ this.bundleInfos.put(id, info);
778
+ }
779
+
780
+ @Override
781
+ public void sendStats(final String action, final String versionName, final String oldVersionName) {
782
+ this.sentStatsActions.add(action);
783
+ this.sentStatsMetadata.add(null);
784
+ this.lastStatsAction = action;
785
+ this.lastStatsVersionName = versionName;
786
+ this.lastStatsOldVersionName = oldVersionName;
787
+ this.lastStatsMetadata = null;
788
+ }
789
+
790
+ @Override
791
+ public void sendStats(
792
+ final String action,
793
+ final String versionName,
794
+ final String oldVersionName,
795
+ final Map<String, String> metadata
796
+ ) {
797
+ this.sentStatsActions.add(action);
798
+ this.sentStatsMetadata.add(metadata);
799
+ this.lastStatsAction = action;
800
+ this.lastStatsVersionName = versionName;
801
+ this.lastStatsOldVersionName = oldVersionName;
802
+ this.lastStatsMetadata = metadata;
803
+ }
804
+
805
+ @Override
806
+ public void sendStats(
807
+ final String action,
808
+ final String versionName,
809
+ final String oldVersionName,
810
+ final Map<String, String> metadata,
811
+ final Runnable onSent
812
+ ) {
813
+ this.sendStats(action, versionName, oldVersionName, metadata);
814
+ if (this.acknowledgeStats && onSent != null) {
815
+ onSent.run();
816
+ }
817
+ }
818
+ }
819
+
820
+ private static final class NoOpThreadCordovaUpdaterPlugin extends TestableCordovaUpdaterPlugin {
821
+
822
+ @Override
823
+ public Thread startNewThread(final Runnable function, Number waitTime) {
824
+ return this.startNewThread(function);
825
+ }
826
+
827
+ @Override
828
+ public Thread startNewThread(final Runnable function) {
829
+ return new Thread();
830
+ }
831
+ }
832
+
833
+ private static final class ConfigurableTimeoutCordovaUpdaterPlugin extends TestableCordovaUpdaterPlugin {
834
+
835
+ private final long minimumPendingBundleAppReadyTimeoutMs;
836
+
837
+ ConfigurableTimeoutCordovaUpdaterPlugin(final long minimumPendingBundleAppReadyTimeoutMs) {
838
+ this.minimumPendingBundleAppReadyTimeoutMs = minimumPendingBundleAppReadyTimeoutMs;
839
+ }
840
+
841
+ @Override
842
+ public Thread startNewThread(final Runnable function, Number waitTime) {
843
+ return this.startNewThread(function);
844
+ }
845
+
846
+ @Override
847
+ public Thread startNewThread(final Runnable function) {
848
+ return new Thread();
849
+ }
850
+
851
+ @Override
852
+ protected long getMinimumPendingBundleAppReadyTimeoutMs() {
853
+ return this.minimumPendingBundleAppReadyTimeoutMs;
854
+ }
855
+ }
856
+
857
+ private static final class AutoResetNativeVersionCapgoUpdater extends CapgoUpdater {
858
+
859
+ private boolean resetCalled = false;
860
+
861
+ AutoResetNativeVersionCapgoUpdater() {
862
+ super(mock(Logger.class));
863
+ }
864
+
865
+ @Override
866
+ public void reset() {
867
+ this.resetCalled = true;
868
+ }
869
+
870
+ @Override
871
+ public void reset(final boolean internal) {
872
+ this.resetCalled = true;
873
+ }
874
+ }
875
+
876
+ private static final class FixedPathCapgoUpdater extends CapgoUpdater {
877
+
878
+ private final String currentBundlePath;
879
+ private final boolean usingBuiltin;
880
+ private final BundleInfo currentBundle;
881
+
882
+ FixedPathCapgoUpdater(final String currentBundlePath, final boolean usingBuiltin) {
883
+ this(
884
+ currentBundlePath,
885
+ usingBuiltin,
886
+ usingBuiltin
887
+ ? new BundleInfo(BundleInfo.ID_BUILTIN, "builtin", BundleStatus.SUCCESS, BundleInfo.DOWNLOADED_BUILTIN, "builtin")
888
+ : new BundleInfo("current-bundle-id", "current-bundle", BundleStatus.SUCCESS, new Date(), "current-bundle-checksum")
889
+ );
890
+ }
891
+
892
+ FixedPathCapgoUpdater(final String currentBundlePath, final boolean usingBuiltin, final BundleInfo currentBundle) {
893
+ super(null);
894
+ this.currentBundlePath = currentBundlePath;
895
+ this.usingBuiltin = usingBuiltin;
896
+ this.currentBundle = currentBundle;
897
+ }
898
+
899
+ @Override
900
+ public String getCurrentBundlePath() {
901
+ return this.currentBundlePath;
902
+ }
903
+
904
+ @Override
905
+ public Boolean isUsingBuiltin() {
906
+ return this.usingBuiltin;
907
+ }
908
+
909
+ @Override
910
+ public BundleInfo getCurrentBundle() {
911
+ return this.currentBundle;
912
+ }
913
+ }
914
+
915
+ private static Path createExistingBundleDirectory(final String prefix, final String bundleId) throws Exception {
916
+ final Path tempDir = Files.createTempDirectory(prefix);
917
+ tempDir.toFile().deleteOnExit();
918
+ final Path versionsDir = tempDir.resolve("versions");
919
+ final Path bundleDir = versionsDir.resolve(bundleId);
920
+ Files.createDirectories(bundleDir);
921
+ versionsDir.toFile().deleteOnExit();
922
+ bundleDir.toFile().deleteOnExit();
923
+ final Path indexFile = bundleDir.resolve("index.html");
924
+ Files.createFile(indexFile);
925
+ indexFile.toFile().deleteOnExit();
926
+ return tempDir;
927
+ }
928
+
929
+ private static Path createZipWithEntry(final String entryName) throws Exception {
930
+ final Path zipPath = Files.createTempFile("capgo-zip-path", ".zip");
931
+ zipPath.toFile().deleteOnExit();
932
+ try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(zipPath))) {
933
+ zip.putNextEntry(new ZipEntry(entryName));
934
+ zip.write("owned".getBytes(StandardCharsets.UTF_8));
935
+ zip.closeEntry();
936
+ }
937
+ return zipPath;
938
+ }
939
+
940
+ private static File invokeUnzip(final CapgoUpdater updater, final String id, final Path zipPath, final String dest) throws Exception {
941
+ final Method method = CapgoUpdater.class.getDeclaredMethod("unzip", String.class, File.class, String.class);
942
+ method.setAccessible(true);
943
+ try {
944
+ return (File) method.invoke(updater, id, zipPath.toFile(), dest);
945
+ } catch (InvocationTargetException e) {
946
+ final Throwable cause = e.getCause();
947
+ if (cause instanceof IOException) {
948
+ throw (IOException) cause;
949
+ }
950
+ if (cause instanceof RuntimeException) {
951
+ throw (RuntimeException) cause;
952
+ }
953
+ throw e;
954
+ }
955
+ }
956
+
957
+ private static void invokeBackgroundDownload(final CordovaUpdaterPlugin plugin) throws Exception {
958
+ final Method backgroundDownload = CordovaUpdaterPlugin.class.getDeclaredMethod("backgroundDownload");
959
+ backgroundDownload.setAccessible(true);
960
+ backgroundDownload.invoke(plugin);
961
+ }
962
+
963
+ private static FreshDownloadCapgoUpdater configureOnlyDownloadBackgroundDownload(final ImmediateThreadCordovaUpdaterPlugin plugin) {
964
+ final FreshDownloadCapgoUpdater updater = new FreshDownloadCapgoUpdater();
965
+
966
+ plugin.implementation = updater;
967
+ plugin.setAutoUpdateModeForTesting("onlyDownload");
968
+ plugin.setLoggerForTesting(mock(Logger.class));
969
+ updater.updateAvailableNotifier = (version) -> {
970
+ final JSObject ret = new JSObject();
971
+ final BundleInfo downloaded = new BundleInfo("downloaded-id", version, BundleStatus.PENDING, new Date(), "checksum");
972
+ ret.put("bundle", InternalUtils.mapToJSObject(downloaded.toJSONMap()));
973
+ plugin.notifyListeners("updateAvailable", ret);
974
+ };
975
+
976
+ return updater;
977
+ }
978
+
979
+ private static void invokePrivateVoidMethod(final CordovaUpdaterPlugin plugin, final String methodName) throws Exception {
980
+ final Method method = CordovaUpdaterPlugin.class.getDeclaredMethod(methodName);
981
+ method.setAccessible(true);
982
+ method.invoke(plugin);
983
+ }
984
+
985
+ private static boolean invokePrivateResetMethod(
986
+ final CordovaUpdaterPlugin plugin,
987
+ final Boolean toLastSuccessful,
988
+ final Boolean usePendingBundle
989
+ ) throws Exception {
990
+ final Method method = CordovaUpdaterPlugin.class.getDeclaredMethod("_reset", Boolean.class, Boolean.class);
991
+ method.setAccessible(true);
992
+ return (boolean) method.invoke(plugin, toLastSuccessful, usePendingBundle);
993
+ }
994
+
995
+ private static boolean invokePrivateInternalResetMethod(
996
+ final CordovaUpdaterPlugin plugin,
997
+ final Boolean toLastSuccessful,
998
+ final Boolean usePendingBundle,
999
+ final boolean internal
1000
+ ) throws Exception {
1001
+ final Method method = CordovaUpdaterPlugin.class.getDeclaredMethod("performReset", Boolean.class, Boolean.class, boolean.class);
1002
+ method.setAccessible(true);
1003
+ return (boolean) method.invoke(plugin, toLastSuccessful, usePendingBundle, internal);
1004
+ }
1005
+
1006
+ private static boolean invokePrivatePreviewFallbackResetMethod(final CordovaUpdaterPlugin plugin) throws Exception {
1007
+ final Method method = CordovaUpdaterPlugin.class.getDeclaredMethod("resetToPreviewFallbackBundle");
1008
+ method.setAccessible(true);
1009
+ return (boolean) method.invoke(plugin);
1010
+ }
1011
+
1012
+ private static void invokePrivateSplashMethod(
1013
+ final CordovaUpdaterPlugin plugin,
1014
+ final String methodName,
1015
+ final JSObject options,
1016
+ final int retriesRemaining,
1017
+ final int requestToken
1018
+ ) throws Exception {
1019
+ final Method method = CordovaUpdaterPlugin.class.getDeclaredMethod(
1020
+ "invokeSplashScreenPluginMethod",
1021
+ String.class,
1022
+ JSObject.class,
1023
+ Integer.TYPE,
1024
+ Integer.TYPE
1025
+ );
1026
+ method.setAccessible(true);
1027
+ method.invoke(plugin, methodName, options, retriesRemaining, requestToken);
1028
+ }
1029
+
1030
+ private static void setPrivateField(final Object target, final String fieldName, final Object value) throws Exception {
1031
+ Class<?> current = target.getClass();
1032
+ while (current != null) {
1033
+ try {
1034
+ final Field field = current.getDeclaredField(fieldName);
1035
+ field.setAccessible(true);
1036
+ field.set(target, value);
1037
+ return;
1038
+ } catch (final NoSuchFieldException ignored) {
1039
+ current = current.getSuperclass();
1040
+ }
1041
+ }
1042
+ throw new NoSuchFieldException(fieldName);
1043
+ }
1044
+
1045
+ private static Object getPrivateField(final Object target, final String fieldName) throws Exception {
1046
+ Class<?> current = target.getClass();
1047
+ while (current != null) {
1048
+ try {
1049
+ final Field field = current.getDeclaredField(fieldName);
1050
+ field.setAccessible(true);
1051
+ return field.get(target);
1052
+ } catch (final NoSuchFieldException ignored) {
1053
+ current = current.getSuperclass();
1054
+ }
1055
+ }
1056
+ throw new NoSuchFieldException(fieldName);
1057
+ }
1058
+
1059
+ @Test
1060
+ public void mapsHealthExitReasonsToStatsActions() {
1061
+ assertEquals("app_crash", CordovaUpdaterPlugin.statsActionForApplicationExitReason(ApplicationExitInfo.REASON_CRASH));
1062
+ assertEquals(
1063
+ "app_crash_native",
1064
+ CordovaUpdaterPlugin.statsActionForApplicationExitReason(ApplicationExitInfo.REASON_CRASH_NATIVE)
1065
+ );
1066
+ assertEquals("app_anr", CordovaUpdaterPlugin.statsActionForApplicationExitReason(ApplicationExitInfo.REASON_ANR));
1067
+ assertEquals(
1068
+ "app_killed_low_memory",
1069
+ CordovaUpdaterPlugin.statsActionForApplicationExitReason(ApplicationExitInfo.REASON_LOW_MEMORY)
1070
+ );
1071
+ assertEquals(
1072
+ "app_killed_excessive_resource_usage",
1073
+ CordovaUpdaterPlugin.statsActionForApplicationExitReason(ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE)
1074
+ );
1075
+ assertEquals(
1076
+ "app_initialization_failure",
1077
+ CordovaUpdaterPlugin.statsActionForApplicationExitReason(ApplicationExitInfo.REASON_INITIALIZATION_FAILURE)
1078
+ );
1079
+ }
1080
+
1081
+ @Test
1082
+ public void pluginMethodsDoNotExposeApplicationExitInfoToReflection() {
1083
+ final String applicationExitInfoClassName = ApplicationExitInfo.class.getName();
1084
+ for (final Method method : CordovaUpdaterPlugin.class.getDeclaredMethods()) {
1085
+ assertFalse(exposesApplicationExitInfoType(method.getReturnType(), applicationExitInfoClassName));
1086
+ for (final Class<?> parameterType : method.getParameterTypes()) {
1087
+ assertFalse(exposesApplicationExitInfoType(parameterType, applicationExitInfoClassName));
1088
+ }
1089
+ }
1090
+ }
1091
+
1092
+ private static boolean exposesApplicationExitInfoType(final Class<?> type, final String applicationExitInfoClassName) {
1093
+ Class<?> currentType = type;
1094
+ while (currentType.isArray()) {
1095
+ currentType = currentType.getComponentType();
1096
+ }
1097
+ return applicationExitInfoClassName.equals(currentType.getName());
1098
+ }
1099
+
1100
+ @Test
1101
+ public void listChannelsResponseKeepsNumericChannelIds() throws Exception {
1102
+ final Map<String, Object> result = CapgoUpdater.parseListChannelsResponse(
1103
+ "[{\"id\":123,\"name\":\"Production\",\"public\":true,\"allow_self_set\":true}]"
1104
+ );
1105
+
1106
+ final Object channelsValue = result.get("channels");
1107
+ assertTrue(channelsValue instanceof List<?>);
1108
+ final Object channelValue = ((List<?>) channelsValue).get(0);
1109
+ assertTrue(channelValue instanceof Map<?, ?>);
1110
+ final Object id = ((Map<?, ?>) channelValue).get("id");
1111
+ assertTrue(id instanceof Number);
1112
+ assertEquals(123, ((Number) id).intValue());
1113
+ }
1114
+
1115
+ @Test
1116
+ public void listChannelsResponseRejectsStringChannelIds() {
1117
+ assertThrows(JSONException.class, () ->
1118
+ CapgoUpdater.parseListChannelsResponse("[{\"id\":\"123\",\"name\":\"Production\",\"public\":true,\"allow_self_set\":true}]")
1119
+ );
1120
+ }
1121
+
1122
+ @Test
1123
+ public void ignoresExpectedApplicationExitReasonsForStats() {
1124
+ assertNull(CordovaUpdaterPlugin.statsActionForApplicationExitReason(ApplicationExitInfo.REASON_EXIT_SELF));
1125
+ assertNull(CordovaUpdaterPlugin.statsActionForApplicationExitReason(ApplicationExitInfo.REASON_USER_REQUESTED));
1126
+ assertNull(CordovaUpdaterPlugin.statsActionForApplicationExitReason(ApplicationExitInfo.REASON_UNKNOWN));
1127
+ }
1128
+
1129
+ @Test
1130
+ public void mapsApplicationExitReasonNamesForMetadata() {
1131
+ assertEquals("crash", CordovaUpdaterPlugin.applicationExitReasonName(ApplicationExitInfo.REASON_CRASH));
1132
+ assertEquals("anr", CordovaUpdaterPlugin.applicationExitReasonName(ApplicationExitInfo.REASON_ANR));
1133
+ assertEquals("unknown", CordovaUpdaterPlugin.applicationExitReasonName(-1));
1134
+ }
1135
+
1136
+ @Test
1137
+ public void mapsWebViewErrorTypesToStatsActions() {
1138
+ assertEquals("webview_javascript_error", CordovaUpdaterPlugin.statsActionForWebViewErrorType("javascript_error"));
1139
+ assertEquals("webview_unhandled_rejection", CordovaUpdaterPlugin.statsActionForWebViewErrorType("unhandled_rejection"));
1140
+ assertEquals("webview_resource_error", CordovaUpdaterPlugin.statsActionForWebViewErrorType("resource_error"));
1141
+ assertEquals(
1142
+ "webview_security_policy_violation",
1143
+ CordovaUpdaterPlugin.statsActionForWebViewErrorType("security_policy_violation")
1144
+ );
1145
+ assertEquals("webview_unclean_restart", CordovaUpdaterPlugin.statsActionForWebViewErrorType("webview_unclean_restart"));
1146
+ assertEquals("webview_render_process_gone", CordovaUpdaterPlugin.statsActionForWebViewErrorType("render_process_gone"));
1147
+ assertEquals(
1148
+ "webview_content_process_terminated",
1149
+ CordovaUpdaterPlugin.statsActionForWebViewErrorType("web_content_process_terminated")
1150
+ );
1151
+ assertEquals("webview_javascript_error", CordovaUpdaterPlugin.statsActionForWebViewErrorType("unknown"));
1152
+ }
1153
+
1154
+ @Test
1155
+ public void buildsWebViewErrorMetadataWithUsefulFields() throws Exception {
1156
+ final JSObject data = new JSObject();
1157
+ data.put("type", "javascript_error");
1158
+ data.put("message", "boom");
1159
+ data.put("source", "app.js");
1160
+ data.put("line", "10");
1161
+ data.put("column", "20");
1162
+ data.put("stack", "x".repeat(3000));
1163
+ data.put("href", "capacitor://localhost");
1164
+ data.put("session_id", "session-1");
1165
+
1166
+ final Map<String, String> metadata = CordovaUpdaterPlugin.buildWebViewErrorMetadata(data);
1167
+
1168
+ assertEquals("javascript_error", metadata.get("error_type"));
1169
+ assertEquals("boom", metadata.get("message"));
1170
+ assertEquals("app.js", metadata.get("source"));
1171
+ assertEquals("10", metadata.get("line"));
1172
+ assertEquals("20", metadata.get("column"));
1173
+ assertEquals("capacitor://localhost", metadata.get("href"));
1174
+ assertEquals("session-1", metadata.get("session_id"));
1175
+ assertEquals(2048, metadata.get("stack").length());
1176
+ }
1177
+
1178
+ @Test
1179
+ public void sanitizesUrlValuesInWebViewErrorMetadata() throws Exception {
1180
+ final String scheme = "https";
1181
+ final String host = "example.com";
1182
+ final String userInfo = String.join(":", "user", "value") + "@";
1183
+ final String sourceQuery = "cache=123";
1184
+ final String hrefQuery = "debug=true";
1185
+ final JSObject data = new JSObject();
1186
+ data.put("source", scheme + "://" + userInfo + host + ":8443/assets/app.js?" + sourceQuery + "#L10");
1187
+ data.put("href", scheme + "://" + host + "/users/123456/dashboard?" + hrefQuery + "#frag");
1188
+ data.put("previous_href", "app.js?" + sourceQuery + "#frag");
1189
+
1190
+ final Map<String, String> metadata = CordovaUpdaterPlugin.buildWebViewErrorMetadata(data);
1191
+
1192
+ assertEquals(scheme + "://" + host + ":8443/assets/app.js", metadata.get("source"));
1193
+ assertEquals(scheme + "://" + host + "/users/redacted/dashboard", metadata.get("href"));
1194
+ assertEquals("app.js", metadata.get("previous_href"));
1195
+ assertFalse(metadata.get("source").contains(sourceQuery));
1196
+ assertFalse(metadata.get("href").contains(hrefQuery));
1197
+ }
1198
+
1199
+ @Test
1200
+ public void webViewStatsReporterScriptCapturesRuntimeAndRestartSignals() {
1201
+ final String script = CordovaUpdaterPlugin.buildWebViewStatsReporterScript();
1202
+
1203
+ assertTrue(script.contains("unhandledrejection"));
1204
+ assertTrue(script.contains("resource_error"));
1205
+ assertTrue(script.contains("securitypolicyviolation"));
1206
+ assertTrue(script.contains("webview_unclean_restart"));
1207
+ assertTrue(script.contains("reportWebViewError"));
1208
+ }
1209
+
1210
+ // BundleInfo Tests
1211
+
1212
+ @Test
1213
+ public void testBundleInfoInitialization() {
1214
+ BundleInfo bundleInfo = new BundleInfo("test-id", "1.0.0", BundleStatus.PENDING, new Date(), "abc123");
1215
+
1216
+ assertEquals("test-id", bundleInfo.getId());
1217
+ assertEquals("1.0.0", bundleInfo.getVersionName());
1218
+ assertEquals(BundleStatus.PENDING, bundleInfo.getStatus());
1219
+ assertEquals("abc123", bundleInfo.getChecksum());
1220
+ }
1221
+
1222
+ @Test
1223
+ public void testBundleInfoBuiltin() {
1224
+ BundleInfo bundleInfo = new BundleInfo(
1225
+ BundleInfo.ID_BUILTIN,
1226
+ "1.0.0",
1227
+ BundleStatus.SUCCESS,
1228
+ BundleInfo.DOWNLOADED_BUILTIN,
1229
+ "abc123"
1230
+ );
1231
+
1232
+ assertTrue(bundleInfo.isBuiltin());
1233
+ assertFalse(bundleInfo.isUnknown());
1234
+ }
1235
+
1236
+ @Test
1237
+ public void testBundleInfoUnknown() {
1238
+ BundleInfo bundleInfo = new BundleInfo(
1239
+ BundleInfo.VERSION_UNKNOWN,
1240
+ "1.0.0",
1241
+ BundleStatus.SUCCESS,
1242
+ BundleInfo.DOWNLOADED_BUILTIN,
1243
+ "abc123"
1244
+ );
1245
+
1246
+ assertTrue(bundleInfo.isUnknown());
1247
+ assertFalse(bundleInfo.isBuiltin());
1248
+ }
1249
+
1250
+ @Test
1251
+ public void testBundleInfoErrorStatus() {
1252
+ BundleInfo bundleInfo = new BundleInfo("test-id", "1.0.0", BundleStatus.ERROR, new Date(), "abc123");
1253
+
1254
+ assertTrue(bundleInfo.isErrorStatus());
1255
+ assertFalse(bundleInfo.isDeleted());
1256
+ }
1257
+
1258
+ @Test
1259
+ public void testBundleInfoDeleted() {
1260
+ BundleInfo bundleInfo = new BundleInfo("test-id", "1.0.0", BundleStatus.DELETED, new Date(), "abc123");
1261
+
1262
+ assertTrue(bundleInfo.isDeleted());
1263
+ assertFalse(bundleInfo.isErrorStatus());
1264
+ }
1265
+
1266
+ @Test
1267
+ public void testBundleInfoIsDownloaded() {
1268
+ BundleInfo bundleInfo = new BundleInfo("test-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc123");
1269
+
1270
+ assertTrue(bundleInfo.isDownloaded());
1271
+
1272
+ BundleInfo builtinBundle = new BundleInfo(BundleInfo.ID_BUILTIN, "1.0.0", BundleStatus.SUCCESS, new Date(), "abc123");
1273
+
1274
+ assertFalse(builtinBundle.isDownloaded());
1275
+ }
1276
+
1277
+ @Test
1278
+ public void testBundleInfoSetters() {
1279
+ BundleInfo original = new BundleInfo("test-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc123");
1280
+
1281
+ BundleInfo withNewChecksum = original.setChecksum("new-checksum");
1282
+ assertEquals("new-checksum", withNewChecksum.getChecksum());
1283
+ assertEquals("test-id", withNewChecksum.getId());
1284
+
1285
+ BundleInfo withNewId = original.setId("new-id");
1286
+ assertEquals("new-id", withNewId.getId());
1287
+ assertEquals("abc123", withNewId.getChecksum());
1288
+
1289
+ BundleInfo withNewStatus = original.setStatus(BundleStatus.ERROR);
1290
+ assertEquals(BundleStatus.ERROR, withNewStatus.getStatus());
1291
+ }
1292
+
1293
+ @Test
1294
+ public void testBundleInfoCopyConstructor() {
1295
+ BundleInfo original = new BundleInfo("test-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc123");
1296
+
1297
+ BundleInfo copy = new BundleInfo(original);
1298
+
1299
+ assertEquals(original.getId(), copy.getId());
1300
+ assertEquals(original.getVersionName(), copy.getVersionName());
1301
+ assertEquals(original.getStatus(), copy.getStatus());
1302
+ assertEquals(original.getChecksum(), copy.getChecksum());
1303
+ }
1304
+
1305
+ // BundleStatus Tests
1306
+
1307
+ @Test
1308
+ public void testBundleStatusToString() {
1309
+ assertEquals("success", BundleStatus.SUCCESS.toString());
1310
+ assertEquals("error", BundleStatus.ERROR.toString());
1311
+ assertEquals("pending", BundleStatus.PENDING.toString());
1312
+ assertEquals("deleted", BundleStatus.DELETED.toString());
1313
+ assertEquals("downloading", BundleStatus.DOWNLOADING.toString());
1314
+ }
1315
+
1316
+ @Test
1317
+ public void testBundleStatusFromString() {
1318
+ assertEquals(BundleStatus.SUCCESS, BundleStatus.fromString("success"));
1319
+ assertEquals(BundleStatus.ERROR, BundleStatus.fromString("error"));
1320
+ assertEquals(BundleStatus.PENDING, BundleStatus.fromString("pending"));
1321
+ assertEquals(BundleStatus.DELETED, BundleStatus.fromString("deleted"));
1322
+ assertEquals(BundleStatus.DOWNLOADING, BundleStatus.fromString("downloading"));
1323
+
1324
+ // Test null/empty string returns PENDING
1325
+ assertEquals(BundleStatus.PENDING, BundleStatus.fromString(null));
1326
+ assertEquals(BundleStatus.PENDING, BundleStatus.fromString(""));
1327
+
1328
+ // Test invalid string returns null
1329
+ assertNull(BundleStatus.fromString("invalid"));
1330
+ }
1331
+
1332
+ // Version Comparison Tests
1333
+
1334
+ @Test
1335
+ public void testVersionComparison() {
1336
+ Version version1 = new Version("1.0.0");
1337
+ Version version2 = new Version("1.0.1");
1338
+ Version version3 = new Version("2.0.0");
1339
+ Version version4 = new Version("1.0.0");
1340
+
1341
+ assertTrue(version1.isLowerThan(version2));
1342
+ assertTrue(version2.isLowerThan(version3));
1343
+ assertTrue(version1.isEqual(version4));
1344
+ assertFalse(version3.isLowerThan(version1));
1345
+ }
1346
+
1347
+ @Test
1348
+ public void testVersionIsAtLeast() {
1349
+ Version version1 = new Version("1.0.0");
1350
+ Version version2 = new Version("1.0.1");
1351
+
1352
+ assertTrue(version2.isAtLeast("1.0.0"));
1353
+ assertTrue(version2.isAtLeast("1.0.1"));
1354
+ assertFalse(version1.isAtLeast("1.0.1"));
1355
+ }
1356
+
1357
+ // Edge Cases Tests
1358
+
1359
+ @Test
1360
+ public void testBundleInfoWithNullValues() {
1361
+ BundleInfo bundleInfo = new BundleInfo(null, null, null, (String) null, null);
1362
+
1363
+ assertNotNull(bundleInfo.getId());
1364
+ assertNotNull(bundleInfo.getStatus());
1365
+ assertNotNull(bundleInfo.getChecksum());
1366
+ }
1367
+
1368
+ @Test
1369
+ public void testLargeDataHandling() {
1370
+ // Create a large string
1371
+ StringBuilder sb = new StringBuilder();
1372
+ for (int i = 0; i < 10000; i++) {
1373
+ sb.append("test-data-");
1374
+ }
1375
+ String largeData = sb.toString();
1376
+
1377
+ BundleInfo bundleInfo = new BundleInfo("test-id", largeData, BundleStatus.SUCCESS, new Date(), largeData);
1378
+
1379
+ assertNotNull(bundleInfo);
1380
+ assertEquals(largeData, bundleInfo.getVersionName());
1381
+ assertEquals(largeData, bundleInfo.getChecksum());
1382
+ }
1383
+
1384
+ @Test
1385
+ public void testBuildUserAgentUsesUnknownForNullOrEmptyValues() {
1386
+ assertEquals("CapacitorUpdater/unknown (unknown) android/unknown", DownloadService.buildUserAgent("", null, ""));
1387
+ }
1388
+
1389
+ @Test
1390
+ public void testBuildUserAgentMatchesExpectedAndroidFormat() {
1391
+ assertEquals(
1392
+ "CapacitorUpdater/8.43.9 (app.capgo.test) android/16",
1393
+ DownloadService.buildUserAgent("app.capgo.test", "8.43.9", "16")
1394
+ );
1395
+ }
1396
+
1397
+ @Test
1398
+ public void testShouldResetForForeignBundleWhenPathIsSetButBundleIsNotStored() {
1399
+ assertTrue(CapgoUpdater.shouldResetForForeignBundle("/data/user/0/app/files/versions/abc123", false, false));
1400
+ }
1401
+
1402
+ @Test
1403
+ public void testShouldNotResetForForeignBundleWhenBundleIsBuiltin() {
1404
+ assertFalse(CapgoUpdater.shouldResetForForeignBundle("public", true, false));
1405
+ }
1406
+
1407
+ @Test
1408
+ public void testShouldNotResetForForeignBundleWhenBundleIsStored() {
1409
+ assertFalse(CapgoUpdater.shouldResetForForeignBundle("/data/user/0/app/files/versions/abc123", false, true));
1410
+ }
1411
+
1412
+ @Test
1413
+ public void testGetBackgroundRunnerLabelFromConfigExtractsLabel() {
1414
+ final String config =
1415
+ "{\"plugins\":{\"BackgroundRunner\":{\"label\":\"com.example.runner\",\"src\":\"runner.js\",\"autoStart\":true}}}";
1416
+
1417
+ assertEquals("com.example.runner", CapgoUpdater.getBackgroundRunnerLabelFromConfig(config));
1418
+ }
1419
+
1420
+ @Test
1421
+ public void testGetBackgroundRunnerLabelFromConfigReturnsNullWhenMissing() {
1422
+ final String config = "{\"plugins\":{\"CapacitorUpdater\":{\"autoUpdate\":true}}}";
1423
+
1424
+ assertNull(CapgoUpdater.getBackgroundRunnerLabelFromConfig(config));
1425
+ }
1426
+
1427
+ @Test
1428
+ public void testGetBackgroundRunnerLabelFromConfigReturnsNullForBlankLabel() {
1429
+ final String config = "{\"plugins\":{\"BackgroundRunner\":{\"label\":\" \",\"src\":\"runner.js\"}}}";
1430
+
1431
+ assertNull(CapgoUpdater.getBackgroundRunnerLabelFromConfig(config));
1432
+ }
1433
+
1434
+ @Test
1435
+ public void testGetBundleInfoBuiltinReturnsVersionBuildWhenPresent() {
1436
+ CapgoUpdater updater = new CapgoUpdater(null);
1437
+ updater.versionBuild = "1.2.3";
1438
+
1439
+ BundleInfo bundleInfo = updater.getBundleInfo(BundleInfo.ID_BUILTIN);
1440
+
1441
+ assertEquals(BundleInfo.ID_BUILTIN, bundleInfo.getId());
1442
+ assertEquals("1.2.3", bundleInfo.getVersionName());
1443
+ assertEquals(BundleStatus.SUCCESS, bundleInfo.getStatus());
1444
+ }
1445
+
1446
+ @Test
1447
+ public void testGetBundleInfoBuiltinHandlesNullVersionBuild() {
1448
+ CapgoUpdater updater = new CapgoUpdater(null);
1449
+ updater.versionBuild = null;
1450
+
1451
+ BundleInfo bundleInfo = updater.getBundleInfo(BundleInfo.ID_BUILTIN);
1452
+
1453
+ assertEquals(BundleInfo.ID_BUILTIN, bundleInfo.getId());
1454
+ assertEquals(BundleInfo.ID_BUILTIN, bundleInfo.getVersionName());
1455
+ assertEquals(BundleStatus.SUCCESS, bundleInfo.getStatus());
1456
+ }
1457
+
1458
+ @Test
1459
+ public void testGetStoredNativeBuildVersionFallsBackToLegacyKey() throws Exception {
1460
+ try (MockedStatic<Looper> looperMock = mockStatic(Looper.class)) {
1461
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1462
+
1463
+ final TestableCordovaUpdaterPlugin plugin = new TestableCordovaUpdaterPlugin();
1464
+ final SharedPreferences prefs = mock(SharedPreferences.class);
1465
+
1466
+ setPrivateField(plugin, "prefs", prefs);
1467
+ when(prefs.getString("LatestNativeBuildVersion", "")).thenReturn("");
1468
+ when(prefs.getString("LatestVersionNative", "")).thenReturn("7");
1469
+
1470
+ assertEquals("7", plugin.getStoredNativeBuildVersion());
1471
+ }
1472
+ }
1473
+
1474
+ @Test
1475
+ public void testPersistCurrentNativeBuildVersionWritesCurrentBuildKey() throws Exception {
1476
+ try (MockedStatic<Looper> looperMock = mockStatic(Looper.class)) {
1477
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1478
+
1479
+ final TestableCordovaUpdaterPlugin plugin = new TestableCordovaUpdaterPlugin();
1480
+ final SharedPreferences.Editor editor = mock(SharedPreferences.Editor.class);
1481
+
1482
+ setPrivateField(plugin, "editor", editor);
1483
+ setPrivateField(plugin, "currentBuildVersion", "8");
1484
+ when(editor.putString("LatestNativeBuildVersion", "8")).thenReturn(editor);
1485
+
1486
+ plugin.persistCurrentNativeBuildVersion();
1487
+
1488
+ verify(editor).putString("LatestNativeBuildVersion", "8");
1489
+ verify(editor).apply();
1490
+ }
1491
+ }
1492
+
1493
+ @Test
1494
+ public void testReportNativeVersionStatsPersistsFirstSnapshotWithoutEvent() throws Exception {
1495
+ try (MockedStatic<Looper> looperMock = mockStatic(Looper.class)) {
1496
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1497
+
1498
+ final TestableCordovaUpdaterPlugin plugin = new TestableCordovaUpdaterPlugin();
1499
+ final PendingReloadFinalizeCapgoUpdater updater = new PendingReloadFinalizeCapgoUpdater();
1500
+ final SharedPreferences prefs = mock(SharedPreferences.class);
1501
+ final SharedPreferences.Editor editor = mock(SharedPreferences.Editor.class);
1502
+
1503
+ plugin.implementation = updater;
1504
+ setPrivateField(plugin, "prefs", prefs);
1505
+ setPrivateField(plugin, "editor", editor);
1506
+ when(prefs.getString("CapacitorUpdater.lastVersionOs", "")).thenReturn("");
1507
+ when(prefs.getString("CapacitorUpdater.lastVersionBuild", "")).thenReturn("");
1508
+ when(prefs.getString("CapacitorUpdater.lastVersionCode", "")).thenReturn("");
1509
+
1510
+ plugin.reportNativeVersionStatsIfChanged("1.0.0", "100", "14");
1511
+
1512
+ assertTrue(updater.sentStatsActions.isEmpty());
1513
+ verify(editor).putString("CapacitorUpdater.lastVersionOs", "14");
1514
+ verify(editor).putString("CapacitorUpdater.lastVersionBuild", "1.0.0");
1515
+ verify(editor).putString("CapacitorUpdater.lastVersionCode", "100");
1516
+ verify(editor).apply();
1517
+ }
1518
+ }
1519
+
1520
+ @Test
1521
+ public void testReportNativeVersionStatsSendsChangedOsAndNativeVersionEvents() throws Exception {
1522
+ try (MockedStatic<Looper> looperMock = mockStatic(Looper.class)) {
1523
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1524
+
1525
+ final TestableCordovaUpdaterPlugin plugin = new TestableCordovaUpdaterPlugin();
1526
+ final PendingReloadFinalizeCapgoUpdater updater = new PendingReloadFinalizeCapgoUpdater();
1527
+ final SharedPreferences prefs = mock(SharedPreferences.class);
1528
+ final SharedPreferences.Editor editor = mock(SharedPreferences.Editor.class);
1529
+
1530
+ plugin.implementation = updater;
1531
+ setPrivateField(plugin, "prefs", prefs);
1532
+ setPrivateField(plugin, "editor", editor);
1533
+ when(prefs.getString("CapacitorUpdater.lastVersionOs", "")).thenReturn("13");
1534
+ when(prefs.getString("CapacitorUpdater.lastVersionBuild", "")).thenReturn("1.0.0");
1535
+ when(prefs.getString("CapacitorUpdater.lastVersionCode", "")).thenReturn("100");
1536
+
1537
+ plugin.reportNativeVersionStatsIfChanged("1.1.0", "101", "14");
1538
+
1539
+ assertEquals(List.of("os_version_changed", "native_app_version_changed"), updater.sentStatsActions);
1540
+ assertEquals("13", updater.sentStatsMetadata.get(0).get("previous_version_os"));
1541
+ assertEquals("14", updater.sentStatsMetadata.get(0).get("current_version_os"));
1542
+ assertEquals("1.0.0", updater.sentStatsMetadata.get(1).get("previous_version_build"));
1543
+ assertEquals("1.1.0", updater.sentStatsMetadata.get(1).get("current_version_build"));
1544
+ assertEquals("100", updater.sentStatsMetadata.get(1).get("previous_version_code"));
1545
+ assertEquals("101", updater.sentStatsMetadata.get(1).get("current_version_code"));
1546
+ verify(editor).putString("CapacitorUpdater.lastVersionOs", "14");
1547
+ verify(editor).putString("CapacitorUpdater.lastVersionBuild", "1.1.0");
1548
+ verify(editor).putString("CapacitorUpdater.lastVersionCode", "101");
1549
+ verify(editor, times(2)).apply();
1550
+ }
1551
+ }
1552
+
1553
+ @Test
1554
+ public void testReportNativeVersionStatsKeepsChangedSnapshotPendingUntilStatsAck() throws Exception {
1555
+ try (MockedStatic<Looper> looperMock = mockStatic(Looper.class)) {
1556
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1557
+
1558
+ final TestableCordovaUpdaterPlugin plugin = new TestableCordovaUpdaterPlugin();
1559
+ final PendingReloadFinalizeCapgoUpdater updater = new PendingReloadFinalizeCapgoUpdater();
1560
+ final SharedPreferences prefs = mock(SharedPreferences.class);
1561
+ final SharedPreferences.Editor editor = mock(SharedPreferences.Editor.class);
1562
+ updater.acknowledgeStats = false;
1563
+
1564
+ plugin.implementation = updater;
1565
+ setPrivateField(plugin, "prefs", prefs);
1566
+ setPrivateField(plugin, "editor", editor);
1567
+ when(prefs.getString("CapacitorUpdater.lastVersionOs", "")).thenReturn("13");
1568
+ when(prefs.getString("CapacitorUpdater.lastVersionBuild", "")).thenReturn("1.0.0");
1569
+ when(prefs.getString("CapacitorUpdater.lastVersionCode", "")).thenReturn("100");
1570
+
1571
+ plugin.reportNativeVersionStatsIfChanged("1.1.0", "101", "14");
1572
+
1573
+ assertEquals(List.of("os_version_changed", "native_app_version_changed"), updater.sentStatsActions);
1574
+ verify(editor, never()).putString("CapacitorUpdater.lastVersionOs", "14");
1575
+ verify(editor, never()).putString("CapacitorUpdater.lastVersionBuild", "1.1.0");
1576
+ verify(editor, never()).putString("CapacitorUpdater.lastVersionCode", "101");
1577
+ verify(editor, never()).apply();
1578
+ }
1579
+ }
1580
+
1581
+ @Test
1582
+ public void testAutoResetFallsBackToLegacyNativeBuildVersionKey() throws Exception {
1583
+ final String bundleId = "legacy-bundle-id";
1584
+ final Path tempDir = createExistingBundleDirectory("capgo-autoreset", bundleId);
1585
+ final Path bundleDir = tempDir.resolve("versions").resolve(bundleId);
1586
+
1587
+ final AutoResetNativeVersionCapgoUpdater updater = new AutoResetNativeVersionCapgoUpdater();
1588
+ final SharedPreferences prefs = mock(SharedPreferences.class);
1589
+ final SharedPreferences.Editor editor = mock(SharedPreferences.Editor.class);
1590
+ final BundleInfo storedBundle = new BundleInfo(bundleId, "1.0.0", BundleStatus.SUCCESS, new Date(), "checksum");
1591
+
1592
+ updater.documentsDir = tempDir.toFile();
1593
+ updater.CAP_SERVER_PATH = "server-path";
1594
+ updater.prefs = prefs;
1595
+ updater.editor = editor;
1596
+
1597
+ when(prefs.getString("server-path", "public")).thenReturn(bundleDir.toString());
1598
+ when(prefs.getString("server-path", null)).thenReturn(bundleDir.toString());
1599
+ when(prefs.contains(bundleId + "_info")).thenReturn(true);
1600
+ when(prefs.getString(bundleId + "_info", "")).thenReturn(storedBundle.toString());
1601
+ when(prefs.getString("LatestNativeBuildVersion", "")).thenReturn("");
1602
+ when(prefs.getString("LatestVersionNative", "")).thenReturn("7");
1603
+
1604
+ updater.autoReset("8");
1605
+
1606
+ assertTrue(updater.resetCalled);
1607
+ }
1608
+
1609
+ @Test
1610
+ public void testAutoResetSkipsNativeBuildVersionResetWhenDisabled() throws Exception {
1611
+ final String bundleId = "legacy-bundle-id";
1612
+ final Path tempDir = createExistingBundleDirectory("capgo-autoreset-disabled", bundleId);
1613
+ final Path bundleDir = tempDir.resolve("versions").resolve(bundleId);
1614
+
1615
+ final AutoResetNativeVersionCapgoUpdater updater = new AutoResetNativeVersionCapgoUpdater();
1616
+ final SharedPreferences prefs = mock(SharedPreferences.class);
1617
+ final BundleInfo storedBundle = new BundleInfo(bundleId, "1.0.0", BundleStatus.SUCCESS, new Date(), "checksum");
1618
+
1619
+ updater.documentsDir = tempDir.toFile();
1620
+ updater.CAP_SERVER_PATH = "server-path";
1621
+ updater.prefs = prefs;
1622
+
1623
+ when(prefs.getString("server-path", "public")).thenReturn(bundleDir.toString());
1624
+ when(prefs.getString("server-path", null)).thenReturn(bundleDir.toString());
1625
+ when(prefs.contains(bundleId + "_info")).thenReturn(true);
1626
+ when(prefs.getString(bundleId + "_info", "")).thenReturn(storedBundle.toString());
1627
+ when(prefs.getString("LatestNativeBuildVersion", "")).thenReturn("");
1628
+ when(prefs.getString("LatestVersionNative", "")).thenReturn("7");
1629
+
1630
+ updater.autoReset("8", false);
1631
+
1632
+ assertFalse(updater.resetCalled);
1633
+ }
1634
+
1635
+ @Test
1636
+ public void testShouldConsumeOnLaunchDirectUpdateForOnLaunchAttempt() {
1637
+ assertTrue(CordovaUpdaterPlugin.shouldConsumeOnLaunchDirectUpdate("onLaunch", true));
1638
+ }
1639
+
1640
+ @Test
1641
+ public void testShouldNotConsumeOnLaunchDirectUpdateForNonLaunchAttempt() {
1642
+ assertFalse(CordovaUpdaterPlugin.shouldConsumeOnLaunchDirectUpdate("onLaunch", false));
1643
+ }
1644
+
1645
+ @Test
1646
+ public void testShouldNotConsumeOnLaunchDirectUpdateForOtherModes() {
1647
+ assertFalse(CordovaUpdaterPlugin.shouldConsumeOnLaunchDirectUpdate("always", true));
1648
+ assertFalse(CordovaUpdaterPlugin.shouldConsumeOnLaunchDirectUpdate("atInstall", true));
1649
+ assertFalse(CordovaUpdaterPlugin.shouldConsumeOnLaunchDirectUpdate("false", true));
1650
+ }
1651
+
1652
+ @Test
1653
+ public void testPeriodCheckDelayZeroDisablesPeriodicChecks() {
1654
+ assertEquals(0, CordovaUpdaterPlugin.normalizedPeriodCheckDelayMs(0));
1655
+ }
1656
+
1657
+ @Test
1658
+ public void testPeriodCheckDelayNegativeDisablesPeriodicChecks() {
1659
+ assertEquals(0, CordovaUpdaterPlugin.normalizedPeriodCheckDelayMs(-1));
1660
+ }
1661
+
1662
+ @Test
1663
+ public void testPeriodCheckDelayBelowMinimumClampsToTenMinutes() {
1664
+ assertEquals(600_000, CordovaUpdaterPlugin.normalizedPeriodCheckDelayMs(1));
1665
+ assertEquals(600_000, CordovaUpdaterPlugin.normalizedPeriodCheckDelayMs(599));
1666
+ }
1667
+
1668
+ @Test
1669
+ public void testPeriodCheckDelayAtMinimumIsAllowed() {
1670
+ assertEquals(600_000, CordovaUpdaterPlugin.normalizedPeriodCheckDelayMs(600));
1671
+ }
1672
+
1673
+ @Test
1674
+ public void testPeriodCheckDelayAboveMinimumIsPreserved() {
1675
+ assertEquals(3_600_000, CordovaUpdaterPlugin.normalizedPeriodCheckDelayMs(3600));
1676
+ }
1677
+
1678
+ @Test
1679
+ public void testPeriodCheckDelayOverflowClampsToMaxInt() {
1680
+ assertEquals(Integer.MAX_VALUE, CordovaUpdaterPlugin.normalizedPeriodCheckDelayMs(Integer.MAX_VALUE));
1681
+ }
1682
+
1683
+ @Test
1684
+ public void testCheckRevertSkipsRollbackDuringPreviewSession() throws Exception {
1685
+ try (
1686
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
1687
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
1688
+ ) {
1689
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1690
+
1691
+ final TestableCordovaUpdaterPlugin plugin = new TestableCordovaUpdaterPlugin();
1692
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
1693
+ updater.currentBundle = new BundleInfo("preview-id", "preview", BundleStatus.PENDING, new Date(), "preview");
1694
+
1695
+ plugin.implementation = updater;
1696
+ plugin.previewSessionEnabled = true;
1697
+ plugin.setLoggerForTesting(mock(Logger.class));
1698
+
1699
+ invokePrivateVoidMethod(plugin, "checkRevert");
1700
+
1701
+ assertFalse(updater.resetCalled);
1702
+ assertFalse(plugin.hasNotifiedEvent("updateFailed"));
1703
+ }
1704
+ }
1705
+
1706
+ @Test
1707
+ public void testCheckRevertSkipsRollbackDuringPreviewTransition() throws Exception {
1708
+ try (
1709
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
1710
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
1711
+ ) {
1712
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1713
+
1714
+ final TestableCordovaUpdaterPlugin plugin = new TestableCordovaUpdaterPlugin();
1715
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
1716
+ updater.currentBundle = new BundleInfo("preview-id", "preview", BundleStatus.PENDING, new Date(), "preview");
1717
+ updater.previewSession = true;
1718
+
1719
+ plugin.implementation = updater;
1720
+ plugin.setLoggerForTesting(mock(Logger.class));
1721
+
1722
+ invokePrivateVoidMethod(plugin, "checkRevert");
1723
+
1724
+ assertFalse(updater.resetCalled);
1725
+ assertFalse(plugin.hasNotifiedEvent("updateFailed"));
1726
+ }
1727
+ }
1728
+
1729
+ @Test
1730
+ public void testTriggerUpdateCheckSkipsDuringPreviewSession() throws Exception {
1731
+ try (
1732
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
1733
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
1734
+ ) {
1735
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1736
+
1737
+ final TestableCordovaUpdaterPlugin plugin = new TestableCordovaUpdaterPlugin();
1738
+ plugin.implementation = new ResetTrackingCapgoUpdater();
1739
+ plugin.previewSessionEnabled = true;
1740
+ plugin.setLoggerForTesting(mock(Logger.class));
1741
+ setPrivateField(plugin, "updateUrl", "https://example.com/updates");
1742
+
1743
+ assertEquals("preview_session", plugin.triggerBackgroundUpdateCheck());
1744
+ }
1745
+ }
1746
+
1747
+ @Test
1748
+ public void testResetToPendingWithoutPendingBundleDoesNotResetState() throws Exception {
1749
+ try (
1750
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
1751
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
1752
+ ) {
1753
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1754
+
1755
+ final ReloadBypassCordovaUpdaterPlugin plugin = new ReloadBypassCordovaUpdaterPlugin();
1756
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
1757
+
1758
+ plugin.implementation = updater;
1759
+ plugin.setLoggerForTesting(mock(Logger.class));
1760
+
1761
+ final boolean result = invokePrivateResetMethod(plugin, false, true);
1762
+
1763
+ assertFalse(result);
1764
+ assertFalse(updater.resetCalled);
1765
+ }
1766
+ }
1767
+
1768
+ @Test
1769
+ public void testResetToPendingWithoutInstallablePendingBundleDoesNotResetState() throws Exception {
1770
+ try (
1771
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
1772
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
1773
+ ) {
1774
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1775
+
1776
+ final ReloadBypassCordovaUpdaterPlugin plugin = new ReloadBypassCordovaUpdaterPlugin();
1777
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
1778
+ updater.nextBundle = new BundleInfo("pending-id", "2.0.0", BundleStatus.PENDING, new Date(), "pending");
1779
+ updater.canSetResult = false;
1780
+
1781
+ plugin.implementation = updater;
1782
+ plugin.setLoggerForTesting(mock(Logger.class));
1783
+
1784
+ final boolean result = invokePrivateResetMethod(plugin, false, true);
1785
+
1786
+ assertFalse(result);
1787
+ assertEquals(1, updater.canSetCalls);
1788
+ assertEquals(0, updater.setCalls);
1789
+ assertFalse(updater.resetCalled);
1790
+ assertEquals(0, updater.restoreResetStateCalls);
1791
+ }
1792
+ }
1793
+
1794
+ @Test
1795
+ public void testResetToPendingRestoresStateWhenSwitchFails() throws Exception {
1796
+ try (
1797
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
1798
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
1799
+ ) {
1800
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1801
+
1802
+ final ReloadBypassCordovaUpdaterPlugin plugin = new ReloadBypassCordovaUpdaterPlugin();
1803
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
1804
+ updater.nextBundle = new BundleInfo("pending-id", "2.0.0", BundleStatus.PENDING, new Date(), "pending");
1805
+ updater.setResult = false;
1806
+
1807
+ plugin.implementation = updater;
1808
+ plugin.setLoggerForTesting(mock(Logger.class));
1809
+
1810
+ final boolean result = invokePrivateResetMethod(plugin, false, true);
1811
+
1812
+ assertFalse(result);
1813
+ assertFalse(updater.resetCalled);
1814
+ assertTrue(updater.prepareResetStateForTransitionCalled);
1815
+ assertFalse(updater.finalizeResetTransitionCalled);
1816
+ assertEquals(1, updater.canSetCalls);
1817
+ assertEquals(1, updater.setCalls);
1818
+ assertEquals(1, updater.restoreResetStateCalls);
1819
+ assertSame(updater.capturedState, updater.restoredState);
1820
+ }
1821
+ }
1822
+
1823
+ @Test
1824
+ public void testResetToPendingRestoresLiveStateWhenReloadFails() throws Exception {
1825
+ try (
1826
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
1827
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
1828
+ ) {
1829
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1830
+
1831
+ final ReloadFailureCordovaUpdaterPlugin plugin = new ReloadFailureCordovaUpdaterPlugin();
1832
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
1833
+ updater.nextBundle = new BundleInfo("pending-id", "2.0.0", BundleStatus.PENDING, new Date(), "pending");
1834
+
1835
+ plugin.implementation = updater;
1836
+ plugin.setLoggerForTesting(mock(Logger.class));
1837
+
1838
+ final boolean result = invokePrivateResetMethod(plugin, false, true);
1839
+
1840
+ assertFalse(result);
1841
+ assertFalse(updater.resetCalled);
1842
+ assertTrue(updater.prepareResetStateForTransitionCalled);
1843
+ assertFalse(updater.finalizeResetTransitionCalled);
1844
+ assertEquals(1, updater.canSetCalls);
1845
+ assertEquals(1, updater.setCalls);
1846
+ assertEquals(1, updater.restoreResetStateCalls);
1847
+ assertSame(updater.capturedState, updater.restoredState);
1848
+ assertEquals(1, plugin.restoreLiveBundleStateAfterFailedReloadCalls);
1849
+ }
1850
+ }
1851
+
1852
+ @Test
1853
+ public void testResetToPendingRestoresStateWhenBuiltinPendingReloadFails() throws Exception {
1854
+ try (
1855
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
1856
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
1857
+ ) {
1858
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1859
+
1860
+ final ReloadFailureCordovaUpdaterPlugin plugin = new ReloadFailureCordovaUpdaterPlugin();
1861
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
1862
+ updater.nextBundle = new BundleInfo(
1863
+ BundleInfo.ID_BUILTIN,
1864
+ "builtin",
1865
+ BundleStatus.SUCCESS,
1866
+ BundleInfo.DOWNLOADED_BUILTIN,
1867
+ "builtin"
1868
+ );
1869
+
1870
+ plugin.implementation = updater;
1871
+ plugin.setLoggerForTesting(mock(Logger.class));
1872
+
1873
+ final boolean result = invokePrivateResetMethod(plugin, false, true);
1874
+
1875
+ assertFalse(result);
1876
+ assertFalse(updater.resetCalled);
1877
+ assertTrue(updater.prepareResetStateForTransitionCalled);
1878
+ assertFalse(updater.finalizeResetTransitionCalled);
1879
+ assertEquals(1, updater.canSetCalls);
1880
+ assertEquals(0, updater.setCalls);
1881
+ assertEquals(1, updater.restoreResetStateCalls);
1882
+ assertSame(updater.capturedState, updater.restoredState);
1883
+ assertEquals(1, plugin.restoreLiveBundleStateAfterFailedReloadCalls);
1884
+ }
1885
+ }
1886
+
1887
+ @Test
1888
+ public void testLeavePreviewUsesBuiltinWhenPreviewFallbackIsMissing() throws Exception {
1889
+ try (
1890
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
1891
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
1892
+ ) {
1893
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1894
+
1895
+ final ReloadBypassCordovaUpdaterPlugin plugin = new ReloadBypassCordovaUpdaterPlugin();
1896
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
1897
+ updater.currentBundle = new BundleInfo("preview-id", "preview", BundleStatus.SUCCESS, new Date(), "preview");
1898
+ updater.previewFallbackBundle = null;
1899
+
1900
+ plugin.implementation = updater;
1901
+ plugin.setLoggerForTesting(mock(Logger.class));
1902
+
1903
+ final boolean result = invokePrivatePreviewFallbackResetMethod(plugin);
1904
+
1905
+ assertTrue(result);
1906
+ assertEquals(1, updater.stagePreviewFallbackReloadCalls);
1907
+ assertEquals(BundleInfo.ID_BUILTIN, updater.stagedPreviewFallbackBundle.getId());
1908
+ assertTrue(updater.finalizeResetTransitionCalled);
1909
+ assertEquals("preview", updater.finalizeResetTransitionPreviousBundleName);
1910
+ assertFalse(updater.finalizeResetTransitionInternal);
1911
+ assertEquals(0, updater.restoreResetStateCalls);
1912
+ }
1913
+ }
1914
+
1915
+ @Test
1916
+ public void testLeavePreviewFromShakeMenuKeepsPreviewGuardUntilAppReady() throws Exception {
1917
+ try (
1918
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
1919
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
1920
+ ) {
1921
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1922
+
1923
+ final ReloadBypassCordovaUpdaterPlugin plugin = new ReloadBypassCordovaUpdaterPlugin();
1924
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
1925
+ final SharedPreferences prefs = mock(SharedPreferences.class);
1926
+ final SharedPreferences.Editor editor = mock(SharedPreferences.Editor.class);
1927
+ final Bridge bridge = mock(Bridge.class);
1928
+ final CapConfig capConfig = mock(CapConfig.class);
1929
+ final PluginConfig pluginConfig = mock(PluginConfig.class);
1930
+ final PluginHandle handle = mock(PluginHandle.class);
1931
+
1932
+ updater.currentBundle = new BundleInfo("preview-id", "preview", BundleStatus.SUCCESS, new Date(), "preview");
1933
+ updater.previewFallbackBundle = new BundleInfo("fallback-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "fallback");
1934
+ updater.previewSession = true;
1935
+ plugin.implementation = updater;
1936
+ plugin.previewSessionEnabled = true;
1937
+ plugin.setLoggerForTesting(mock(Logger.class));
1938
+ setPrivateField(plugin, "prefs", prefs);
1939
+ setPrivateField(plugin, "editor", editor);
1940
+ setPrivateField(plugin, "bridge", bridge);
1941
+ plugin.setPluginHandle(handle);
1942
+
1943
+ when(handle.getId()).thenReturn("CapacitorUpdater");
1944
+ when(bridge.getConfig()).thenReturn(capConfig);
1945
+ when(capConfig.getPluginConfiguration("CapacitorUpdater")).thenReturn(pluginConfig);
1946
+ when(pluginConfig.getString(anyString(), nullable(String.class))).thenAnswer((invocation) -> invocation.getArgument(1));
1947
+ when(pluginConfig.getBoolean(anyString(), anyBoolean())).thenAnswer((invocation) -> invocation.getArgument(1));
1948
+ when(prefs.getString(anyString(), nullable(String.class))).thenAnswer((invocation) -> invocation.getArgument(1));
1949
+ when(prefs.getBoolean(anyString(), anyBoolean())).thenAnswer((invocation) -> invocation.getArgument(1));
1950
+ when(editor.remove(anyString())).thenReturn(editor);
1951
+
1952
+ assertTrue(plugin.leavePreviewSessionFromShakeMenu());
1953
+ assertFalse(plugin.hasActivePreviewSession());
1954
+ assertTrue(updater.previewSession);
1955
+ assertEquals(1, updater.stagePreviewFallbackReloadCalls);
1956
+ assertEquals("fallback-id", updater.stagedPreviewFallbackBundle.getId());
1957
+ assertTrue(updater.finalizeResetTransitionCalled);
1958
+ assertEquals("preview", updater.finalizeResetTransitionPreviousBundleName);
1959
+ assertEquals(0, updater.restoreResetStateCalls);
1960
+ assertNull(updater.lastPreviewFallbackBundle);
1961
+ assertTrue(updater.setPreviewFallbackBundleCalls > 0);
1962
+ }
1963
+ }
1964
+
1965
+ @Test
1966
+ public void testPreviewMenuListsStoredPreviewsAndCleansMissingBundles() throws Exception {
1967
+ try (
1968
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
1969
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
1970
+ ) {
1971
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
1972
+
1973
+ final TestableCordovaUpdaterPlugin plugin = new TestableCordovaUpdaterPlugin();
1974
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
1975
+ final SharedPreferences prefs = mock(SharedPreferences.class);
1976
+ final SharedPreferences.Editor editor = mock(SharedPreferences.Editor.class);
1977
+ final String previewsKey = "CapacitorUpdater.previewSessions";
1978
+ final BundleInfo current = new BundleInfo("preview-current", "2.0.0", BundleStatus.SUCCESS, new Date(), "current");
1979
+ final BundleInfo other = new BundleInfo("preview-other", "1.5.0", BundleStatus.SUCCESS, new Date(), "other");
1980
+ final String sessions = new JSONObject()
1981
+ .put(
1982
+ "preview-current",
1983
+ new JSONObject()
1984
+ .put("name", "Current preview")
1985
+ .put("source", "qr")
1986
+ .put("createdAt", "2026-01-01T00:00:00.000Z")
1987
+ .put("updatedAt", "2026-01-02T00:00:00.000Z")
1988
+ .put("lastUsedAt", "2026-01-03T00:00:00.000Z")
1989
+ )
1990
+ .put(
1991
+ "preview-other",
1992
+ new JSONObject()
1993
+ .put("name", "Other preview")
1994
+ .put("createdAt", "2026-01-01T00:00:00.000Z")
1995
+ .put("updatedAt", "2026-01-01T00:00:00.000Z")
1996
+ .put("lastUsedAt", "2026-01-02T00:00:00.000Z")
1997
+ )
1998
+ .put("missing-preview", new JSONObject().put("name", "Missing preview").put("lastUsedAt", "2026-01-04T00:00:00.000Z"))
1999
+ .toString();
2000
+
2001
+ updater.currentBundle = current;
2002
+ updater.listedBundles.add(current);
2003
+ updater.listedBundles.add(other);
2004
+ plugin.implementation = updater;
2005
+ plugin.previewSessionEnabled = true;
2006
+ plugin.setLoggerForTesting(mock(Logger.class));
2007
+ setPrivateField(plugin, "prefs", prefs);
2008
+ setPrivateField(plugin, "editor", editor);
2009
+
2010
+ when(prefs.getString(eq(previewsKey), nullable(String.class))).thenReturn(sessions);
2011
+ when(editor.putString(eq(previewsKey), anyString())).thenReturn(editor);
2012
+
2013
+ final JSArray previews = plugin.previewMenuPreviews();
2014
+
2015
+ assertEquals(2, previews.length());
2016
+ final JSONObject first = previews.getJSONObject(0);
2017
+ assertEquals("preview-current", first.getString("id"));
2018
+ assertEquals("Current preview", first.getString("name"));
2019
+ assertEquals("qr", first.getString("source"));
2020
+ assertTrue(first.getBoolean("isActive"));
2021
+
2022
+ final JSONObject second = previews.getJSONObject(1);
2023
+ assertEquals("preview-other", second.getString("id"));
2024
+ assertFalse(second.getBoolean("isActive"));
2025
+
2026
+ final ArgumentCaptor<String> cleanedSessions = ArgumentCaptor.forClass(String.class);
2027
+ verify(editor).putString(eq(previewsKey), cleanedSessions.capture());
2028
+ assertFalse(new JSONObject(cleanedSessions.getValue()).has("missing-preview"));
2029
+ }
2030
+ }
2031
+
2032
+ @Test
2033
+ public void testResetToLastSuccessfulWithoutInstallableFallbackFallsBackToBuiltin() throws Exception {
2034
+ try (
2035
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2036
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2037
+ ) {
2038
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2039
+
2040
+ final ReloadBypassCordovaUpdaterPlugin plugin = new ReloadBypassCordovaUpdaterPlugin();
2041
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
2042
+ updater.fallbackBundle = new BundleInfo("fallback-id", "1.5.0", BundleStatus.SUCCESS, new Date(), "fallback");
2043
+ updater.canSetResult = false;
2044
+
2045
+ plugin.implementation = updater;
2046
+ plugin.setLoggerForTesting(mock(Logger.class));
2047
+
2048
+ final boolean result = invokePrivateResetMethod(plugin, true, false);
2049
+
2050
+ assertTrue(result);
2051
+ assertEquals(1, updater.canSetCalls);
2052
+ assertEquals(0, updater.setCalls);
2053
+ assertFalse(updater.resetCalled);
2054
+ assertTrue(updater.prepareResetStateForTransitionCalled);
2055
+ assertTrue(updater.finalizeResetTransitionCalled);
2056
+ assertEquals("1.0.0", updater.finalizeResetTransitionPreviousBundleName);
2057
+ assertFalse(updater.finalizeResetTransitionInternal);
2058
+ assertEquals(0, updater.restoreResetStateCalls);
2059
+ }
2060
+ }
2061
+
2062
+ @Test
2063
+ public void testResetToLastSuccessfulRestoresStateWhenSwitchFails() throws Exception {
2064
+ try (
2065
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2066
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2067
+ ) {
2068
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2069
+
2070
+ final ReloadBypassCordovaUpdaterPlugin plugin = new ReloadBypassCordovaUpdaterPlugin();
2071
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
2072
+ updater.fallbackBundle = new BundleInfo("fallback-id", "1.5.0", BundleStatus.SUCCESS, new Date(), "fallback");
2073
+ updater.setResult = false;
2074
+
2075
+ plugin.implementation = updater;
2076
+ plugin.setLoggerForTesting(mock(Logger.class));
2077
+
2078
+ final boolean result = invokePrivateResetMethod(plugin, true, false);
2079
+
2080
+ assertFalse(result);
2081
+ assertFalse(updater.resetCalled);
2082
+ assertTrue(updater.prepareResetStateForTransitionCalled);
2083
+ assertFalse(updater.finalizeResetTransitionCalled);
2084
+ assertEquals(1, updater.canSetCalls);
2085
+ assertEquals(1, updater.setCalls);
2086
+ assertEquals(1, updater.restoreResetStateCalls);
2087
+ assertSame(updater.capturedState, updater.restoredState);
2088
+ }
2089
+ }
2090
+
2091
+ @Test
2092
+ public void testResetToLastSuccessfulRestoresStateWhenFallbackReloadFails() throws Exception {
2093
+ try (
2094
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2095
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2096
+ ) {
2097
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2098
+
2099
+ final SequenceReloadCordovaUpdaterPlugin plugin = new SequenceReloadCordovaUpdaterPlugin(false);
2100
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
2101
+ updater.fallbackBundle = new BundleInfo("fallback-id", "1.5.0", BundleStatus.SUCCESS, new Date(), "fallback");
2102
+
2103
+ plugin.implementation = updater;
2104
+ plugin.setLoggerForTesting(mock(Logger.class));
2105
+
2106
+ final boolean result = invokePrivateResetMethod(plugin, true, false);
2107
+
2108
+ assertFalse(result);
2109
+ assertTrue(updater.prepareResetStateForTransitionCalled);
2110
+ assertEquals(1, updater.prepareResetStateForTransitionCalls);
2111
+ assertFalse(updater.finalizeResetTransitionCalled);
2112
+ assertEquals(1, updater.canSetCalls);
2113
+ assertEquals(1, updater.setCalls);
2114
+ assertEquals(1, updater.restoreResetStateCalls);
2115
+ assertSame(updater.capturedState, updater.restoredState);
2116
+ assertEquals(1, plugin.restoreLiveBundleStateAfterFailedReloadCalls);
2117
+ }
2118
+ }
2119
+
2120
+ @Test
2121
+ public void testInternalResetToLastSuccessfulFallsBackToBuiltinWhenFallbackReloadFails() throws Exception {
2122
+ try (
2123
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2124
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2125
+ ) {
2126
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2127
+
2128
+ final SequenceReloadCordovaUpdaterPlugin plugin = new SequenceReloadCordovaUpdaterPlugin(false, true);
2129
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
2130
+ updater.currentBundle = new BundleInfo("current-id", "2.0.0", BundleStatus.ERROR, new Date(), "abc123");
2131
+ updater.fallbackBundle = new BundleInfo("fallback-id", "1.5.0", BundleStatus.SUCCESS, new Date(), "fallback");
2132
+
2133
+ plugin.implementation = updater;
2134
+ plugin.setLoggerForTesting(mock(Logger.class));
2135
+
2136
+ final boolean result = invokePrivateInternalResetMethod(plugin, true, false, true);
2137
+
2138
+ assertTrue(result);
2139
+ assertTrue(updater.prepareResetStateForTransitionCalled);
2140
+ assertEquals(2, updater.prepareResetStateForTransitionCalls);
2141
+ assertTrue(updater.finalizeResetTransitionCalled);
2142
+ assertEquals(1, updater.finalizeResetTransitionCalls);
2143
+ assertEquals("2.0.0", updater.finalizeResetTransitionPreviousBundleName);
2144
+ assertTrue(updater.finalizeResetTransitionInternal);
2145
+ assertEquals(1, updater.canSetCalls);
2146
+ assertEquals(1, updater.setCalls);
2147
+ assertEquals(0, updater.restoreResetStateCalls);
2148
+ assertEquals(0, plugin.restoreLiveBundleStateAfterFailedReloadCalls);
2149
+ }
2150
+ }
2151
+
2152
+ @Test
2153
+ public void testFinalizePendingReloadPreservesSuccessfulBundleStatus() {
2154
+ final PendingReloadFinalizeCapgoUpdater updater = new PendingReloadFinalizeCapgoUpdater();
2155
+ final BundleInfo successfulBundle = new BundleInfo("pending-id", "2.0.0", BundleStatus.SUCCESS, new Date(), "pending");
2156
+ final BundleInfo pendingBundle = new BundleInfo("pending-id", "2.0.0", BundleStatus.PENDING, new Date(), "pending");
2157
+ updater.bundleInfos.put("pending-id", successfulBundle);
2158
+
2159
+ updater.finalizePendingReload(pendingBundle, "1.0.0");
2160
+
2161
+ assertEquals(BundleStatus.SUCCESS, updater.bundleInfos.get("pending-id").getStatus());
2162
+ assertEquals("set", updater.lastStatsAction);
2163
+ assertEquals("2.0.0", updater.lastStatsVersionName);
2164
+ assertEquals("1.0.0", updater.lastStatsOldVersionName);
2165
+ }
2166
+
2167
+ @Test
2168
+ public void testReloadRestoresStateWhenPendingApplyReloadFails() {
2169
+ try (
2170
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2171
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2172
+ ) {
2173
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2174
+
2175
+ final ReloadFailureCordovaUpdaterPlugin plugin = new ReloadFailureCordovaUpdaterPlugin();
2176
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
2177
+ final PluginCall call = mock(PluginCall.class);
2178
+ updater.nextBundle = new BundleInfo("pending-id", "2.0.0", BundleStatus.PENDING, new Date(), "pending");
2179
+
2180
+ plugin.implementation = updater;
2181
+ plugin.setLoggerForTesting(mock(Logger.class));
2182
+
2183
+ plugin.reload(call);
2184
+
2185
+ assertEquals(0, updater.setCalls);
2186
+ assertEquals(1, updater.stagePendingReloadCalls);
2187
+ assertEquals(0, updater.finalizePendingReloadCalls);
2188
+ assertEquals(1, updater.restoreResetStateCalls);
2189
+ assertSame(updater.capturedState, updater.restoredState);
2190
+ assertEquals(1, plugin.restoreLiveBundleStateAfterFailedReloadCalls);
2191
+ verify(call).reject("Reload failed after applying pending bundle: 2.0.0");
2192
+ }
2193
+ }
2194
+
2195
+ @Test
2196
+ public void testReloadFinalizesPendingBundleSideEffectsAfterSuccess() {
2197
+ try (
2198
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2199
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2200
+ ) {
2201
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2202
+
2203
+ final ReloadBypassCordovaUpdaterPlugin plugin = new ReloadBypassCordovaUpdaterPlugin();
2204
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
2205
+ final PluginCall call = mock(PluginCall.class);
2206
+ updater.nextBundle = new BundleInfo("pending-id", "2.0.0", BundleStatus.PENDING, new Date(), "pending");
2207
+
2208
+ plugin.implementation = updater;
2209
+ plugin.setLoggerForTesting(mock(Logger.class));
2210
+
2211
+ plugin.reload(call);
2212
+
2213
+ assertEquals(0, updater.setCalls);
2214
+ assertEquals(1, updater.stagePendingReloadCalls);
2215
+ assertEquals(1, updater.finalizePendingReloadCalls);
2216
+ assertEquals("1.0.0", updater.finalizePendingReloadPreviousBundleName);
2217
+ assertSame(updater.nextBundle, updater.finalizedPendingReloadBundle);
2218
+ verify(call).resolve();
2219
+ }
2220
+ }
2221
+
2222
+ @Test
2223
+ public void testReloadRestoresStateWhenBuiltinPendingReloadFails() {
2224
+ try (
2225
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2226
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2227
+ ) {
2228
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2229
+
2230
+ final ReloadFailureCordovaUpdaterPlugin plugin = new ReloadFailureCordovaUpdaterPlugin();
2231
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
2232
+ final PluginCall call = mock(PluginCall.class);
2233
+ updater.nextBundle = new BundleInfo(
2234
+ BundleInfo.ID_BUILTIN,
2235
+ "builtin",
2236
+ BundleStatus.SUCCESS,
2237
+ BundleInfo.DOWNLOADED_BUILTIN,
2238
+ "builtin"
2239
+ );
2240
+
2241
+ plugin.implementation = updater;
2242
+ plugin.setLoggerForTesting(mock(Logger.class));
2243
+
2244
+ plugin.reload(call);
2245
+
2246
+ assertEquals(0, updater.setCalls);
2247
+ assertEquals(0, updater.stagePendingReloadCalls);
2248
+ assertEquals(0, updater.finalizePendingReloadCalls);
2249
+ assertTrue(updater.prepareResetStateForTransitionCalled);
2250
+ assertFalse(updater.finalizeResetTransitionCalled);
2251
+ assertEquals(1, updater.restoreResetStateCalls);
2252
+ assertSame(updater.capturedState, updater.restoredState);
2253
+ assertEquals(1, plugin.restoreLiveBundleStateAfterFailedReloadCalls);
2254
+ verify(call).reject("Reload failed after applying pending bundle: builtin");
2255
+ }
2256
+ }
2257
+
2258
+ @Test
2259
+ public void testReloadTimeoutUsesMilliseconds() throws Exception {
2260
+ try (
2261
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2262
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2263
+ ) {
2264
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2265
+
2266
+ final NoOpThreadCordovaUpdaterPlugin plugin = new NoOpThreadCordovaUpdaterPlugin();
2267
+ final Bridge bridge = mock(Bridge.class);
2268
+ final WebView webView = mock(WebView.class);
2269
+
2270
+ plugin.implementation = new FixedPathCapgoUpdater("/tmp/capgo-bundle", false);
2271
+ plugin.setLoggerForTesting(mock(Logger.class));
2272
+ plugin.setBridge(bridge);
2273
+ setPrivateField(plugin, "appReadyTimeout", 1);
2274
+
2275
+ when(bridge.getWebView()).thenReturn(webView);
2276
+ when(bridge.getAppUrl()).thenReturn("https://local-app-domain.com");
2277
+ when(webView.post(any(Runnable.class))).thenReturn(true);
2278
+
2279
+ final long start = System.nanoTime();
2280
+ assertFalse(plugin._reload());
2281
+ final long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
2282
+
2283
+ assertTrue("Expected millisecond timeout but reload took " + elapsedMs + "ms", elapsedMs < 900);
2284
+ verify(bridge).setServerBasePath("/tmp/capgo-bundle");
2285
+ }
2286
+ }
2287
+
2288
+ @Test
2289
+ public void testReloadUsesExtendedTimeoutForPendingBundleValidation() throws Exception {
2290
+ try (
2291
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2292
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2293
+ ) {
2294
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2295
+
2296
+ final ConfigurableTimeoutCordovaUpdaterPlugin plugin = new ConfigurableTimeoutCordovaUpdaterPlugin(50);
2297
+ final Bridge bridge = mock(Bridge.class);
2298
+ final WebView webView = mock(WebView.class);
2299
+ final BundleInfo pendingBundle = new BundleInfo("pending-bundle-id", "2.0.0", BundleStatus.PENDING, new Date(), "abc123");
2300
+
2301
+ plugin.implementation = new FixedPathCapgoUpdater("/tmp/capgo-bundle", false, pendingBundle);
2302
+ plugin.setLoggerForTesting(mock(Logger.class));
2303
+ plugin.setBridge(bridge);
2304
+ setPrivateField(plugin, "appReadyTimeout", 1);
2305
+
2306
+ when(bridge.getWebView()).thenReturn(webView);
2307
+ when(bridge.getAppUrl()).thenReturn("https://local-app-domain.com");
2308
+ when(webView.post(any(Runnable.class))).thenReturn(true);
2309
+
2310
+ final long start = System.nanoTime();
2311
+ assertFalse(plugin._reload());
2312
+ final long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
2313
+
2314
+ assertTrue("Expected pending bundle timeout extension but reload took only " + elapsedMs + "ms", elapsedMs >= 40);
2315
+ assertTrue("Expected bounded pending bundle timeout but reload took " + elapsedMs + "ms", elapsedMs < 1000);
2316
+ verify(bridge).setServerBasePath("/tmp/capgo-bundle");
2317
+ }
2318
+ }
2319
+
2320
+ @Test
2321
+ public void testReloadTimeoutCleansUpPendingSemaphoreParty() throws Exception {
2322
+ try (
2323
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2324
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2325
+ ) {
2326
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2327
+
2328
+ final NoOpThreadCordovaUpdaterPlugin plugin = new NoOpThreadCordovaUpdaterPlugin();
2329
+ final Bridge bridge = mock(Bridge.class);
2330
+ final WebView webView = mock(WebView.class);
2331
+
2332
+ plugin.implementation = new FixedPathCapgoUpdater("/tmp/capgo-bundle", false);
2333
+ plugin.setLoggerForTesting(mock(Logger.class));
2334
+ plugin.setBridge(bridge);
2335
+ setPrivateField(plugin, "appReadyTimeout", 1);
2336
+
2337
+ when(bridge.getWebView()).thenReturn(webView);
2338
+ when(bridge.getAppUrl()).thenReturn("https://local-app-domain.com");
2339
+ when(webView.post(any(Runnable.class))).thenReturn(true);
2340
+
2341
+ assertFalse(plugin._reload());
2342
+
2343
+ final Phaser semaphore = (Phaser) getPrivateField(plugin, "semaphoreReady");
2344
+ assertEquals(0, semaphore.getRegisteredParties());
2345
+ }
2346
+ }
2347
+
2348
+ @Test
2349
+ public void testNotifyAppReadyWithoutPendingReloadKeepsSemaphoreReusable() throws Exception {
2350
+ try (MockedStatic<Looper> looperMock = mockStatic(Looper.class)) {
2351
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2352
+
2353
+ final TestableCordovaUpdaterPlugin plugin = new TestableCordovaUpdaterPlugin();
2354
+ final PluginCall call = mock(PluginCall.class);
2355
+ final CapgoUpdater updater = mock(CapgoUpdater.class);
2356
+ final BundleInfo bundle = new BundleInfo("current-bundle-id", "current-bundle", BundleStatus.SUCCESS, new Date(), "checksum");
2357
+
2358
+ plugin.implementation = updater;
2359
+ plugin.setLoggerForTesting(mock(Logger.class));
2360
+
2361
+ when(updater.getCurrentBundle()).thenReturn(bundle);
2362
+
2363
+ plugin.notifyAppReady(call);
2364
+
2365
+ final Phaser semaphore = (Phaser) getPrivateField(plugin, "semaphoreReady");
2366
+ assertFalse(semaphore.isTerminated());
2367
+ assertEquals(0, semaphore.getRegisteredParties());
2368
+ verify(call).resolve(any(JSObject.class));
2369
+ }
2370
+ }
2371
+
2372
+ @Test
2373
+ public void testInstallNextDispatchesReloadOffLifecycleThread() throws Exception {
2374
+ try (MockedStatic<Looper> looperMock = mockStatic(Looper.class)) {
2375
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2376
+
2377
+ final InstallNextDispatchPlugin plugin = new InstallNextDispatchPlugin();
2378
+ final InstallNextCapgoUpdater updater = new InstallNextCapgoUpdater();
2379
+ final SharedPreferences prefs = mock(SharedPreferences.class);
2380
+ final DelayUpdateUtils delayUpdateUtils = mock(DelayUpdateUtils.class);
2381
+
2382
+ plugin.implementation = updater;
2383
+ plugin.setLoggerForTesting(mock(Logger.class));
2384
+
2385
+ when(prefs.getString(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES, "[]")).thenReturn("[]");
2386
+ when(delayUpdateUtils.parseDelayConditions("[]")).thenReturn(new ArrayList<>());
2387
+
2388
+ setPrivateField(plugin, "prefs", prefs);
2389
+ setPrivateField(plugin, "delayUpdateUtils", delayUpdateUtils);
2390
+
2391
+ invokePrivateVoidMethod(plugin, "installNext");
2392
+
2393
+ assertTrue(plugin.startNewThreadCalled);
2394
+ assertTrue(plugin.reloadCalled);
2395
+ assertEquals(1, updater.setCalls);
2396
+ assertNull(updater.lastSetNextBundleId);
2397
+ }
2398
+ }
2399
+
2400
+ @Test
2401
+ public void testOnLaunchCompletionConsumesWindowWithoutClearingInFlightDirectUpdate() {
2402
+ try (
2403
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2404
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2405
+ ) {
2406
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2407
+
2408
+ TestableCordovaUpdaterPlugin plugin = new TestableCordovaUpdaterPlugin();
2409
+ plugin.implementation = new CapgoUpdater(null);
2410
+ plugin.implementation.directUpdate = true;
2411
+ plugin.configureDirectUpdateModeForTesting("onLaunch", false);
2412
+ plugin.setLoggerForTesting(mock(Logger.class));
2413
+
2414
+ BundleInfo current = new BundleInfo("test-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc123");
2415
+
2416
+ assertTrue(plugin.shouldUseDirectUpdateForTesting());
2417
+ assertFalse(plugin.hasConsumedOnLaunchDirectUpdateForTesting());
2418
+
2419
+ plugin.completeBackgroundTaskForTesting(current, true);
2420
+
2421
+ assertTrue(plugin.hasConsumedOnLaunchDirectUpdateForTesting());
2422
+ assertFalse(plugin.shouldUseDirectUpdateForTesting());
2423
+ assertTrue(plugin.implementation.directUpdate);
2424
+ }
2425
+ }
2426
+
2427
+ @Test
2428
+ public void testOnLaunchFreshDownloadConsumesWindowBeforeDownloadStarts() throws Exception {
2429
+ try (
2430
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2431
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2432
+ ) {
2433
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2434
+
2435
+ ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
2436
+ FreshDownloadCapgoUpdater updater = new FreshDownloadCapgoUpdater();
2437
+
2438
+ plugin.implementation = updater;
2439
+ plugin.configureDirectUpdateModeForTesting("onLaunch", false);
2440
+ plugin.setLoggerForTesting(mock(Logger.class));
2441
+ updater.consumedStateSupplier = plugin::hasConsumedOnLaunchDirectUpdateForTesting;
2442
+ updater.directUpdateStateSupplier = () -> Boolean.TRUE.equals(plugin.implementation.directUpdate);
2443
+
2444
+ assertTrue(plugin.shouldUseDirectUpdateForTesting());
2445
+ assertFalse(plugin.hasConsumedOnLaunchDirectUpdateForTesting());
2446
+
2447
+ invokeBackgroundDownload(plugin);
2448
+
2449
+ assertTrue(updater.downloadBackgroundCalled);
2450
+ assertTrue(updater.consumedWhenDownloadStarted);
2451
+ assertTrue(updater.directUpdateWhenDownloadStarted);
2452
+ assertTrue(plugin.hasConsumedOnLaunchDirectUpdateForTesting());
2453
+ assertFalse(plugin.shouldUseDirectUpdateForTesting());
2454
+ assertTrue(plugin.implementation.directUpdate);
2455
+ }
2456
+ }
2457
+
2458
+ @Test
2459
+ public void testOnlyDownloadModeDownloadsWithoutSchedulingOrDirectUpdate() throws Exception {
2460
+ try (
2461
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2462
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2463
+ ) {
2464
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2465
+
2466
+ final ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
2467
+ final FreshDownloadCapgoUpdater updater = configureOnlyDownloadBackgroundDownload(plugin);
2468
+
2469
+ plugin.implementation.directUpdate = true;
2470
+ updater.directUpdateStateSupplier = () -> Boolean.TRUE.equals(plugin.implementation.directUpdate);
2471
+
2472
+ assertFalse(plugin.shouldUseDirectUpdateForTesting());
2473
+
2474
+ invokeBackgroundDownload(plugin);
2475
+
2476
+ assertTrue(updater.downloadBackgroundCalled);
2477
+ assertFalse(updater.downloadBackgroundSetNext);
2478
+ assertFalse(updater.setNextBundleCalled);
2479
+ assertNull(updater.lastSetNextBundleId);
2480
+ assertFalse(updater.directUpdateWhenDownloadStarted);
2481
+ assertFalse(plugin.implementation.directUpdate);
2482
+ assertTrue(plugin.hasNotifiedEvent("updateAvailable"));
2483
+ }
2484
+ }
2485
+
2486
+ @Test
2487
+ public void testOnlyDownloadModeDoesNotScheduleExistingDownloadedBundle() throws Exception {
2488
+ try (
2489
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2490
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2491
+ ) {
2492
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2493
+
2494
+ final ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
2495
+ final FreshDownloadCapgoUpdater updater = configureOnlyDownloadBackgroundDownload(plugin);
2496
+ updater.existingLatestBundle = new BundleInfo("downloaded-id", "2.0.0", BundleStatus.PENDING, new Date(), "checksum");
2497
+
2498
+ invokeBackgroundDownload(plugin);
2499
+
2500
+ assertFalse(updater.downloadBackgroundCalled);
2501
+ assertFalse(updater.setNextBundleCalled);
2502
+ assertNull(updater.lastSetNextBundleId);
2503
+ assertTrue(plugin.hasNotifiedEvent("updateAvailable"));
2504
+ assertFalse(plugin.hasNotifiedEvent("noNeedUpdate"));
2505
+
2506
+ final JSObject completionPayload = plugin.getNotifiedEventPayload("appReady");
2507
+ final JSONObject completionBundle = completionPayload.getJSONObject("bundle");
2508
+ assertEquals(updater.currentBundle.getId(), completionBundle.getString("id"));
2509
+ }
2510
+ }
2511
+
2512
+ @Test
2513
+ public void testOnlyDownloadModeBuiltinNotifiesUpdateAvailableWithoutScheduling() throws Exception {
2514
+ try (
2515
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2516
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2517
+ ) {
2518
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2519
+
2520
+ final ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
2521
+ final BuiltinLatestCapgoUpdater updater = new BuiltinLatestCapgoUpdater();
2522
+
2523
+ plugin.implementation = updater;
2524
+ plugin.setAutoUpdateModeForTesting("onlyDownload");
2525
+ plugin.setLoggerForTesting(mock(Logger.class));
2526
+
2527
+ assertFalse(plugin.shouldUseDirectUpdateForTesting());
2528
+
2529
+ invokeBackgroundDownload(plugin);
2530
+
2531
+ assertFalse(updater.setNextBundleCalled);
2532
+ assertNull(updater.lastSetNextBundleId);
2533
+ assertTrue(plugin.hasNotifiedEvent("updateAvailable"));
2534
+
2535
+ final JSObject updatePayload = plugin.getNotifiedEventPayload("updateAvailable");
2536
+ final JSONObject updateBundle = updatePayload.getJSONObject("bundle");
2537
+ assertEquals(BundleInfo.ID_BUILTIN, updateBundle.getString("id"));
2538
+ assertFalse(plugin.hasNotifiedEvent("noNeedUpdate"));
2539
+ }
2540
+ }
2541
+
2542
+ @Test
2543
+ public void testOnlyDownloadModeBuiltinDoesNotNotifyWhenBuiltinIsCurrent() throws Exception {
2544
+ try (
2545
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2546
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2547
+ ) {
2548
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2549
+
2550
+ final ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
2551
+ final BundleInfo current = new BundleInfo(
2552
+ BundleInfo.ID_BUILTIN,
2553
+ "builtin",
2554
+ BundleStatus.SUCCESS,
2555
+ BundleInfo.DOWNLOADED_BUILTIN,
2556
+ "builtin"
2557
+ );
2558
+ final BuiltinLatestCapgoUpdater updater = new BuiltinLatestCapgoUpdater(current);
2559
+
2560
+ plugin.implementation = updater;
2561
+ plugin.setAutoUpdateModeForTesting("onlyDownload");
2562
+ plugin.setLoggerForTesting(mock(Logger.class));
2563
+
2564
+ invokeBackgroundDownload(plugin);
2565
+
2566
+ assertFalse(updater.setNextBundleCalled);
2567
+ assertNull(updater.lastSetNextBundleId);
2568
+ assertFalse(plugin.hasNotifiedEvent("updateAvailable"));
2569
+ assertTrue(plugin.hasNotifiedEvent("noNeedUpdate"));
2570
+ }
2571
+ }
2572
+
2573
+ @Test
2574
+ public void testNoNewVersionAvailableDoesNotNotifyDownloadFailed() throws Exception {
2575
+ try (
2576
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2577
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2578
+ ) {
2579
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2580
+
2581
+ ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
2582
+ NoNewVersionCapgoUpdater updater = new NoNewVersionCapgoUpdater();
2583
+
2584
+ plugin.implementation = updater;
2585
+ plugin.setLoggerForTesting(mock(Logger.class));
2586
+
2587
+ invokeBackgroundDownload(plugin);
2588
+
2589
+ assertTrue(plugin.hasNotifiedEvent("noNeedUpdate"));
2590
+ assertTrue(plugin.hasNotifiedEvent("updateCheckResult"));
2591
+ JSObject payload = plugin.getNotifiedEventPayload("updateCheckResult");
2592
+ assertNotNull(payload);
2593
+ assertEquals("up_to_date", payload.getString("kind"));
2594
+ assertEquals(200, payload.getInt("statusCode"));
2595
+ assertEquals("1.0.0", payload.getString("version"));
2596
+ assertFalse(plugin.hasNotifiedEvent("downloadFailed"));
2597
+ assertFalse(updater.sendStatsCalled);
2598
+ }
2599
+ }
2600
+
2601
+ @Test
2602
+ public void testGetLatestRejectsLegacyErrorWithoutBackendKind() {
2603
+ try (
2604
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2605
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2606
+ ) {
2607
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2608
+
2609
+ ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
2610
+ PluginCall call = mock(PluginCall.class);
2611
+
2612
+ plugin.implementation = new NoNewVersionCapgoUpdater(false);
2613
+ plugin.setLoggerForTesting(mock(Logger.class));
2614
+
2615
+ plugin.getLatest(call);
2616
+
2617
+ verify(call, never()).resolve(any(JSObject.class));
2618
+ verify(call).reject("no_new_version_available");
2619
+ }
2620
+ }
2621
+
2622
+ @Test
2623
+ public void testBlockedUpdateCheckDoesNotNotifyDownloadFailed() throws Exception {
2624
+ try (
2625
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2626
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2627
+ ) {
2628
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2629
+
2630
+ ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
2631
+ BlockedUpdateCapgoUpdater updater = new BlockedUpdateCapgoUpdater();
2632
+
2633
+ plugin.implementation = updater;
2634
+ plugin.setLoggerForTesting(mock(Logger.class));
2635
+
2636
+ invokeBackgroundDownload(plugin);
2637
+
2638
+ assertTrue(plugin.hasNotifiedEvent("noNeedUpdate"));
2639
+ assertTrue(plugin.hasNotifiedEvent("updateCheckResult"));
2640
+ JSObject payload = plugin.getNotifiedEventPayload("updateCheckResult");
2641
+ assertNotNull(payload);
2642
+ assertEquals("blocked", payload.getString("kind"));
2643
+ assertEquals(200, payload.getInt("statusCode"));
2644
+ assertEquals("1.0.0", payload.getString("version"));
2645
+ assertFalse(plugin.hasNotifiedEvent("downloadFailed"));
2646
+ assertFalse(updater.sendStatsCalled);
2647
+ }
2648
+ }
2649
+
2650
+ @Test
2651
+ public void testBreakingNoUrlUpdateCheckNotifiesBreakingListeners() throws Exception {
2652
+ try (
2653
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2654
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2655
+ ) {
2656
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2657
+
2658
+ ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
2659
+ BreakingNoUrlCapgoUpdater updater = new BreakingNoUrlCapgoUpdater();
2660
+
2661
+ plugin.implementation = updater;
2662
+ plugin.setLoggerForTesting(mock(Logger.class));
2663
+
2664
+ invokeBackgroundDownload(plugin);
2665
+
2666
+ assertTrue(plugin.hasNotifiedEvent("breakingAvailable"));
2667
+ assertTrue(plugin.hasNotifiedEvent("majorAvailable"));
2668
+ assertEquals("1.0.17", plugin.getNotifiedEventPayload("breakingAvailable").getString("version"));
2669
+ assertEquals("1.0.17", plugin.getNotifiedEventPayload("majorAvailable").getString("version"));
2670
+ assertTrue(plugin.hasNotifiedEvent("downloadFailed"));
2671
+ assertTrue(updater.downloadFailStatsCalled);
2672
+ }
2673
+ }
2674
+
2675
+ @Test
2676
+ public void testGetLatestBreakingResponseNotifiesBreakingListeners() {
2677
+ try (
2678
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2679
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2680
+ ) {
2681
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2682
+
2683
+ ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
2684
+ PluginCall call = mock(PluginCall.class);
2685
+
2686
+ plugin.implementation = new BreakingNoUrlCapgoUpdater();
2687
+ plugin.setLoggerForTesting(mock(Logger.class));
2688
+
2689
+ plugin.getLatest(call);
2690
+
2691
+ assertTrue(plugin.hasNotifiedEvent("breakingAvailable"));
2692
+ assertTrue(plugin.hasNotifiedEvent("majorAvailable"));
2693
+ assertEquals("1.0.17", plugin.getNotifiedEventPayload("breakingAvailable").getString("version"));
2694
+ assertEquals("1.0.17", plugin.getNotifiedEventPayload("majorAvailable").getString("version"));
2695
+ verify(call).reject("store_update_required");
2696
+ }
2697
+ }
2698
+
2699
+ @Test
2700
+ public void testFailedUpdateCheckNotifiesDownloadFailed() throws Exception {
2701
+ try (
2702
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2703
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2704
+ ) {
2705
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2706
+
2707
+ ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
2708
+ FailedUpdateCapgoUpdater updater = new FailedUpdateCapgoUpdater();
2709
+
2710
+ plugin.implementation = updater;
2711
+ plugin.setLoggerForTesting(mock(Logger.class));
2712
+
2713
+ invokeBackgroundDownload(plugin);
2714
+
2715
+ assertTrue(plugin.hasNotifiedEvent("updateCheckResult"));
2716
+ JSObject payload = plugin.getNotifiedEventPayload("updateCheckResult");
2717
+ assertNotNull(payload);
2718
+ assertEquals("failed", payload.getString("kind"));
2719
+ assertEquals(500, payload.getInt("statusCode"));
2720
+ assertEquals("1.0.0", payload.getString("version"));
2721
+ assertTrue(plugin.hasNotifiedEvent("downloadFailed"));
2722
+ assertTrue(updater.downloadFailStatsCalled);
2723
+ }
2724
+ }
2725
+
2726
+ @Test
2727
+ public void testInFlightDirectUpdateIsNotClearedByFollowUpForegroundCheck() throws Exception {
2728
+ try (
2729
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2730
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2731
+ ) {
2732
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2733
+
2734
+ ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
2735
+ FreshDownloadCapgoUpdater updater = new FreshDownloadCapgoUpdater();
2736
+ updater.existingLatestBundle = new BundleInfo("download-id", "2.0.0", BundleStatus.DOWNLOADING, new Date(), "next-checksum");
2737
+
2738
+ plugin.implementation = updater;
2739
+ plugin.implementation.directUpdate = true;
2740
+ plugin.configureDirectUpdateModeForTesting("onLaunch", true);
2741
+ plugin.versionDownloadInProgress = true;
2742
+ plugin.setLoggerForTesting(mock(Logger.class));
2743
+ updater.directUpdateStateSupplier = () -> Boolean.TRUE.equals(plugin.implementation.directUpdate);
2744
+
2745
+ invokeBackgroundDownload(plugin);
2746
+
2747
+ assertTrue(updater.downloadBackgroundCalled);
2748
+ assertTrue(updater.directUpdateWhenDownloadStarted);
2749
+ assertTrue(plugin.implementation.directUpdate);
2750
+ assertFalse(plugin.shouldUseDirectUpdateForTesting());
2751
+ }
2752
+ }
2753
+
2754
+ @Test
2755
+ public void testStaleDirectUpdateFlagIsClearedBeforeRetryingStaleDownloadingBundle() throws Exception {
2756
+ try (
2757
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2758
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2759
+ ) {
2760
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2761
+
2762
+ ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
2763
+ FreshDownloadCapgoUpdater updater = new FreshDownloadCapgoUpdater();
2764
+
2765
+ plugin.implementation = updater;
2766
+ plugin.implementation.directUpdate = true;
2767
+ plugin.configureDirectUpdateModeForTesting("onLaunch", true);
2768
+ updater.existingLatestBundle = new BundleInfo("stale-download-id", "2.0.0", BundleStatus.DOWNLOADING, new Date(), "checksum");
2769
+ plugin.setLoggerForTesting(mock(Logger.class));
2770
+ updater.directUpdateStateSupplier = () -> Boolean.TRUE.equals(plugin.implementation.directUpdate);
2771
+
2772
+ assertFalse(plugin.shouldUseDirectUpdateForTesting());
2773
+
2774
+ invokeBackgroundDownload(plugin);
2775
+
2776
+ assertTrue(updater.downloadBackgroundCalled);
2777
+ assertFalse(updater.directUpdateWhenDownloadStarted);
2778
+ assertFalse(plugin.implementation.directUpdate);
2779
+ assertFalse(plugin.shouldUseDirectUpdateForTesting());
2780
+ }
2781
+ }
2782
+
2783
+ @Test
2784
+ public void testHideSplashscreenInvokesSplashPluginWithoutMessageHandler() throws Exception {
2785
+ try (
2786
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2787
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2788
+ ) {
2789
+ final Looper mainLooper = mock(Looper.class);
2790
+ looperMock.when(Looper::getMainLooper).thenReturn(mainLooper);
2791
+ looperMock.when(Looper::myLooper).thenReturn(mainLooper);
2792
+
2793
+ final TestableCordovaUpdaterPlugin plugin = new TestableCordovaUpdaterPlugin();
2794
+ final Bridge bridge = mock(Bridge.class);
2795
+ final PluginHandle splashScreenPlugin = mock(PluginHandle.class);
2796
+
2797
+ when(bridge.getPlugin("SplashScreen")).thenReturn(splashScreenPlugin);
2798
+
2799
+ plugin.setBridge(bridge);
2800
+ plugin.setLoggerForTesting(mock(Logger.class));
2801
+
2802
+ invokePrivateVoidMethod(plugin, "hideSplashscreenInternal");
2803
+
2804
+ final ArgumentCaptor<PluginCall> callCaptor = ArgumentCaptor.forClass(PluginCall.class);
2805
+ verify(splashScreenPlugin).invoke(eq("hide"), callCaptor.capture());
2806
+ callCaptor.getValue().resolve();
2807
+ assertEquals(PluginCall.CALLBACK_ID_DANGLING, callCaptor.getValue().getCallbackId());
2808
+ assertEquals("hide", callCaptor.getValue().getMethodName());
2809
+ }
2810
+ }
2811
+
2812
+ @Test
2813
+ public void testScheduleDirectUpdateFinishUsesBackgroundDispatchPath() {
2814
+ try (
2815
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2816
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2817
+ ) {
2818
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2819
+
2820
+ final DirectUpdateDispatchPlugin plugin = new DirectUpdateDispatchPlugin();
2821
+ final ResetTrackingCapgoUpdater updater = new ResetTrackingCapgoUpdater();
2822
+ final BundleInfo latest = new BundleInfo("download-id", "2.0.0", BundleStatus.SUCCESS, new Date(), "checksum");
2823
+
2824
+ plugin.implementation = updater;
2825
+ plugin.setLoggerForTesting(mock(Logger.class));
2826
+
2827
+ plugin.scheduleDirectUpdateFinish(latest);
2828
+
2829
+ assertTrue(plugin.startNewThreadCalled);
2830
+ assertTrue(plugin.reloadCalled);
2831
+ assertEquals(0, updater.setCalls);
2832
+ assertEquals(1, updater.stagePendingReloadCalls);
2833
+ assertEquals(1, updater.finalizePendingReloadCalls);
2834
+ assertSame(latest, updater.finalizedPendingReloadBundle);
2835
+ assertSame(plugin.activity, updater.activity);
2836
+ }
2837
+ }
2838
+
2839
+ @Test
2840
+ public void testReloadUsesBackgroundDispatchPath() {
2841
+ try (
2842
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2843
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2844
+ ) {
2845
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
2846
+
2847
+ final ReloadDispatchPlugin plugin = new ReloadDispatchPlugin();
2848
+ final ReloadCapgoUpdater updater = new ReloadCapgoUpdater();
2849
+ final PluginCall call = mock(PluginCall.class);
2850
+
2851
+ plugin.implementation = updater;
2852
+ plugin.setLoggerForTesting(mock(Logger.class));
2853
+
2854
+ plugin.reload(call);
2855
+
2856
+ assertTrue(plugin.startNewThreadCalled);
2857
+ assertTrue(plugin.reloadCalled);
2858
+ verify(call).resolve();
2859
+ }
2860
+ }
2861
+
2862
+ @Test
2863
+ public void testShowSplashscreenDisablesPluginAutoHide() throws Exception {
2864
+ try (
2865
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2866
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2867
+ ) {
2868
+ final Looper mainLooper = mock(Looper.class);
2869
+ looperMock.when(Looper::getMainLooper).thenReturn(mainLooper);
2870
+ looperMock.when(Looper::myLooper).thenReturn(mainLooper);
2871
+
2872
+ final TestableCordovaUpdaterPlugin plugin = new TestableCordovaUpdaterPlugin();
2873
+ final Bridge bridge = mock(Bridge.class);
2874
+ final PluginHandle splashScreenPlugin = mock(PluginHandle.class);
2875
+
2876
+ when(bridge.getPlugin("SplashScreen")).thenReturn(splashScreenPlugin);
2877
+
2878
+ plugin.setBridge(bridge);
2879
+ plugin.setLoggerForTesting(mock(Logger.class));
2880
+
2881
+ invokePrivateVoidMethod(plugin, "showSplashscreenNow");
2882
+
2883
+ final ArgumentCaptor<PluginCall> callCaptor = ArgumentCaptor.forClass(PluginCall.class);
2884
+ verify(splashScreenPlugin).invoke(eq("show"), callCaptor.capture());
2885
+ callCaptor.getValue().resolve();
2886
+ assertEquals(Boolean.FALSE, callCaptor.getValue().getBoolean("autoHide"));
2887
+ assertEquals("show", callCaptor.getValue().getMethodName());
2888
+ }
2889
+ }
2890
+
2891
+ @Test
2892
+ public void testStaleSplashscreenRetryTokenSkipsInvocation() throws Exception {
2893
+ try (
2894
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
2895
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
2896
+ ) {
2897
+ final Looper mainLooper = mock(Looper.class);
2898
+ looperMock.when(Looper::getMainLooper).thenReturn(mainLooper);
2899
+ looperMock.when(Looper::myLooper).thenReturn(mainLooper);
2900
+
2901
+ final TestableCordovaUpdaterPlugin plugin = new TestableCordovaUpdaterPlugin();
2902
+ final Bridge bridge = mock(Bridge.class);
2903
+ final PluginHandle splashScreenPlugin = mock(PluginHandle.class);
2904
+
2905
+ when(bridge.getPlugin("SplashScreen")).thenReturn(splashScreenPlugin);
2906
+
2907
+ plugin.setBridge(bridge);
2908
+ plugin.setLoggerForTesting(mock(Logger.class));
2909
+
2910
+ invokePrivateVoidMethod(plugin, "showSplashscreenNow");
2911
+ assertFalse(plugin.isCurrentSplashscreenInvocationTokenForTesting(0));
2912
+
2913
+ invokePrivateSplashMethod(plugin, "hide", new JSObject(), 1, 0);
2914
+
2915
+ verify(splashScreenPlugin, never()).invoke(eq("hide"), any(PluginCall.class));
2916
+ }
2917
+ }
2918
+
2919
+ @Test
2920
+ public void testZipEntryRejectsSiblingPrefixPathTraversal() throws Exception {
2921
+ final Path documentsDir = Files.createTempDirectory("capgo-zip-path");
2922
+ documentsDir.toFile().deleteOnExit();
2923
+ final Path zipPath = createZipWithEntry("../bundle-evil/pwned.txt");
2924
+ final Path escapedPath = documentsDir.resolve("bundle-evil").resolve("pwned.txt");
2925
+
2926
+ final CapgoUpdater updater = new StatsIgnoringCapgoUpdater();
2927
+ updater.documentsDir = documentsDir.toFile();
2928
+
2929
+ assertThrows(IOException.class, () -> invokeUnzip(updater, "bundle-id", zipPath, "bundle"));
2930
+ assertFalse(Files.exists(escapedPath));
2931
+ }
2932
+
2933
+ @Test
2934
+ public void testManifestTargetRejectsPathTraversalAfterBrotliSuffixIsRemoved() throws Exception {
2935
+ final Path documentsDir = Files.createTempDirectory("capgo-manifest-path");
2936
+ documentsDir.toFile().deleteOnExit();
2937
+ final Path destFolder = documentsDir.resolve("bundle");
2938
+ Files.createDirectories(destFolder);
2939
+
2940
+ assertThrows(IOException.class, () -> DownloadService.resolveManifestTargetFile(destFolder.toFile(), "../bundle-evil/app.js.br"));
2941
+ assertFalse(Files.exists(documentsDir.resolve("bundle-evil")));
2942
+ }
2943
+
2944
+ @Test
2945
+ public void testManifestTargetAllowsNestedBrotliFileInsideBundle() throws Exception {
2946
+ final Path documentsDir = Files.createTempDirectory("capgo-manifest-valid");
2947
+ documentsDir.toFile().deleteOnExit();
2948
+ final Path destFolder = documentsDir.resolve("bundle");
2949
+ Files.createDirectories(destFolder);
2950
+
2951
+ final File resolved = DownloadService.resolveManifestTargetFile(destFolder.toFile(), "assets/app.js.br");
2952
+
2953
+ assertEquals(destFolder.resolve("assets").resolve("app.js").toFile().getCanonicalFile(), resolved);
2954
+ }
2955
+
2956
+ @Test
2957
+ public void buildUserAgentStripsNonIsoCharacters() {
2958
+ String ua = DownloadService.buildUserAgent("com.example.ั‚ะตัั‚", "1.2.3๐Ÿ”ฅ", "Android 14 ๐Ÿ˜Š");
2959
+ assertEquals("CapacitorUpdater/1.2.3 (com.example.) android/Android 14", ua);
2960
+ }
2961
+
2962
+ @Test
2963
+ public void buildUserAgentFallsBackToUnknown() {
2964
+ String ua = DownloadService.buildUserAgent("", "", "");
2965
+ assertEquals("CapacitorUpdater/unknown (unknown) android/unknown", ua);
2966
+ }
2967
+
2968
+ @Test
2969
+ public void persistDefaultChannelFromResponseStoresServerChannel() {
2970
+ final CapgoUpdater updater = new CapgoUpdater(mock(Logger.class));
2971
+ final SharedPreferences.Editor editor = mock(SharedPreferences.Editor.class);
2972
+ when(editor.putString("CapacitorUpdater.defaultChannel", "company-a")).thenReturn(editor);
2973
+
2974
+ updater.persistDefaultChannelFromResponse(" company-a ", editor, "CapacitorUpdater.defaultChannel");
2975
+
2976
+ assertEquals("company-a", updater.defaultChannel);
2977
+ verify(editor).putString("CapacitorUpdater.defaultChannel", "company-a");
2978
+ verify(editor).apply();
2979
+ }
2980
+
2981
+ @Test
2982
+ public void persistDefaultChannelFromResponseIgnoresBuiltinVersionName() {
2983
+ final CapgoUpdater updater = new CapgoUpdater(mock(Logger.class));
2984
+ final SharedPreferences.Editor editor = mock(SharedPreferences.Editor.class);
2985
+ updater.defaultChannel = "stable";
2986
+
2987
+ updater.persistDefaultChannelFromResponse("builtin", editor, "CapacitorUpdater.defaultChannel");
2988
+
2989
+ assertEquals("stable", updater.defaultChannel);
2990
+ verify(editor, never()).putString(anyString(), anyString());
2991
+ verify(editor, never()).apply();
2992
+ }
2993
+
2994
+ /**
2995
+ * Regression test for: NoSuchMethodError crash on Android 8.0/8.1 (API 26/27).
2996
+ * getLongVersionCode() was introduced in API 28; the plugin must use
2997
+ * PackageInfoCompat.getLongVersionCode() to support API 24-27.
2998
+ *
2999
+ * <p>The @SuppressWarnings("deprecation") is intentional: we set PackageInfo.versionCode
3000
+ * (deprecated since API 28) directly to simulate a pre-API-28 device and verify
3001
+ * that PackageInfoCompat falls back to it correctly on older Android versions.
3002
+ */
3003
+ @Test
3004
+ @SuppressWarnings("deprecation")
3005
+ public void getVersionCodeReturnsStringVersionCodeViaPackageInfoCompat() throws Exception {
3006
+ try (
3007
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
3008
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
3009
+ ) {
3010
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
3011
+ looperMock.when(Looper::myLooper).thenReturn(mock(Looper.class));
3012
+
3013
+ final TestableCordovaUpdaterPlugin plugin = new TestableCordovaUpdaterPlugin();
3014
+ final PackageInfo packageInfo = new PackageInfo();
3015
+ packageInfo.versionCode = 42;
3016
+
3017
+ final Method getVersionCode = CordovaUpdaterPlugin.class.getDeclaredMethod("getVersionCode", PackageInfo.class);
3018
+ getVersionCode.setAccessible(true);
3019
+ final String result = (String) getVersionCode.invoke(plugin, packageInfo);
3020
+
3021
+ assertEquals("42", result);
3022
+ }
3023
+ }
3024
+
3025
+ @Test
3026
+ public void normalizeShakeMenuGestureSupportsThreeFingerPinch() {
3027
+ assertEquals(CordovaUpdaterPlugin.SHAKE_MENU_GESTURE_SHAKE, CordovaUpdaterPlugin.normalizedShakeMenuGesture(null));
3028
+ assertEquals(CordovaUpdaterPlugin.SHAKE_MENU_GESTURE_SHAKE, CordovaUpdaterPlugin.normalizedShakeMenuGesture("shake"));
3029
+ assertEquals(CordovaUpdaterPlugin.SHAKE_MENU_GESTURE_SHAKE, CordovaUpdaterPlugin.normalizedShakeMenuGesture("unknown"));
3030
+ assertEquals(
3031
+ CordovaUpdaterPlugin.SHAKE_MENU_GESTURE_THREE_FINGER_PINCH,
3032
+ CordovaUpdaterPlugin.normalizedShakeMenuGesture("threeFingerPinch")
3033
+ );
3034
+ assertTrue(CordovaUpdaterPlugin.isSupportedShakeMenuGesture("shake"));
3035
+ assertTrue(CordovaUpdaterPlugin.isSupportedShakeMenuGesture("threeFingerPinch"));
3036
+ assertFalse(CordovaUpdaterPlugin.isSupportedShakeMenuGesture(" "));
3037
+ assertFalse(CordovaUpdaterPlugin.isSupportedShakeMenuGesture("pinch"));
3038
+ }
3039
+ }