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

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 (240) hide show
  1. package/lib/commonjs/checkForUpdate.js +7 -9
  2. package/lib/commonjs/checkForUpdate.js.map +1 -1
  3. package/lib/commonjs/clientPlugin.js +7 -43
  4. package/lib/commonjs/clientPlugin.js.map +1 -1
  5. package/lib/commonjs/httpClient.js +7 -9
  6. package/lib/commonjs/httpClient.js.map +1 -1
  7. package/lib/commonjs/index.js +10 -0
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/native.js +4 -4
  10. package/lib/commonjs/native.js.map +1 -1
  11. package/lib/commonjs/pluginHost.js +26 -190
  12. package/lib/commonjs/pluginHost.js.map +1 -1
  13. package/lib/commonjs/releaseCatalogCache.js +7 -59
  14. package/lib/commonjs/releaseCatalogCache.js.map +1 -1
  15. package/lib/commonjs/sdkVersion.js +1 -1
  16. package/lib/module/checkForUpdate.js +1 -3
  17. package/lib/module/checkForUpdate.js.map +1 -1
  18. package/lib/module/clientPlugin.js +1 -41
  19. package/lib/module/clientPlugin.js.map +1 -1
  20. package/lib/module/httpClient.js +1 -3
  21. package/lib/module/httpClient.js.map +1 -1
  22. package/lib/module/index.js +2 -0
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/module/native.js +1 -1
  25. package/lib/module/native.js.map +1 -1
  26. package/lib/module/pluginHost.js +22 -187
  27. package/lib/module/pluginHost.js.map +1 -1
  28. package/lib/module/releaseCatalogCache.js +3 -54
  29. package/lib/module/releaseCatalogCache.js.map +1 -1
  30. package/lib/module/sdkVersion.js +1 -1
  31. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/clientPlugin.d.ts +1 -184
  33. package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/httpClient.d.ts +1 -2
  35. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/index.d.ts +1 -0
  37. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/native.d.ts +1 -1
  39. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/pluginHost.d.ts +3 -28
  41. package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +1 -13
  43. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  45. package/lib/typescript/commonjs/types.d.ts +1 -1
  46. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/wrap.types.d.ts +2 -2
  48. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  49. package/lib/typescript/module/clientPlugin.d.ts +1 -184
  50. package/lib/typescript/module/clientPlugin.d.ts.map +1 -1
  51. package/lib/typescript/module/httpClient.d.ts +1 -2
  52. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  53. package/lib/typescript/module/index.d.ts +1 -0
  54. package/lib/typescript/module/index.d.ts.map +1 -1
  55. package/lib/typescript/module/native.d.ts +1 -1
  56. package/lib/typescript/module/native.d.ts.map +1 -1
  57. package/lib/typescript/module/pluginHost.d.ts +3 -28
  58. package/lib/typescript/module/pluginHost.d.ts.map +1 -1
  59. package/lib/typescript/module/releaseCatalogCache.d.ts +1 -13
  60. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
  61. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  62. package/lib/typescript/module/types.d.ts +1 -1
  63. package/lib/typescript/module/types.d.ts.map +1 -1
  64. package/lib/typescript/module/wrap.types.d.ts +2 -2
  65. package/package.json +7 -16
  66. package/src/checkForUpdate.ts +4 -6
  67. package/src/clientPlugin.ts +17 -231
  68. package/src/httpClient.ts +4 -4
  69. package/src/index.ts +7 -0
  70. package/src/native.ts +1 -1
  71. package/src/pluginHost.ts +24 -241
  72. package/src/releaseCatalogCache.ts +7 -136
  73. package/src/sdkVersion.ts +1 -1
  74. package/src/types.ts +1 -1
  75. package/src/wrap.types.ts +2 -2
  76. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +0 -46
  77. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -2236
  78. package/android/src/test/java/com/hotupdater/HotUpdaterExceptionTest.kt +0 -220
  79. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -264
  80. package/android/src/test/java/com/hotupdater/HotUpdaterRecoveryStateTest.kt +0 -97
  81. package/android/src/test/java/com/hotupdater/InstallIdentityServiceTest.kt +0 -71
  82. package/android/src/test/java/com/hotupdater/KeyValueStorageServiceTest.kt +0 -106
  83. package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
  84. package/android/src/test/java/com/hotupdater/NoBackupContext.kt +0 -11
  85. package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -397
  86. package/android/src/test/java/com/hotupdater/PendingCrashRecoveryTest.kt +0 -44
  87. package/android/src/test/java/com/hotupdater/PreviousProcessExitTest.kt +0 -57
  88. package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
  89. package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
  90. package/android/src/test/java/com/hotupdater/TarBrArchiveExtractorTest.kt +0 -206
  91. package/lib/commonjs/appReady.spec.js +0 -360
  92. package/lib/commonjs/appReady.spec.js.map +0 -1
  93. package/lib/commonjs/appReady.test-utils.js +0 -25
  94. package/lib/commonjs/appReady.test-utils.js.map +0 -1
  95. package/lib/commonjs/baseURL.js +0 -16
  96. package/lib/commonjs/baseURL.js.map +0 -1
  97. package/lib/commonjs/catalogCacheNative.spec.js +0 -31
  98. package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
  99. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -936
  100. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  101. package/lib/commonjs/entryImportGraph.spec.js +0 -50
  102. package/lib/commonjs/entryImportGraph.spec.js.map +0 -1
  103. package/lib/commonjs/httpClient.spec.js +0 -171
  104. package/lib/commonjs/httpClient.spec.js.map +0 -1
  105. package/lib/commonjs/index.spec.js +0 -395
  106. package/lib/commonjs/index.spec.js.map +0 -1
  107. package/lib/commonjs/iosInterop.spec.js +0 -18
  108. package/lib/commonjs/iosInterop.spec.js.map +0 -1
  109. package/lib/commonjs/native.spec.js +0 -910
  110. package/lib/commonjs/native.spec.js.map +0 -1
  111. package/lib/commonjs/pluginHost.spec.js +0 -292
  112. package/lib/commonjs/pluginHost.spec.js.map +0 -1
  113. package/lib/commonjs/plugins/insights/eventId.js +0 -129
  114. package/lib/commonjs/plugins/insights/eventId.js.map +0 -1
  115. package/lib/commonjs/plugins/insights/eventId.spec.js +0 -47
  116. package/lib/commonjs/plugins/insights/eventId.spec.js.map +0 -1
  117. package/lib/commonjs/plugins/insights/index.js +0 -272
  118. package/lib/commonjs/plugins/insights/index.js.map +0 -1
  119. package/lib/commonjs/plugins/insights/insights.spec.js +0 -640
  120. package/lib/commonjs/plugins/insights/insights.spec.js.map +0 -1
  121. package/lib/commonjs/plugins/insights/sender.js +0 -128
  122. package/lib/commonjs/plugins/insights/sender.js.map +0 -1
  123. package/lib/commonjs/plugins/insights/sender.spec.js +0 -265
  124. package/lib/commonjs/plugins/insights/sender.spec.js.map +0 -1
  125. package/lib/commonjs/plugins/insights/state.js +0 -145
  126. package/lib/commonjs/plugins/insights/state.js.map +0 -1
  127. package/lib/commonjs/releaseCatalogCache.spec.js +0 -213
  128. package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
  129. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
  130. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  131. package/lib/commonjs/store.notification.spec.js +0 -84
  132. package/lib/commonjs/store.notification.spec.js.map +0 -1
  133. package/lib/commonjs/store.spec.js +0 -182
  134. package/lib/commonjs/store.spec.js.map +0 -1
  135. package/lib/commonjs/updateError.spec.js +0 -149
  136. package/lib/commonjs/updateError.spec.js.map +0 -1
  137. package/lib/commonjs/wrap.progress.spec.js +0 -195
  138. package/lib/commonjs/wrap.progress.spec.js.map +0 -1
  139. package/lib/commonjs/wrap.spec.js +0 -232
  140. package/lib/commonjs/wrap.spec.js.map +0 -1
  141. package/lib/module/appReady.spec.js +0 -359
  142. package/lib/module/appReady.spec.js.map +0 -1
  143. package/lib/module/appReady.test-utils.js +0 -19
  144. package/lib/module/appReady.test-utils.js.map +0 -1
  145. package/lib/module/baseURL.js +0 -11
  146. package/lib/module/baseURL.js.map +0 -1
  147. package/lib/module/catalogCacheNative.spec.js +0 -31
  148. package/lib/module/catalogCacheNative.spec.js.map +0 -1
  149. package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -935
  150. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  151. package/lib/module/entryImportGraph.spec.js +0 -50
  152. package/lib/module/entryImportGraph.spec.js.map +0 -1
  153. package/lib/module/httpClient.spec.js +0 -171
  154. package/lib/module/httpClient.spec.js.map +0 -1
  155. package/lib/module/index.spec.js +0 -394
  156. package/lib/module/index.spec.js.map +0 -1
  157. package/lib/module/iosInterop.spec.js +0 -18
  158. package/lib/module/iosInterop.spec.js.map +0 -1
  159. package/lib/module/native.spec.js +0 -909
  160. package/lib/module/native.spec.js.map +0 -1
  161. package/lib/module/pluginHost.spec.js +0 -291
  162. package/lib/module/pluginHost.spec.js.map +0 -1
  163. package/lib/module/plugins/insights/eventId.js +0 -122
  164. package/lib/module/plugins/insights/eventId.js.map +0 -1
  165. package/lib/module/plugins/insights/eventId.spec.js +0 -47
  166. package/lib/module/plugins/insights/eventId.spec.js.map +0 -1
  167. package/lib/module/plugins/insights/index.js +0 -268
  168. package/lib/module/plugins/insights/index.js.map +0 -1
  169. package/lib/module/plugins/insights/insights.spec.js +0 -639
  170. package/lib/module/plugins/insights/insights.spec.js.map +0 -1
  171. package/lib/module/plugins/insights/sender.js +0 -123
  172. package/lib/module/plugins/insights/sender.js.map +0 -1
  173. package/lib/module/plugins/insights/sender.spec.js +0 -265
  174. package/lib/module/plugins/insights/sender.spec.js.map +0 -1
  175. package/lib/module/plugins/insights/state.js +0 -139
  176. package/lib/module/plugins/insights/state.js.map +0 -1
  177. package/lib/module/releaseCatalogCache.spec.js +0 -213
  178. package/lib/module/releaseCatalogCache.spec.js.map +0 -1
  179. package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
  180. package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  181. package/lib/module/store.notification.spec.js +0 -85
  182. package/lib/module/store.notification.spec.js.map +0 -1
  183. package/lib/module/store.spec.js +0 -181
  184. package/lib/module/store.spec.js.map +0 -1
  185. package/lib/module/updateError.spec.js +0 -149
  186. package/lib/module/updateError.spec.js.map +0 -1
  187. package/lib/module/wrap.progress.spec.js +0 -196
  188. package/lib/module/wrap.progress.spec.js.map +0 -1
  189. package/lib/module/wrap.spec.js +0 -233
  190. package/lib/module/wrap.spec.js.map +0 -1
  191. package/lib/typescript/commonjs/appReady.test-utils.d.ts +0 -9
  192. package/lib/typescript/commonjs/appReady.test-utils.d.ts.map +0 -1
  193. package/lib/typescript/commonjs/baseURL.d.ts +0 -4
  194. package/lib/typescript/commonjs/baseURL.d.ts.map +0 -1
  195. package/lib/typescript/commonjs/plugins/insights/eventId.d.ts +0 -18
  196. package/lib/typescript/commonjs/plugins/insights/eventId.d.ts.map +0 -1
  197. package/lib/typescript/commonjs/plugins/insights/index.d.ts +0 -40
  198. package/lib/typescript/commonjs/plugins/insights/index.d.ts.map +0 -1
  199. package/lib/typescript/commonjs/plugins/insights/sender.d.ts +0 -74
  200. package/lib/typescript/commonjs/plugins/insights/sender.d.ts.map +0 -1
  201. package/lib/typescript/commonjs/plugins/insights/state.d.ts +0 -46
  202. package/lib/typescript/commonjs/plugins/insights/state.d.ts.map +0 -1
  203. package/lib/typescript/module/appReady.test-utils.d.ts +0 -9
  204. package/lib/typescript/module/appReady.test-utils.d.ts.map +0 -1
  205. package/lib/typescript/module/baseURL.d.ts +0 -4
  206. package/lib/typescript/module/baseURL.d.ts.map +0 -1
  207. package/lib/typescript/module/plugins/insights/eventId.d.ts +0 -18
  208. package/lib/typescript/module/plugins/insights/eventId.d.ts.map +0 -1
  209. package/lib/typescript/module/plugins/insights/index.d.ts +0 -40
  210. package/lib/typescript/module/plugins/insights/index.d.ts.map +0 -1
  211. package/lib/typescript/module/plugins/insights/sender.d.ts +0 -74
  212. package/lib/typescript/module/plugins/insights/sender.d.ts.map +0 -1
  213. package/lib/typescript/module/plugins/insights/state.d.ts +0 -46
  214. package/lib/typescript/module/plugins/insights/state.d.ts.map +0 -1
  215. package/plugins/insights/package.json +0 -8
  216. package/src/appReady.spec.ts +0 -393
  217. package/src/appReady.test-utils.ts +0 -31
  218. package/src/baseURL.ts +0 -15
  219. package/src/catalogCacheNative.spec.ts +0 -63
  220. package/src/checkForUpdate.releaseCatalog.spec.ts +0 -966
  221. package/src/entryImportGraph.spec.ts +0 -65
  222. package/src/httpClient.spec.ts +0 -216
  223. package/src/index.spec.ts +0 -434
  224. package/src/iosInterop.spec.ts +0 -30
  225. package/src/native.spec.ts +0 -978
  226. package/src/pluginHost.spec.ts +0 -297
  227. package/src/plugins/insights/eventId.spec.ts +0 -60
  228. package/src/plugins/insights/eventId.ts +0 -171
  229. package/src/plugins/insights/index.ts +0 -367
  230. package/src/plugins/insights/insights.spec.ts +0 -732
  231. package/src/plugins/insights/sender.spec.ts +0 -287
  232. package/src/plugins/insights/sender.ts +0 -221
  233. package/src/plugins/insights/state.ts +0 -184
  234. package/src/releaseCatalogCache.spec.ts +0 -304
  235. package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
  236. package/src/store.notification.spec.ts +0 -102
  237. package/src/store.spec.ts +0 -215
  238. package/src/updateError.spec.ts +0 -144
  239. package/src/wrap.progress.spec.ts +0 -232
  240. package/src/wrap.spec.ts +0 -262
@@ -1,106 +0,0 @@
1
- package com.hotupdater
2
-
3
- import org.json.JSONObject
4
- import org.junit.Assert.assertEquals
5
- import org.junit.Assert.assertNull
6
- import org.junit.Rule
7
- import org.junit.Test
8
- import org.junit.rules.TemporaryFolder
9
- import java.io.File
10
-
11
- class KeyValueStorageServiceTest {
12
- @get:Rule
13
- val temporaryFolder = TemporaryFolder()
14
-
15
- @Test
16
- fun `sets replaces and removes values`() {
17
- val storage = KeyValueStorageService(storageFile())
18
- assertNull(storage.getItem("plugins/insights/queue"))
19
-
20
- storage.setItem("plugins/insights/queue", "[1]")
21
- storage.setItem("plugins/insights/day", "2026-09-30")
22
- storage.setItem("plugins/insights/queue", "[1,2]")
23
- storage.setItem("plugins/other/empty", "")
24
- assertEquals("[1,2]", storage.getItem("plugins/insights/queue"))
25
- assertEquals("", storage.getItem("plugins/other/empty"))
26
-
27
- storage.setItem("plugins/insights/queue", null)
28
- storage.setItem("plugins/never-set", null)
29
- assertNull(storage.getItem("plugins/insights/queue"))
30
- assertEquals("2026-09-30", storage.getItem("plugins/insights/day"))
31
- }
32
-
33
- @Test
34
- fun `values persist across instances`() {
35
- val file = storageFile()
36
- KeyValueStorageService(file).apply {
37
- setItem("plugins/insights/queue", "[{\"id\":\"é\",\"note\":\"line\\nbreak\"}]")
38
- setItem("plugins/insights/day", "2026-09-30")
39
- setItem("plugins/insights/day", null)
40
- }
41
-
42
- val reloaded = KeyValueStorageService(file)
43
-
44
- assertEquals("[{\"id\":\"é\",\"note\":\"line\\nbreak\"}]", reloaded.getItem("plugins/insights/queue"))
45
- assertNull(reloaded.getItem("plugins/insights/day"))
46
- }
47
-
48
- @Test
49
- fun `a corrupt file reads as empty and the next set replaces it`() {
50
- val file = storageFile().apply { writeText("{\"plugins/a\":") }
51
- val storage = KeyValueStorageService(file)
52
-
53
- assertNull(storage.getItem("plugins/a"))
54
- storage.setItem("plugins/b", "value")
55
-
56
- assertEquals(mapOf("plugins/b" to "value"), readStored(file))
57
- }
58
-
59
- @Test
60
- fun `values that are not strings read as missing`() {
61
- val file = storageFile().apply { writeText("{\"plugins/a\":1,\"plugins/b\":\"kept\",\"plugins/c\":null}") }
62
- val storage = KeyValueStorageService(file)
63
-
64
- assertNull(storage.getItem("plugins/a"))
65
- assertEquals("kept", storage.getItem("plugins/b"))
66
- assertNull(storage.getItem("plugins/c"))
67
- }
68
-
69
- @Test
70
- fun `concurrent sets are all persisted`() {
71
- val file = storageFile()
72
- val storage = KeyValueStorageService(file)
73
-
74
- (0 until 4)
75
- .map { worker ->
76
- Thread {
77
- repeat(25) { index -> storage.setItem("plugins/worker-$worker/$index", "$worker:$index") }
78
- }.apply { start() }
79
- }.forEach(Thread::join)
80
-
81
- val stored = readStored(file)
82
- assertEquals(100, stored.size)
83
- (0 until 4).forEach { worker ->
84
- repeat(25) { index -> assertEquals("$worker:$index", stored["plugins/worker-$worker/$index"]) }
85
- }
86
- }
87
-
88
- @Test
89
- fun `is stored under noBackupFilesDir`() {
90
- val noBackupDirectory = temporaryFolder.newFolder("no_backup")
91
-
92
- KeyValueStorageService.create(NoBackupContext(noBackupDirectory)).setItem("plugins/insights/queue", "[]")
93
-
94
- assertEquals(
95
- mapOf("plugins/insights/queue" to "[]"),
96
- readStored(File(noBackupDirectory, "hot-updater/storage.json")),
97
- )
98
- }
99
-
100
- private fun storageFile(): File = File(temporaryFolder.newFolder(), "hot-updater/storage.json").apply { parentFile?.mkdirs() }
101
-
102
- private fun readStored(file: File): Map<String, String> {
103
- val json = JSONObject(file.readText())
104
- return json.keys().asSequence().associateWith { json.getString(it) }
105
- }
106
- }
@@ -1,20 +0,0 @@
1
- package com.hotupdater
2
-
3
- import org.junit.Assert.assertEquals
4
- import org.junit.Assert.assertNull
5
- import org.junit.Test
6
-
7
- class NativeConfigUtilsTest {
8
- @Test
9
- fun `coerces typed manifest metadata to strings`() {
10
- assertEquals("123", NativeConfigUtils.coerceManifestMetaDataString(123))
11
- assertEquals("true", NativeConfigUtils.coerceManifestMetaDataString(true))
12
- assertEquals("-16711936", NativeConfigUtils.coerceManifestMetaDataString(-16711936))
13
- }
14
-
15
- @Test
16
- fun `ignores missing and empty manifest metadata`() {
17
- assertNull(NativeConfigUtils.coerceManifestMetaDataString(null))
18
- assertNull(NativeConfigUtils.coerceManifestMetaDataString(""))
19
- }
20
- }
@@ -1,11 +0,0 @@
1
- package com.hotupdater
2
-
3
- import android.content.ContextWrapper
4
- import java.io.File
5
-
6
- /** A context whose `noBackupFilesDir` is a test directory. */
7
- internal class NoBackupContext(
8
- private val noBackupDirectory: File,
9
- ) : ContextWrapper(null) {
10
- override fun getNoBackupFilesDir(): File = noBackupDirectory
11
- }
@@ -1,397 +0,0 @@
1
- package com.hotupdater
2
-
3
- import kotlinx.coroutines.runBlocking
4
- import okhttp3.ResponseBody.Companion.toResponseBody
5
- import org.junit.Assert.assertArrayEquals
6
- import org.junit.Assert.assertEquals
7
- import org.junit.Assert.assertFalse
8
- import org.junit.Assert.assertNull
9
- import org.junit.Assert.assertTrue
10
- import org.junit.Rule
11
- import org.junit.Test
12
- import org.junit.rules.TemporaryFolder
13
- import java.io.File
14
- import java.net.InetAddress
15
- import java.net.ServerSocket
16
- import java.net.Socket
17
- import java.net.URL
18
- import kotlin.concurrent.thread
19
-
20
- class OkHttpDownloadServiceTest {
21
- @get:Rule
22
- val temporaryFolder = TemporaryFolder()
23
-
24
- @Test
25
- fun `downloadFile succeeds when content length is unknown`() =
26
- runBlocking {
27
- val payload = "bundle-content-without-content-length".repeat(512).toByteArray()
28
- val server = ChunkedResponseServer(payload)
29
-
30
- try {
31
- val destinationDir = temporaryFolder.newFolder("downloads")
32
- val destination = File(destinationDir, "bundle.android.bundle")
33
- var reportedSize: Long? = null
34
-
35
- val result =
36
- OkHttpDownloadService().downloadFile(
37
- fileUrl = URL("http://127.0.0.1:${server.port}/bundle"),
38
- destination = destination,
39
- fileSizeCallback = { size -> reportedSize = size },
40
- progressCallback = {},
41
- )
42
-
43
- assertTrue(result is DownloadResult.Success)
44
- assertEquals(payload.size.toLong(), destination.length())
45
- assertArrayEquals(payload, destination.readBytes())
46
- assertNull(reportedSize)
47
- } finally {
48
- server.close()
49
- }
50
- }
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
-
334
- private class ChunkedResponseServer(
335
- private val payload: ByteArray,
336
- private val statusLine: String = "200 OK",
337
- ) : AutoCloseable {
338
- private val serverSocket = ServerSocket(0, 1, InetAddress.getByName("127.0.0.1"))
339
- private val worker =
340
- thread(start = true, isDaemon = true) {
341
- serverSocket.use { socket ->
342
- socket.accept().use(::respond)
343
- }
344
- }
345
-
346
- val port: Int = serverSocket.localPort
347
-
348
- private fun respond(client: Socket) {
349
- drainRequestHeaders(client)
350
-
351
- client.getOutputStream().use { output ->
352
- output.write(
353
- (
354
- "HTTP/1.1 $statusLine\r\n" +
355
- "Content-Type: application/octet-stream\r\n" +
356
- "Transfer-Encoding: chunked\r\n" +
357
- "Connection: close\r\n" +
358
- "\r\n"
359
- ).toByteArray(),
360
- )
361
- output.write(payload.size.toString(16).toByteArray())
362
- output.write("\r\n".toByteArray())
363
- output.write(payload)
364
- output.write("\r\n0\r\n\r\n".toByteArray())
365
- output.flush()
366
- }
367
- }
368
-
369
- override fun close() {
370
- serverSocket.close()
371
- worker.join(1_000)
372
- }
373
- }
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
- }
@@ -1,44 +0,0 @@
1
- package com.hotupdater
2
-
3
- import org.junit.Assert.assertEquals
4
- import org.junit.Assert.assertFalse
5
- import org.junit.Assert.assertNull
6
- import org.junit.Rule
7
- import org.junit.Test
8
- import org.junit.rules.TemporaryFolder
9
- import java.io.File
10
-
11
- class PendingCrashRecoveryTest {
12
- @get:Rule
13
- val temporaryFolder = TemporaryFolder()
14
-
15
- @Test
16
- fun `a watchdog reader waits until the native crash marker is fully written`() {
17
- val marker = temporaryFolder.newFile("crash-marker.json")
18
- marker.writeText("{\"bundleId\":\"crashed-bundle\",\"shouldRollback\":")
19
-
20
- assertNull(PendingCrashRecovery.loadFromFile(marker))
21
-
22
- marker.appendText("true}\n")
23
- assertEquals(
24
- PendingCrashRecovery("crashed-bundle", true),
25
- PendingCrashRecovery.loadFromFile(marker),
26
- )
27
- }
28
-
29
- @Test
30
- fun `missing or empty markers do not request a restart`() {
31
- assertNull(PendingCrashRecovery.loadFromFile(File(temporaryFolder.root, "missing.json")))
32
- assertNull(PendingCrashRecovery.loadFromFile(temporaryFolder.newFile("empty.json")))
33
- }
34
-
35
- @Test
36
- fun `a crash after content appeared does not request rollback`() {
37
- val marker = temporaryFolder.newFile("verified-crash.json")
38
- marker.writeText("{\"bundleId\":\"verified-bundle\",\"shouldRollback\":false}")
39
-
40
- val recovery = requireNotNull(PendingCrashRecovery.loadFromFile(marker))
41
- assertFalse(recovery.shouldRollback)
42
- assertEquals("verified-bundle", recovery.launchedBundleId)
43
- }
44
- }
@@ -1,57 +0,0 @@
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
- }
@@ -1,41 +0,0 @@
1
- package com.hotupdater
2
-
3
- import org.junit.Assert.assertEquals
4
- import org.junit.Assert.assertNull
5
- import org.junit.Rule
6
- import org.junit.Test
7
- import org.junit.rules.TemporaryFolder
8
- import java.io.File
9
-
10
- class RelativePathResolverTest {
11
- @get:Rule
12
- val temporaryFolder = TemporaryFolder()
13
-
14
- @Test
15
- fun `resolves a standard archive directory entry ending in a slash`() {
16
- val root = temporaryFolder.root
17
-
18
- val resolved = RelativePathResolver.resolveInside(root, "drawable-mdpi/")
19
-
20
- assertEquals(
21
- File(root, "drawable-mdpi").canonicalFile,
22
- resolved?.canonicalFile,
23
- )
24
- }
25
-
26
- @Test
27
- fun `rejects unsafe archive directory paths ending in a slash`() {
28
- val root = temporaryFolder.root
29
- val unsafePaths =
30
- listOf(
31
- "/",
32
- "../",
33
- "drawable-mdpi/../",
34
- "drawable-mdpi//",
35
- )
36
-
37
- unsafePaths.forEach { path ->
38
- assertNull(path, RelativePathResolver.resolveInside(root, path))
39
- }
40
- }
41
- }