From daa3125e577b71faa37196d9b851cbf8f795e828 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 29 Sep 2021 13:14:22 +0100 Subject: [PATCH] adding test cases around the SharedSecureStorageViewModel initial state and back flow - introduces a temporary workaorund the unit tests Mavericks by including a no op LifecycleRegistry - manually sets instant rx schedulers via the static helpers, the layers that set the schedulers are not currently injectable --- .../androidx/lifecycle/LifecycleRegistry.kt | 44 +++++ .../features/crypto/keys/KeysExporterTest.kt | 2 +- .../quads/SharedSecureStorageViewModelTest.kt | 151 ++++++++++++++++++ .../java/im/vector/app/test/Extensions.kt | 27 ++++ .../app/test/fakes/FakeCryptoService.kt | 11 ++ .../im/vector/app/test/fakes/FakeSession.kt | 7 +- .../fakes/FakeSharedSecretStorageService.kt | 72 +++++++++ .../app/test/fakes/FakeStringProvider.kt | 32 ++++ 8 files changed, 342 insertions(+), 4 deletions(-) create mode 100644 vector/src/test/java/androidx/lifecycle/LifecycleRegistry.kt create mode 100644 vector/src/test/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModelTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt diff --git a/vector/src/test/java/androidx/lifecycle/LifecycleRegistry.kt b/vector/src/test/java/androidx/lifecycle/LifecycleRegistry.kt new file mode 100644 index 0000000000..15a76f5e1e --- /dev/null +++ b/vector/src/test/java/androidx/lifecycle/LifecycleRegistry.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.lifecycle + +/** + * Manual test override to stop BaseMvRxViewModel from interacting with the android looper/main thread + * Tests will run on their original test worker threads + * + * This has been fixed is newer versions of Mavericks via LifecycleRegistry.createUnsafe + * https://github.com/airbnb/mavericks/blob/master/mvrx-rxjava2/src/main/kotlin/com/airbnb/mvrx/BaseMvRxViewModel.kt#L61 + */ +@Suppress("UNUSED") +class LifecycleRegistry(@Suppress("UNUSED_PARAMETER") lifecycleOwner: LifecycleOwner) : Lifecycle() { + + private var state = State.INITIALIZED + + fun setCurrentState(state: State) { + this.state = state + } + + override fun addObserver(observer: LifecycleObserver) { + TODO("Not yet implemented") + } + + override fun removeObserver(observer: LifecycleObserver) { + TODO("Not yet implemented") + } + + override fun getCurrentState() = state +} diff --git a/vector/src/test/java/im/vector/app/features/crypto/keys/KeysExporterTest.kt b/vector/src/test/java/im/vector/app/features/crypto/keys/KeysExporterTest.kt index a8997db855..c75abf5db4 100644 --- a/vector/src/test/java/im/vector/app/features/crypto/keys/KeysExporterTest.kt +++ b/vector/src/test/java/im/vector/app/features/crypto/keys/KeysExporterTest.kt @@ -40,7 +40,7 @@ class KeysExporterTest { private val cryptoService = FakeCryptoService() private val context = FakeContext() private val keysExporter = KeysExporter( - session = FakeSession(cryptoService = cryptoService), + session = FakeSession(fakeCryptoService = cryptoService), context = context.instance, dispatchers = CoroutineDispatchers(Dispatchers.Unconfined) ) diff --git a/vector/src/test/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModelTest.kt b/vector/src/test/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModelTest.kt new file mode 100644 index 0000000000..630cf753bd --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModelTest.kt @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.crypto.quads + +import com.airbnb.mvrx.Uninitialized +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.FakeStringProvider +import im.vector.app.test.test +import io.reactivex.android.plugins.RxAndroidPlugins +import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.schedulers.Schedulers +import org.junit.Test +import org.matrix.android.sdk.api.session.securestorage.IntegrityResult +import org.matrix.android.sdk.api.session.securestorage.KeyInfo +import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult +import org.matrix.android.sdk.api.session.securestorage.SecretStorageKeyContent +import org.matrix.android.sdk.api.session.securestorage.SsssPassphrase + +private const val IGNORED_PASSPHRASE_INTEGRITY = false +private val KEY_INFO_WITH_PASSPHRASE = KeyInfo( + id = "id", + content = SecretStorageKeyContent(passphrase = SsssPassphrase(null, 0, null)) +) +private val KEY_INFO_WITHOUT_PASSPHRASE = KeyInfo(id = "id", content = SecretStorageKeyContent(passphrase = null)) + +class SharedSecureStorageViewModelTest { + + private val stringProvider = FakeStringProvider() + private val session = FakeSession() + + init { + RxJavaPlugins.setInitNewThreadSchedulerHandler { Schedulers.trampoline() } + RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() } + } + + @Test + fun `given a key info with passphrase when initialising then step is EnterPassphrase`() { + givenKey(KEY_INFO_WITH_PASSPHRASE) + + val viewModel = createViewModel() + + viewModel.test().assertState(aViewState( + hasPassphrase = true, + step = SharedSecureStorageViewState.Step.EnterPassphrase + )) + } + + @Test + fun `given a key info without passphrase when initialising then step is EnterKey`() { + givenKey(KEY_INFO_WITHOUT_PASSPHRASE) + + val viewModel = createViewModel() + + viewModel.test().assertState(aViewState( + hasPassphrase = false, + step = SharedSecureStorageViewState.Step.EnterKey + )) + } + + @Test + fun `given on EnterKey step when going back then dismisses`() { + givenKey(KEY_INFO_WITHOUT_PASSPHRASE) + + val viewModel = createViewModel() + val test = viewModel.test() + + viewModel.handle(SharedSecureStorageAction.Back) + + test.assertEvents(SharedSecureStorageViewEvent.Dismiss) + } + + @Test + fun `given on passphrase step when using key then step is EnterKey`() { + givenKey(KEY_INFO_WITH_PASSPHRASE) + val viewModel = createViewModel() + val test = viewModel.test() + + viewModel.handle(SharedSecureStorageAction.UseKey) + + test.assertState(aViewState( + hasPassphrase = true, + step = SharedSecureStorageViewState.Step.EnterKey + )) + } + + @Test + fun `given a key info with passphrase and on EnterKey step when going back then step is EnterPassphrase`() { + givenKey(KEY_INFO_WITH_PASSPHRASE) + val viewModel = createViewModel() + val test = viewModel.test() + + viewModel.handle(SharedSecureStorageAction.UseKey) + viewModel.handle(SharedSecureStorageAction.Back) + + test.assertState(aViewState( + hasPassphrase = true, + step = SharedSecureStorageViewState.Step.EnterPassphrase + )) + } + + @Test + fun `given on passphrase step when going back then dismisses`() { + givenKey(KEY_INFO_WITH_PASSPHRASE) + val viewModel = createViewModel() + val test = viewModel.test() + + viewModel.handle(SharedSecureStorageAction.Back) + + test.assertEvents(SharedSecureStorageViewEvent.Dismiss) + } + + private fun createViewModel() = SharedSecureStorageViewModel( + SharedSecureStorageViewState(), + SharedSecureStorageActivity.Args(keyId = null, emptyList(), "alias"), + stringProvider.instance, + session + ) + + private fun aViewState(hasPassphrase: Boolean, step: SharedSecureStorageViewState.Step) = SharedSecureStorageViewState( + ready = true, + hasPassphrase = hasPassphrase, + checkingSSSSAction = Uninitialized, + step = step, + activeDeviceCount = 0, + showResetAllAction = false, + userId = "" + ) + + private fun givenKey(keyInfo: KeyInfo) { + givenHasAccessToSecrets() + session.fakeSharedSecretStorageService._defaultKey = KeyInfoResult.Success(keyInfo) + } + + private fun givenHasAccessToSecrets() { + session.fakeSharedSecretStorageService.integrityResult = IntegrityResult.Success(passphraseBased = IGNORED_PASSPHRASE_INTEGRITY) + } +} diff --git a/vector/src/test/java/im/vector/app/test/Extensions.kt b/vector/src/test/java/im/vector/app/test/Extensions.kt index 290268df1c..0d89208b2e 100644 --- a/vector/src/test/java/im/vector/app/test/Extensions.kt +++ b/vector/src/test/java/im/vector/app/test/Extensions.kt @@ -16,4 +16,31 @@ package im.vector.app.test +import com.airbnb.mvrx.MvRxState +import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.platform.VectorViewModelAction +import io.reactivex.observers.TestObserver +import org.amshove.kluent.shouldBeEqualTo + fun String.trimIndentOneLine() = trimIndent().replace("\n", "") + +fun VectorViewModel.test(): ViewModelTest { + val state = { com.airbnb.mvrx.withState(this) { it } } + val viewEvents = viewEvents.observe().test() + return ViewModelTest(state, viewEvents) +} + +class ViewModelTest( + val state: () -> S, + val viewEvents: TestObserver +) { + + fun assertEvents(vararg expected: VE) { + viewEvents.assertValues(*expected) + } + + fun assertState(expected: S) { + state() shouldBeEqualTo expected + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt index 735af4ea11..1ec1f31b45 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt @@ -16,12 +16,23 @@ package im.vector.app.test.fakes +import androidx.lifecycle.MutableLiveData import io.mockk.mockk import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo class FakeCryptoService : CryptoService by mockk() { var roomKeysExport = ByteArray(size = 1) + var cryptoDeviceInfos = mutableMapOf() override suspend fun exportRoomKeys(password: String) = roomKeysExport + + override fun getLiveCryptoDeviceInfo() = MutableLiveData(cryptoDeviceInfos.values.toList()) + + override fun getLiveCryptoDeviceInfo(userId: String) = getLiveCryptoDeviceInfo(listOf(userId)) + + override fun getLiveCryptoDeviceInfo(userIds: List) = MutableLiveData( + cryptoDeviceInfos.filterKeys { userIds.contains(it) }.values.toList() + ) } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt index 3400436705..f5ee51b020 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt @@ -18,10 +18,11 @@ package im.vector.app.test.fakes import io.mockk.mockk import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.crypto.CryptoService class FakeSession( - private val cryptoService: CryptoService = FakeCryptoService() + val fakeCryptoService: FakeCryptoService = FakeCryptoService(), + val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService() ) : Session by mockk(relaxed = true) { - override fun cryptoService() = cryptoService + override fun cryptoService() = fakeCryptoService + override val sharedSecretStorageService = fakeSharedSecretStorageService } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt new file mode 100644 index 0000000000..4f349f8506 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.session.securestorage.IntegrityResult +import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult +import org.matrix.android.sdk.api.session.securestorage.KeySigner +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService +import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo +import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec + +class FakeSharedSecretStorageService : SharedSecretStorageService { + + var integrityResult: IntegrityResult = IntegrityResult.Error(SharedSecretStorageError.OtherError(IllegalStateException())) + var _defaultKey: KeyInfoResult = KeyInfoResult.Error(SharedSecretStorageError.OtherError(IllegalStateException())) + + override suspend fun generateKey(keyId: String, key: SsssKeySpec?, keyName: String, keySigner: KeySigner?): SsssKeyCreationInfo { + TODO("Not yet implemented") + } + + override suspend fun generateKeyWithPassphrase(keyId: String, keyName: String, passphrase: String, keySigner: KeySigner, progressListener: ProgressListener?): SsssKeyCreationInfo { + TODO("Not yet implemented") + } + + override fun getKey(keyId: String): KeyInfoResult { + TODO("Not yet implemented") + } + + override fun getDefaultKey() = _defaultKey + + override suspend fun setDefaultKey(keyId: String) { + TODO("Not yet implemented") + } + + override fun hasKey(keyId: String): Boolean { + TODO("Not yet implemented") + } + + override suspend fun storeSecret(name: String, secretBase64: String, keys: List) { + TODO("Not yet implemented") + } + + override fun getAlgorithmsForSecret(name: String): List { + TODO("Not yet implemented") + } + + override suspend fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec): String { + TODO("Not yet implemented") + } + + override fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?) = integrityResult + + override fun requestSecret(name: String, myOtherDeviceId: String) { + TODO("Not yet implemented") + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt new file mode 100644 index 0000000000..f9001e3f8a --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import im.vector.app.core.resources.StringProvider +import io.mockk.every +import io.mockk.mockk + +class FakeStringProvider { + + val instance = mockk() + + init { + every { instance.getString(any()) } answers { + "test-${args[0]}" + } + } +}