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
This commit is contained in:
Adam Brown 2021-09-29 13:14:22 +01:00
parent edce14f48f
commit daa3125e57
8 changed files with 342 additions and 4 deletions

View File

@ -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
}

View File

@ -40,7 +40,7 @@ class KeysExporterTest {
private val cryptoService = FakeCryptoService() private val cryptoService = FakeCryptoService()
private val context = FakeContext() private val context = FakeContext()
private val keysExporter = KeysExporter( private val keysExporter = KeysExporter(
session = FakeSession(cryptoService = cryptoService), session = FakeSession(fakeCryptoService = cryptoService),
context = context.instance, context = context.instance,
dispatchers = CoroutineDispatchers(Dispatchers.Unconfined) dispatchers = CoroutineDispatchers(Dispatchers.Unconfined)
) )

View File

@ -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)
}
}

View File

@ -16,4 +16,31 @@
package im.vector.app.test 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 String.trimIndentOneLine() = trimIndent().replace("\n", "")
fun <S : MvRxState, VA : VectorViewModelAction, VE : VectorViewEvents> VectorViewModel<S, VA, VE>.test(): ViewModelTest<S, VE> {
val state = { com.airbnb.mvrx.withState(this) { it } }
val viewEvents = viewEvents.observe().test()
return ViewModelTest(state, viewEvents)
}
class ViewModelTest<S, VE>(
val state: () -> S,
val viewEvents: TestObserver<VE>
) {
fun assertEvents(vararg expected: VE) {
viewEvents.assertValues(*expected)
}
fun assertState(expected: S) {
state() shouldBeEqualTo expected
}
}

View File

@ -16,12 +16,23 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import androidx.lifecycle.MutableLiveData
import io.mockk.mockk import io.mockk.mockk
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
class FakeCryptoService : CryptoService by mockk() { class FakeCryptoService : CryptoService by mockk() {
var roomKeysExport = ByteArray(size = 1) var roomKeysExport = ByteArray(size = 1)
var cryptoDeviceInfos = mutableMapOf<String, CryptoDeviceInfo>()
override suspend fun exportRoomKeys(password: String) = roomKeysExport 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<String>) = MutableLiveData(
cryptoDeviceInfos.filterKeys { userIds.contains(it) }.values.toList()
)
} }

View File

@ -18,10 +18,11 @@ package im.vector.app.test.fakes
import io.mockk.mockk import io.mockk.mockk
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.CryptoService
class FakeSession( class FakeSession(
private val cryptoService: CryptoService = FakeCryptoService() val fakeCryptoService: FakeCryptoService = FakeCryptoService(),
val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService()
) : Session by mockk(relaxed = true) { ) : Session by mockk(relaxed = true) {
override fun cryptoService() = cryptoService override fun cryptoService() = fakeCryptoService
override val sharedSecretStorageService = fakeSharedSecretStorageService
} }

View File

@ -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<SharedSecretStorageService.KeyRef>) {
TODO("Not yet implemented")
}
override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> {
TODO("Not yet implemented")
}
override suspend fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec): String {
TODO("Not yet implemented")
}
override fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) = integrityResult
override fun requestSecret(name: String, myOtherDeviceId: String) {
TODO("Not yet implemented")
}
}

View File

@ -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<StringProvider>()
init {
every { instance.getString(any()) } answers {
"test-${args[0]}"
}
}
}