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

258 lines
9.0 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.android
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.nextcloud.client.migrations.Migrations
import com.nextcloud.client.migrations.MigrationsManager
import com.nextcloud.client.migrations.MigrationsManagerImpl
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.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
@Suppress("FunctionNaming")
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()
migrations = listOf(
Migrations.Step(0, "first migration", migrationStep1),
Migrations.Step(1, "second migration", migrationStep2)
)
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)
assertEquals(setOf("0", "1"), 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.last()
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)
)
}
}