@hot-updater/react-native 1.0.0-rc.2 → 1.0.0-rc.22

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 (291) hide show
  1. package/android/proguard-rules.pro +17 -2
  2. package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
  3. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +954 -837
  4. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +15 -11
  5. package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
  6. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +287 -22
  7. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +43 -31
  8. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +37 -54
  9. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
  10. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
  11. package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
  12. package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
  13. package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
  14. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +127 -50
  15. package/android/src/main/java/com/hotupdater/PreviousProcessExit.kt +89 -0
  16. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -0
  17. package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
  18. package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
  19. package/android/src/newarch/HotUpdater.kt +7 -9
  20. package/android/src/newarch/HotUpdaterModule.kt +109 -84
  21. package/android/src/oldarch/HotUpdater.kt +7 -9
  22. package/android/src/oldarch/HotUpdaterModule.kt +98 -74
  23. package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
  24. package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
  25. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
  26. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +950 -1112
  27. package/ios/HotUpdater/Internal/BundleMetadata.swift +8 -53
  28. package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
  29. package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
  30. package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
  31. package/ios/HotUpdater/Internal/HotUpdater.mm +13 -45
  32. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +190 -73
  33. package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
  34. package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
  35. package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
  36. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +119 -71
  37. package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
  38. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
  39. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
  40. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
  41. package/lib/commonjs/appReady.js +111 -0
  42. package/lib/commonjs/appReady.js.map +1 -0
  43. package/lib/commonjs/checkForUpdate.js +125 -70
  44. package/lib/commonjs/checkForUpdate.js.map +1 -1
  45. package/lib/commonjs/clientPlugin.js +13 -0
  46. package/lib/commonjs/clientPlugin.js.map +1 -0
  47. package/lib/commonjs/error.js +4 -13
  48. package/lib/commonjs/error.js.map +1 -1
  49. package/lib/commonjs/fetchJSON.js +5 -2
  50. package/lib/commonjs/fetchJSON.js.map +1 -1
  51. package/lib/commonjs/httpClient.js +55 -92
  52. package/lib/commonjs/httpClient.js.map +1 -1
  53. package/lib/commonjs/index.js +68 -20
  54. package/lib/commonjs/index.js.map +1 -1
  55. package/lib/commonjs/native.js +82 -62
  56. package/lib/commonjs/native.js.map +1 -1
  57. package/lib/commonjs/pluginHost.js +44 -0
  58. package/lib/commonjs/pluginHost.js.map +1 -0
  59. package/lib/commonjs/releaseCatalogCache.js +20 -61
  60. package/lib/commonjs/releaseCatalogCache.js.map +1 -1
  61. package/lib/commonjs/sdkVersion.js +1 -1
  62. package/lib/commonjs/sdkVersion.js.map +1 -1
  63. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  64. package/lib/commonjs/store.js +3 -10
  65. package/lib/commonjs/store.js.map +1 -1
  66. package/lib/commonjs/updateError.js +132 -0
  67. package/lib/commonjs/updateError.js.map +1 -0
  68. package/lib/commonjs/wrap.js +44 -27
  69. package/lib/commonjs/wrap.js.map +1 -1
  70. package/lib/module/appReady.js +103 -0
  71. package/lib/module/appReady.js.map +1 -0
  72. package/lib/module/checkForUpdate.js +118 -65
  73. package/lib/module/checkForUpdate.js.map +1 -1
  74. package/lib/module/clientPlugin.js +4 -0
  75. package/lib/module/clientPlugin.js.map +1 -0
  76. package/lib/module/error.js +4 -13
  77. package/lib/module/error.js.map +1 -1
  78. package/lib/module/fetchJSON.js +3 -1
  79. package/lib/module/fetchJSON.js.map +1 -1
  80. package/lib/module/httpClient.js +53 -90
  81. package/lib/module/httpClient.js.map +1 -1
  82. package/lib/module/index.js +55 -22
  83. package/lib/module/index.js.map +1 -1
  84. package/lib/module/native.js +74 -54
  85. package/lib/module/native.js.map +1 -1
  86. package/lib/module/pluginHost.js +35 -0
  87. package/lib/module/pluginHost.js.map +1 -0
  88. package/lib/module/releaseCatalogCache.js +16 -56
  89. package/lib/module/releaseCatalogCache.js.map +1 -1
  90. package/lib/module/sdkVersion.js +1 -1
  91. package/lib/module/sdkVersion.js.map +1 -1
  92. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  93. package/lib/module/store.js +3 -10
  94. package/lib/module/store.js.map +1 -1
  95. package/lib/module/updateError.js +126 -0
  96. package/lib/module/updateError.js.map +1 -0
  97. package/lib/module/wrap.js +45 -28
  98. package/lib/module/wrap.js.map +1 -1
  99. package/lib/typescript/commonjs/appReady.d.ts +23 -0
  100. package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
  101. package/lib/typescript/commonjs/checkForUpdate.d.ts +16 -4
  102. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/clientPlugin.d.ts +2 -0
  104. package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/error.d.ts +4 -13
  106. package/lib/typescript/commonjs/error.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/fetchJSON.d.ts +3 -0
  108. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/httpClient.d.ts +3 -32
  110. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/index.d.ts +7 -12
  112. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/native.d.ts +33 -23
  114. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/pluginHost.d.ts +4 -0
  116. package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
  117. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +6 -14
  118. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  120. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +42 -47
  122. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/store.d.ts +1 -3
  124. package/lib/typescript/commonjs/store.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/types.d.ts +1 -1
  126. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/updateError.d.ts +26 -0
  128. package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/wrap.types.d.ts +6 -7
  131. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  132. package/lib/typescript/module/appReady.d.ts +23 -0
  133. package/lib/typescript/module/appReady.d.ts.map +1 -0
  134. package/lib/typescript/module/checkForUpdate.d.ts +16 -4
  135. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  136. package/lib/typescript/module/clientPlugin.d.ts +2 -0
  137. package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
  138. package/lib/typescript/module/error.d.ts +4 -13
  139. package/lib/typescript/module/error.d.ts.map +1 -1
  140. package/lib/typescript/module/fetchJSON.d.ts +3 -0
  141. package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
  142. package/lib/typescript/module/httpClient.d.ts +3 -32
  143. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  144. package/lib/typescript/module/index.d.ts +7 -12
  145. package/lib/typescript/module/index.d.ts.map +1 -1
  146. package/lib/typescript/module/native.d.ts +33 -23
  147. package/lib/typescript/module/native.d.ts.map +1 -1
  148. package/lib/typescript/module/pluginHost.d.ts +4 -0
  149. package/lib/typescript/module/pluginHost.d.ts.map +1 -0
  150. package/lib/typescript/module/releaseCatalogCache.d.ts +6 -14
  151. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
  152. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  153. package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
  154. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +42 -47
  155. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  156. package/lib/typescript/module/store.d.ts +1 -3
  157. package/lib/typescript/module/store.d.ts.map +1 -1
  158. package/lib/typescript/module/types.d.ts +1 -1
  159. package/lib/typescript/module/types.d.ts.map +1 -1
  160. package/lib/typescript/module/updateError.d.ts +26 -0
  161. package/lib/typescript/module/updateError.d.ts.map +1 -0
  162. package/lib/typescript/module/wrap.d.ts.map +1 -1
  163. package/lib/typescript/module/wrap.types.d.ts +6 -7
  164. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  165. package/package.json +7 -4
  166. package/src/appReady.ts +149 -0
  167. package/src/checkForUpdate.ts +152 -89
  168. package/src/clientPlugin.ts +17 -0
  169. package/src/error.ts +4 -14
  170. package/src/fetchJSON.ts +5 -1
  171. package/src/httpClient.ts +78 -157
  172. package/src/index.ts +87 -29
  173. package/src/native.ts +109 -104
  174. package/src/pluginHost.ts +45 -0
  175. package/src/releaseCatalogCache.ts +26 -140
  176. package/src/sdkVersion.ts +1 -1
  177. package/src/specs/NativeHotUpdater.ts +41 -48
  178. package/src/store.ts +2 -23
  179. package/src/types.ts +1 -1
  180. package/src/updateError.ts +153 -0
  181. package/src/wrap.tsx +59 -28
  182. package/src/wrap.types.ts +6 -7
  183. package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
  184. package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
  185. package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
  186. package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
  187. package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
  188. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +0 -46
  189. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -799
  190. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -273
  191. package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
  192. package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -111
  193. package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
  194. package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
  195. package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
  196. package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
  197. package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
  198. package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
  199. package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
  200. package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
  201. package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
  202. package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
  203. package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
  204. package/lib/commonjs/catalogCacheNative.spec.js +0 -31
  205. package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
  206. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -604
  207. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  208. package/lib/commonjs/httpClient.spec.js +0 -102
  209. package/lib/commonjs/httpClient.spec.js.map +0 -1
  210. package/lib/commonjs/index.spec.js +0 -298
  211. package/lib/commonjs/index.spec.js.map +0 -1
  212. package/lib/commonjs/iosInterop.spec.js +0 -18
  213. package/lib/commonjs/iosInterop.spec.js.map +0 -1
  214. package/lib/commonjs/native.spec.js +0 -795
  215. package/lib/commonjs/native.spec.js.map +0 -1
  216. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -218
  217. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  218. package/lib/commonjs/notifyAppReadyInsights.js +0 -133
  219. package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
  220. package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -246
  221. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
  222. package/lib/commonjs/notifyAppReadyInsights.test-utils.js +0 -24
  223. package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
  224. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -117
  225. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
  226. package/lib/commonjs/releaseCatalogCache.spec.js +0 -195
  227. package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
  228. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
  229. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  230. package/lib/commonjs/store.notification.spec.js +0 -76
  231. package/lib/commonjs/store.notification.spec.js.map +0 -1
  232. package/lib/commonjs/store.spec.js +0 -316
  233. package/lib/commonjs/store.spec.js.map +0 -1
  234. package/lib/commonjs/wrap.spec.js +0 -296
  235. package/lib/commonjs/wrap.spec.js.map +0 -1
  236. package/lib/module/catalogCacheNative.spec.js +0 -31
  237. package/lib/module/catalogCacheNative.spec.js.map +0 -1
  238. package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -603
  239. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  240. package/lib/module/httpClient.spec.js +0 -102
  241. package/lib/module/httpClient.spec.js.map +0 -1
  242. package/lib/module/index.spec.js +0 -297
  243. package/lib/module/index.spec.js.map +0 -1
  244. package/lib/module/iosInterop.spec.js +0 -18
  245. package/lib/module/iosInterop.spec.js.map +0 -1
  246. package/lib/module/native.spec.js +0 -794
  247. package/lib/module/native.spec.js.map +0 -1
  248. package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -217
  249. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  250. package/lib/module/notifyAppReadyInsights.js +0 -128
  251. package/lib/module/notifyAppReadyInsights.js.map +0 -1
  252. package/lib/module/notifyAppReadyInsights.spec.js +0 -245
  253. package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
  254. package/lib/module/notifyAppReadyInsights.test-utils.js +0 -18
  255. package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
  256. package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -116
  257. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
  258. package/lib/module/releaseCatalogCache.spec.js +0 -195
  259. package/lib/module/releaseCatalogCache.spec.js.map +0 -1
  260. package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
  261. package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  262. package/lib/module/store.notification.spec.js +0 -77
  263. package/lib/module/store.notification.spec.js.map +0 -1
  264. package/lib/module/store.spec.js +0 -315
  265. package/lib/module/store.spec.js.map +0 -1
  266. package/lib/module/wrap.spec.js +0 -295
  267. package/lib/module/wrap.spec.js.map +0 -1
  268. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -13
  269. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
  270. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
  271. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  272. package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -13
  273. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
  274. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
  275. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  276. package/src/catalogCacheNative.spec.ts +0 -63
  277. package/src/checkForUpdate.releaseCatalog.spec.ts +0 -596
  278. package/src/httpClient.spec.ts +0 -132
  279. package/src/index.spec.ts +0 -331
  280. package/src/iosInterop.spec.ts +0 -30
  281. package/src/native.spec.ts +0 -855
  282. package/src/notifyAppReadyInsights.boundary.spec.ts +0 -234
  283. package/src/notifyAppReadyInsights.spec.ts +0 -243
  284. package/src/notifyAppReadyInsights.test-utils.ts +0 -31
  285. package/src/notifyAppReadyInsights.transport.spec.ts +0 -130
  286. package/src/notifyAppReadyInsights.ts +0 -194
  287. package/src/releaseCatalogCache.spec.ts +0 -282
  288. package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
  289. package/src/store.notification.spec.ts +0 -92
  290. package/src/store.spec.ts +0 -372
  291. package/src/wrap.spec.ts +0 -367
@@ -1,799 +0,0 @@
1
- package com.hotupdater
2
-
3
- import android.content.ContextWrapper
4
- import kotlinx.coroutines.asCoroutineDispatcher
5
- import kotlinx.coroutines.runBlocking
6
- import org.json.JSONObject
7
- import org.junit.Assert.assertEquals
8
- import org.junit.Assert.assertFalse
9
- import org.junit.Assert.assertNotNull
10
- import org.junit.Assert.assertNull
11
- import org.junit.Assert.assertTrue
12
- import org.junit.Rule
13
- import org.junit.Test
14
- import org.junit.rules.TemporaryFolder
15
- import java.io.File
16
- import java.net.URL
17
- import java.util.concurrent.CopyOnWriteArrayList
18
- import java.util.concurrent.Executors
19
- import java.util.zip.ZipEntry
20
- import java.util.zip.ZipOutputStream
21
-
22
- class BundleFileStorageServiceTest {
23
- @get:Rule
24
- val temporaryFolder = TemporaryFolder()
25
-
26
- @Test
27
- fun `resolveBundleFile uses single manifest bundle at root`() {
28
- val rootDir = temporaryFolder.newFolder("root-manifest-bundle")
29
- val service = createService(rootDir)
30
- val bundleDir = createBundleDir(rootDir, "bundle-root")
31
- val expectedBundleFile = writeFile(bundleDir, "foo.android.bundle")
32
-
33
- writeManifest(bundleDir, listOf("foo.android.bundle"))
34
-
35
- assertResolvedBundlePath(service, bundleDir, expectedBundleFile)
36
- }
37
-
38
- @Test
39
- fun `resolveBundleFile uses single nested manifest bundle`() {
40
- val rootDir = temporaryFolder.newFolder("nested-manifest-bundle")
41
- val service = createService(rootDir)
42
- val bundleDir = createBundleDir(rootDir, "bundle-nested")
43
- val expectedBundleFile = writeFile(bundleDir, "dist/foo.android.bundle")
44
-
45
- writeManifest(bundleDir, listOf("dist/foo.android.bundle"))
46
-
47
- assertResolvedBundlePath(service, bundleDir, expectedBundleFile)
48
- }
49
-
50
- @Test
51
- fun `resolveBundleFile rejects a manifest with missing assets even when root index exists`() {
52
- val rootDir = temporaryFolder.newFolder("no-android-candidate")
53
- val service = createService(rootDir)
54
- val bundleDir = createBundleDir(rootDir, "bundle-no-candidate")
55
- writeFile(bundleDir, "index.android.bundle")
56
-
57
- writeManifest(bundleDir, listOf("index.ios.bundle", "assets/image.png"))
58
-
59
- assertNull(invokeResolveBundleFile(service, bundleDir))
60
- }
61
-
62
- @Test
63
- fun `resolveBundleFile rejects a manifest with multiple android bundle candidates`() {
64
- val rootDir = temporaryFolder.newFolder("multiple-android-candidates")
65
- val service = createService(rootDir)
66
- val bundleDir = createBundleDir(rootDir, "bundle-multiple-candidates")
67
- writeFile(bundleDir, "index.android.bundle")
68
-
69
- writeFile(bundleDir, "foo.android.bundle")
70
- writeFile(bundleDir, "dist/bar.android.bundle")
71
- writeManifest(bundleDir, listOf("foo.android.bundle", "dist/bar.android.bundle"))
72
-
73
- assertNull(invokeResolveBundleFile(service, bundleDir))
74
- }
75
-
76
- @Test
77
- fun `resolveBundleFile returns null when manifest escapes root and no fallback exists`() {
78
- val rootDir = temporaryFolder.newFolder("escaped-manifest-path")
79
- val service = createService(rootDir)
80
- val bundleDir = createBundleDir(rootDir, "bundle-escaped-path")
81
-
82
- writeFile(bundleStoreDir(rootDir), "outside.android.bundle")
83
- writeManifest(bundleDir, listOf("../outside.android.bundle"))
84
-
85
- assertNull(invokeResolveBundleFile(service, bundleDir))
86
- }
87
-
88
- @Test
89
- fun `resolveBundleFile returns null when manifest target is missing and no fallback exists`() {
90
- val rootDir = temporaryFolder.newFolder("missing-manifest-target")
91
- val service = createService(rootDir)
92
- val bundleDir = createBundleDir(rootDir, "bundle-missing-target")
93
-
94
- writeManifest(bundleDir, listOf("dist/missing.android.bundle"))
95
-
96
- assertNull(invokeResolveBundleFile(service, bundleDir))
97
- }
98
-
99
- @Test
100
- fun `resolveBundleFile allows legacy root index without manifest`() {
101
- val rootDir = temporaryFolder.newFolder("legacy-root-index")
102
- val service = createService(rootDir)
103
- val bundleDir = createBundleDir(rootDir, "bundle-legacy")
104
- val fallbackBundleFile = writeFile(bundleDir, "index.android.bundle")
105
-
106
- assertResolvedBundlePath(service, bundleDir, fallbackBundleFile)
107
- }
108
-
109
- @Test
110
- fun `resolveBundleFile returns null when manifest and root index are both missing`() {
111
- val rootDir = temporaryFolder.newFolder("missing-everything")
112
- val service = createService(rootDir)
113
- val bundleDir = createBundleDir(rootDir, "bundle-invalid")
114
-
115
- assertNull(invokeResolveBundleFile(service, bundleDir))
116
- }
117
-
118
- @Test
119
- fun `prepareLaunch rolls back invalid staging and selects stable bundle`() {
120
- val rootDir = temporaryFolder.newFolder("rollback-to-stable")
121
- val preferences = InMemoryPreferencesService()
122
- val service = createService(rootDir, preferences)
123
-
124
- val stagingDir = createBundleDir(rootDir, "staging-bundle")
125
- writeFile(stagingDir, "dist/staging.android.bundle")
126
- writeManifest(
127
- stagingDir,
128
- listOf(
129
- "dist/staging.android.bundle",
130
- "assets/missing.png",
131
- ),
132
- )
133
-
134
- val stableDir = createBundleDir(rootDir, "stable-bundle")
135
- val stableBundleFile = writeFile(stableDir, "dist/stable.android.bundle")
136
- writeManifest(stableDir, listOf("dist/stable.android.bundle"))
137
-
138
- writeMetadata(
139
- rootDir,
140
- BundleMetadata(
141
- isolationKey = TEST_ISOLATION_KEY,
142
- stableBundleId = stableDir.name,
143
- stagingBundleId = stagingDir.name,
144
- verificationPending = true,
145
- ),
146
- )
147
-
148
- val selection = service.prepareLaunch(null)
149
- val report = service.notifyAppReady()
150
-
151
- assertEquals(stableBundleFile.absolutePath, selection.bundleUrl)
152
- assertEquals(stableDir.name, selection.launchedBundleId)
153
- assertFalse(selection.shouldRollbackOnCrash)
154
- assertFalse(stagingDir.exists())
155
- assertEquals("RECOVERED", report["status"])
156
- assertEquals(stagingDir.name, report["fromBundleId"])
157
- assertEquals(stableDir.name, report["toBundleId"])
158
- assertEquals("appVersion", report["updateStrategy"])
159
-
160
- val metadata = loadMetadata(rootDir)
161
- assertNotNull(metadata)
162
- assertEquals(stableDir.name, metadata?.stagingBundleId)
163
- assertNull(metadata?.stableBundleId)
164
- assertFalse(metadata?.verificationPending ?: true)
165
- assertEquals(stableBundleFile.absolutePath, preferences.getItem("HotUpdaterBundleURL"))
166
- }
167
-
168
- @Test
169
- fun `prepareLaunch falls back to built in bundle when staging and stable are both invalid`() {
170
- val rootDir = temporaryFolder.newFolder("fallback-to-built-in")
171
- val service = createService(rootDir)
172
-
173
- val stagingDir = createBundleDir(rootDir, "staging-bundle")
174
- writeManifest(stagingDir, listOf("dist/missing.android.bundle"))
175
-
176
- val stableDir = createBundleDir(rootDir, "stable-bundle")
177
- writeManifest(stableDir, listOf("../outside.android.bundle"))
178
-
179
- writeMetadata(
180
- rootDir,
181
- BundleMetadata(
182
- isolationKey = TEST_ISOLATION_KEY,
183
- stableBundleId = stableDir.name,
184
- stagingBundleId = stagingDir.name,
185
- verificationPending = true,
186
- ),
187
- )
188
-
189
- val selection = service.prepareLaunch(null)
190
- val report = service.notifyAppReady()
191
-
192
- assertEquals("assets://index.android.bundle", selection.bundleUrl)
193
- assertNull(selection.launchedBundleId)
194
- assertFalse(selection.shouldRollbackOnCrash)
195
- assertFalse(stagingDir.exists())
196
- assertEquals("RECOVERED", report["status"])
197
- assertEquals(stagingDir.name, report["fromBundleId"])
198
- assertEquals(HotUpdaterImpl.getMinBundleId(), report["toBundleId"])
199
- assertEquals("appVersion", report["updateStrategy"])
200
- }
201
-
202
- @Test
203
- fun `getBundleId falls back to built in while staging verification is pending`() {
204
- val rootDir = temporaryFolder.newFolder("pending-staging-built-in")
205
- val service = createService(rootDir)
206
-
207
- val stagingDir = createBundleDir(rootDir, "staging-bundle")
208
- writeFile(stagingDir, "index.android.bundle")
209
- writeManifest(stagingDir, listOf("index.android.bundle"))
210
-
211
- writeMetadata(
212
- rootDir,
213
- BundleMetadata(
214
- isolationKey = TEST_ISOLATION_KEY,
215
- stableBundleId = null,
216
- stagingBundleId = stagingDir.name,
217
- verificationPending = true,
218
- ),
219
- )
220
-
221
- assertNull(service.getBundleId())
222
- assertEquals("", service.getBaseURL())
223
- assertTrue(service.getManifest().isEmpty())
224
- }
225
-
226
- @Test
227
- fun `getBundleId returns launched staging bundle while verification is pending`() {
228
- val rootDir = temporaryFolder.newFolder("pending-staging-active")
229
- val preferences = InMemoryPreferencesService()
230
- val service = createService(rootDir, preferences)
231
-
232
- val stagingDir = createBundleDir(rootDir, "staging-bundle")
233
- val stagingBundleFile = writeFile(stagingDir, "index.android.bundle")
234
- writeManifest(stagingDir, listOf("index.android.bundle"))
235
-
236
- writeMetadata(
237
- rootDir,
238
- BundleMetadata(
239
- isolationKey = TEST_ISOLATION_KEY,
240
- stableBundleId = null,
241
- stagingBundleId = stagingDir.name,
242
- verificationPending = true,
243
- ),
244
- )
245
-
246
- preferences.setItem("HotUpdaterBundleURL", stagingBundleFile.absolutePath)
247
-
248
- assertEquals(stagingDir.name, service.getBundleId())
249
- }
250
-
251
- @Test
252
- fun `markLaunchCompleted records update applied transition`() {
253
- val rootDir = temporaryFolder.newFolder("update-applied-transition")
254
- val service = createService(rootDir)
255
-
256
- val stableDir = createBundleDir(rootDir, "stable-bundle")
257
- writeFile(stableDir, "index.android.bundle")
258
- writeManifest(stableDir, listOf("index.android.bundle"))
259
-
260
- val stagingDir = createBundleDir(rootDir, "staging-bundle")
261
- writeFile(stagingDir, "index.android.bundle")
262
- writeManifest(stagingDir, listOf("index.android.bundle"))
263
-
264
- writeMetadata(
265
- rootDir,
266
- BundleMetadata(
267
- isolationKey = TEST_ISOLATION_KEY,
268
- stableBundleId = stableDir.name,
269
- stagingBundleId = stagingDir.name,
270
- verificationPending = true,
271
- ),
272
- )
273
-
274
- assertEquals(mapOf("status" to "PENDING"), service.notifyAppReady())
275
-
276
- service.markLaunchCompleted(stagingDir.name)
277
-
278
- assertEquals(
279
- mapOf(
280
- "status" to "UPDATE_APPLIED",
281
- "fromBundleId" to stableDir.name,
282
- "toBundleId" to stagingDir.name,
283
- "updateStrategy" to "appVersion",
284
- ),
285
- service.notifyAppReady(),
286
- )
287
- }
288
-
289
- @Test
290
- fun `install identity persists across service instances and user clear keeps install id`() {
291
- val rootDir = temporaryFolder.newFolder("install-identity")
292
- val firstService = createService(rootDir)
293
-
294
- val firstInstallId = firstService.getInstallId()
295
- assertTrue(firstInstallId.isNotBlank())
296
-
297
- firstService.setUser("user-123", "alice")
298
- assertEquals(
299
- InstallationIdentity(
300
- installId = firstInstallId,
301
- userId = "user-123",
302
- username = "alice",
303
- ),
304
- loadInstallationIdentity(rootDir),
305
- )
306
-
307
- val restartedService = createService(rootDir)
308
- assertEquals(firstInstallId, restartedService.getInstallId())
309
-
310
- restartedService.setUser(null, null)
311
- assertEquals(
312
- InstallationIdentity(
313
- installId = firstInstallId,
314
- userId = null,
315
- username = null,
316
- ),
317
- loadInstallationIdentity(rootDir),
318
- )
319
- }
320
-
321
- @Test
322
- fun `user update keeps cached install id when persisted identity becomes unreadable`() {
323
- val rootDir = temporaryFolder.newFolder("install-identity-corruption")
324
- val service = createService(rootDir)
325
- val installId = service.getInstallId()
326
- val identityFile = File(bundleStoreDir(rootDir), InstallationIdentity.IDENTITY_FILENAME)
327
- identityFile.writeText("{")
328
-
329
- service.setUser("user-123", "alice")
330
-
331
- assertEquals(installId, service.getInstallId())
332
- assertEquals(
333
- InstallationIdentity(
334
- installId = installId,
335
- userId = "user-123",
336
- username = "alice",
337
- ),
338
- loadInstallationIdentity(rootDir),
339
- )
340
- }
341
-
342
- @Test
343
- fun `manifest driven install is disabled before first OTA`() {
344
- val rootDir = temporaryFolder.newFolder("first-ota-manifest-disabled")
345
- val service = createService(rootDir)
346
-
347
- assertFalse(invokeCanUseManifestDrivenInstall(service))
348
- }
349
-
350
- @Test
351
- fun `manifest driven install is enabled for active OTA bundle with manifest`() {
352
- val rootDir = temporaryFolder.newFolder("active-ota-manifest-enabled")
353
- val preferences = InMemoryPreferencesService()
354
- val service = createService(rootDir, preferences)
355
- val activeDir = createBundleDir(rootDir, "active-bundle")
356
- val activeBundleFile = writeFile(activeDir, "index.android.bundle")
357
- writeManifest(activeDir, listOf("index.android.bundle"))
358
-
359
- preferences.setItem("HotUpdaterBundleURL", activeBundleFile.absolutePath)
360
-
361
- assertTrue(invokeCanUseManifestDrivenInstall(service))
362
- }
363
-
364
- @Test
365
- fun `manifest driven install rejects unsafe asset paths`() {
366
- val rootDir = temporaryFolder.newFolder("active-ota-unsafe-manifest")
367
- val preferences = InMemoryPreferencesService()
368
- val service = createService(rootDir, preferences)
369
- val activeDir = createBundleDir(rootDir, "active-bundle")
370
- val activeBundleFile = writeFile(activeDir, "index.android.bundle")
371
- writeManifest(activeDir, listOf("../active-bundle_evil/index.android.bundle"))
372
-
373
- preferences.setItem("HotUpdaterBundleURL", activeBundleFile.absolutePath)
374
-
375
- assertFalse(invokeCanUseManifestDrivenInstall(service))
376
- }
377
-
378
- @Test
379
- fun `catalog high water rejects replay and survives channel reset`() =
380
- runBlocking {
381
- val rootDir = temporaryFolder.newFolder("release-high-water")
382
- val service = createService(rootDir)
383
-
384
- assertTrue(
385
- service.acceptReleaseCatalog(
386
- catalogId = "project-a",
387
- scopeKey = "scope-production",
388
- generation = 2,
389
- catalogHash = "hash-2",
390
- channel = "production",
391
- selectionContextHash = "context-2",
392
- ),
393
- )
394
- assertFalse(
395
- service.acceptReleaseCatalog(
396
- catalogId = "project-a",
397
- scopeKey = "scope-production",
398
- generation = 1,
399
- catalogHash = "hash-1",
400
- channel = "production",
401
- selectionContextHash = "context-1",
402
- ),
403
- )
404
- assertFalse(
405
- service.acceptReleaseCatalog(
406
- catalogId = "project-a",
407
- scopeKey = "scope-production",
408
- generation = 2,
409
- catalogHash = "different-hash",
410
- channel = "production",
411
- selectionContextHash = "context-2",
412
- ),
413
- )
414
-
415
- assertTrue(service.resetChannel())
416
- val metadata = loadMetadata(rootDir)
417
- assertEquals(
418
- CatalogHighWater(generation = 2, catalogHash = "hash-2"),
419
- metadata?.highestSeenCatalogs?.get("project-a|scope-production"),
420
- )
421
- }
422
-
423
- @Test
424
- fun `same bundle adoption refreshes its receipt without changing bytes`() =
425
- runBlocking {
426
- val rootDir = temporaryFolder.newFolder("same-bundle-adoption")
427
- val service = createService(rootDir)
428
- val bundleDir = createBundleDir(rootDir, "bundle-one")
429
- writeFile(bundleDir, "index.android.bundle")
430
- writeManifest(bundleDir, listOf("index.android.bundle"))
431
- val oldSelection =
432
- releaseSelection(
433
- releaseId = "release-one",
434
- bundleId = bundleDir.name,
435
- generation = 1,
436
- catalogHash = "hash-1",
437
- selectionContextHash = "context-1",
438
- )
439
- writeMetadata(
440
- rootDir,
441
- BundleMetadata(
442
- isolationKey = TEST_ISOLATION_KEY,
443
- stagingBundleId = bundleDir.name,
444
- stagingSelection = oldSelection,
445
- ),
446
- )
447
- assertTrue(
448
- service.acceptReleaseCatalog(
449
- catalogId = "project-a",
450
- scopeKey = "scope-production",
451
- generation = 2,
452
- catalogHash = "hash-2",
453
- channel = "production",
454
- selectionContextHash = "context-2",
455
- ),
456
- )
457
- val newSelection =
458
- releaseSelection(
459
- releaseId = "release-two",
460
- bundleId = bundleDir.name,
461
- generation = 2,
462
- catalogHash = "hash-2",
463
- selectionContextHash = "context-2",
464
- )
465
-
466
- assertTrue(service.commitReleaseSelection(newSelection))
467
-
468
- assertTrue(bundleDir.isDirectory)
469
- assertEquals(newSelection, loadMetadata(rootDir)?.stagingSelection)
470
- assertFalse(loadMetadata(rootDir)?.verificationPending ?: true)
471
- }
472
-
473
- @Test
474
- fun `crash restores the complete stable receipt but retains newer high water`() {
475
- val rootDir = temporaryFolder.newFolder("release-crash-ledger")
476
- val service = createService(rootDir)
477
- val stableDir = createBundleDir(rootDir, "bundle-one")
478
- writeFile(stableDir, "index.android.bundle")
479
- writeManifest(stableDir, listOf("index.android.bundle"))
480
- val stagingDir = createBundleDir(rootDir, "bundle-two")
481
- writeFile(stagingDir, "index.android.bundle")
482
- writeManifest(stagingDir, listOf("index.android.bundle"))
483
- val stableSelection =
484
- releaseSelection("release-one", stableDir.name, 1, "hash-1", "context-1")
485
- val stagingSelection =
486
- releaseSelection("release-two", stagingDir.name, 2, "hash-2", "context-2")
487
- writeMetadata(
488
- rootDir,
489
- BundleMetadata(
490
- isolationKey = TEST_ISOLATION_KEY,
491
- stableBundleId = stableDir.name,
492
- stagingBundleId = stagingDir.name,
493
- stableSelection = stableSelection,
494
- stagingSelection = stagingSelection,
495
- pendingUpdateStrategy = "appVersion",
496
- pendingTransition =
497
- PendingSelectionTransition(
498
- fromReleaseId = stableSelection.releaseId,
499
- fromBundleId = stableSelection.bundleId,
500
- toReleaseId = stagingSelection.releaseId,
501
- toBundleId = stagingSelection.bundleId,
502
- ),
503
- verificationPending = true,
504
- highestSeenCatalogs =
505
- mapOf(
506
- "project-a|scope-production" to
507
- CatalogHighWater(generation = 2, catalogHash = "hash-2"),
508
- ),
509
- currentSelectionContexts =
510
- mapOf("project-a|scope-production" to "production\ncontext-2"),
511
- ),
512
- )
513
-
514
- val launch =
515
- service.prepareLaunch(
516
- PendingCrashRecovery(
517
- launchedBundleId = stagingDir.name,
518
- shouldRollback = true,
519
- ),
520
- )
521
- val report = service.notifyAppReady()
522
- val metadata = loadMetadata(rootDir)
523
-
524
- assertEquals(stableDir.name, launch.launchedBundleId)
525
- assertEquals("RECOVERED", report["status"])
526
- assertEquals("release-two", report["fromReleaseId"])
527
- assertEquals("release-one", report["toReleaseId"])
528
- assertEquals(stableSelection, metadata?.stagingSelection)
529
- assertEquals(
530
- CatalogHighWater(generation = 2, catalogHash = "hash-2"),
531
- metadata?.highestSeenCatalogs?.get("project-a|scope-production"),
532
- )
533
- assertTrue(service.getCrashHistory().contains(stagingDir.name))
534
- }
535
-
536
- @Test
537
- fun `manifest driven install moves blocking work off caller dispatcher`() {
538
- val rootDir = temporaryFolder.newFolder("manifest-install-dispatcher")
539
- val preferences = InMemoryPreferencesService()
540
- val downloadService = RecordingFailedDownloadService()
541
- val service = createService(rootDir, preferences, downloadService)
542
- val activeDir = createBundleDir(rootDir, "active-bundle")
543
- val activeBundleFile = writeFile(activeDir, "index.android.bundle")
544
- writeManifest(activeDir, listOf("index.android.bundle"))
545
-
546
- preferences.setItem("HotUpdaterBundleURL", activeBundleFile.absolutePath)
547
-
548
- Executors
549
- .newSingleThreadExecutor { runnable -> Thread(runnable, "manifest-caller") }
550
- .asCoroutineDispatcher()
551
- .use { callerDispatcher ->
552
- runBlocking(callerDispatcher) {
553
- val result =
554
- runCatching {
555
- service.updateBundle(
556
- bundleId = "target-bundle",
557
- fileUrl = "https://example.com/bundle.zip",
558
- fileHash = null,
559
- manifestUrl = "https://example.com/manifest.json",
560
- manifestFileHash = "manifest-hash",
561
- changedAssets = emptyMap(),
562
- progressCallback = {},
563
- )
564
- }
565
- assertTrue(result.exceptionOrNull() is HotUpdaterException)
566
- }
567
- }
568
-
569
- val manifestCall = downloadService.calls.first()
570
- assertEquals("https://example.com/manifest.json", manifestCall.first)
571
- assertFalse(
572
- "Manifest install ran on the caller dispatcher: ${manifestCall.second}",
573
- manifestCall.second.contains("manifest-caller"),
574
- )
575
- }
576
-
577
- @Test
578
- fun `zip decompression does not write sibling prefix traversal entries`() {
579
- val rootDir = temporaryFolder.newFolder("zip-sibling-prefix")
580
- val zipFile = File(rootDir, "bundle.zip")
581
- ZipOutputStream(zipFile.outputStream()).use { zip ->
582
- writeZipEntry(zip, "../bundle-temp_evil/escape.txt", "blocked")
583
- writeZipEntry(zip, "safe/kept.txt", "kept")
584
- }
585
-
586
- val destinationDir = File(rootDir, "bundle-temp")
587
- val extracted =
588
- ZipDecompressionStrategy().decompress(
589
- zipFile.absolutePath,
590
- destinationDir.absolutePath,
591
- ) {}
592
-
593
- assertTrue(extracted)
594
- assertTrue(File(destinationDir, "safe/kept.txt").isFile)
595
- assertFalse(File(rootDir, "bundle-temp_evil/escape.txt").exists())
596
- }
597
-
598
- private fun createService(
599
- rootDir: File,
600
- preferences: InMemoryPreferencesService = InMemoryPreferencesService(),
601
- downloadService: DownloadService = UnusedDownloadService,
602
- ): BundleFileStorageService =
603
- BundleFileStorageService(
604
- ContextWrapper(null),
605
- TestFileSystemService(rootDir),
606
- downloadService,
607
- DecompressService(),
608
- preferences,
609
- TEST_ISOLATION_KEY,
610
- { "production" },
611
- )
612
-
613
- private fun releaseSelection(
614
- releaseId: String,
615
- bundleId: String,
616
- generation: Long,
617
- catalogHash: String,
618
- selectionContextHash: String,
619
- ): PersistedSelection =
620
- PersistedSelection(
621
- kind = "BUNDLE",
622
- releaseId = releaseId,
623
- bundleId = bundleId,
624
- catalogId = "project-a",
625
- scopeKey = "scope-production",
626
- generation = generation,
627
- catalogHash = catalogHash,
628
- channel = "production",
629
- selectionContextHash = selectionContextHash,
630
- )
631
-
632
- private fun createBundleDir(
633
- rootDir: File,
634
- bundleId: String,
635
- ): File = File(bundleStoreDir(rootDir), bundleId).apply { mkdirs() }
636
-
637
- private fun writeManifest(
638
- bundleDir: File,
639
- assetPaths: List<String>,
640
- ) {
641
- val assets =
642
- JSONObject().apply {
643
- assetPaths.forEach { assetPath ->
644
- put(assetPath, JSONObject().put("fileHash", "$assetPath-hash"))
645
- }
646
- }
647
-
648
- File(bundleDir, "manifest.json").writeText(
649
- JSONObject()
650
- .put("bundleId", bundleDir.name)
651
- .put("assets", assets)
652
- .toString(),
653
- )
654
- }
655
-
656
- private fun writeMetadata(
657
- rootDir: File,
658
- metadata: BundleMetadata,
659
- ) {
660
- assertTrue(metadata.saveToFile(File(bundleStoreDir(rootDir), BundleMetadata.METADATA_FILENAME)))
661
- }
662
-
663
- private fun loadMetadata(rootDir: File): BundleMetadata? =
664
- BundleMetadata.loadFromFile(
665
- File(bundleStoreDir(rootDir), BundleMetadata.METADATA_FILENAME),
666
- TEST_ISOLATION_KEY,
667
- )
668
-
669
- private fun loadInstallationIdentity(rootDir: File): InstallationIdentity? =
670
- InstallationIdentity.loadFromFile(
671
- File(bundleStoreDir(rootDir), InstallationIdentity.IDENTITY_FILENAME),
672
- )
673
-
674
- private fun writeFile(
675
- rootDir: File,
676
- relativePath: String,
677
- content: String = "bundle-content",
678
- ): File =
679
- File(rootDir, relativePath).apply {
680
- parentFile?.mkdirs()
681
- writeText(content)
682
- }
683
-
684
- private fun writeZipEntry(
685
- zip: ZipOutputStream,
686
- path: String,
687
- content: String,
688
- ) {
689
- zip.putNextEntry(ZipEntry(path))
690
- zip.write(content.toByteArray())
691
- zip.closeEntry()
692
- }
693
-
694
- private fun bundleStoreDir(rootDir: File): File = File(rootDir, "bundle-store").apply { mkdirs() }
695
-
696
- private fun invokeResolveBundleFile(
697
- service: BundleFileStorageService,
698
- bundleDir: File,
699
- ): File? {
700
- val method =
701
- BundleFileStorageService::class.java.getDeclaredMethod(
702
- "resolveBundleFile",
703
- File::class.java,
704
- String::class.java,
705
- )
706
- method.isAccessible = true
707
- return method.invoke(service, bundleDir, bundleDir.name) as File?
708
- }
709
-
710
- private fun invokeCanUseManifestDrivenInstall(service: BundleFileStorageService): Boolean {
711
- val method = BundleFileStorageService::class.java.getDeclaredMethod("canUseManifestDrivenInstall")
712
- method.isAccessible = true
713
- return method.invoke(service) as Boolean
714
- }
715
-
716
- private fun assertResolvedBundlePath(
717
- service: BundleFileStorageService,
718
- bundleDir: File,
719
- expected: File,
720
- ) {
721
- val resolved = invokeResolveBundleFile(service, bundleDir)
722
-
723
- assertNotNull(resolved)
724
- assertEquals(expected.canonicalFile.absolutePath, resolved?.canonicalFile?.absolutePath)
725
- }
726
-
727
- private class TestFileSystemService(
728
- private val internalFilesDir: File,
729
- ) : FileSystemService {
730
- override fun fileExists(path: String): Boolean = File(path).exists()
731
-
732
- override fun createDirectory(path: String): Boolean = File(path).mkdirs()
733
-
734
- override fun removeItem(path: String): Boolean = File(path).deleteRecursively()
735
-
736
- override fun moveItem(
737
- sourcePath: String,
738
- destinationPath: String,
739
- ): Boolean = File(sourcePath).renameTo(File(destinationPath))
740
-
741
- override fun copyItem(
742
- sourcePath: String,
743
- destinationPath: String,
744
- ): Boolean =
745
- try {
746
- File(sourcePath).copyRecursively(File(destinationPath), overwrite = true)
747
- } catch (_: Exception) {
748
- false
749
- }
750
-
751
- override fun contentsOfDirectory(path: String): List<String> = File(path).list()?.toList() ?: emptyList()
752
-
753
- override fun getInternalFilesDir(): File = internalFilesDir
754
- }
755
-
756
- private class InMemoryPreferencesService : PreferencesService {
757
- private val values = mutableMapOf<String, String?>()
758
-
759
- override fun getItem(key: String): String? = values[key]
760
-
761
- override fun setItem(
762
- key: String,
763
- value: String?,
764
- ) {
765
- if (value == null) {
766
- values.remove(key)
767
- } else {
768
- values[key] = value
769
- }
770
- }
771
- }
772
-
773
- private object UnusedDownloadService : DownloadService {
774
- override suspend fun downloadFile(
775
- fileUrl: URL,
776
- destination: File,
777
- fileSizeCallback: ((Long) -> Unit)?,
778
- progressCallback: (DownloadProgress) -> Unit,
779
- ): DownloadResult = error("downloadFile should not be called in these tests")
780
- }
781
-
782
- private class RecordingFailedDownloadService : DownloadService {
783
- val calls = CopyOnWriteArrayList<Pair<String, String>>()
784
-
785
- override suspend fun downloadFile(
786
- fileUrl: URL,
787
- destination: File,
788
- fileSizeCallback: ((Long) -> Unit)?,
789
- progressCallback: (DownloadProgress) -> Unit,
790
- ): DownloadResult {
791
- calls += fileUrl.toString() to Thread.currentThread().name
792
- return DownloadResult.Error(IllegalStateException("expected download failure"))
793
- }
794
- }
795
-
796
- companion object {
797
- private const val TEST_ISOLATION_KEY = "test-isolation-key"
798
- }
799
- }