@hot-updater/react-native 1.0.0-rc.4 → 1.0.0-rc.40

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 (306) hide show
  1. package/android/build.gradle +13 -2
  2. package/android/proguard-rules.pro +8 -2
  3. package/android/src/bridge/BridgeReactInstance.kt +38 -0
  4. package/android/src/bridgeless/BridgeReactInstance.kt +24 -0
  5. package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +94 -0
  6. package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
  7. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +1064 -844
  8. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +50 -11
  9. package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
  10. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +287 -22
  11. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +46 -30
  12. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +75 -192
  13. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
  14. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
  15. package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
  16. package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
  17. package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
  18. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +127 -50
  19. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -0
  20. package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
  21. package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
  22. package/android/src/newarch/HotUpdater.kt +7 -9
  23. package/android/src/newarch/HotUpdaterModule.kt +109 -84
  24. package/android/src/newarch/JavaScriptExceptionHooks.kt +48 -0
  25. package/android/src/newarch/ReactIntegrationManager.kt +6 -32
  26. package/android/src/oldarch/HotUpdater.kt +7 -9
  27. package/android/src/oldarch/HotUpdaterModule.kt +98 -74
  28. package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
  29. package/android/src/oldarch/JavaScriptExceptionHooks.kt +71 -0
  30. package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
  31. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
  32. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +1032 -1116
  33. package/ios/HotUpdater/Internal/BundleMetadata.swift +61 -54
  34. package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
  35. package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
  36. package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
  37. package/ios/HotUpdater/Internal/HotUpdater.mm +32 -67
  38. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +229 -73
  39. package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
  40. package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
  41. package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
  42. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +119 -71
  43. package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
  44. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
  45. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
  46. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
  47. package/lib/commonjs/appReady.js +102 -0
  48. package/lib/commonjs/appReady.js.map +1 -0
  49. package/lib/commonjs/checkForUpdate.js +120 -68
  50. package/lib/commonjs/checkForUpdate.js.map +1 -1
  51. package/lib/commonjs/clientPlugin.js +13 -0
  52. package/lib/commonjs/clientPlugin.js.map +1 -0
  53. package/lib/commonjs/error.js +4 -13
  54. package/lib/commonjs/error.js.map +1 -1
  55. package/lib/commonjs/fetchJSON.js +16 -27
  56. package/lib/commonjs/fetchJSON.js.map +1 -1
  57. package/lib/commonjs/fetchUpdateResponse.js +60 -0
  58. package/lib/commonjs/fetchUpdateResponse.js.map +1 -0
  59. package/lib/commonjs/httpClient.js +63 -94
  60. package/lib/commonjs/httpClient.js.map +1 -1
  61. package/lib/commonjs/index.js +71 -18
  62. package/lib/commonjs/index.js.map +1 -1
  63. package/lib/commonjs/native.js +74 -60
  64. package/lib/commonjs/native.js.map +1 -1
  65. package/lib/commonjs/pluginHost.js +45 -0
  66. package/lib/commonjs/pluginHost.js.map +1 -0
  67. package/lib/commonjs/releaseCatalogCache.js +36 -87
  68. package/lib/commonjs/releaseCatalogCache.js.map +1 -1
  69. package/lib/commonjs/sdkVersion.js +1 -1
  70. package/lib/commonjs/sdkVersion.js.map +1 -1
  71. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  72. package/lib/commonjs/store.js +3 -10
  73. package/lib/commonjs/store.js.map +1 -1
  74. package/lib/commonjs/updateError.js +145 -0
  75. package/lib/commonjs/updateError.js.map +1 -0
  76. package/lib/commonjs/wrap.js +43 -26
  77. package/lib/commonjs/wrap.js.map +1 -1
  78. package/lib/module/appReady.js +95 -0
  79. package/lib/module/appReady.js.map +1 -0
  80. package/lib/module/checkForUpdate.js +113 -63
  81. package/lib/module/checkForUpdate.js.map +1 -1
  82. package/lib/module/clientPlugin.js +4 -0
  83. package/lib/module/clientPlugin.js.map +1 -0
  84. package/lib/module/error.js +4 -13
  85. package/lib/module/error.js.map +1 -1
  86. package/lib/module/fetchJSON.js +14 -26
  87. package/lib/module/fetchJSON.js.map +1 -1
  88. package/lib/module/fetchUpdateResponse.js +55 -0
  89. package/lib/module/fetchUpdateResponse.js.map +1 -0
  90. package/lib/module/httpClient.js +62 -93
  91. package/lib/module/httpClient.js.map +1 -1
  92. package/lib/module/index.js +58 -20
  93. package/lib/module/index.js.map +1 -1
  94. package/lib/module/native.js +67 -53
  95. package/lib/module/native.js.map +1 -1
  96. package/lib/module/pluginHost.js +36 -0
  97. package/lib/module/pluginHost.js.map +1 -0
  98. package/lib/module/releaseCatalogCache.js +32 -82
  99. package/lib/module/releaseCatalogCache.js.map +1 -1
  100. package/lib/module/sdkVersion.js +1 -1
  101. package/lib/module/sdkVersion.js.map +1 -1
  102. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  103. package/lib/module/store.js +3 -10
  104. package/lib/module/store.js.map +1 -1
  105. package/lib/module/updateError.js +139 -0
  106. package/lib/module/updateError.js.map +1 -0
  107. package/lib/module/wrap.js +43 -26
  108. package/lib/module/wrap.js.map +1 -1
  109. package/lib/typescript/commonjs/appReady.d.ts +21 -0
  110. package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/checkForUpdate.d.ts +16 -4
  112. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/clientPlugin.d.ts +2 -0
  114. package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/error.d.ts +4 -13
  116. package/lib/typescript/commonjs/error.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/fetchJSON.d.ts +5 -5
  118. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts +14 -0
  120. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts.map +1 -0
  121. package/lib/typescript/commonjs/httpClient.d.ts +4 -33
  122. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/index.d.ts +4 -8
  124. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/native.d.ts +29 -22
  126. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/pluginHost.d.ts +4 -0
  128. package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +8 -14
  130. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  132. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +35 -47
  134. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  135. package/lib/typescript/commonjs/store.d.ts +1 -3
  136. package/lib/typescript/commonjs/store.d.ts.map +1 -1
  137. package/lib/typescript/commonjs/types.d.ts +1 -1
  138. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  139. package/lib/typescript/commonjs/updateError.d.ts +26 -0
  140. package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
  141. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  142. package/lib/typescript/commonjs/wrap.types.d.ts +6 -7
  143. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  144. package/lib/typescript/module/appReady.d.ts +21 -0
  145. package/lib/typescript/module/appReady.d.ts.map +1 -0
  146. package/lib/typescript/module/checkForUpdate.d.ts +16 -4
  147. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  148. package/lib/typescript/module/clientPlugin.d.ts +2 -0
  149. package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
  150. package/lib/typescript/module/error.d.ts +4 -13
  151. package/lib/typescript/module/error.d.ts.map +1 -1
  152. package/lib/typescript/module/fetchJSON.d.ts +5 -5
  153. package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
  154. package/lib/typescript/module/fetchUpdateResponse.d.ts +14 -0
  155. package/lib/typescript/module/fetchUpdateResponse.d.ts.map +1 -0
  156. package/lib/typescript/module/httpClient.d.ts +4 -33
  157. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  158. package/lib/typescript/module/index.d.ts +4 -8
  159. package/lib/typescript/module/index.d.ts.map +1 -1
  160. package/lib/typescript/module/native.d.ts +29 -22
  161. package/lib/typescript/module/native.d.ts.map +1 -1
  162. package/lib/typescript/module/pluginHost.d.ts +4 -0
  163. package/lib/typescript/module/pluginHost.d.ts.map +1 -0
  164. package/lib/typescript/module/releaseCatalogCache.d.ts +8 -14
  165. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
  166. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  167. package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
  168. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +35 -47
  169. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  170. package/lib/typescript/module/store.d.ts +1 -3
  171. package/lib/typescript/module/store.d.ts.map +1 -1
  172. package/lib/typescript/module/types.d.ts +1 -1
  173. package/lib/typescript/module/types.d.ts.map +1 -1
  174. package/lib/typescript/module/updateError.d.ts +26 -0
  175. package/lib/typescript/module/updateError.d.ts.map +1 -0
  176. package/lib/typescript/module/wrap.d.ts.map +1 -1
  177. package/lib/typescript/module/wrap.types.d.ts +6 -7
  178. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  179. package/package.json +7 -4
  180. package/src/appReady.ts +141 -0
  181. package/src/checkForUpdate.ts +145 -86
  182. package/src/clientPlugin.ts +18 -0
  183. package/src/error.ts +4 -14
  184. package/src/fetchJSON.ts +14 -30
  185. package/src/fetchUpdateResponse.ts +60 -0
  186. package/src/httpClient.ts +88 -159
  187. package/src/index.ts +90 -24
  188. package/src/native.ts +98 -102
  189. package/src/pluginHost.ts +47 -0
  190. package/src/releaseCatalogCache.ts +43 -172
  191. package/src/sdkVersion.ts +1 -1
  192. package/src/specs/NativeHotUpdater.ts +34 -48
  193. package/src/store.ts +2 -23
  194. package/src/types.ts +1 -1
  195. package/src/updateError.ts +167 -0
  196. package/src/wrap.tsx +57 -26
  197. package/src/wrap.types.ts +6 -7
  198. package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
  199. package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
  200. package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
  201. package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
  202. package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
  203. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +0 -46
  204. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -799
  205. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -273
  206. package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
  207. package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -111
  208. package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
  209. package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
  210. package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
  211. package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
  212. package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
  213. package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
  214. package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
  215. package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
  216. package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
  217. package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
  218. package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
  219. package/lib/commonjs/catalogCacheNative.spec.js +0 -31
  220. package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
  221. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -616
  222. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  223. package/lib/commonjs/httpClient.spec.js +0 -102
  224. package/lib/commonjs/httpClient.spec.js.map +0 -1
  225. package/lib/commonjs/index.spec.js +0 -302
  226. package/lib/commonjs/index.spec.js.map +0 -1
  227. package/lib/commonjs/iosInterop.spec.js +0 -18
  228. package/lib/commonjs/iosInterop.spec.js.map +0 -1
  229. package/lib/commonjs/native.spec.js +0 -883
  230. package/lib/commonjs/native.spec.js.map +0 -1
  231. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -218
  232. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  233. package/lib/commonjs/notifyAppReadyInsights.js +0 -133
  234. package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
  235. package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -246
  236. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
  237. package/lib/commonjs/notifyAppReadyInsights.test-utils.js +0 -24
  238. package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
  239. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -117
  240. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
  241. package/lib/commonjs/releaseCatalogCache.spec.js +0 -195
  242. package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
  243. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
  244. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  245. package/lib/commonjs/store.notification.spec.js +0 -76
  246. package/lib/commonjs/store.notification.spec.js.map +0 -1
  247. package/lib/commonjs/store.spec.js +0 -316
  248. package/lib/commonjs/store.spec.js.map +0 -1
  249. package/lib/commonjs/wrap.spec.js +0 -329
  250. package/lib/commonjs/wrap.spec.js.map +0 -1
  251. package/lib/module/catalogCacheNative.spec.js +0 -31
  252. package/lib/module/catalogCacheNative.spec.js.map +0 -1
  253. package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -615
  254. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  255. package/lib/module/httpClient.spec.js +0 -102
  256. package/lib/module/httpClient.spec.js.map +0 -1
  257. package/lib/module/index.spec.js +0 -301
  258. package/lib/module/index.spec.js.map +0 -1
  259. package/lib/module/iosInterop.spec.js +0 -18
  260. package/lib/module/iosInterop.spec.js.map +0 -1
  261. package/lib/module/native.spec.js +0 -882
  262. package/lib/module/native.spec.js.map +0 -1
  263. package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -217
  264. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  265. package/lib/module/notifyAppReadyInsights.js +0 -128
  266. package/lib/module/notifyAppReadyInsights.js.map +0 -1
  267. package/lib/module/notifyAppReadyInsights.spec.js +0 -245
  268. package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
  269. package/lib/module/notifyAppReadyInsights.test-utils.js +0 -18
  270. package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
  271. package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -116
  272. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
  273. package/lib/module/releaseCatalogCache.spec.js +0 -195
  274. package/lib/module/releaseCatalogCache.spec.js.map +0 -1
  275. package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
  276. package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  277. package/lib/module/store.notification.spec.js +0 -77
  278. package/lib/module/store.notification.spec.js.map +0 -1
  279. package/lib/module/store.spec.js +0 -315
  280. package/lib/module/store.spec.js.map +0 -1
  281. package/lib/module/wrap.spec.js +0 -330
  282. package/lib/module/wrap.spec.js.map +0 -1
  283. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -13
  284. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
  285. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
  286. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  287. package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -13
  288. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
  289. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
  290. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  291. package/src/catalogCacheNative.spec.ts +0 -63
  292. package/src/checkForUpdate.releaseCatalog.spec.ts +0 -612
  293. package/src/httpClient.spec.ts +0 -132
  294. package/src/index.spec.ts +0 -337
  295. package/src/iosInterop.spec.ts +0 -30
  296. package/src/native.spec.ts +0 -945
  297. package/src/notifyAppReadyInsights.boundary.spec.ts +0 -234
  298. package/src/notifyAppReadyInsights.spec.ts +0 -243
  299. package/src/notifyAppReadyInsights.test-utils.ts +0 -31
  300. package/src/notifyAppReadyInsights.transport.spec.ts +0 -130
  301. package/src/notifyAppReadyInsights.ts +0 -194
  302. package/src/releaseCatalogCache.spec.ts +0 -282
  303. package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
  304. package/src/store.notification.spec.ts +0 -92
  305. package/src/store.spec.ts +0 -372
  306. package/src/wrap.spec.ts +0 -406
@@ -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
- }