123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- /*
- * Nextcloud Android client application
- *
- * @author Chris Narkiewicz
- * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
- package com.nextcloud.client.migrations
- import com.nextcloud.client.account.UserAccountManager
- import com.nextcloud.client.appinfo.AppInfo
- import com.nextcloud.client.core.ManualAsyncRunner
- import com.nhaarman.mockitokotlin2.mock
- import com.nhaarman.mockitokotlin2.never
- import com.nhaarman.mockitokotlin2.verify
- import com.nhaarman.mockitokotlin2.whenever
- import org.junit.Assert.assertEquals
- import org.junit.Assert.assertFalse
- import org.junit.Assert.assertTrue
- import org.junit.Before
- import org.junit.Test
- import org.junit.runner.RunWith
- import org.mockito.Mock
- import org.mockito.MockitoAnnotations
- import org.robolectric.RobolectricTestRunner
- import java.lang.RuntimeException
- import java.util.LinkedHashSet
- @RunWith(RobolectricTestRunner::class)
- class TestMigrationsManager {
- companion object {
- const val OLD_APP_VERSION = 41
- const val NEW_APP_VERSION = 42
- }
- lateinit var migrations: List<Migrations.Step>
- @Mock
- lateinit var appInfo: AppInfo
- lateinit var migrationsDb: MockSharedPreferences
- @Mock
- lateinit var userAccountManager: UserAccountManager
- lateinit var asyncRunner: ManualAsyncRunner
- internal lateinit var migrationsManager: MigrationsManagerImpl
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- val migrationStep1: Runnable = mock()
- val migrationStep2: Runnable = mock()
- val migrationStep3: Runnable = mock()
- migrations = listOf(
- Migrations.Step(0, "first migration", migrationStep1, true),
- Migrations.Step(1, "second migration", migrationStep2, true),
- Migrations.Step(2, "third optional migration", migrationStep3, false)
- )
- asyncRunner = ManualAsyncRunner()
- migrationsDb = MockSharedPreferences()
- whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
- migrationsManager = MigrationsManagerImpl(
- appInfo = appInfo,
- migrationsDb = migrationsDb,
- asyncRunner = asyncRunner,
- migrations = migrations
- )
- }
- @Test
- fun `inital status is unknown`() {
- // GIVEN
- // migration manager has not been used yets
- // THEN
- // status is not set
- assertEquals(MigrationsManager.Status.UNKNOWN, migrationsManager.status.value)
- }
- @Test
- fun `applied migrations are returned in order`() {
- // GIVEN
- // some migrations are marked as applied
- // migration ids are stored in random order
- val storedMigrationIds = LinkedHashSet<String>()
- storedMigrationIds.apply {
- add("3")
- add("0")
- add("2")
- add("1")
- }
- migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS, storedMigrationIds)
- // WHEN
- // applied migration ids are retrieved
- val ids = migrationsManager.getAppliedMigrations()
- // THEN
- // returned list is sorted
- assertEquals(ids, ids.sorted())
- }
- @Test
- @Suppress("MagicNumber")
- fun `registering new applied migration preserves old ids`() {
- // WHEN
- // some applied migrations are registered
- val appliedMigrationIds = setOf("0", "1", "2")
- migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS, appliedMigrationIds)
- // WHEN
- // new set of migration ids are registered
- // some ids are added again
- migrationsManager.addAppliedMigration(2, 3, 4)
- // THEN
- // new ids are appended to set of existing ids
- val expectedIds = setOf("0", "1", "2", "3", "4")
- assertEquals(expectedIds, migrationsDb.store.get(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS))
- }
- @Test
- fun `migrations are scheduled on background thread`() {
- // GIVEN
- // migrations can be applied
- assertEquals(0, migrationsManager.getAppliedMigrations().size)
- // WHEN
- // migration is started
- val count = migrationsManager.startMigration()
- // THEN
- // all migrations are scheduled on background thread
- // single task is scheduled
- assertEquals(migrations.size, count)
- assertEquals(1, asyncRunner.size)
- assertEquals(MigrationsManager.Status.RUNNING, migrationsManager.status.value)
- }
- @Test
- fun `applied migrations are recorded`() {
- // GIVEN
- // no migrations are applied yet
- // current app version is newer then last recorded migrated version
- whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
- migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_LAST_MIGRATED_VERSION, OLD_APP_VERSION)
- // WHEN
- // migration is run
- whenever(userAccountManager.migrateUserId()).thenReturn(true)
- val count = migrationsManager.startMigration()
- assertTrue(asyncRunner.runOne())
- // THEN
- // total migrations count is returned
- // migration functions are called
- // applied migrations are recorded
- // new app version code is recorded
- assertEquals(migrations.size, count)
- val allAppliedIds = migrations.map { it.id.toString() }.toSet()
- assertEquals(allAppliedIds, migrationsDb.store.get(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS))
- assertEquals(NEW_APP_VERSION, migrationsDb.store.get(MigrationsManagerImpl.DB_KEY_LAST_MIGRATED_VERSION))
- }
- @Test
- fun `migration error is recorded`() {
- // GIVEN
- // no migrations applied yet
- // WHEN
- // migrations are applied
- // one migration throws
- val lastMigration = migrations.findLast { it.mandatory } ?: throw IllegalStateException("Test fixture error")
- val errorMessage = "error message"
- whenever(lastMigration.function.run()).thenThrow(RuntimeException(errorMessage))
- migrationsManager.startMigration()
- assertTrue(asyncRunner.runOne())
- // THEN
- // failure is marked in the migration db
- // failure message is recorded
- // failed migration id is recorded
- assertEquals(MigrationsManager.Status.FAILED, migrationsManager.status.value)
- assertTrue(migrationsDb.getBoolean(MigrationsManagerImpl.DB_KEY_FAILED, false))
- assertEquals(
- errorMessage,
- migrationsDb.getString(MigrationsManagerImpl.DB_KEY_FAILED_MIGRATION_ERROR_MESSAGE, "")
- )
- assertEquals(
- lastMigration.id,
- migrationsDb.getInt(MigrationsManagerImpl.DB_KEY_FAILED_MIGRATION_ID, -1)
- )
- }
- @Test
- fun `migrations are not run if already run for an app version`() {
- // GIVEN
- // migrations were already run for the current app version
- whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
- migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_LAST_MIGRATED_VERSION, NEW_APP_VERSION)
- // WHEN
- // app is migrated again
- val migrationCount = migrationsManager.startMigration()
- // THEN
- // migration processing is skipped entirely
- // status is set to applied
- assertEquals(0, migrationCount)
- migrations.forEach {
- verify(it.function, never()).run()
- }
- assertEquals(MigrationsManager.Status.APPLIED, migrationsManager.status.value)
- }
- @Test
- fun `new app version is marked as migrated if no new migrations are available`() {
- // GIVEN
- // migrations were applied in previous version
- // new version has no new migrations
- whenever(appInfo.versionCode).thenReturn(NEW_APP_VERSION)
- migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_LAST_MIGRATED_VERSION, OLD_APP_VERSION)
- val applied = migrations.map { it.id.toString() }.toSet()
- migrationsDb.store.put(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS, applied)
- // WHEN
- // migration is started
- val startedCount = migrationsManager.startMigration()
- // THEN
- // no new migrations are run
- // new version is marked as migrated
- assertEquals(0, startedCount)
- assertEquals(
- NEW_APP_VERSION,
- migrationsDb.getInt(MigrationsManagerImpl.DB_KEY_LAST_MIGRATED_VERSION, -1)
- )
- }
- @Test
- fun `optional migration failure does not trigger a migration failure`() {
- // GIVEN
- // pending migrations
- // mandatory migrations are passing
- // one migration is optional and fails
- val optionalFailingMigration = migrations.first { !it.mandatory }
- whenever(optionalFailingMigration.function.run()).thenThrow(RuntimeException())
- // WHEN
- // migration is started
- val startedCount = migrationsManager.startMigration()
- asyncRunner.runOne()
- assertEquals(migrations.size, startedCount)
- // THEN
- // mandatory migrations are marked as applied
- // optional failed migration is not marked
- // no error
- // status is applied
- // failed migration is available during next migration
- val appliedMigrations = migrations.filter { it.mandatory }
- .map { it.id.toString() }
- .toSet()
- assertTrue("Fixture error", appliedMigrations.isNotEmpty())
- assertEquals(appliedMigrations, migrationsDb.store.get(MigrationsManagerImpl.DB_KEY_APPLIED_MIGRATIONS))
- assertFalse(migrationsDb.getBoolean(MigrationsManagerImpl.DB_KEY_FAILED, false))
- assertEquals(MigrationsManager.Status.APPLIED, migrationsManager.status.value)
- }
- }
|