@capgo/cordova-updater 8.1.4 → 8.2.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 (36) hide show
  1. package/dist/esm/definitions.d.ts +116 -86
  2. package/dist/esm/definitions.js +2 -2
  3. package/dist/esm/definitions.js.map +1 -1
  4. package/dist/esm/exec.js +11 -1
  5. package/dist/esm/exec.js.map +1 -1
  6. package/dist/esm/history.js +7 -7
  7. package/dist/esm/history.js.map +1 -1
  8. package/dist/plugin.cjs.js +13 -3
  9. package/dist/plugin.cjs.js.map +1 -1
  10. package/dist/plugin.js +13 -3
  11. package/dist/plugin.js.map +1 -1
  12. package/package.json +5 -3
  13. package/plugin.xml +9 -9
  14. package/src/android/CordovaUpdaterPlugin.java +173 -5
  15. package/src/android/app/capgo/cordova/updater/CapgoUpdater.java +313 -55
  16. package/src/android/app/capgo/cordova/updater/CryptoCipher.java +51 -18
  17. package/src/android/app/capgo/cordova/updater/DelayUpdateUtils.java +0 -1
  18. package/src/android/app/capgo/cordova/updater/DownloadService.java +402 -89
  19. package/src/android/app/capgo/cordova/updater/Version.java +312 -0
  20. package/src/android/capgo-cordova-updater.gradle +3 -5
  21. package/src/android/test/app/capgo/cordova/updater/BackgroundDownloadSettlementTest.java +29 -0
  22. package/src/android/test/app/capgo/cordova/updater/CapacitorUpdaterUnitTest.java.skip +2 -12
  23. package/src/android/test/app/capgo/cordova/updater/DelayUpdateUtilsTest.java +0 -1
  24. package/src/android/test/app/capgo/cordova/updater/RsaContractTest.java +137 -0
  25. package/src/android/test/app/capgo/cordova/updater/SecurityHardeningTest.java +197 -0
  26. package/src/android/test/app/capgo/cordova/updater/SessionKeyRequiredTest.java +344 -0
  27. package/src/ios/CapgoSemanticVersion.swift +181 -0
  28. package/src/ios/CapgoUpdater.swift +427 -114
  29. package/src/ios/CordovaUpdaterPlugin.swift +171 -40
  30. package/src/ios/CryptoCipher.swift +41 -38
  31. package/src/ios/DelayUpdateUtils.swift +19 -45
  32. package/src/ios/Logger.swift +81 -5
  33. package/src/ios/RedirectPolicyDelegate.swift +53 -0
  34. package/src/ios/WebViewStatsReporter.swift +5 -0
  35. package/src/ios/ZipArchiveReader.swift +263 -0
  36. package/src/ios/ZipCentralDirectory.swift +283 -0
@@ -0,0 +1,197 @@
1
+ package app.capgo.cordova.updater;
2
+
3
+ import static org.junit.Assert.assertArrayEquals;
4
+ import static org.junit.Assert.assertEquals;
5
+ import static org.junit.Assert.assertFalse;
6
+ import static org.junit.Assert.assertThrows;
7
+ import static org.junit.Assert.assertTrue;
8
+ import static org.junit.Assert.fail;
9
+ import static org.mockito.Mockito.mock;
10
+ import java.io.File;
11
+ import java.io.IOException;
12
+ import java.nio.charset.StandardCharsets;
13
+ import java.nio.file.Files;
14
+ import java.nio.file.Path;
15
+ import org.junit.Test;
16
+ import org.junit.runner.RunWith;
17
+ import org.robolectric.RobolectricTestRunner;
18
+ import org.robolectric.annotation.Config;
19
+
20
+ @RunWith(RobolectricTestRunner.class)
21
+ @Config(manifest = Config.NONE)
22
+ public class SecurityHardeningTest {
23
+
24
+ @Test
25
+ public void testResolvePathInsideDirectoryRejectsAbsolutePaths() throws Exception {
26
+ final Path base = Files.createTempDirectory("capgo-abs-path");
27
+ base.toFile().deleteOnExit();
28
+
29
+ assertThrows(IOException.class, () -> CapgoUpdater.resolvePathInsideDirectory(base.toFile(), "/etc/passwd"));
30
+ }
31
+
32
+ @Test
33
+ public void testResolvePathInsideDirectoryRejectsBackslashes() throws Exception {
34
+ final Path base = Files.createTempDirectory("capgo-backslash-path");
35
+ base.toFile().deleteOnExit();
36
+
37
+ assertThrows(IOException.class, () -> CapgoUpdater.resolvePathInsideDirectory(base.toFile(), "assets\\app.js"));
38
+ }
39
+
40
+ @Test
41
+ public void testResolvePathInsideDirectoryRejectsNullBytes() throws Exception {
42
+ final Path base = Files.createTempDirectory("capgo-null-path");
43
+ base.toFile().deleteOnExit();
44
+
45
+ assertThrows(IOException.class, () -> CapgoUpdater.resolvePathInsideDirectory(base.toFile(), "assets\0app.js"));
46
+ }
47
+
48
+ @Test
49
+ public void testResolvePathInsideDirectoryRejectsDotDotSegments() throws Exception {
50
+ final Path base = Files.createTempDirectory("capgo-dotdot-path");
51
+ base.toFile().deleteOnExit();
52
+
53
+ assertThrows(IOException.class, () -> CapgoUpdater.resolvePathInsideDirectory(base.toFile(), "assets/../../secret.js"));
54
+ assertThrows(IOException.class, () -> CapgoUpdater.resolvePathInsideDirectory(base.toFile(), "../secret.js"));
55
+ }
56
+
57
+ @Test
58
+ public void testResolvePathInsideDirectoryRejectsDotAsBaseDirectory() throws Exception {
59
+ final Path base = Files.createTempDirectory("capgo-dot-path");
60
+ base.toFile().deleteOnExit();
61
+
62
+ assertThrows(IOException.class, () -> CapgoUpdater.resolvePathInsideDirectory(base.toFile(), "."));
63
+ }
64
+
65
+ @Test
66
+ public void testResolvePathInsideDirectoryAllowsNestedRelativePath() throws Exception {
67
+ final Path base = Files.createTempDirectory("capgo-nested-path");
68
+ base.toFile().deleteOnExit();
69
+
70
+ final File resolved = CapgoUpdater.resolvePathInsideDirectory(base.toFile(), "assets/app.js");
71
+
72
+ assertEquals(base.resolve("assets").resolve("app.js").toFile().getCanonicalFile(), resolved);
73
+ }
74
+
75
+ @Test
76
+ public void resolveBundleDirectoryRejectsAbsolutePath() throws Exception {
77
+ final Path tempDir = Files.createTempDirectory("capgo-delete-abs");
78
+ tempDir.toFile().deleteOnExit();
79
+ assertThrows(IOException.class, () -> CapgoUpdater.resolveBundleDirectory(tempDir.toFile(), "/tmp/evil"));
80
+ }
81
+
82
+ @Test
83
+ public void resolveBundleDirectoryRejectsPathTraversal() throws Exception {
84
+ final Path tempDir = Files.createTempDirectory("capgo-delete-traversal");
85
+ tempDir.toFile().deleteOnExit();
86
+ assertThrows(IOException.class, () -> CapgoUpdater.resolveBundleDirectory(tempDir.toFile(), "../outside-target"));
87
+ }
88
+
89
+ @Test
90
+ public void resolveBundleDirectoryRejectsDotAsBundleRoot() throws Exception {
91
+ final Path tempDir = Files.createTempDirectory("capgo-delete-dot");
92
+ tempDir.toFile().deleteOnExit();
93
+ assertThrows(IOException.class, () -> CapgoUpdater.resolveBundleDirectory(tempDir.toFile(), "."));
94
+ }
95
+
96
+ @Test
97
+ public void deleteRejectsDotBundleId() throws Exception {
98
+ final Path tempDir = Files.createTempDirectory("capgo-delete-dot-id");
99
+ tempDir.toFile().deleteOnExit();
100
+ final CapgoUpdater updater = new CapgoUpdater(mock(Logger.class));
101
+ updater.documentsDir = tempDir.toFile();
102
+
103
+ assertFalse(Boolean.TRUE.equals(updater.delete(".", true)));
104
+ }
105
+
106
+ @Test
107
+ public void resolveBundleDirectoryRejectsWindowsSeparators() throws Exception {
108
+ final Path tempDir = Files.createTempDirectory("capgo-delete-windows");
109
+ tempDir.toFile().deleteOnExit();
110
+ assertThrows(IOException.class, () -> CapgoUpdater.resolveBundleDirectory(tempDir.toFile(), "..\\outside-target"));
111
+ }
112
+
113
+ @Test
114
+ public void deleteRejectsPathTraversalOutsideBundleRoot() throws Exception {
115
+ final Path tempDir = Files.createTempDirectory("capgo-delete-escape");
116
+ tempDir.toFile().deleteOnExit();
117
+ final Path outsideTarget = tempDir.resolve("outside-target");
118
+ Files.createDirectories(outsideTarget);
119
+ Files.write(outsideTarget.resolve("marker.txt"), "keep".getBytes(StandardCharsets.UTF_8));
120
+
121
+ final CapgoUpdater updater = new CapgoUpdater(mock(Logger.class));
122
+ updater.documentsDir = tempDir.toFile();
123
+
124
+ assertFalse(Boolean.TRUE.equals(updater.delete("../outside-target", true)));
125
+ assertTrue("Path outside bundle root must remain untouched", Files.exists(outsideTarget.resolve("marker.txt")));
126
+ }
127
+
128
+ @Test
129
+ public void deleteRejectsAbsolutePathId() throws Exception {
130
+ final Path tempDir = Files.createTempDirectory("capgo-delete-abs-id");
131
+ tempDir.toFile().deleteOnExit();
132
+ final CapgoUpdater updater = new CapgoUpdater(mock(Logger.class));
133
+ updater.documentsDir = tempDir.toFile();
134
+
135
+ assertFalse(Boolean.TRUE.equals(updater.delete("/tmp/evil", true)));
136
+ }
137
+
138
+ @Test
139
+ public void decryptAesFileRejectsNullKeyWithoutNpe() throws Exception {
140
+ final Path dir = Files.createTempDirectory("capgo-aes-null-key");
141
+ dir.toFile().deleteOnExit();
142
+ File file = dir.resolve("cipher.bin").toFile();
143
+ byte[] iv = new byte[16];
144
+ Files.write(file.toPath(), new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 });
145
+ try {
146
+ CryptoCipher.decryptAesFile(file, null, iv);
147
+ fail("expected null session key to be rejected");
148
+ } catch (NullPointerException e) {
149
+ fail("null session key must not cause NPE");
150
+ } catch (IOException e) {
151
+ assertTrue(e.getMessage().contains("missing session key"));
152
+ }
153
+ }
154
+
155
+ @Test
156
+ public void decryptAesFileRejectsBadKeyWithoutNpe() throws Exception {
157
+ final Path dir = Files.createTempDirectory("capgo-aes-bad-key");
158
+ dir.toFile().deleteOnExit();
159
+ File file = dir.resolve("cipher.bin").toFile();
160
+ byte[] iv = new byte[16];
161
+ byte[] keyBytes = new byte[16];
162
+ for (int i = 0; i < 16; i++) {
163
+ iv[i] = (byte) i;
164
+ keyBytes[i] = (byte) (31 - i);
165
+ }
166
+ javax.crypto.SecretKey encryptKey = new javax.crypto.spec.SecretKeySpec(keyBytes, "AES");
167
+ javax.crypto.SecretKey decryptKey = new javax.crypto.spec.SecretKeySpec(new byte[16], "AES");
168
+ byte[] plain = "capgo-aes-failure".getBytes(StandardCharsets.UTF_8);
169
+ javax.crypto.Cipher enc = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding");
170
+ enc.init(javax.crypto.Cipher.ENCRYPT_MODE, encryptKey, new javax.crypto.spec.IvParameterSpec(iv));
171
+ byte[] cipherBytes = enc.doFinal(plain);
172
+ Files.write(file.toPath(), cipherBytes);
173
+ byte[] originalCipher = Files.readAllBytes(file.toPath());
174
+ try {
175
+ CryptoCipher.decryptAesFile(file, decryptKey, iv);
176
+ fail("expected AES decrypt failure");
177
+ } catch (NullPointerException e) {
178
+ fail("AES decrypt failure must not cause NPE");
179
+ } catch (IOException e) {
180
+ assertTrue(e.getMessage().contains("AES file decryption failed"));
181
+ assertArrayEquals(originalCipher, Files.readAllBytes(file.toPath()));
182
+ }
183
+ }
184
+
185
+ @Test
186
+ public void decryptAESRejectsFailureWithoutNull() throws Exception {
187
+ byte[] iv = new byte[16];
188
+ byte[] keyBytes = new byte[16];
189
+ javax.crypto.SecretKey key = new javax.crypto.spec.SecretKeySpec(keyBytes, "AES");
190
+ try {
191
+ CryptoCipher.decryptAES(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, key, iv);
192
+ fail("expected AES decrypt failure");
193
+ } catch (javax.crypto.BadPaddingException expected) {
194
+ // expected decrypt failure
195
+ }
196
+ }
197
+ }
@@ -0,0 +1,344 @@
1
+ package app.capgo.cordova.updater;
2
+
3
+ import static org.junit.Assert.assertEquals;
4
+ import static org.junit.Assert.assertFalse;
5
+ import static org.junit.Assert.assertTrue;
6
+ import static org.junit.Assert.fail;
7
+ import static org.mockito.ArgumentMatchers.anyString;
8
+ import static org.mockito.Mockito.mock;
9
+ import static org.mockito.Mockito.mockConstruction;
10
+ import static org.mockito.Mockito.mockStatic;
11
+ import static org.mockito.Mockito.when;
12
+
13
+ import android.content.SharedPreferences;
14
+ import android.os.Handler;
15
+ import android.os.Looper;
16
+ import java.io.IOException;
17
+ import java.lang.reflect.InvocationTargetException;
18
+ import java.lang.reflect.Method;
19
+ import java.nio.charset.StandardCharsets;
20
+ import java.nio.file.Files;
21
+ import java.nio.file.Path;
22
+ import java.util.ArrayList;
23
+ import java.util.Date;
24
+ import java.util.HashMap;
25
+ import java.util.List;
26
+ import java.util.Map;
27
+ import org.json.JSONArray;
28
+ import org.junit.BeforeClass;
29
+ import org.junit.Test;
30
+ import org.junit.runner.RunWith;
31
+ import org.mockito.MockedConstruction;
32
+ import org.mockito.MockedStatic;
33
+ import org.robolectric.RobolectricTestRunner;
34
+ import org.robolectric.annotation.Config;
35
+
36
+ @RunWith(RobolectricTestRunner.class)
37
+ @Config(manifest = Config.NONE)
38
+ public class SessionKeyRequiredTest {
39
+
40
+ private static String fixturePublicKey;
41
+
42
+ @BeforeClass
43
+ public static void setUpClass() throws Exception {
44
+ CryptoCipher.setLogger(new Logger("SessionKeyRequiredTest", new Logger.Options(Logger.LogLevel.silent)));
45
+ Path contractFile = Path.of(System.getProperty("user.dir")).toAbsolutePath();
46
+ while (contractFile != null) {
47
+ Path candidate = contractFile.resolve("native-contract-tests/crypto-rsa.json");
48
+ if (Files.exists(candidate)) {
49
+ fixturePublicKey = new org.json.JSONObject(new String(Files.readAllBytes(candidate), StandardCharsets.UTF_8)).getString(
50
+ "publicKeyPem"
51
+ );
52
+ return;
53
+ }
54
+ contractFile = contractFile.getParent();
55
+ }
56
+ throw new IOException("native-contract-tests/crypto-rsa.json not found");
57
+ }
58
+
59
+ private static class StatsTrackingCapgoUpdater extends CapgoUpdater {
60
+
61
+ private final List<String> sentStatsActions = new ArrayList<>();
62
+
63
+ StatsTrackingCapgoUpdater() {
64
+ super(mock(Logger.class));
65
+ }
66
+
67
+ @Override
68
+ public void sendStats(final String action) {
69
+ this.sentStatsActions.add(action);
70
+ }
71
+
72
+ List<String> getSentStatsActions() {
73
+ return this.sentStatsActions;
74
+ }
75
+ }
76
+
77
+ private static final class AutoUpdateSessionKeyCapgoUpdater extends StatsTrackingCapgoUpdater {
78
+
79
+ @Override
80
+ public void getLatest(final String updateUrl, final String channel, final Callback callback) {
81
+ final Map<String, Object> response = new HashMap<>();
82
+ response.put("version", "2.0.0");
83
+ response.put("url", "https://example.com/update.zip");
84
+ callback.callback(response);
85
+ }
86
+
87
+ @Override
88
+ public BundleInfo getCurrentBundle() {
89
+ return new BundleInfo("current-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc");
90
+ }
91
+ }
92
+
93
+ private static final class PendingBundleAutoUpdateCapgoUpdater extends StatsTrackingCapgoUpdater {
94
+
95
+ @Override
96
+ public void getLatest(final String updateUrl, final String channel, final Callback callback) {
97
+ final Map<String, Object> response = new HashMap<>();
98
+ response.put("version", "2.0.0");
99
+ response.put("url", "https://example.com/update.zip");
100
+ response.put("checksum", "abc123");
101
+ callback.callback(response);
102
+ }
103
+
104
+ @Override
105
+ public BundleInfo getCurrentBundle() {
106
+ return new BundleInfo("current-id", "1.0.0", BundleStatus.SUCCESS, new Date(), "abc");
107
+ }
108
+
109
+ @Override
110
+ public BundleInfo getBundleInfoByName(final String versionName) {
111
+ if ("2.0.0".equals(versionName)) {
112
+ return new BundleInfo("pending-id", "2.0.0", BundleStatus.PENDING, new Date(), "abc123");
113
+ }
114
+ return null;
115
+ }
116
+ }
117
+
118
+ private static final class ImmediateThreadCordovaUpdaterPlugin extends CordovaUpdaterPlugin {
119
+
120
+ @Override
121
+ public Thread startNewThread(final Runnable function) {
122
+ function.run();
123
+ return new Thread();
124
+ }
125
+ }
126
+
127
+ private static void configureFinishDownloadTestState(final StatsTrackingCapgoUpdater updater) {
128
+ updater.prefs = mock(SharedPreferences.class);
129
+ updater.editor = mock(SharedPreferences.Editor.class);
130
+ when(updater.prefs.getString(anyString(), anyString())).thenReturn("");
131
+ }
132
+
133
+ private static void invokeBackgroundDownload(final CordovaUpdaterPlugin plugin) throws Exception {
134
+ final Method backgroundDownload = CordovaUpdaterPlugin.class.getDeclaredMethod("backgroundDownload");
135
+ backgroundDownload.setAccessible(true);
136
+ backgroundDownload.invoke(plugin);
137
+ }
138
+
139
+ private static void invokeDownloadBundle(
140
+ final CordovaUpdaterPlugin plugin,
141
+ final String url,
142
+ final String version,
143
+ final String sessionKey,
144
+ final String checksum,
145
+ final JSONArray manifest
146
+ ) throws Exception {
147
+ final Method downloadBundle = CordovaUpdaterPlugin.class.getDeclaredMethod(
148
+ "downloadBundle",
149
+ String.class,
150
+ String.class,
151
+ String.class,
152
+ String.class,
153
+ JSONArray.class
154
+ );
155
+ downloadBundle.setAccessible(true);
156
+ downloadBundle.invoke(plugin, url, version, sessionKey, checksum, manifest);
157
+ }
158
+
159
+ @Test
160
+ public void finishDownloadRejectsManifestWhenSessionKeyMissing() throws Exception {
161
+ final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
162
+ configureFinishDownloadTestState(updater);
163
+ updater.setPublicKey(fixturePublicKey);
164
+ updater.documentsDir = Files.createTempDirectory("capgo-session-key-manifest").toFile();
165
+
166
+ final boolean success = updater.finishDownload("bundle-id", "dest", "1.0.0", "", "checksum", false, true);
167
+
168
+ assertFalse(success);
169
+ assertTrue(updater.getSentStatsActions().contains("session_key_required"));
170
+ }
171
+
172
+ @Test
173
+ public void finishDownloadRejectsZipWhenSessionKeyMissing() throws Exception {
174
+ final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
175
+ configureFinishDownloadTestState(updater);
176
+ updater.setPublicKey(fixturePublicKey);
177
+ final Path tempDir = Files.createTempDirectory("capgo-session-key-zip");
178
+ updater.documentsDir = tempDir.toFile();
179
+ final String dest = "bundle.zip";
180
+ Files.write(tempDir.resolve(dest), "plaintext".getBytes(StandardCharsets.UTF_8));
181
+
182
+ final boolean success = updater.finishDownload("bundle-id", dest, "1.0.0", "", "checksum", false, false);
183
+
184
+ assertFalse(success);
185
+ assertTrue(updater.getSentStatsActions().contains("session_key_required"));
186
+ }
187
+
188
+ @Test
189
+ public void downloadRejectsWhenSessionKeyMissing() {
190
+ final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
191
+ updater.setPublicKey(fixturePublicKey);
192
+
193
+ try {
194
+ updater.download("https://example.com/update.zip", "1.0.0", "", "checksum");
195
+ fail("Expected IOException when session key is missing");
196
+ } catch (IOException e) {
197
+ assertEquals("Session key required when public key is present", e.getMessage());
198
+ }
199
+
200
+ assertTrue(updater.getSentStatsActions().contains("session_key_required"));
201
+ }
202
+
203
+ @Test
204
+ public void isValidSessionKeyRejectsEmptyComponents() {
205
+ assertFalse(CryptoCipher.isValidSessionKey(null));
206
+ assertFalse(CryptoCipher.isValidSessionKey(""));
207
+ assertFalse(CryptoCipher.isValidSessionKey(":"));
208
+ assertFalse(CryptoCipher.isValidSessionKey("abc:"));
209
+ assertFalse(CryptoCipher.isValidSessionKey(":xyz"));
210
+ assertFalse(CryptoCipher.isValidSessionKey("invalid-format"));
211
+ assertTrue(CryptoCipher.isValidSessionKey("abc:def"));
212
+ }
213
+
214
+ @Test
215
+ public void downloadRejectsWhenSessionKeyHasEmptyComponents() {
216
+ final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
217
+ updater.setPublicKey(fixturePublicKey);
218
+
219
+ try {
220
+ updater.download("https://example.com/update.zip", "1.0.0", "abc:", "checksum");
221
+ fail("Expected IOException when session key has empty components");
222
+ } catch (IOException e) {
223
+ assertEquals("Session key required when public key is present", e.getMessage());
224
+ }
225
+
226
+ assertTrue(updater.getSentStatsActions().contains("session_key_required"));
227
+ }
228
+
229
+ @Test
230
+ public void downloadRejectsWhenSessionKeyFormatInvalid() {
231
+ final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
232
+ updater.setPublicKey(fixturePublicKey);
233
+
234
+ try {
235
+ updater.download("https://example.com/update.zip", "1.0.0", "invalid-format", "checksum");
236
+ fail("Expected IOException when session key format is invalid");
237
+ } catch (IOException e) {
238
+ assertEquals("Session key required when public key is present", e.getMessage());
239
+ }
240
+
241
+ assertTrue(updater.getSentStatsActions().contains("session_key_required"));
242
+ }
243
+
244
+ @Test
245
+ public void downloadManifestRejectsWhenSessionKeyMissing() {
246
+ final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
247
+ updater.setPublicKey(fixturePublicKey);
248
+ final JSONArray manifest = new JSONArray();
249
+
250
+ try {
251
+ updater.downloadManifest("https://example.com/update.zip", "1.0.0", "", "checksum", manifest);
252
+ fail("Expected IOException when session key is missing");
253
+ } catch (IOException e) {
254
+ assertEquals("Session key required when public key is present", e.getMessage());
255
+ }
256
+
257
+ assertTrue(updater.getSentStatsActions().contains("session_key_required"));
258
+ }
259
+
260
+ @Test
261
+ public void downloadBackgroundDoesNotStartWhenSessionKeyMissing() {
262
+ final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
263
+ updater.setPublicKey(fixturePublicKey);
264
+
265
+ updater.downloadBackground(
266
+ "https://example.com/update.zip",
267
+ "9.9.9-session-key-guard",
268
+ "",
269
+ "checksum",
270
+ null,
271
+ true,
272
+ 1L
273
+ );
274
+
275
+ assertTrue(updater.getSentStatsActions().contains("session_key_required"));
276
+ assertEquals(null, updater.getBundleInfoByName("9.9.9-session-key-guard"));
277
+ }
278
+
279
+ @Test
280
+ public void downloadBundleRejectsWhenSessionKeyMissing() throws Exception {
281
+ final ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
282
+ final StatsTrackingCapgoUpdater updater = new StatsTrackingCapgoUpdater();
283
+ updater.setPublicKey(fixturePublicKey);
284
+ plugin.implementation = updater;
285
+ plugin.setLoggerForTesting(mock(Logger.class));
286
+
287
+ try {
288
+ invokeDownloadBundle(plugin, "https://example.com/update.zip", "1.0.0", "", "checksum", null);
289
+ fail("Expected IOException when session key is missing");
290
+ } catch (InvocationTargetException e) {
291
+ assertTrue(e.getCause() instanceof IOException);
292
+ assertEquals("Session key required when public key is present", e.getCause().getMessage());
293
+ }
294
+
295
+ assertTrue(updater.getSentStatsActions().contains("session_key_required"));
296
+ }
297
+
298
+ @Test
299
+ public void autoUpdateRejectsMissingSessionKeyWhenPendingBundleExists() throws Exception {
300
+ try (
301
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
302
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
303
+ ) {
304
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
305
+
306
+ final ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
307
+ final PendingBundleAutoUpdateCapgoUpdater updater = new PendingBundleAutoUpdateCapgoUpdater();
308
+ updater.setPublicKey(fixturePublicKey);
309
+
310
+ plugin.implementation = updater;
311
+ plugin.setAutoUpdateModeForTesting("onlyDownload");
312
+ plugin.setLoggerForTesting(mock(Logger.class));
313
+
314
+ invokeBackgroundDownload(plugin);
315
+
316
+ assertTrue(updater.getSentStatsActions().contains("session_key_required"));
317
+ final BundleInfo pending = updater.getBundleInfoByName("2.0.0");
318
+ assertTrue(pending != null && pending.getStatus() == BundleStatus.PENDING);
319
+ }
320
+ }
321
+
322
+ @Test
323
+ public void autoUpdateBackgroundDownloadRejectsWhenSessionKeyMissing() throws Exception {
324
+ try (
325
+ MockedStatic<Looper> looperMock = mockStatic(Looper.class);
326
+ MockedConstruction<Handler> ignored = mockConstruction(Handler.class)
327
+ ) {
328
+ looperMock.when(Looper::getMainLooper).thenReturn(mock(Looper.class));
329
+
330
+ final ImmediateThreadCordovaUpdaterPlugin plugin = new ImmediateThreadCordovaUpdaterPlugin();
331
+ final AutoUpdateSessionKeyCapgoUpdater updater = new AutoUpdateSessionKeyCapgoUpdater();
332
+ updater.setPublicKey(fixturePublicKey);
333
+
334
+ plugin.implementation = updater;
335
+ plugin.setAutoUpdateModeForTesting("onlyDownload");
336
+ plugin.setLoggerForTesting(mock(Logger.class));
337
+
338
+ invokeBackgroundDownload(plugin);
339
+
340
+ assertTrue(updater.getSentStatsActions().contains("session_key_required"));
341
+ assertEquals(null, updater.getBundleInfoByName("2.0.0"));
342
+ }
343
+ }
344
+ }