@hot-updater/react-native 1.0.0-rc.15 → 1.0.0-rc.17

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 (302) hide show
  1. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +491 -388
  2. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +5 -11
  3. package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
  4. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +290 -12
  5. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +35 -19
  6. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
  7. package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
  8. package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
  9. package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
  10. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +110 -52
  11. package/android/src/main/java/com/hotupdater/PreviousProcessExit.kt +89 -0
  12. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -0
  13. package/android/src/newarch/HotUpdaterModule.kt +74 -69
  14. package/android/src/oldarch/HotUpdaterModule.kt +63 -59
  15. package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
  16. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +1 -1
  17. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +446 -61
  18. package/android/src/test/java/com/hotupdater/HotUpdaterExceptionTest.kt +220 -0
  19. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +19 -27
  20. package/android/src/test/java/com/hotupdater/InstallIdentityServiceTest.kt +71 -0
  21. package/android/src/test/java/com/hotupdater/KeyValueStorageServiceTest.kt +106 -0
  22. package/android/src/test/java/com/hotupdater/NoBackupContext.kt +11 -0
  23. package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +309 -23
  24. package/android/src/test/java/com/hotupdater/PreviousProcessExitTest.kt +57 -0
  25. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +238 -157
  26. package/ios/HotUpdater/Internal/BundleMetadata.swift +3 -53
  27. package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
  28. package/ios/HotUpdater/Internal/HotUpdater.mm +8 -18
  29. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +48 -29
  30. package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
  31. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +108 -1
  32. package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
  33. package/lib/commonjs/appReady.js +111 -0
  34. package/lib/commonjs/appReady.js.map +1 -0
  35. package/lib/commonjs/appReady.spec.js +360 -0
  36. package/lib/commonjs/appReady.spec.js.map +1 -0
  37. package/lib/commonjs/{notifyAppReadyInsights.test-utils.js → appReady.test-utils.js} +4 -3
  38. package/lib/commonjs/appReady.test-utils.js.map +1 -0
  39. package/lib/commonjs/baseURL.js +16 -0
  40. package/lib/commonjs/baseURL.js.map +1 -0
  41. package/lib/commonjs/checkForUpdate.js +104 -56
  42. package/lib/commonjs/checkForUpdate.js.map +1 -1
  43. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +337 -74
  44. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  45. package/lib/commonjs/clientPlugin.js +49 -0
  46. package/lib/commonjs/clientPlugin.js.map +1 -0
  47. package/lib/commonjs/entryImportGraph.spec.js +50 -0
  48. package/lib/commonjs/entryImportGraph.spec.js.map +1 -0
  49. package/lib/commonjs/fetchJSON.js +2 -6
  50. package/lib/commonjs/fetchJSON.js.map +1 -1
  51. package/lib/commonjs/httpClient.js +15 -67
  52. package/lib/commonjs/httpClient.js.map +1 -1
  53. package/lib/commonjs/httpClient.spec.js +26 -9
  54. package/lib/commonjs/httpClient.spec.js.map +1 -1
  55. package/lib/commonjs/index.js +49 -25
  56. package/lib/commonjs/index.js.map +1 -1
  57. package/lib/commonjs/index.spec.js +126 -40
  58. package/lib/commonjs/index.spec.js.map +1 -1
  59. package/lib/commonjs/native.js +67 -48
  60. package/lib/commonjs/native.js.map +1 -1
  61. package/lib/commonjs/native.spec.js +80 -58
  62. package/lib/commonjs/native.spec.js.map +1 -1
  63. package/lib/commonjs/pluginHost.js +208 -0
  64. package/lib/commonjs/pluginHost.js.map +1 -0
  65. package/lib/commonjs/pluginHost.spec.js +292 -0
  66. package/lib/commonjs/pluginHost.spec.js.map +1 -0
  67. package/lib/commonjs/plugins/insights/eventId.js +129 -0
  68. package/lib/commonjs/plugins/insights/eventId.js.map +1 -0
  69. package/lib/commonjs/plugins/insights/eventId.spec.js +47 -0
  70. package/lib/commonjs/plugins/insights/eventId.spec.js.map +1 -0
  71. package/lib/commonjs/plugins/insights/index.js +272 -0
  72. package/lib/commonjs/plugins/insights/index.js.map +1 -0
  73. package/lib/commonjs/plugins/insights/insights.spec.js +640 -0
  74. package/lib/commonjs/plugins/insights/insights.spec.js.map +1 -0
  75. package/lib/commonjs/plugins/insights/sender.js +128 -0
  76. package/lib/commonjs/plugins/insights/sender.js.map +1 -0
  77. package/lib/commonjs/plugins/insights/sender.spec.js +265 -0
  78. package/lib/commonjs/plugins/insights/sender.spec.js.map +1 -0
  79. package/lib/commonjs/plugins/insights/state.js +145 -0
  80. package/lib/commonjs/plugins/insights/state.js.map +1 -0
  81. package/lib/commonjs/releaseCatalogCache.js +13 -2
  82. package/lib/commonjs/releaseCatalogCache.js.map +1 -1
  83. package/lib/commonjs/releaseCatalogCache.spec.js +18 -0
  84. package/lib/commonjs/releaseCatalogCache.spec.js.map +1 -1
  85. package/lib/commonjs/sdkVersion.js +1 -1
  86. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  87. package/lib/commonjs/updateError.js +132 -0
  88. package/lib/commonjs/updateError.js.map +1 -0
  89. package/lib/commonjs/updateError.spec.js +149 -0
  90. package/lib/commonjs/updateError.spec.js.map +1 -0
  91. package/lib/commonjs/wrap.js +43 -25
  92. package/lib/commonjs/wrap.js.map +1 -1
  93. package/lib/commonjs/wrap.progress.spec.js +195 -0
  94. package/lib/commonjs/wrap.progress.spec.js.map +1 -0
  95. package/lib/commonjs/wrap.spec.js +94 -230
  96. package/lib/commonjs/wrap.spec.js.map +1 -1
  97. package/lib/module/appReady.js +103 -0
  98. package/lib/module/appReady.js.map +1 -0
  99. package/lib/module/appReady.spec.js +359 -0
  100. package/lib/module/appReady.spec.js.map +1 -0
  101. package/lib/module/{notifyAppReadyInsights.test-utils.js → appReady.test-utils.js} +4 -3
  102. package/lib/module/appReady.test-utils.js.map +1 -0
  103. package/lib/module/baseURL.js +11 -0
  104. package/lib/module/baseURL.js.map +1 -0
  105. package/lib/module/checkForUpdate.js +103 -57
  106. package/lib/module/checkForUpdate.js.map +1 -1
  107. package/lib/module/checkForUpdate.releaseCatalog.spec.js +337 -74
  108. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  109. package/lib/module/clientPlugin.js +44 -0
  110. package/lib/module/clientPlugin.js.map +1 -0
  111. package/lib/module/entryImportGraph.spec.js +50 -0
  112. package/lib/module/entryImportGraph.spec.js.map +1 -0
  113. package/lib/module/fetchJSON.js +2 -6
  114. package/lib/module/fetchJSON.js.map +1 -1
  115. package/lib/module/httpClient.js +14 -66
  116. package/lib/module/httpClient.js.map +1 -1
  117. package/lib/module/httpClient.spec.js +26 -9
  118. package/lib/module/httpClient.spec.js.map +1 -1
  119. package/lib/module/index.js +44 -27
  120. package/lib/module/index.js.map +1 -1
  121. package/lib/module/index.spec.js +127 -41
  122. package/lib/module/index.spec.js.map +1 -1
  123. package/lib/module/native.js +63 -44
  124. package/lib/module/native.js.map +1 -1
  125. package/lib/module/native.spec.js +80 -58
  126. package/lib/module/native.spec.js.map +1 -1
  127. package/lib/module/pluginHost.js +200 -0
  128. package/lib/module/pluginHost.js.map +1 -0
  129. package/lib/module/pluginHost.spec.js +291 -0
  130. package/lib/module/pluginHost.spec.js.map +1 -0
  131. package/lib/module/plugins/insights/eventId.js +122 -0
  132. package/lib/module/plugins/insights/eventId.js.map +1 -0
  133. package/lib/module/plugins/insights/eventId.spec.js +47 -0
  134. package/lib/module/plugins/insights/eventId.spec.js.map +1 -0
  135. package/lib/module/plugins/insights/index.js +268 -0
  136. package/lib/module/plugins/insights/index.js.map +1 -0
  137. package/lib/module/plugins/insights/insights.spec.js +639 -0
  138. package/lib/module/plugins/insights/insights.spec.js.map +1 -0
  139. package/lib/module/plugins/insights/sender.js +123 -0
  140. package/lib/module/plugins/insights/sender.js.map +1 -0
  141. package/lib/module/plugins/insights/sender.spec.js +265 -0
  142. package/lib/module/plugins/insights/sender.spec.js.map +1 -0
  143. package/lib/module/plugins/insights/state.js +139 -0
  144. package/lib/module/plugins/insights/state.js.map +1 -0
  145. package/lib/module/releaseCatalogCache.js +13 -2
  146. package/lib/module/releaseCatalogCache.js.map +1 -1
  147. package/lib/module/releaseCatalogCache.spec.js +18 -0
  148. package/lib/module/releaseCatalogCache.spec.js.map +1 -1
  149. package/lib/module/sdkVersion.js +1 -1
  150. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  151. package/lib/module/updateError.js +126 -0
  152. package/lib/module/updateError.js.map +1 -0
  153. package/lib/module/updateError.spec.js +149 -0
  154. package/lib/module/updateError.spec.js.map +1 -0
  155. package/lib/module/wrap.js +43 -25
  156. package/lib/module/wrap.js.map +1 -1
  157. package/lib/module/wrap.progress.spec.js +196 -0
  158. package/lib/module/wrap.progress.spec.js.map +1 -0
  159. package/lib/module/wrap.spec.js +93 -229
  160. package/lib/module/wrap.spec.js.map +1 -1
  161. package/lib/typescript/commonjs/appReady.d.ts +23 -0
  162. package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
  163. package/lib/typescript/commonjs/appReady.test-utils.d.ts +9 -0
  164. package/lib/typescript/commonjs/appReady.test-utils.d.ts.map +1 -0
  165. package/lib/typescript/commonjs/baseURL.d.ts +4 -0
  166. package/lib/typescript/commonjs/baseURL.d.ts.map +1 -0
  167. package/lib/typescript/commonjs/checkForUpdate.d.ts +15 -2
  168. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/clientPlugin.d.ts +185 -0
  170. package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
  171. package/lib/typescript/commonjs/fetchJSON.d.ts +2 -3
  172. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
  173. package/lib/typescript/commonjs/httpClient.d.ts +2 -30
  174. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  175. package/lib/typescript/commonjs/index.d.ts +3 -6
  176. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  177. package/lib/typescript/commonjs/native.d.ts +29 -15
  178. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  179. package/lib/typescript/commonjs/pluginHost.d.ts +29 -0
  180. package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
  181. package/lib/typescript/commonjs/plugins/insights/eventId.d.ts +18 -0
  182. package/lib/typescript/commonjs/plugins/insights/eventId.d.ts.map +1 -0
  183. package/lib/typescript/commonjs/plugins/insights/index.d.ts +40 -0
  184. package/lib/typescript/commonjs/plugins/insights/index.d.ts.map +1 -0
  185. package/lib/typescript/commonjs/plugins/insights/sender.d.ts +74 -0
  186. package/lib/typescript/commonjs/plugins/insights/sender.d.ts.map +1 -0
  187. package/lib/typescript/commonjs/plugins/insights/state.d.ts +46 -0
  188. package/lib/typescript/commonjs/plugins/insights/state.d.ts.map +1 -0
  189. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +5 -1
  190. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
  191. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  192. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +34 -12
  193. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  194. package/lib/typescript/commonjs/updateError.d.ts +26 -0
  195. package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
  196. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  197. package/lib/typescript/commonjs/wrap.types.d.ts +6 -5
  198. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  199. package/lib/typescript/module/appReady.d.ts +23 -0
  200. package/lib/typescript/module/appReady.d.ts.map +1 -0
  201. package/lib/typescript/module/appReady.test-utils.d.ts +9 -0
  202. package/lib/typescript/module/appReady.test-utils.d.ts.map +1 -0
  203. package/lib/typescript/module/baseURL.d.ts +4 -0
  204. package/lib/typescript/module/baseURL.d.ts.map +1 -0
  205. package/lib/typescript/module/checkForUpdate.d.ts +15 -2
  206. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  207. package/lib/typescript/module/clientPlugin.d.ts +185 -0
  208. package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
  209. package/lib/typescript/module/fetchJSON.d.ts +2 -3
  210. package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
  211. package/lib/typescript/module/httpClient.d.ts +2 -30
  212. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  213. package/lib/typescript/module/index.d.ts +3 -6
  214. package/lib/typescript/module/index.d.ts.map +1 -1
  215. package/lib/typescript/module/native.d.ts +29 -15
  216. package/lib/typescript/module/native.d.ts.map +1 -1
  217. package/lib/typescript/module/pluginHost.d.ts +29 -0
  218. package/lib/typescript/module/pluginHost.d.ts.map +1 -0
  219. package/lib/typescript/module/plugins/insights/eventId.d.ts +18 -0
  220. package/lib/typescript/module/plugins/insights/eventId.d.ts.map +1 -0
  221. package/lib/typescript/module/plugins/insights/index.d.ts +40 -0
  222. package/lib/typescript/module/plugins/insights/index.d.ts.map +1 -0
  223. package/lib/typescript/module/plugins/insights/sender.d.ts +74 -0
  224. package/lib/typescript/module/plugins/insights/sender.d.ts.map +1 -0
  225. package/lib/typescript/module/plugins/insights/state.d.ts +46 -0
  226. package/lib/typescript/module/plugins/insights/state.d.ts.map +1 -0
  227. package/lib/typescript/module/releaseCatalogCache.d.ts +5 -1
  228. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
  229. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  230. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +34 -12
  231. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  232. package/lib/typescript/module/updateError.d.ts +26 -0
  233. package/lib/typescript/module/updateError.d.ts.map +1 -0
  234. package/lib/typescript/module/wrap.d.ts.map +1 -1
  235. package/lib/typescript/module/wrap.types.d.ts +6 -5
  236. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  237. package/package.json +14 -2
  238. package/plugins/insights/package.json +8 -0
  239. package/src/appReady.spec.ts +393 -0
  240. package/src/{notifyAppReadyInsights.test-utils.ts → appReady.test-utils.ts} +7 -7
  241. package/src/appReady.ts +149 -0
  242. package/src/baseURL.ts +15 -0
  243. package/src/checkForUpdate.releaseCatalog.spec.ts +370 -76
  244. package/src/checkForUpdate.ts +135 -80
  245. package/src/clientPlugin.ts +231 -0
  246. package/src/entryImportGraph.spec.ts +65 -0
  247. package/src/fetchJSON.ts +3 -10
  248. package/src/httpClient.spec.ts +28 -9
  249. package/src/httpClient.ts +16 -109
  250. package/src/index.spec.ts +129 -36
  251. package/src/index.ts +67 -37
  252. package/src/native.spec.ts +98 -72
  253. package/src/native.ts +93 -77
  254. package/src/pluginHost.spec.ts +297 -0
  255. package/src/pluginHost.ts +262 -0
  256. package/src/plugins/insights/eventId.spec.ts +60 -0
  257. package/src/plugins/insights/eventId.ts +171 -0
  258. package/src/plugins/insights/index.ts +367 -0
  259. package/src/plugins/insights/insights.spec.ts +732 -0
  260. package/src/plugins/insights/sender.spec.ts +287 -0
  261. package/src/plugins/insights/sender.ts +221 -0
  262. package/src/plugins/insights/state.ts +184 -0
  263. package/src/releaseCatalogCache.spec.ts +22 -0
  264. package/src/releaseCatalogCache.ts +19 -4
  265. package/src/sdkVersion.ts +1 -1
  266. package/src/specs/NativeHotUpdater.ts +34 -13
  267. package/src/updateError.spec.ts +144 -0
  268. package/src/updateError.ts +153 -0
  269. package/src/wrap.progress.spec.ts +232 -0
  270. package/src/wrap.spec.ts +100 -283
  271. package/src/wrap.tsx +57 -26
  272. package/src/wrap.types.ts +6 -5
  273. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -225
  274. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  275. package/lib/commonjs/notifyAppReadyInsights.js +0 -188
  276. package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
  277. package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -378
  278. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
  279. package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
  280. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -124
  281. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
  282. package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -224
  283. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  284. package/lib/module/notifyAppReadyInsights.js +0 -181
  285. package/lib/module/notifyAppReadyInsights.js.map +0 -1
  286. package/lib/module/notifyAppReadyInsights.spec.js +0 -377
  287. package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
  288. package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
  289. package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -123
  290. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
  291. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -25
  292. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
  293. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
  294. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  295. package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -25
  296. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
  297. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
  298. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  299. package/src/notifyAppReadyInsights.boundary.spec.ts +0 -242
  300. package/src/notifyAppReadyInsights.spec.ts +0 -362
  301. package/src/notifyAppReadyInsights.transport.spec.ts +0 -138
  302. package/src/notifyAppReadyInsights.ts +0 -293
@@ -14,11 +14,16 @@ import org.junit.Rule
14
14
  import org.junit.Test
15
15
  import org.junit.rules.TemporaryFolder
16
16
  import java.io.File
17
+ import java.io.IOException
18
+ import java.net.ConnectException
19
+ import java.net.SocketTimeoutException
17
20
  import java.net.URL
21
+ import java.net.UnknownHostException
18
22
  import java.util.Base64
19
23
  import java.util.concurrent.CopyOnWriteArrayList
20
24
  import java.util.concurrent.Executors
21
25
  import java.util.concurrent.atomic.AtomicInteger
26
+ import javax.net.ssl.SSLHandshakeException
22
27
 
23
28
  class BundleFileStorageServiceTest {
24
29
  @get:Rule
@@ -352,59 +357,6 @@ class BundleFileStorageServiceTest {
352
357
  )
353
358
  }
354
359
 
355
- @Test
356
- fun `install identity persists across service instances and user clear keeps install id`() {
357
- val rootDir = temporaryFolder.newFolder("install-identity")
358
- val firstService = createService(rootDir)
359
-
360
- val firstInstallId = firstService.getInstallId()
361
- assertTrue(firstInstallId.isNotBlank())
362
-
363
- firstService.setUser("user-123", "alice")
364
- assertEquals(
365
- InstallationIdentity(
366
- installId = firstInstallId,
367
- userId = "user-123",
368
- username = "alice",
369
- ),
370
- loadInstallationIdentity(rootDir),
371
- )
372
-
373
- val restartedService = createService(rootDir)
374
- assertEquals(firstInstallId, restartedService.getInstallId())
375
-
376
- restartedService.setUser(null, null)
377
- assertEquals(
378
- InstallationIdentity(
379
- installId = firstInstallId,
380
- userId = null,
381
- username = null,
382
- ),
383
- loadInstallationIdentity(rootDir),
384
- )
385
- }
386
-
387
- @Test
388
- fun `user update keeps cached install id when persisted identity becomes unreadable`() {
389
- val rootDir = temporaryFolder.newFolder("install-identity-corruption")
390
- val service = createService(rootDir)
391
- val installId = service.getInstallId()
392
- val identityFile = File(bundleStoreDir(rootDir), InstallationIdentity.IDENTITY_FILENAME)
393
- identityFile.writeText("{")
394
-
395
- service.setUser("user-123", "alice")
396
-
397
- assertEquals(installId, service.getInstallId())
398
- assertEquals(
399
- InstallationIdentity(
400
- installId = installId,
401
- userId = "user-123",
402
- username = "alice",
403
- ),
404
- loadInstallationIdentity(rootDir),
405
- )
406
- }
407
-
408
360
  @Test
409
361
  fun `manifest driven install reuses matching built in asset before first OTA`() =
410
362
  runBlocking {
@@ -841,7 +793,11 @@ class BundleFileStorageServiceTest {
841
793
  ) {}
842
794
  }
843
795
 
844
- assertTrue(result.isFailure)
796
+ assertUpdateFailure(
797
+ result.exceptionOrNull(),
798
+ "MOVE_OPERATION_FAILED",
799
+ UpdateFailure.install(UpdateFailureReason.STORAGE),
800
+ )
845
801
  assertEquals("old", oldBundleFile.readText())
846
802
  assertFalse(File(bundleStoreDir(root), "target.install-backup").exists())
847
803
  assertEquals(oldBundleFile.absolutePath, preferences.getItem("HotUpdaterBundleURL"))
@@ -891,7 +847,11 @@ class BundleFileStorageServiceTest {
891
847
  ) {}
892
848
  }
893
849
 
894
- assertTrue(result.isFailure)
850
+ assertUpdateFailure(
851
+ result.exceptionOrNull(),
852
+ "UNKNOWN_ERROR",
853
+ UpdateFailure.install(UpdateFailureReason.STORAGE),
854
+ )
895
855
  assertEquals("old", oldBundleFile.readText())
896
856
  assertFalse(File(bundleStoreDir(root), "target.install-backup").exists())
897
857
  assertEquals(oldBundleFile.absolutePath, preferences.getItem("HotUpdaterBundleURL"))
@@ -951,7 +911,11 @@ class BundleFileStorageServiceTest {
951
911
  ) {}
952
912
  }
953
913
 
954
- assertTrue(result.isFailure)
914
+ assertUpdateFailure(
915
+ result.exceptionOrNull(),
916
+ "UNKNOWN_ERROR",
917
+ UpdateFailure.install(UpdateFailureReason.STORAGE),
918
+ )
955
919
  assertEquals(0, metadataWrites)
956
920
  assertEquals("old", oldBundleFile.readText())
957
921
  assertEquals(oldBundleFile.absolutePath, preferences.getItem("HotUpdaterBundleURL"))
@@ -1493,6 +1457,423 @@ class BundleFileStorageServiceTest {
1493
1457
  }
1494
1458
  }
1495
1459
 
1460
+ @Test
1461
+ fun `download failures of the manifest or a file report network http or storage`() =
1462
+ runBlocking {
1463
+ val network = UpdateFailure.download(UpdateFailureReason.NETWORK)
1464
+ val cases =
1465
+ listOf(
1466
+ Triple(HttpStatusException(404, "Not Found"), "DOWNLOAD_FAILED", UpdateFailure.http(404)),
1467
+ Triple(
1468
+ HttpStatusException(403, "Forbidden", originCode = "AccessDenied"),
1469
+ "DOWNLOAD_FAILED",
1470
+ UpdateFailure.http(403, originCode = "AccessDenied"),
1471
+ ),
1472
+ Triple(
1473
+ UnknownHostException("example.com"),
1474
+ "DOWNLOAD_FAILED",
1475
+ network.copy(transport = UpdateFailureTransport.DNS),
1476
+ ),
1477
+ Triple(
1478
+ SocketTimeoutException("timeout"),
1479
+ "DOWNLOAD_FAILED",
1480
+ network.copy(transport = UpdateFailureTransport.TIMEOUT),
1481
+ ),
1482
+ Triple(
1483
+ ConnectException("Connection refused"),
1484
+ "DOWNLOAD_FAILED",
1485
+ network.copy(transport = UpdateFailureTransport.CONNECTION),
1486
+ ),
1487
+ Triple(
1488
+ SSLHandshakeException("Chain validation failed"),
1489
+ "DOWNLOAD_FAILED",
1490
+ network.copy(transport = UpdateFailureTransport.TLS),
1491
+ ),
1492
+ Triple(IncompleteDownloadException(expectedSize = 10, actualSize = 4), "INCOMPLETE_DOWNLOAD", network),
1493
+ Triple(
1494
+ LocalStorageException(IOException("No space left on device")),
1495
+ "DOWNLOAD_FAILED",
1496
+ UpdateFailure.install(UpdateFailureReason.STORAGE),
1497
+ ),
1498
+ )
1499
+ val resources = mapOf(MANIFEST_URL to UpdateFailureResource.MANIFEST, BUNDLE_URL to UpdateFailureResource.FILE)
1500
+
1501
+ cases.forEachIndexed { index, (error, code, failure) ->
1502
+ resources.forEach { (failingUrl, resource) ->
1503
+ val root = temporaryFolder.newFolder("download-failure-$index-${resource.value}")
1504
+ val result = runOneFileUpdate(root, failures = mapOf(failingUrl to error))
1505
+
1506
+ assertUpdateFailure(result, code, failure.copy(resource = resource))
1507
+ assertNull(loadMetadata(root)?.stagingBundleId)
1508
+ }
1509
+ }
1510
+ }
1511
+
1512
+ @Test
1513
+ fun `verification failures report hash mismatch signature or invalid response`() =
1514
+ runBlocking {
1515
+ val hashMismatch = UpdateFailure.download(UpdateFailureReason.HASH_MISMATCH)
1516
+ val hashMismatchRoot = temporaryFolder.newFolder("manifest-hash-mismatch")
1517
+ assertUpdateFailure(
1518
+ runOneFileUpdate(hashMismatchRoot, manifestFileHash = sha256(hashMismatchRoot, "another manifest")),
1519
+ "SIGNATURE_VERIFICATION_FAILED",
1520
+ hashMismatch.copy(resource = UpdateFailureResource.MANIFEST),
1521
+ )
1522
+
1523
+ assertUpdateFailure(
1524
+ runOneFileUpdate(temporaryFolder.newFolder("file-hash-mismatch"), servedFile = "tampered".toByteArray()),
1525
+ "SIGNATURE_VERIFICATION_FAILED",
1526
+ hashMismatch.copy(resource = UpdateFailureResource.FILE),
1527
+ )
1528
+
1529
+ HotUpdaterConfig.publicKey = "configured-public-key"
1530
+ try {
1531
+ assertUpdateFailure(
1532
+ runOneFileUpdate(temporaryFolder.newFolder("unsigned-with-public-key")),
1533
+ "SIGNATURE_VERIFICATION_FAILED",
1534
+ UpdateFailure.download(UpdateFailureReason.SIGNATURE).copy(resource = UpdateFailureResource.MANIFEST),
1535
+ )
1536
+ } finally {
1537
+ HotUpdaterConfig.publicKey = null
1538
+ }
1539
+
1540
+ val invalidManifest =
1541
+ UpdateFailure.download(UpdateFailureReason.INVALID_RESPONSE).copy(resource = UpdateFailureResource.MANIFEST)
1542
+ val otherBundleRoot = temporaryFolder.newFolder("manifest-for-other-bundle")
1543
+ assertUpdateFailure(
1544
+ runOneFileUpdate(
1545
+ otherBundleRoot,
1546
+ manifest = manifestJson("other-bundle", mapOf("index.android.bundle" to sha256(otherBundleRoot, BUNDLE_CONTENT))),
1547
+ ),
1548
+ "INVALID_BUNDLE",
1549
+ invalidManifest,
1550
+ )
1551
+ assertUpdateFailure(
1552
+ runOneFileUpdate(temporaryFolder.newFolder("unparsable-manifest"), manifest = "not json"),
1553
+ "INVALID_BUNDLE",
1554
+ invalidManifest,
1555
+ )
1556
+ assertUpdateFailure(
1557
+ runOneFileUpdate(temporaryFolder.newFolder("missing-platform-bundle"), assetPath = "assets/image.png"),
1558
+ "INVALID_BUNDLE",
1559
+ invalidManifest,
1560
+ )
1561
+ val malformedUrlRoot = temporaryFolder.newFolder("malformed-file-url")
1562
+ assertUpdateFailure(
1563
+ runOneFileUpdate(
1564
+ malformedUrlRoot,
1565
+ descriptor = ChangedAssetDescriptor("not a url", sha256(malformedUrlRoot, BUNDLE_CONTENT)),
1566
+ ),
1567
+ "UNKNOWN_ERROR",
1568
+ invalidManifest.copy(resource = UpdateFailureResource.FILE),
1569
+ )
1570
+ }
1571
+
1572
+ @Test
1573
+ fun `install failures report extract or storage`() =
1574
+ runBlocking {
1575
+ val brotliRoot = temporaryFolder.newFolder("corrupt-brotli-file")
1576
+ val truncatedBrotli = Base64.getDecoder().decode(TarBrArchiveExtractorTest.VALID).let { it.copyOf(it.size / 2) }
1577
+ assertUpdateFailure(
1578
+ runOneFileUpdate(
1579
+ brotliRoot,
1580
+ descriptor = ChangedAssetDescriptor(BUNDLE_URL, sha256(brotliRoot, BUNDLE_CONTENT), fileCompression = "br"),
1581
+ servedFile = truncatedBrotli,
1582
+ ),
1583
+ "DOWNLOAD_FAILED",
1584
+ UpdateFailure.install(UpdateFailureReason.EXTRACT).copy(resource = UpdateFailureResource.FILE),
1585
+ )
1586
+
1587
+ val stagingRoot = temporaryFolder.newFolder("staging-directory-blocked")
1588
+ File(bundleStoreDir(stagingRoot), "target.tmp").writeText("a file where the staging directory belongs")
1589
+ assertUpdateFailure(
1590
+ runOneFileUpdate(stagingRoot),
1591
+ "DIRECTORY_CREATION_FAILED",
1592
+ UpdateFailure.install(UpdateFailureReason.STORAGE),
1593
+ )
1594
+ }
1595
+
1596
+ @Test
1597
+ fun `unanticipated failures report unknown in the stage they happened in`() =
1598
+ runBlocking {
1599
+ val downloadRoot = temporaryFolder.newFolder("unknown-download-failure")
1600
+ val downloadResult =
1601
+ runCatching {
1602
+ createService(downloadRoot).updateBundle("target", MANIFEST_URL, "hash", emptyMap()) {}
1603
+ }.exceptionOrNull()
1604
+ assertUpdateFailure(
1605
+ downloadResult,
1606
+ "UNKNOWN_ERROR",
1607
+ UpdateFailure.download(UpdateFailureReason.UNKNOWN).copy(resource = UpdateFailureResource.MANIFEST),
1608
+ )
1609
+ assertEquals("downloadFile should not be called in these tests", downloadResult?.message)
1610
+
1611
+ val installResult =
1612
+ runOneFileUpdate(
1613
+ temporaryFolder.newFolder("unknown-install-failure"),
1614
+ directoryRenamer = { source, destination ->
1615
+ if (source.name == "target.tmp") throw SecurityException("Promotion denied")
1616
+ source.renameTo(destination)
1617
+ },
1618
+ )
1619
+ assertUpdateFailure(installResult, "UNKNOWN_ERROR", UpdateFailure.install(UpdateFailureReason.UNKNOWN))
1620
+ assertEquals("Promotion denied", installResult?.message)
1621
+ }
1622
+
1623
+ @Test
1624
+ fun `a bundle built from changed files reports manifest delivery`() =
1625
+ runBlocking {
1626
+ val root = temporaryFolder.newFolder("manifest-delivery")
1627
+ val hash = sha256(root, BUNDLE_CONTENT)
1628
+ val manifest = manifestJson("target", mapOf("index.android.bundle" to hash))
1629
+ val downloads =
1630
+ BinaryMappingDownloadService(mapOf(MANIFEST_URL to manifest.toByteArray(), BUNDLE_URL to BUNDLE_CONTENT.toByteArray()))
1631
+ val service = createService(root, downloadService = downloads, builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()))
1632
+ val assets = mapOf("index.android.bundle" to ChangedAssetDescriptor(BUNDLE_URL, hash))
1633
+
1634
+ val downloaded = service.updateBundle("target", MANIFEST_URL, sha256(root, manifest), assets) {}
1635
+ // Installing it again reuses the staged file, so nothing is downloaded.
1636
+ val reused = service.updateBundle("target", MANIFEST_URL, sha256(root, manifest), assets) {}
1637
+
1638
+ assertEquals(UpdateBundleResult(BundleDelivery.MANIFEST, patchFallback = false), downloaded)
1639
+ assertEquals(UpdateBundleResult(BundleDelivery.MANIFEST, patchFallback = false), reused)
1640
+ assertEquals(1, downloads.calls.count { it == BUNDLE_URL })
1641
+ }
1642
+
1643
+ @Test
1644
+ fun `a bundle installed from the archive reports archive delivery`() =
1645
+ runBlocking {
1646
+ val root = temporaryFolder.newFolder("archive-delivery")
1647
+ val archiveBytes = Base64.getDecoder().decode(TarBrArchiveExtractorTest.VALID)
1648
+ val contents = mapOf("assets/image.png" to "image", "index.android.bundle" to "bundle")
1649
+ val hashes = contents.mapValues { sha256(root, it.value) }
1650
+ val manifest =
1651
+ manifestJson(
1652
+ bundleId = "target",
1653
+ assets = hashes,
1654
+ byteSizes = contents.mapValues { it.value.length.toLong() },
1655
+ downloadByteSizes = mapOf("assets/image.png" to 50, "index.android.bundle" to 55),
1656
+ archive = archiveJson(root, archiveBytes, tarByteSize = 3072),
1657
+ )
1658
+ val downloads =
1659
+ BinaryMappingDownloadService(mapOf(MANIFEST_URL to manifest.toByteArray(), ARCHIVE_URL to archiveBytes))
1660
+
1661
+ val result =
1662
+ createService(root, downloadService = downloads, builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()))
1663
+ .updateBundle(
1664
+ bundleId = "target",
1665
+ manifestUrl = MANIFEST_URL,
1666
+ manifestFileHash = sha256(root, manifest),
1667
+ assets = hashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) },
1668
+ archiveUrl = ARCHIVE_URL,
1669
+ progressCallback = {},
1670
+ )
1671
+
1672
+ assertEquals(UpdateBundleResult(BundleDelivery.ARCHIVE, patchFallback = false), result)
1673
+ assertEquals(listOf(MANIFEST_URL, ARCHIVE_URL), downloads.calls)
1674
+ }
1675
+
1676
+ @Test
1677
+ fun `a file produced by a patch reports patch delivery`() =
1678
+ runBlocking {
1679
+ val patchUpdate = PatchUpdate(temporaryFolder.newFolder("patch-delivery"))
1680
+
1681
+ val result = patchUpdate.run(servedPatch = patchUpdate.patchBytes)
1682
+
1683
+ assertEquals(UpdateBundleResult(BundleDelivery.PATCH, patchFallback = false), result.getOrThrow())
1684
+ assertEquals(listOf(MANIFEST_URL, PATCH_URL), patchUpdate.downloads.calls)
1685
+ assertEquals(PATCHED_CONTENT, File(bundleStoreDir(patchUpdate.root), "target/index.android.bundle").readText())
1686
+ }
1687
+
1688
+ @Test
1689
+ fun `a patch that cannot produce its file falls back to the file`() =
1690
+ runBlocking {
1691
+ val corruptPatch = PatchUpdate(temporaryFolder.newFolder("patch-fallback-corrupt"))
1692
+ assertEquals(
1693
+ UpdateBundleResult(BundleDelivery.MANIFEST, patchFallback = true),
1694
+ corruptPatch.run(servedPatch = "not a bsdiff patch".toByteArray()).getOrThrow(),
1695
+ )
1696
+ assertEquals(listOf(MANIFEST_URL, PATCH_URL, BUNDLE_URL), corruptPatch.downloads.calls)
1697
+
1698
+ val unreachablePatch = PatchUpdate(temporaryFolder.newFolder("patch-fallback-http"))
1699
+ assertEquals(
1700
+ UpdateBundleResult(BundleDelivery.MANIFEST, patchFallback = true),
1701
+ unreachablePatch.run(servedPatch = null).getOrThrow(),
1702
+ )
1703
+
1704
+ val corruptBase = PatchUpdate(temporaryFolder.newFolder("patch-fallback-base"))
1705
+ File(bundleStoreDir(corruptBase.root), "base-bundle/index.android.bundle").writeText("corrupt base")
1706
+ assertEquals(
1707
+ UpdateBundleResult(BundleDelivery.MANIFEST, patchFallback = true),
1708
+ corruptBase.run(servedPatch = corruptBase.patchBytes).getOrThrow(),
1709
+ )
1710
+ assertFalse(corruptBase.downloads.calls.contains(PATCH_URL))
1711
+
1712
+ // A patch for a bundle other than the running one is never attempted.
1713
+ val otherBase = PatchUpdate(temporaryFolder.newFolder("patch-for-other-base"))
1714
+ assertEquals(
1715
+ UpdateBundleResult(BundleDelivery.MANIFEST, patchFallback = false),
1716
+ otherBase.run(servedPatch = otherBase.patchBytes, patchBaseBundleId = "other-bundle").getOrThrow(),
1717
+ )
1718
+ assertEquals(listOf(MANIFEST_URL, BUNDLE_URL), otherBase.downloads.calls)
1719
+ }
1720
+
1721
+ /**
1722
+ * Bundle "target" whose index.android.bundle can be produced by a bsdiff
1723
+ * patch against the running bundle "base-bundle", or downloaded whole.
1724
+ */
1725
+ private inner class PatchUpdate(
1726
+ val root: File,
1727
+ ) {
1728
+ val patchBytes: ByteArray = Base64.getDecoder().decode(BsdiffPatchTest.BSDIFF_PATCH_FIXTURE_BASE64)
1729
+ private val preferences = InMemoryPreferencesService()
1730
+ lateinit var downloads: BinaryMappingDownloadService
1731
+
1732
+ init {
1733
+ val base = createBundleDir(root, "base-bundle")
1734
+ val baseFile = writeFile(base, "index.android.bundle", BASE_CONTENT)
1735
+ File(base, "manifest.json").writeText(
1736
+ manifestJson("base-bundle", mapOf("index.android.bundle" to sha256(root, BASE_CONTENT))),
1737
+ )
1738
+ writeMetadata(root, BundleMetadata(isolationKey = TEST_ISOLATION_KEY, stagingBundleId = "base-bundle"))
1739
+ preferences.setItem("HotUpdaterBundleURL", baseFile.absolutePath)
1740
+ }
1741
+
1742
+ /** Serves [servedPatch] with its hash, or a 404 for the patch when it is null. */
1743
+ suspend fun run(
1744
+ servedPatch: ByteArray?,
1745
+ patchBaseBundleId: String = "base-bundle",
1746
+ ): Result<UpdateBundleResult> {
1747
+ val targetHash = sha256(root, PATCHED_CONTENT)
1748
+ val manifest = manifestJson("target", mapOf("index.android.bundle" to targetHash))
1749
+ downloads =
1750
+ BinaryMappingDownloadService(
1751
+ buildMap {
1752
+ put(MANIFEST_URL, manifest.toByteArray())
1753
+ put(BUNDLE_URL, PATCHED_CONTENT.toByteArray())
1754
+ servedPatch?.let { put(PATCH_URL, it) }
1755
+ },
1756
+ failures = if (servedPatch == null) mapOf(PATCH_URL to HttpStatusException(404, "Not Found")) else emptyMap(),
1757
+ )
1758
+ val service =
1759
+ createService(
1760
+ root,
1761
+ preferences = preferences,
1762
+ downloadService = downloads,
1763
+ builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()),
1764
+ )
1765
+ val descriptor =
1766
+ ChangedAssetDescriptor(
1767
+ fileUrl = BUNDLE_URL,
1768
+ fileHash = targetHash,
1769
+ patch =
1770
+ BsdiffPatchDescriptor(
1771
+ algorithm = "bsdiff",
1772
+ baseBundleId = patchBaseBundleId,
1773
+ baseFileHash = sha256(root, BASE_CONTENT),
1774
+ patchFileHash = sha256(root, servedPatch ?: ByteArray(0)),
1775
+ patchUrl = PATCH_URL,
1776
+ ),
1777
+ )
1778
+ return runCatching {
1779
+ service.updateBundle("target", MANIFEST_URL, sha256(root, manifest), mapOf("index.android.bundle" to descriptor)) {}
1780
+ }
1781
+ }
1782
+ }
1783
+
1784
+ @Test
1785
+ fun `rejections that are not update failures carry no classification`() =
1786
+ runBlocking {
1787
+ val crashedRoot = temporaryFolder.newFolder("crashed-bundle")
1788
+ assertTrue(
1789
+ CrashedHistory(mutableListOf(CrashedBundleEntry(bundleId = "target", crashedAt = 1)))
1790
+ .saveToFile(File(bundleStoreDir(crashedRoot), CrashedHistory.CRASHED_HISTORY_FILENAME)),
1791
+ )
1792
+ assertUpdateFailure(
1793
+ runOneFileUpdate(crashedRoot),
1794
+ "BUNDLE_IN_CRASHED_HISTORY",
1795
+ null,
1796
+ )
1797
+
1798
+ val staleRoot = temporaryFolder.newFolder("stale-selection")
1799
+ val hash = sha256(staleRoot, BUNDLE_CONTENT)
1800
+ val manifest = manifestJson("target", mapOf("index.android.bundle" to hash))
1801
+ val served =
1802
+ BinaryMappingDownloadService(
1803
+ mapOf(MANIFEST_URL to manifest.toByteArray(), BUNDLE_URL to BUNDLE_CONTENT.toByteArray()),
1804
+ )
1805
+ lateinit var service: BundleFileStorageService
1806
+ val downloads =
1807
+ object : DownloadService {
1808
+ override suspend fun downloadFile(
1809
+ fileUrl: URL,
1810
+ destination: File,
1811
+ fileSizeCallback: ((Long) -> Unit)?,
1812
+ progressCallback: (DownloadProgress) -> Unit,
1813
+ ): DownloadResult {
1814
+ if (fileUrl.toString() == BUNDLE_URL) {
1815
+ // A newer catalog is accepted while the selected bundle downloads.
1816
+ service.acceptReleaseCatalog("project-a", "scope-production", 2, "hash-2", "production", "context-2")
1817
+ }
1818
+ return served.downloadFile(fileUrl, destination, fileSizeCallback, progressCallback)
1819
+ }
1820
+ }
1821
+ service = createService(staleRoot, downloadService = downloads, builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()))
1822
+ assertTrue(service.acceptReleaseCatalog("project-a", "scope-production", 1, "hash-1", "production", "context-1"))
1823
+ assertTrue(service.stageReleaseSelection(releaseSelection("release-one", "target", 1, "hash-1", "context-1")))
1824
+
1825
+ val staleResult =
1826
+ runCatching {
1827
+ service.updateBundle(
1828
+ "target",
1829
+ MANIFEST_URL,
1830
+ sha256(staleRoot, manifest),
1831
+ mapOf("index.android.bundle" to ChangedAssetDescriptor(BUNDLE_URL, hash)),
1832
+ ) {}
1833
+ }.exceptionOrNull()
1834
+
1835
+ assertTrue("Expected a stale selection, got $staleResult", staleResult is StaleReleaseSelectionException)
1836
+ assertNull(loadMetadata(staleRoot)?.stagingBundleId)
1837
+ }
1838
+
1839
+ /** Installs bundle "target" with one file, returning the failure, if any. */
1840
+ private suspend fun runOneFileUpdate(
1841
+ root: File,
1842
+ assetPath: String = "index.android.bundle",
1843
+ manifest: String = manifestJson("target", mapOf(assetPath to sha256(root, BUNDLE_CONTENT))),
1844
+ manifestFileHash: String = sha256(root, manifest),
1845
+ descriptor: ChangedAssetDescriptor = ChangedAssetDescriptor(BUNDLE_URL, sha256(root, BUNDLE_CONTENT)),
1846
+ servedFile: ByteArray = BUNDLE_CONTENT.toByteArray(),
1847
+ failures: Map<String, Exception> = emptyMap(),
1848
+ directoryRenamer: (File, File) -> Boolean = { source, destination -> source.renameTo(destination) },
1849
+ ): Throwable? {
1850
+ val service =
1851
+ createService(
1852
+ root,
1853
+ downloadService =
1854
+ BinaryMappingDownloadService(
1855
+ mapOf(MANIFEST_URL to manifest.toByteArray(), BUNDLE_URL to servedFile),
1856
+ failures,
1857
+ ),
1858
+ builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()),
1859
+ directoryRenamer = directoryRenamer,
1860
+ )
1861
+ return runCatching {
1862
+ service.updateBundle("target", MANIFEST_URL, manifestFileHash, mapOf(assetPath to descriptor)) {}
1863
+ }.exceptionOrNull()
1864
+ }
1865
+
1866
+ private fun assertUpdateFailure(
1867
+ error: Throwable?,
1868
+ code: String,
1869
+ failure: UpdateFailure?,
1870
+ ) {
1871
+ assertTrue("Expected HotUpdaterException, got $error", error is HotUpdaterException)
1872
+ val updateError = error as HotUpdaterException
1873
+ assertEquals(code, updateError.code)
1874
+ assertEquals(failure, updateError.failure)
1875
+ }
1876
+
1496
1877
  private fun createService(
1497
1878
  rootDir: File,
1498
1879
  preferences: InMemoryPreferencesService = InMemoryPreferencesService(),
@@ -1586,11 +1967,6 @@ class BundleFileStorageServiceTest {
1586
1967
  TEST_ISOLATION_KEY,
1587
1968
  )
1588
1969
 
1589
- private fun loadInstallationIdentity(rootDir: File): InstallationIdentity? =
1590
- InstallationIdentity.loadFromFile(
1591
- File(bundleStoreDir(rootDir), InstallationIdentity.IDENTITY_FILENAME),
1592
- )
1593
-
1594
1970
  private fun writeFile(
1595
1971
  rootDir: File,
1596
1972
  relativePath: String,
@@ -1727,6 +2103,7 @@ class BundleFileStorageServiceTest {
1727
2103
 
1728
2104
  private class BinaryMappingDownloadService(
1729
2105
  private val contents: Map<String, ByteArray>,
2106
+ private val failures: Map<String, Exception> = emptyMap(),
1730
2107
  ) : DownloadService {
1731
2108
  val calls = CopyOnWriteArrayList<String>()
1732
2109
 
@@ -1751,6 +2128,7 @@ class BundleFileStorageServiceTest {
1751
2128
  progressCallback: (DownloadProgress) -> Unit,
1752
2129
  ): DownloadResult {
1753
2130
  calls += fileUrl.toString()
2131
+ failures[fileUrl.toString()]?.let { return DownloadResult.Error(it) }
1754
2132
  val bytes = contents[fileUrl.toString()] ?: return DownloadResult.Error(IllegalArgumentException("Unexpected URL: $fileUrl"))
1755
2133
  destination.parentFile?.mkdirs()
1756
2134
  destination.writeBytes(bytes)
@@ -1847,5 +2225,12 @@ class BundleFileStorageServiceTest {
1847
2225
  private const val TEST_ISOLATION_KEY = "test-isolation-key"
1848
2226
  private const val MANIFEST_URL = "https://example.com/manifest.json"
1849
2227
  private const val ARCHIVE_URL = "https://example.com/bundle.tar.br"
2228
+ private const val BUNDLE_URL = "https://example.com/index.android.bundle"
2229
+ private const val BUNDLE_CONTENT = "bundle"
2230
+ private const val PATCH_URL = "https://example.com/index.android.bundle.bsdiff"
2231
+
2232
+ // The base and output of BsdiffPatchTest.BSDIFF_PATCH_FIXTURE_BASE64.
2233
+ private const val BASE_CONTENT = "console.log(\"base bundle\");\n"
2234
+ private const val PATCHED_CONTENT = "console.log(\"patched bundle\");\n"
1850
2235
  }
1851
2236
  }