nextcloud-android/src/androidTest/java/com/nextcloud/client/migrations/MigrationsManagerTest.kt

289 lines
10 KiB
Kotlin

/*
* 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 androidx.test.annotation.UiThreadTest
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.mockito.Mock
import org.mockito.MockitoAnnotations
import java.lang.RuntimeException
import java.util.LinkedHashSet
class MigrationsManagerTest {
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
@UiThreadTest
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
@UiThreadTest
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
@UiThreadTest
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
@UiThreadTest
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
@UiThreadTest
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
@UiThreadTest
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)
}
}