@hot-updater/react-native 1.0.0-rc.3 → 1.0.0-rc.30

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