|
@@ -0,0 +1,257 @@
|
|
|
+/*
|
|
|
+ * 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)
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|