Adding unit tests about reAuth actions for devices view model

This commit is contained in:
Maxime NATUREL 2022-10-25 16:14:32 +02:00
parent 5bcf2ac51e
commit 880ee4058c
2 changed files with 170 additions and 45 deletions

View file

@ -113,7 +113,6 @@ class DevicesViewModel @AssistedInject constructor(
}
}
// TODO update unit tests
override fun handle(action: DevicesAction) {
when (action) {
is DevicesAction.PasswordAuthDone -> handlePasswordAuthDone(action)

View file

@ -19,16 +19,17 @@ package im.vector.app.features.settings.devices.v2
import android.os.SystemClock
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.test.MavericksTestRule
import im.vector.app.R
import im.vector.app.core.session.clientinfo.MatrixClientInfoContent
import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo
import im.vector.app.features.settings.devices.v2.list.DeviceType
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo
import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakePendingAuthHandler
import im.vector.app.test.fakes.FakeSignoutSessionsUseCase
import im.vector.app.test.fakes.FakeStringProvider
import im.vector.app.test.fakes.FakeVerificationService
import im.vector.app.test.test
@ -36,20 +37,32 @@ import im.vector.app.test.testDispatcher
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkAll
import io.mockk.verify
import io.mockk.verifyAll
import kotlinx.coroutines.flow.flowOf
import org.amshove.kluent.shouldBeEqualTo
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
import javax.net.ssl.HttpsURLConnection
private const val A_CURRENT_DEVICE_ID = "current-device-id"
private const val A_DEVICE_ID_1 = "device-id-1"
private const val A_DEVICE_ID_2 = "device-id-2"
private const val A_PASSWORD = "password"
private const val AUTH_ERROR_MESSAGE = "auth-error-message"
private const val AN_ERROR_MESSAGE = "error-message"
class DevicesViewModelTest {
@ -60,9 +73,9 @@ class DevicesViewModelTest {
private val fakeStringProvider = FakeStringProvider()
private val getCurrentSessionCrossSigningInfoUseCase = mockk<GetCurrentSessionCrossSigningInfoUseCase>()
private val getDeviceFullInfoListUseCase = mockk<GetDeviceFullInfoListUseCase>()
private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk<RefreshDevicesOnCryptoDevicesChangeUseCase>()
private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk<RefreshDevicesOnCryptoDevicesChangeUseCase>(relaxed = true)
private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>()
private val fakeSignoutSessionsUseCase = mockk<SignoutSessionsUseCase>()
private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase()
private val fakeInterceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>()
private val fakePendingAuthHandler = FakePendingAuthHandler()
private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxUnitFun = true)
@ -76,7 +89,7 @@ class DevicesViewModelTest {
getDeviceFullInfoListUseCase = getDeviceFullInfoListUseCase,
refreshDevicesOnCryptoDevicesChangeUseCase = refreshDevicesOnCryptoDevicesChangeUseCase,
checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase,
signoutSessionsUseCase = fakeSignoutSessionsUseCase,
signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance,
interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase,
pendingAuthHandler = fakePendingAuthHandler.instance,
refreshDevicesUseCase = refreshDevicesUseCase,
@ -88,6 +101,20 @@ class DevicesViewModelTest {
// Needed for internal usage of Flow<T>.throttleFirst() inside the ViewModel
mockkStatic(SystemClock::class)
every { SystemClock.elapsedRealtime() } returns 1234
givenVerificationService()
givenCurrentSessionCrossSigningInfo()
givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2)
}
private fun givenVerificationService(): FakeVerificationService {
val fakeVerificationService = fakeActiveSessionHolder
.fakeSession
.fakeCryptoService
.fakeVerificationService
fakeVerificationService.givenAddListenerSucceeds()
fakeVerificationService.givenRemoveListenerSucceeds()
return fakeVerificationService
}
@After
@ -99,9 +126,6 @@ class DevicesViewModelTest {
fun `given the viewModel when initializing it then verification listener is added`() {
// Given
val fakeVerificationService = givenVerificationService()
givenCurrentSessionCrossSigningInfo()
givenDeviceFullInfoList()
givenRefreshDevicesOnCryptoDevicesChange()
// When
val viewModel = createViewModel()
@ -116,9 +140,6 @@ class DevicesViewModelTest {
fun `given the viewModel when clearing it then verification listener is removed`() {
// Given
val fakeVerificationService = givenVerificationService()
givenCurrentSessionCrossSigningInfo()
givenDeviceFullInfoList()
givenRefreshDevicesOnCryptoDevicesChange()
// When
val viewModel = createViewModel()
@ -133,10 +154,7 @@ class DevicesViewModelTest {
@Test
fun `given the viewModel when initializing it then view state is updated with current session cross signing info`() {
// Given
givenVerificationService()
val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo()
givenDeviceFullInfoList()
givenRefreshDevicesOnCryptoDevicesChange()
// When
val viewModelTest = createViewModel().test()
@ -149,10 +167,7 @@ class DevicesViewModelTest {
@Test
fun `given the viewModel when initializing it then view state is updated with current device full info list`() {
// Given
givenVerificationService()
givenCurrentSessionCrossSigningInfo()
val deviceFullInfoList = givenDeviceFullInfoList()
givenRefreshDevicesOnCryptoDevicesChange()
val deviceFullInfoList = givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2)
// When
val viewModelTest = createViewModel().test()
@ -168,10 +183,6 @@ class DevicesViewModelTest {
@Test
fun `given the viewModel when initializing it then devices are refreshed on crypto devices change`() {
// Given
givenVerificationService()
givenCurrentSessionCrossSigningInfo()
givenDeviceFullInfoList()
givenRefreshDevicesOnCryptoDevicesChange()
// When
createViewModel()
@ -183,10 +194,6 @@ class DevicesViewModelTest {
@Test
fun `given current session can be verified when handling verify current session action then self verification event is posted`() {
// Given
givenVerificationService()
givenCurrentSessionCrossSigningInfo()
givenDeviceFullInfoList()
givenRefreshDevicesOnCryptoDevicesChange()
val verifyCurrentSessionAction = DevicesAction.VerifyCurrentSession
coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns true
@ -207,10 +214,6 @@ class DevicesViewModelTest {
@Test
fun `given current session cannot be verified when handling verify current session action then reset secrets event is posted`() {
// Given
givenVerificationService()
givenCurrentSessionCrossSigningInfo()
givenDeviceFullInfoList()
givenRefreshDevicesOnCryptoDevicesChange()
val verifyCurrentSessionAction = DevicesAction.VerifyCurrentSession
coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns false
@ -228,18 +231,128 @@ class DevicesViewModelTest {
}
}
private fun givenVerificationService(): FakeVerificationService {
val fakeVerificationService = fakeActiveSessionHolder
.fakeSession
.fakeCryptoService
.fakeVerificationService
fakeVerificationService.givenAddListenerSucceeds()
fakeVerificationService.givenRemoveListenerSucceeds()
return fakeVerificationService
@Test
fun `given server error during multiSignout when handling multiSignout other sessions action then signout process is performed`() {
// Given
val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED)
fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError)
val expectedViewState = givenInitialViewState()
fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE)
// When
val viewModel = createViewModel()
val viewModelTest = viewModel.test()
viewModel.handle(DevicesAction.MultiSignoutOtherSessions)
// Then
viewModelTest
.assertStatesChanges(
expectedViewState,
{ copy(isLoading = true) },
{ copy(isLoading = false) }
)
.assertEvent { it is DevicesViewEvent.SignoutError && it.error.message == AUTH_ERROR_MESSAGE }
.finish()
}
@Test
fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() {
// Given
val error = Exception()
fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error)
val expectedViewState = givenInitialViewState()
fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE)
// When
val viewModel = createViewModel()
val viewModelTest = viewModel.test()
viewModel.handle(DevicesAction.MultiSignoutOtherSessions)
// Then
viewModelTest
.assertStatesChanges(
expectedViewState,
{ copy(isLoading = true) },
{ copy(isLoading = false) }
)
.assertEvent { it is DevicesViewEvent.SignoutError && it.error.message == AN_ERROR_MESSAGE }
.finish()
}
@Test
fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() {
// Given
val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase)
val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
val expectedReAuthEvent = DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)
// When
val viewModel = createViewModel()
val viewModelTest = viewModel.test()
viewModel.handle(DevicesAction.MultiSignoutOtherSessions)
// Then
viewModelTest
.assertEvent { it == expectedReAuthEvent }
.finish()
fakePendingAuthHandler.instance.pendingAuth shouldBeEqualTo expectedPendingAuth
fakePendingAuthHandler.instance.uiaContinuation shouldBeEqualTo reAuthNeeded.uiaContinuation
}
@Test
fun `given SSO auth has been done when handling ssoAuthDone action then corresponding method of pending auth handler is called`() {
// Given
justRun { fakePendingAuthHandler.instance.ssoAuthDone() }
// When
val viewModel = createViewModel()
val viewModelTest = viewModel.test()
viewModel.handle(DevicesAction.SsoAuthDone)
// Then
viewModelTest.finish()
verifyAll {
fakePendingAuthHandler.instance.ssoAuthDone()
}
}
@Test
fun `given password auth has been done when handling passwordAuthDone action then corresponding method of pending auth handler is called`() {
// Given
justRun { fakePendingAuthHandler.instance.passwordAuthDone(any()) }
// When
val viewModel = createViewModel()
val viewModelTest = viewModel.test()
viewModel.handle(DevicesAction.PasswordAuthDone(A_PASSWORD))
// Then
viewModelTest.finish()
verifyAll {
fakePendingAuthHandler.instance.passwordAuthDone(A_PASSWORD)
}
}
@Test
fun `given reAuth has been cancelled when handling reAuthCancelled action then corresponding method of pending auth handler is called`() {
// Given
justRun { fakePendingAuthHandler.instance.reAuthCancelled() }
// When
val viewModel = createViewModel()
val viewModelTest = viewModel.test()
viewModel.handle(DevicesAction.ReAuthCancelled)
// Then
viewModelTest.finish()
verifyAll {
fakePendingAuthHandler.instance.reAuthCancelled()
}
}
private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo {
val currentSessionCrossSigningInfo = mockk<CurrentSessionCrossSigningInfo>()
every { currentSessionCrossSigningInfo.deviceId } returns A_CURRENT_DEVICE_ID
every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns flowOf(currentSessionCrossSigningInfo)
return currentSessionCrossSigningInfo
}
@ -247,14 +360,19 @@ class DevicesViewModelTest {
/**
* Generate mocked deviceFullInfo list with 1 unverified and inactive + 1 verified and active.
*/
private fun givenDeviceFullInfoList(): List<DeviceFullInfo> {
private fun givenDeviceFullInfoList(deviceId1: String, deviceId2: String): List<DeviceFullInfo> {
val verifiedCryptoDeviceInfo = mockk<CryptoDeviceInfo>()
every { verifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true)
val unverifiedCryptoDeviceInfo = mockk<CryptoDeviceInfo>()
every { unverifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
val deviceInfo1 = mockk<DeviceInfo>()
every { deviceInfo1.deviceId } returns deviceId1
val deviceInfo2 = mockk<DeviceInfo>()
every { deviceInfo2.deviceId } returns deviceId2
val deviceFullInfo1 = DeviceFullInfo(
deviceInfo = mockk(),
deviceInfo = deviceInfo1,
cryptoDeviceInfo = verifiedCryptoDeviceInfo,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false,
@ -263,7 +381,7 @@ class DevicesViewModelTest {
matrixClientInfo = MatrixClientInfoContent(),
)
val deviceFullInfo2 = DeviceFullInfo(
deviceInfo = mockk(),
deviceInfo = deviceInfo2,
cryptoDeviceInfo = unverifiedCryptoDeviceInfo,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = true,
@ -277,7 +395,15 @@ class DevicesViewModelTest {
return deviceFullInfoList
}
private fun givenRefreshDevicesOnCryptoDevicesChange() {
coEvery { refreshDevicesOnCryptoDevicesChangeUseCase.execute() } just runs
private fun givenInitialViewState(): DevicesViewState {
val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo()
val deviceFullInfoList = givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2)
return DevicesViewState(
currentSessionCrossSigningInfo = currentSessionCrossSigningInfo,
devices = Success(deviceFullInfoList),
unverifiedSessionsCount = 1,
inactiveSessionsCount = 1,
isLoading = false,
)
}
}