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:
parent
edce14f48f
commit
daa3125e57
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue