TestMigrationsManager.kt 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. /*
  2. * Nextcloud Android client application
  3. *
  4. * @author Chris Narkiewicz
  5. * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU Affero General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU Affero General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Affero General Public License
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. package com.nextcloud.client.migrations
  21. import com.nextcloud.client.account.UserAccountManager
  22. import com.nextcloud.client.appinfo.AppInfo
  23. import com.nextcloud.client.core.ManualAsyncRunner
  24. import com.nhaarman.mockitokotlin2.mock
  25. import com.nhaarman.mockitokotlin2.never
  26. import com.nhaarman.mockitokotlin2.verify
  27. import com.nhaarman.mockitokotlin2.whenever
  28. import org.junit.Assert.assertEquals
  29. import org.junit.Assert.assertFalse
  30. import org.junit.Assert.assertTrue
  31. import org.junit.Before
  32. import org.junit.Test
  33. import org.junit.runner.RunWith
  34. import org.mockito.Mock
  35. import org.mockito.MockitoAnnotations
  36. import org.robolectric.RobolectricTestRunner
  37. import java.lang.RuntimeException
  38. import java.util.LinkedHashSet
  39. @RunWith(RobolectricTestRunner::class)
  40. class TestMigrationsManager {
  41. companion object {
  42. const val OLD_APP_VERSION = 41
  43. const val NEW_APP_VERSION = 42
  44. }
  45. lateinit var migrations: List<Migrations.Step>
  46. @Mock
  47. lateinit var appInfo: AppInfo
  48. lateinit var migrationsDb: MockSharedPreferences
  49. @Mock
  50. lateinit var userAccountManager: UserAccountManager
  51. lateinit var asyncRunner: ManualAsyncRunner
  52. internal lateinit var migrationsManager: MigrationsManagerImpl
  53. @Before
  54. fun setUp() {
  55. MockitoAnnotations.initMocks(this)
  56. val migrationStep1: Runnable = mock()
  57. val migrationStep2: Runnable = mock()
  58. val migrationStep3: Runnable = mock()
  59. migrations = listOf(
  60. Migrations.Step(0, "first migration", migrationStep1, true),
  61. Migrations.Step(1, "second migration", migrationStep2, true),
  62. Migrations.Step(2, "third optional migration", migrationStep3, false)
  63. )
  64. asyncRunner = ManualAsyncRunner()
  65. migrationsDb = MockSharedPreferences()
  66. whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
  67. migrationsManager = MigrationsManagerImpl(
  68. appInfo = appInfo,
  69. migrationsDb = migrationsDb,
  70. asyncRunner = asyncRunner,
  71. migrations = migrations
  72. )
  73. }
  74. @Test
  75. fun `inital status is unknown`() {
  76. // GIVEN
  77. // migration manager has not been used yets
  78. // THEN
  79. // status is not set
  80. assertEquals(MigrationsManager.Status.UNKNOWN, migrationsManager.status.value)
  81. }
  82. @Test
  83. fun `applied migrations are returned in order`() {
  84. // GIVEN
  85. // some migrations are marked as applied
  86. // migration ids are stored in random order
  87. val storedMigrationIds = LinkedHashSet<String>()
  88. storedMigrationIds.apply {
  89. add("3")
  90. add("0")
  91. add("2")
  92. add("1")
  93. }
  94. migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS, storedMigrationIds)
  95. // WHEN
  96. // applied migration ids are retrieved
  97. val ids = migrationsManager.getAppliedMigrations()
  98. // THEN
  99. // returned list is sorted
  100. assertEquals(ids, ids.sorted())
  101. }
  102. @Test
  103. @Suppress("MagicNumber")
  104. fun `registering new applied migration preserves old ids`() {
  105. // WHEN
  106. // some applied migrations are registered
  107. val appliedMigrationIds = setOf("0", "1", "2")
  108. migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS, appliedMigrationIds)
  109. // WHEN
  110. // new set of migration ids are registered
  111. // some ids are added again
  112. migrationsManager.addAppliedMigration(2, 3, 4)
  113. // THEN
  114. // new ids are appended to set of existing ids
  115. val expectedIds = setOf("0", "1", "2", "3", "4")
  116. assertEquals(expectedIds, migrationsDb.store.get(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS))
  117. }
  118. @Test
  119. fun `migrations are scheduled on background thread`() {
  120. // GIVEN
  121. // migrations can be applied
  122. assertEquals(0, migrationsManager.getAppliedMigrations().size)
  123. // WHEN
  124. // migration is started
  125. val count = migrationsManager.startMigration()
  126. // THEN
  127. // all migrations are scheduled on background thread
  128. // single task is scheduled
  129. assertEquals(migrations.size, count)
  130. assertEquals(1, asyncRunner.size)
  131. assertEquals(MigrationsManager.Status.RUNNING, migrationsManager.status.value)
  132. }
  133. @Test
  134. fun `applied migrations are recorded`() {
  135. // GIVEN
  136. // no migrations are applied yet
  137. // current app version is newer then last recorded migrated version
  138. whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
  139. migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_LAST_MIGRATED_VERSION, OLD_APP_VERSION)
  140. // WHEN
  141. // migration is run
  142. whenever(userAccountManager.migrateUserId()).thenReturn(true)
  143. val count = migrationsManager.startMigration()
  144. assertTrue(asyncRunner.runOne())
  145. // THEN
  146. // total migrations count is returned
  147. // migration functions are called
  148. // applied migrations are recorded
  149. // new app version code is recorded
  150. assertEquals(migrations.size, count)
  151. val allAppliedIds = migrations.map { it.id.toString() }.toSet()
  152. assertEquals(allAppliedIds, migrationsDb.store.get(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS))
  153. assertEquals(NEW_APP_VERSION, migrationsDb.store.get(MigrationsManagerImpl.DB_KEY_LAST_MIGRATED_VERSION))
  154. }
  155. @Test
  156. fun `migration error is recorded`() {
  157. // GIVEN
  158. // no migrations applied yet
  159. // WHEN
  160. // migrations are applied
  161. // one migration throws
  162. val lastMigration = migrations.findLast { it.mandatory } ?: throw IllegalStateException("Test fixture error")
  163. val errorMessage = "error message"
  164. whenever(lastMigration.function.run()).thenThrow(RuntimeException(errorMessage))
  165. migrationsManager.startMigration()
  166. assertTrue(asyncRunner.runOne())
  167. // THEN
  168. // failure is marked in the migration db
  169. // failure message is recorded
  170. // failed migration id is recorded
  171. assertEquals(MigrationsManager.Status.FAILED, migrationsManager.status.value)
  172. assertTrue(migrationsDb.getBoolean(MigrationsManagerImpl.DB_KEY_FAILED, false))
  173. assertEquals(
  174. errorMessage,
  175. migrationsDb.getString(MigrationsManagerImpl.DB_KEY_FAILED_MIGRATION_ERROR_MESSAGE, "")
  176. )
  177. assertEquals(
  178. lastMigration.id,
  179. migrationsDb.getInt(MigrationsManagerImpl.DB_KEY_FAILED_MIGRATION_ID, -1)
  180. )
  181. }
  182. @Test
  183. fun `migrations are not run if already run for an app version`() {
  184. // GIVEN
  185. // migrations were already run for the current app version
  186. whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
  187. migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_LAST_MIGRATED_VERSION, NEW_APP_VERSION)
  188. // WHEN
  189. // app is migrated again
  190. val migrationCount = migrationsManager.startMigration()
  191. // THEN
  192. // migration processing is skipped entirely
  193. // status is set to applied
  194. assertEquals(0, migrationCount)
  195. migrations.forEach {
  196. verify(it.function, never()).run()
  197. }
  198. assertEquals(MigrationsManager.Status.APPLIED, migrationsManager.status.value)
  199. }
  200. @Test
  201. fun `new app version is marked as migrated if no new migrations are available`() {
  202. // GIVEN
  203. // migrations were applied in previous version
  204. // new version has no new migrations
  205. whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
  206. migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_LAST_MIGRATED_VERSION, OLD_APP_VERSION)
  207. val applied = migrations.map { it.id.toString() }.toSet()
  208. migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS, applied)
  209. // WHEN
  210. // migration is started
  211. val startedCount = migrationsManager.startMigration()
  212. // THEN
  213. // no new migrations are run
  214. // new version is marked as migrated
  215. assertEquals(0, startedCount)
  216. assertEquals(
  217. NEW_APP_VERSION,
  218. migrationsDb.getInt(MigrationsManagerImpl.DB_KEY_LAST_MIGRATED_VERSION, -1)
  219. )
  220. }
  221. @Test
  222. fun `optional migration failure does not trigger a migration failure`() {
  223. // GIVEN
  224. // pending migrations
  225. // mandatory migrations are passing
  226. // one migration is optional and fails
  227. val optionalFailingMigration = migrations.first { !it.mandatory }
  228. whenever(optionalFailingMigration.function.run()).thenThrow(RuntimeException())
  229. // WHEN
  230. // migration is started
  231. val startedCount = migrationsManager.startMigration()
  232. asyncRunner.runOne()
  233. assertEquals(migrations.size, startedCount)
  234. // THEN
  235. // mandatory migrations are marked as applied
  236. // optional failed migration is not marked
  237. // no error
  238. // status is applied
  239. // failed migration is available during next migration
  240. val appliedMigrations = migrations.filter { it.mandatory }
  241. .map { it.id.toString() }
  242. .toSet()
  243. assertTrue("Fixture error", appliedMigrations.isNotEmpty())
  244. assertEquals(appliedMigrations, migrationsDb.store.get(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS))
  245. assertFalse(migrationsDb.getBoolean(MigrationsManagerImpl.DB_KEY_FAILED, false))
  246. assertEquals(MigrationsManager.Status.APPLIED, migrationsManager.status.value)
  247. }
  248. }