MigrationsManagerTest.kt 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. /*
  2. * Nextcloud - Android Client
  3. *
  4. * SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. package com.nextcloud.client.migrations
  8. import androidx.test.annotation.UiThreadTest
  9. import com.nextcloud.client.appinfo.AppInfo
  10. import com.nextcloud.client.core.ManualAsyncRunner
  11. import org.junit.Assert.assertEquals
  12. import org.junit.Assert.assertFalse
  13. import org.junit.Assert.assertTrue
  14. import org.junit.Before
  15. import org.junit.Test
  16. import org.mockito.Mock
  17. import org.mockito.MockitoAnnotations
  18. import org.mockito.kotlin.any
  19. import org.mockito.kotlin.anyOrNull
  20. import org.mockito.kotlin.inOrder
  21. import org.mockito.kotlin.mock
  22. import org.mockito.kotlin.never
  23. import org.mockito.kotlin.verify
  24. import org.mockito.kotlin.whenever
  25. class MigrationsManagerTest {
  26. companion object {
  27. const val OLD_APP_VERSION = 41
  28. const val NEW_APP_VERSION = 42
  29. }
  30. lateinit var migrationStep1Body: (Migrations.Step) -> Unit
  31. lateinit var migrationStep1: Migrations.Step
  32. lateinit var migrationStep2Body: (Migrations.Step) -> Unit
  33. lateinit var migrationStep2: Migrations.Step
  34. lateinit var migrationStep3Body: (Migrations.Step) -> Unit
  35. lateinit var migrationStep3: Migrations.Step
  36. lateinit var migrations: List<Migrations.Step>
  37. @Mock
  38. lateinit var appInfo: AppInfo
  39. lateinit var migrationsDbStore: MockSharedPreferences
  40. lateinit var migrationsDb: MigrationsDb
  41. lateinit var asyncRunner: ManualAsyncRunner
  42. internal lateinit var migrationsManager: MigrationsManagerImpl
  43. @Before
  44. fun setUp() {
  45. MockitoAnnotations.initMocks(this)
  46. migrationStep1Body = mock()
  47. migrationStep1 = Migrations.Step(0, "first migration", true, migrationStep1Body)
  48. migrationStep2Body = mock()
  49. migrationStep2 = Migrations.Step(1, "second optional migration", false, migrationStep2Body)
  50. migrationStep3Body = mock()
  51. migrationStep3 = Migrations.Step(2, "third migration", true, migrationStep3Body)
  52. migrations = listOf(migrationStep1, migrationStep2, migrationStep3)
  53. asyncRunner = ManualAsyncRunner()
  54. migrationsDbStore = MockSharedPreferences()
  55. migrationsDb = MigrationsDb(migrationsDbStore)
  56. whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
  57. migrationsManager = MigrationsManagerImpl(
  58. appInfo = appInfo,
  59. migrationsDb = migrationsDb,
  60. asyncRunner = asyncRunner,
  61. migrations = migrations
  62. )
  63. }
  64. @Test
  65. fun inital_status_is_unknown() {
  66. // GIVEN
  67. // migration manager has not been used yets
  68. // THEN
  69. // status is not set
  70. assertEquals(MigrationsManager.Status.UNKNOWN, migrationsManager.status.value)
  71. }
  72. @Test
  73. @UiThreadTest
  74. fun migrations_are_scheduled_on_background_thread() {
  75. // GIVEN
  76. // migrations can be applied
  77. assertEquals(0, migrationsDb.getAppliedMigrations().size)
  78. // WHEN
  79. // migration is started
  80. val count = migrationsManager.startMigration()
  81. // THEN
  82. // all migrations are scheduled on background thread
  83. // single task is scheduled
  84. assertEquals(migrations.size, count)
  85. assertEquals(1, asyncRunner.size)
  86. assertEquals(MigrationsManager.Status.RUNNING, migrationsManager.status.value)
  87. }
  88. @Test
  89. @UiThreadTest
  90. fun applied_migrations_are_recorded() {
  91. // GIVEN
  92. // no migrations are applied yet
  93. // current app version is newer then last recorded migrated version
  94. whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
  95. migrationsDb.lastMigratedVersion = OLD_APP_VERSION
  96. // WHEN
  97. // migration is run
  98. val count = migrationsManager.startMigration()
  99. assertTrue(asyncRunner.runOne())
  100. // THEN
  101. // total migrations count is returned
  102. // migration functions are called with step as argument
  103. // migrations are invoked in order
  104. // applied migrations are recorded
  105. // new app version code is recorded
  106. assertEquals(migrations.size, count)
  107. inOrder(migrationStep1.run, migrationStep2.run, migrationStep3.run).apply {
  108. verify(migrationStep1.run).invoke(migrationStep1)
  109. verify(migrationStep2.run).invoke(migrationStep2)
  110. verify(migrationStep3.run).invoke(migrationStep3)
  111. }
  112. val allAppliedIds = migrations.map { it.id }
  113. assertEquals(allAppliedIds, migrationsDb.getAppliedMigrations())
  114. assertEquals(NEW_APP_VERSION, migrationsDb.lastMigratedVersion)
  115. }
  116. @Test
  117. @UiThreadTest
  118. fun previously_run_migrations_are_not_run_again() {
  119. // GIVEN
  120. // some migrations were run before
  121. whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
  122. migrationsDb.lastMigratedVersion = OLD_APP_VERSION
  123. migrationsDb.addAppliedMigration(migrationStep1.id, migrationStep2.id)
  124. // WHEN
  125. // migrations are applied
  126. val count = migrationsManager.startMigration()
  127. assertTrue(asyncRunner.runOne())
  128. // THEN
  129. // applied migrations count is returned
  130. // previously applied migrations are not run
  131. // required migrations are applied
  132. // applied migrations are recorded
  133. // new app version code is recorded
  134. assertEquals(1, count)
  135. verify(migrationStep1.run, never()).invoke(anyOrNull())
  136. verify(migrationStep2.run, never()).invoke(anyOrNull())
  137. verify(migrationStep3.run).invoke(migrationStep3)
  138. val allAppliedIds = migrations.map { it.id }
  139. assertEquals(allAppliedIds, migrationsDb.getAppliedMigrations())
  140. assertEquals(NEW_APP_VERSION, migrationsDb.lastMigratedVersion)
  141. }
  142. @Test
  143. @UiThreadTest
  144. fun migration_error_is_recorded() {
  145. // GIVEN
  146. // no migrations applied yet
  147. // no prior failed migrations
  148. assertFalse(migrationsDb.isFailed)
  149. assertEquals(MigrationsDb.NO_FAILED_MIGRATION_ID, migrationsDb.failedMigrationId)
  150. // WHEN
  151. // migrations are applied
  152. // one migration throws
  153. val lastMigration = migrations.findLast { it.mandatory } ?: throw IllegalStateException("Test fixture error")
  154. val errorMessage = "error message"
  155. whenever(lastMigration.run.invoke(any())).thenThrow(RuntimeException(errorMessage))
  156. migrationsManager.startMigration()
  157. assertTrue(asyncRunner.runOne())
  158. // THEN
  159. // failure is marked in the migration db
  160. // failure message is recorded
  161. // failed migration id is recorded
  162. assertEquals(MigrationsManager.Status.FAILED, migrationsManager.status.value)
  163. assertTrue(migrationsDb.isFailed)
  164. assertEquals(errorMessage, migrationsDb.failureReason)
  165. assertEquals(lastMigration.id, migrationsDb.failedMigrationId)
  166. }
  167. @Test
  168. @UiThreadTest
  169. fun migrations_are_not_run_if_already_run_for_an_app_version() {
  170. // GIVEN
  171. // migrations were already run for the current app version
  172. whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
  173. migrationsDb.lastMigratedVersion = NEW_APP_VERSION
  174. // WHEN
  175. // app is migrated again
  176. val migrationCount = migrationsManager.startMigration()
  177. // THEN
  178. // migration processing is skipped entirely
  179. // status is set to applied
  180. assertEquals(0, migrationCount)
  181. listOf(migrationStep1, migrationStep2, migrationStep3).forEach {
  182. verify(it.run, never()).invoke(any())
  183. }
  184. assertEquals(MigrationsManager.Status.APPLIED, migrationsManager.status.value)
  185. }
  186. @Test
  187. @UiThreadTest
  188. fun new_app_version_is_marked_as_migrated_if_no_new_migrations_are_available() {
  189. // GIVEN
  190. // migrations were applied in previous version
  191. // new version has no new migrations
  192. whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
  193. migrationsDb.lastMigratedVersion = OLD_APP_VERSION
  194. migrations.forEach {
  195. migrationsDb.addAppliedMigration(it.id)
  196. }
  197. // WHEN
  198. // migration is started
  199. val startedCount = migrationsManager.startMigration()
  200. // THEN
  201. // no new migrations are run
  202. // new version is marked as migrated
  203. assertEquals(0, startedCount)
  204. assertEquals(
  205. NEW_APP_VERSION,
  206. migrationsDb.lastMigratedVersion
  207. )
  208. }
  209. @Test
  210. @UiThreadTest
  211. fun optional_migration_failure_does_not_trigger_a_migration_failure() {
  212. // GIVEN
  213. // pending migrations
  214. // mandatory migrations are passing
  215. // one migration is optional and fails
  216. assertEquals("Fixture should provide 1 optional, failing migration", 1, migrations.count { !it.mandatory })
  217. val optionalFailingMigration = migrations.first { !it.mandatory }
  218. whenever(optionalFailingMigration.run.invoke(any())).thenThrow(RuntimeException())
  219. // WHEN
  220. // migration is started
  221. val startedCount = migrationsManager.startMigration()
  222. asyncRunner.runOne()
  223. assertEquals(migrations.size, startedCount)
  224. // THEN
  225. // mandatory migrations are marked as applied
  226. // optional failed migration is not marked
  227. // no error
  228. // status is applied
  229. // failed migration is available during next migration
  230. val appliedMigrations = migrations.filter { it.mandatory }.map { it.id }
  231. assertTrue("Fixture error", appliedMigrations.isNotEmpty())
  232. assertEquals(appliedMigrations, migrationsDb.getAppliedMigrations())
  233. assertFalse(migrationsDb.isFailed)
  234. assertEquals(MigrationsManager.Status.APPLIED, migrationsManager.status.value)
  235. }
  236. }