@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
@@ -1,8 +1,10 @@
1
1
  package com.hotupdater
2
2
 
3
3
  import kotlinx.coroutines.runBlocking
4
+ import okhttp3.ResponseBody.Companion.toResponseBody
4
5
  import org.junit.Assert.assertArrayEquals
5
6
  import org.junit.Assert.assertEquals
7
+ import org.junit.Assert.assertFalse
6
8
  import org.junit.Assert.assertNull
7
9
  import org.junit.Assert.assertTrue
8
10
  import org.junit.Rule
@@ -47,8 +49,291 @@ class OkHttpDownloadServiceTest {
47
49
  }
48
50
  }
49
51
 
52
+ @Test
53
+ fun `downloadFile reports a non-2xx response with its status and origin error code`() =
54
+ runBlocking {
55
+ val errorBody =
56
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
57
+ "<Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message>" +
58
+ "<Key>bundles/secret/manifest.json</Key></Error>"
59
+ val server = ChunkedResponseServer(errorBody.toByteArray(), statusLine = "404 Not Found")
60
+
61
+ try {
62
+ val destination = File(temporaryFolder.newFolder("http-error"), "manifest.json")
63
+
64
+ val result =
65
+ OkHttpDownloadService().downloadFile(
66
+ fileUrl = URL("http://127.0.0.1:${server.port}/manifest.json"),
67
+ destination = destination,
68
+ progressCallback = {},
69
+ )
70
+
71
+ val error = (result as DownloadResult.Error).exception
72
+ assertTrue("Expected HttpStatusException, got $error", error is HttpStatusException)
73
+ assertEquals(404, (error as HttpStatusException).statusCode)
74
+ assertEquals("NoSuchKey", error.originCode)
75
+ assertEquals("HTTP error 404: Not Found", error.message)
76
+ assertFalse(destination.exists())
77
+ } finally {
78
+ server.close()
79
+ }
80
+ }
81
+
82
+ @Test
83
+ fun `origin error codes come only from an XML Error Code of safe characters`() {
84
+ val cases =
85
+ mapOf(
86
+ "<Error><Code>AccessDenied</Code><Message>Access Denied</Message></Error>" to "AccessDenied",
87
+ "<?xml version='1.0' encoding='UTF-8'?><Error><Code>SignatureDoesNotMatch</Code></Error>" to "SignatureDoesNotMatch",
88
+ "<Error xmlns=\"urn:storage\">\n <Message>first</Message>\n <Code> ExpiredToken </Code>\n</Error>" to "ExpiredToken",
89
+ "<Error><Code>Rate.Limit_Exceeded-2</Code></Error>" to "Rate.Limit_Exceeded-2",
90
+ "<Error><Code>${"A".repeat(64)}</Code></Error>" to "A".repeat(64),
91
+ "<Error><Code>${"A".repeat(65)}</Code></Error>" to null,
92
+ "<Error><Code>Access Denied</Code></Error>" to null,
93
+ "<Error><Code>bundles/secret.json</Code></Error>" to null,
94
+ "<Error><Code></Code></Error>" to null,
95
+ "<Error><Message>No code</Message></Error><Code>Outside</Code>" to null,
96
+ "<Response><Code>NotAnError</Code></Response>" to null,
97
+ "Not Found" to null,
98
+ )
99
+
100
+ cases.forEach { (body, expected) -> assertEquals(body, expected, parseOriginErrorCode(body)) }
101
+ }
102
+
103
+ @Test
104
+ fun `origin error codes are read from the first 4 KB of the body only`() {
105
+ val limit = MAX_ORIGIN_ERROR_BODY_BYTES.toInt()
106
+ val code = "<Error><Code>AccessDenied</Code>"
107
+ val endsAtLimit = " ".repeat(limit - code.length) + code + "</Error>"
108
+ val closesPastLimit = " ".repeat(limit - code.length + 1) + code + "</Error>"
109
+
110
+ assertEquals("AccessDenied", readOriginErrorCode(endsAtLimit.toResponseBody()))
111
+ assertNull(readOriginErrorCode(closesPastLimit.toResponseBody()))
112
+ assertNull(readOriginErrorCode(null))
113
+ }
114
+
115
+ @Test
116
+ fun `downloadFile reports a destination it cannot write as a local storage failure`() =
117
+ runBlocking {
118
+ val server = ChunkedResponseServer("bundle-content".toByteArray())
119
+
120
+ try {
121
+ val notADirectory = temporaryFolder.newFile("not-a-directory")
122
+
123
+ val result =
124
+ OkHttpDownloadService().downloadFile(
125
+ fileUrl = URL("http://127.0.0.1:${server.port}/bundle"),
126
+ destination = File(notADirectory, "index.android.bundle"),
127
+ progressCallback = {},
128
+ )
129
+
130
+ val error = (result as DownloadResult.Error).exception
131
+ assertTrue("Expected LocalStorageException, got $error", error is LocalStorageException)
132
+ } finally {
133
+ server.close()
134
+ }
135
+ }
136
+
137
+ @Test
138
+ fun `downloadFile tries a transient failure again and succeeds on the next attempt`() =
139
+ runBlocking {
140
+ val payload = "bundle-after-retry".toByteArray()
141
+ val server =
142
+ SequencedResponseServer(
143
+ listOf(
144
+ "503 Service Unavailable" to "busy".toByteArray(),
145
+ "200 OK" to payload,
146
+ ),
147
+ )
148
+
149
+ try {
150
+ val destination = File(temporaryFolder.newFolder("retry"), "index.android.bundle")
151
+
152
+ val result =
153
+ OkHttpDownloadService(initialRetryDelayMs = 1).downloadFile(
154
+ fileUrl = URL("http://127.0.0.1:${server.port}/bundle"),
155
+ destination = destination,
156
+ progressCallback = {},
157
+ )
158
+
159
+ assertTrue("Expected success, got $result", result is DownloadResult.Success)
160
+ assertArrayEquals(payload, destination.readBytes())
161
+ assertEquals(2, server.requestCount)
162
+ } finally {
163
+ server.close()
164
+ }
165
+ }
166
+
167
+ @Test
168
+ fun `downloadFile tries a 404 once`() =
169
+ runBlocking {
170
+ val server =
171
+ SequencedResponseServer(
172
+ listOf(
173
+ "404 Not Found" to "missing".toByteArray(),
174
+ "200 OK" to "never-sent".toByteArray(),
175
+ ),
176
+ )
177
+
178
+ try {
179
+ val result =
180
+ OkHttpDownloadService(initialRetryDelayMs = 1).downloadFile(
181
+ fileUrl = URL("http://127.0.0.1:${server.port}/bundle"),
182
+ destination = File(temporaryFolder.newFolder("not-found"), "index.android.bundle"),
183
+ progressCallback = {},
184
+ )
185
+
186
+ val error = (result as DownloadResult.Error).exception
187
+ assertEquals(404, (error as HttpStatusException).statusCode)
188
+ assertEquals(1, server.requestCount)
189
+ } finally {
190
+ server.close()
191
+ }
192
+ }
193
+
194
+ @Test
195
+ fun `downloadFile stops after three attempts and returns the last classified failure`() =
196
+ runBlocking {
197
+ val server =
198
+ SequencedResponseServer(
199
+ listOf(
200
+ "500 Internal Server Error" to "one".toByteArray(),
201
+ "429 Too Many Requests" to "two".toByteArray(),
202
+ "502 Bad Gateway" to "<Error><Code>SlowDown</Code></Error>".toByteArray(),
203
+ "200 OK" to "never-sent".toByteArray(),
204
+ ),
205
+ )
206
+
207
+ try {
208
+ val result =
209
+ OkHttpDownloadService(initialRetryDelayMs = 1).downloadFile(
210
+ fileUrl = URL("http://127.0.0.1:${server.port}/bundle"),
211
+ destination = File(temporaryFolder.newFolder("exhausted"), "index.android.bundle"),
212
+ progressCallback = {},
213
+ )
214
+
215
+ val error = (result as DownloadResult.Error).exception as HttpStatusException
216
+ assertEquals(502, error.statusCode)
217
+ assertEquals("SlowDown", error.originCode)
218
+ assertEquals(UpdateFailure.http(502, "SlowDown"), UpdateFailure.ofDownloadError(error))
219
+ assertEquals(OkHttpDownloadService.MAX_ATTEMPTS, server.requestCount)
220
+ } finally {
221
+ server.close()
222
+ }
223
+ }
224
+
225
+ @Test
226
+ fun `downloadFileOnce does not retry`() =
227
+ runBlocking {
228
+ val server =
229
+ SequencedResponseServer(
230
+ listOf(
231
+ "503 Service Unavailable" to "busy".toByteArray(),
232
+ "200 OK" to "never-sent".toByteArray(),
233
+ ),
234
+ )
235
+
236
+ try {
237
+ val result =
238
+ OkHttpDownloadService(initialRetryDelayMs = 1).downloadFileOnce(
239
+ fileUrl = URL("http://127.0.0.1:${server.port}/bundle.tar.br"),
240
+ destination = File(temporaryFolder.newFolder("once"), "bundle.tar.br"),
241
+ progressCallback = {},
242
+ )
243
+
244
+ assertTrue(result is DownloadResult.Error)
245
+ assertEquals(1, server.requestCount)
246
+ } finally {
247
+ server.close()
248
+ }
249
+ }
250
+
251
+ @Test
252
+ fun `only transient download failures are retryable`() {
253
+ val retryable =
254
+ listOf(
255
+ HttpStatusException(408, "Request Timeout"),
256
+ HttpStatusException(429, "Too Many Requests"),
257
+ HttpStatusException(500, "Internal Server Error"),
258
+ HttpStatusException(503, "Service Unavailable"),
259
+ java.net.SocketTimeoutException("timeout"),
260
+ java.net.UnknownHostException("updates.example.com"),
261
+ java.net.ConnectException("Connection refused"),
262
+ IncompleteDownloadException(expectedSize = 10, actualSize = 4),
263
+ )
264
+ val final =
265
+ listOf(
266
+ HttpStatusException(400, "Bad Request"),
267
+ HttpStatusException(403, "Forbidden", "ExpiredToken"),
268
+ HttpStatusException(404, "Not Found"),
269
+ LocalStorageException(java.io.IOException("No space left on device")),
270
+ javax.net.ssl.SSLHandshakeException("certificate expired"),
271
+ java.io.IOException("Canceled"),
272
+ java.net.MalformedURLException("no protocol"),
273
+ IllegalArgumentException("unexpected url"),
274
+ Exception("Response body is null"),
275
+ )
276
+
277
+ retryable.forEach { assertTrue("$it should be retryable", isRetryableDownloadError(it)) }
278
+ final.forEach { assertFalse("$it should not be retryable", isRetryableDownloadError(it)) }
279
+ }
280
+
281
+ /** Answers each connection with the next response, then closes. */
282
+ private class SequencedResponseServer(
283
+ private val responses: List<Pair<String, ByteArray>>,
284
+ ) : AutoCloseable {
285
+ private val serverSocket = ServerSocket(0, responses.size, InetAddress.getByName("127.0.0.1"))
286
+
287
+ @Volatile
288
+ var requestCount = 0
289
+ private set
290
+
291
+ private val worker =
292
+ thread(start = true, isDaemon = true) {
293
+ try {
294
+ for ((statusLine, payload) in responses) {
295
+ serverSocket.accept().use { client ->
296
+ requestCount++
297
+ respond(client, statusLine, payload)
298
+ }
299
+ }
300
+ } catch (_: java.net.SocketException) {
301
+ // The test closed the server before every response was asked for.
302
+ }
303
+ }
304
+
305
+ val port: Int = serverSocket.localPort
306
+
307
+ private fun respond(
308
+ client: Socket,
309
+ statusLine: String,
310
+ payload: ByteArray,
311
+ ) {
312
+ drainRequestHeaders(client)
313
+ client.getOutputStream().use { output ->
314
+ output.write(
315
+ (
316
+ "HTTP/1.1 $statusLine\r\n" +
317
+ "Content-Type: application/octet-stream\r\n" +
318
+ "Content-Length: ${payload.size}\r\n" +
319
+ "Connection: close\r\n" +
320
+ "\r\n"
321
+ ).toByteArray(),
322
+ )
323
+ output.write(payload)
324
+ output.flush()
325
+ }
326
+ }
327
+
328
+ override fun close() {
329
+ serverSocket.close()
330
+ worker.join(1_000)
331
+ }
332
+ }
333
+
50
334
  private class ChunkedResponseServer(
51
335
  private val payload: ByteArray,
336
+ private val statusLine: String = "200 OK",
52
337
  ) : AutoCloseable {
53
338
  private val serverSocket = ServerSocket(0, 1, InetAddress.getByName("127.0.0.1"))
54
339
  private val worker =
@@ -66,7 +351,7 @@ class OkHttpDownloadServiceTest {
66
351
  client.getOutputStream().use { output ->
67
352
  output.write(
68
353
  (
69
- "HTTP/1.1 200 OK\r\n" +
354
+ "HTTP/1.1 $statusLine\r\n" +
70
355
  "Content-Type: application/octet-stream\r\n" +
71
356
  "Transfer-Encoding: chunked\r\n" +
72
357
  "Connection: close\r\n" +
@@ -81,31 +366,32 @@ class OkHttpDownloadServiceTest {
81
366
  }
82
367
  }
83
368
 
84
- private fun drainRequestHeaders(client: Socket) {
85
- val input = client.getInputStream()
86
- var matched = 0
87
- val terminator = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte(), '\r'.code.toByte(), '\n'.code.toByte())
88
-
89
- while (matched < terminator.size) {
90
- val next = input.read()
91
- if (next == -1) {
92
- break
93
- }
94
-
95
- matched =
96
- if (next.toByte() == terminator[matched]) {
97
- matched + 1
98
- } else if (next.toByte() == terminator[0]) {
99
- 1
100
- } else {
101
- 0
102
- }
103
- }
104
- }
105
-
106
369
  override fun close() {
107
370
  serverSocket.close()
108
371
  worker.join(1_000)
109
372
  }
110
373
  }
111
374
  }
375
+
376
+ /** Reads a request up to the blank line after its headers. */
377
+ private fun drainRequestHeaders(client: Socket) {
378
+ val input = client.getInputStream()
379
+ var matched = 0
380
+ val terminator = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte(), '\r'.code.toByte(), '\n'.code.toByte())
381
+
382
+ while (matched < terminator.size) {
383
+ val next = input.read()
384
+ if (next == -1) {
385
+ break
386
+ }
387
+
388
+ matched =
389
+ if (next.toByte() == terminator[matched]) {
390
+ matched + 1
391
+ } else if (next.toByte() == terminator[0]) {
392
+ 1
393
+ } else {
394
+ 0
395
+ }
396
+ }
397
+ }
@@ -0,0 +1,57 @@
1
+ package com.hotupdater
2
+
3
+ import com.hotupdater.PreviousProcessExit.ExitRecord
4
+ import org.junit.Assert.assertEquals
5
+ import org.junit.Assert.assertNull
6
+ import org.junit.Test
7
+
8
+ class PreviousProcessExitTest {
9
+ @Test
10
+ fun `reasons are named after ApplicationExitInfo without the REASON prefix`() {
11
+ val names =
12
+ listOf(
13
+ "UNKNOWN",
14
+ "EXIT_SELF",
15
+ "SIGNALED",
16
+ "LOW_MEMORY",
17
+ "CRASH",
18
+ "CRASH_NATIVE",
19
+ "ANR",
20
+ "INITIALIZATION_FAILURE",
21
+ "PERMISSION_CHANGE",
22
+ "EXCESSIVE_RESOURCE_USAGE",
23
+ "USER_REQUESTED",
24
+ "USER_STOPPED",
25
+ "DEPENDENCY_DIED",
26
+ "OTHER",
27
+ "FREEZER",
28
+ "PACKAGE_STATE_CHANGE",
29
+ "PACKAGE_UPDATED",
30
+ )
31
+
32
+ assertEquals(names, names.indices.map(PreviousProcessExit::reasonName))
33
+ assertEquals("UNKNOWN", PreviousProcessExit.reasonName(99))
34
+ assertEquals("UNKNOWN", PreviousProcessExit.reasonName(-1))
35
+ }
36
+
37
+ @Test
38
+ fun `the most recent exit of the main process is selected`() {
39
+ val records =
40
+ listOf(
41
+ ExitRecord("com.example:hotupdater_restart", reason = 1, timestamp = 300),
42
+ ExitRecord("com.example", reason = 4, timestamp = 100),
43
+ ExitRecord("com.example", reason = 6, timestamp = 200),
44
+ ExitRecord("com.example:sync", reason = 3, timestamp = 250),
45
+ ExitRecord(null, reason = 5, timestamp = 400),
46
+ )
47
+
48
+ assertEquals(6, PreviousProcessExit.selectReason(records, "com.example"))
49
+ assertNull(PreviousProcessExit.selectReason(records.filter { it.processName != "com.example" }, "com.example"))
50
+ assertNull(PreviousProcessExit.selectReason(emptyList(), "com.example"))
51
+ }
52
+
53
+ @Test
54
+ fun `nothing is reported before HotUpdater initializes`() {
55
+ assertNull(PreviousProcessExit.get())
56
+ }
57
+ }