MigrationsManagerTest.kt 11 KB

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