diff --git a/changelog.d/3898.bugfix b/changelog.d/3898.bugfix new file mode 100644 index 0000000000..49cab4adad --- /dev/null +++ b/changelog.d/3898.bugfix @@ -0,0 +1,2 @@ +Fixes the passphrase screen being incorrectly shown when pressing back on the key verification screen. +When the user doesn't have a passphrase set we don't show the passphrase screen. \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt index 9a5fc4ca06..b4ff9ab22c 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt @@ -76,7 +76,6 @@ class SharedSecureStorageViewModel @AssistedInject constructor( } init { - setState { copy(userId = session.myUserId) } @@ -167,10 +166,14 @@ class SharedSecureStorageViewModel @AssistedInject constructor( if (state.checkingSSSSAction is Loading) return@withState // ignore when (state.step) { SharedSecureStorageViewState.Step.EnterKey -> { - setState { - copy( - step = SharedSecureStorageViewState.Step.EnterPassphrase - ) + if (state.hasPassphrase) { + setState { + copy( + step = SharedSecureStorageViewState.Step.EnterPassphrase + ) + } + } else { + _viewEvents.post(SharedSecureStorageViewEvent.Dismiss) } } SharedSecureStorageViewState.Step.ResetAll -> { 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..8f48f10868 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModelTest.kt @@ -0,0 +1,148 @@ +/* + * 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.InstantRxRule +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.FakeStringProvider +import im.vector.app.test.test +import org.junit.Rule +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 { + + @get:Rule + val instantRx = InstantRxRule() + + private val stringProvider = FakeStringProvider() + private val session = FakeSession() + + @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/InstantRxRule.kt b/vector/src/test/java/im/vector/app/test/InstantRxRule.kt new file mode 100644 index 0000000000..1145cb7dd1 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/InstantRxRule.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 + +import io.reactivex.android.plugins.RxAndroidPlugins +import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.schedulers.Schedulers +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +class InstantRxRule : TestRule { + override fun apply(base: Statement, description: Description?): Statement { + RxJavaPlugins.setInitNewThreadSchedulerHandler { Schedulers.trampoline() } + RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() } + return base + } +} 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]}" + } + } +}