1
0
mirror of https://github.com/vector-im/riotX-android synced 2025-10-06 00:02:48 +02:00

Handling incoming verification

Fix SAS state signaling problem, more tests, back navigation
This commit is contained in:
valere
2023-01-09 09:19:04 +01:00
parent 8606ac92e1
commit a217ec220f
19 changed files with 529 additions and 163 deletions

View File

@@ -17,21 +17,36 @@
package im.vector.app
import android.net.Uri
import android.view.View
import androidx.lifecycle.Observer
import im.vector.app.ui.robot.ElementRobot
import androidx.test.espresso.Espresso
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import im.vector.app.espresso.tools.waitUntilActivityVisible
import im.vector.app.espresso.tools.waitUntilViewVisible
import im.vector.app.features.home.HomeActivity
import im.vector.app.ui.robot.AnalyticsRobot
import im.vector.app.ui.robot.OnboardingRobot
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.hamcrest.CoreMatchers
import org.junit.Assert
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.getRequest
import org.matrix.android.sdk.api.session.sync.SyncState
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@@ -42,7 +57,8 @@ abstract class VerificationTestBase {
val homeServerUrl: String = "http://10.0.2.2:8080"
protected val uiTestBase = OnboardingRobot()
protected val elementRobot = ElementRobot()
protected val testScope = CoroutineScope(SupervisorJob())
fun createAccountAndSync(
matrix: Matrix,
@@ -136,4 +152,67 @@ abstract class VerificationTestBase {
lock.await(20_000, TimeUnit.MILLISECONDS)
}
protected fun loginAndClickVerifyToast(userId: String): Session {
uiTestBase.login(userId = userId, password = password, homeServerUrl = homeServerUrl)
tryOrNull {
val analyticsRobot = AnalyticsRobot()
analyticsRobot.optOut()
}
waitUntilActivityVisible<HomeActivity> {
waitUntilViewVisible(ViewMatchers.withId(R.id.roomListContainer))
}
val activity = EspressoHelper.getCurrentActivity()!!
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
withIdlingResource(initialSyncIdlingResource(uiSession)) {
waitUntilViewVisible(ViewMatchers.withId(R.id.roomListContainer))
}
// THIS IS THE ONLY WAY I FOUND TO CLICK ON ALERTERS... :(
// Cannot wait for view because of alerter animation? ...
Espresso.onView(ViewMatchers.isRoot())
.perform(waitForView(ViewMatchers.withId(com.tapadoo.alerter.R.id.llAlertBackground)))
Thread.sleep(1000)
val popup = activity.findViewById<View>(com.tapadoo.alerter.R.id.llAlertBackground)
activity.runOnUiThread {
popup.performClick()
}
Espresso.onView(ViewMatchers.isRoot())
.perform(waitForView(ViewMatchers.withId(R.id.bottomSheetFragmentContainer)))
Espresso.onView(ViewMatchers.withText(R.string.verification_verify_identity))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
// 4S is not setup so passphrase option should be hidden
Espresso.onView(ViewMatchers.withId(R.id.bottomSheetVerificationRecyclerView))
.check(ViewAssertions.matches(CoreMatchers.not(ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.verification_cannot_access_other_session)))))
Espresso.onView(ViewMatchers.withId(R.id.bottomSheetVerificationRecyclerView))
.check(ViewAssertions.matches(ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.verification_verify_with_another_device))))
Espresso.onView(ViewMatchers.withId(R.id.bottomSheetVerificationRecyclerView))
.check(ViewAssertions.matches(ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.bad_passphrase_key_reset_all_action))))
return uiSession
}
protected fun deferredRequestUntil(session: Session, block: ((PendingVerificationRequest) -> Boolean)): CompletableDeferred<PendingVerificationRequest> {
val completableDeferred = CompletableDeferred<PendingVerificationRequest>()
testScope.launch {
session.cryptoService().verificationService().requestEventFlow().collect {
val request = it.getRequest()
if (request != null && block(request)) {
completableDeferred.complete(request)
return@collect cancel()
}
}
}
return completableDeferred
}
}

View File

@@ -16,38 +16,24 @@
package im.vector.app
import android.view.View
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import im.vector.app.core.utils.getMatrixInstance
import im.vector.app.espresso.tools.waitUntilActivityVisible
import im.vector.app.espresso.tools.waitUntilViewVisible
import im.vector.app.features.MainActivity
import im.vector.app.features.home.HomeActivity
import im.vector.app.ui.robot.AnalyticsRobot
import im.vector.app.ui.robot.ElementRobot
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.internal.assertEquals
import org.hamcrest.CoreMatchers.not
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
@@ -58,12 +44,8 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.getRequest
import org.matrix.android.sdk.api.session.crypto.verification.getTransaction
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.random.Random
@@ -77,8 +59,6 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
private val testScope = CoroutineScope(SupervisorJob())
@Before
fun createSessionWithCrossSigning() {
val matrix = getMatrixInstance()
@@ -102,60 +82,33 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
}
}
@After
fun cleanUp() {
runTest {
existingSession?.signOutService()?.signOut(true)
}
val app = EspressoHelper.getCurrentActivity()!!.application as VectorApplication
while (app.authenticationService.getLastAuthenticatedSession() != null) {
val session = app.authenticationService.getLastAuthenticatedSession()!!
runTest {
session.signOutService().signOut(true)
}
}
val activity = EspressoHelper.getCurrentActivity()!!
val editor = PreferenceManager.getDefaultSharedPreferences(activity).edit()
editor.clear()
editor.commit()
}
@Test
fun checkVerifyPopup() {
val userId: String = existingSession!!.myUserId
uiTestBase.login(userId = userId, password = password, homeServerUrl = homeServerUrl)
val uiSession = loginAndClickVerifyToast(userId)
val analyticsRobot = AnalyticsRobot()
analyticsRobot.optOut()
waitUntilActivityVisible<HomeActivity> {
waitUntilViewVisible(withId(R.id.roomListContainer))
}
val activity = EspressoHelper.getCurrentActivity()!!
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
withIdlingResource(initialSyncIdlingResource(uiSession)) {
waitUntilViewVisible(withId(R.id.roomListContainer))
}
// THIS IS THE ONLY WAY I FOUND TO CLICK ON ALERTERS... :(
// Cannot wait for view because of alerter animation? ...
onView(isRoot())
.perform(waitForView(withId(com.tapadoo.alerter.R.id.llAlertBackground)))
Thread.sleep(1000)
val popup = activity.findViewById<View>(com.tapadoo.alerter.R.id.llAlertBackground)
activity.runOnUiThread {
popup.performClick()
}
onView(isRoot())
.perform(waitForView(withId(R.id.bottomSheetFragmentContainer)))
onView(withText(R.string.verification_verify_identity))
.check(matches(isDisplayed()))
// 4S is not setup so passphrase option should be hidden
onView(withId(R.id.bottomSheetVerificationRecyclerView))
.check(matches(not(hasDescendant(withText(R.string.verification_cannot_access_other_session)))))
onView(withId(R.id.bottomSheetVerificationRecyclerView))
.check(matches(hasDescendant(withText(R.string.verification_verify_with_another_device))))
onView(withId(R.id.bottomSheetVerificationRecyclerView))
.check(matches(hasDescendant(withText(R.string.bad_passphrase_key_reset_all_action))))
val otherRequest = CompletableDeferred<PendingVerificationRequest>()
testScope.launch {
existingSession!!.cryptoService().verificationService().requestEventFlow().collect {
if (it.getRequest() != null) {
otherRequest.complete(it.getRequest()!!)
return@collect cancel()
}
}
val otherRequest = deferredRequestUntil(existingSession!!) {
true
}
// Send out a self verification request
@@ -261,55 +214,4 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
ElementRobot().signout(false)
}
fun signout() {
onView(withId(R.id.groupToolbarAvatarImageView))
.perform(click())
onView(withId(R.id.homeDrawerHeaderSettingsView))
.perform(click())
onView(withText("General"))
.perform(click())
}
fun verificationStateIdleResource(transactionId: String, checkForState: SasTransactionState, session: Session): IdlingResource {
val scope = CoroutineScope(SupervisorJob())
val idle = object : IdlingResource {
private var callback: IdlingResource.ResourceCallback? = null
private var currentState: SasTransactionState? = null
override fun getName() = "verificationSuccessIdle"
override fun isIdleNow(): Boolean {
return currentState == checkForState
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
this.callback = callback
}
fun update(state: SasTransactionState) {
currentState = state
if (state == checkForState) {
callback?.onTransitionToIdle()
scope.cancel()
}
}
}
session.cryptoService().verificationService()
.requestEventFlow()
.filter {
it.transactionId == transactionId
}
.onEach {
(it.getTransaction() as? SasVerificationTransaction)?.state()?.let {
idle.update(it)
}
}.launchIn(scope)
return idle
}
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright (c) 2023 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
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.rules.ActivityScenarioRule
import im.vector.app.core.utils.getMatrixInstance
import im.vector.app.espresso.tools.waitUntilActivityVisible
import im.vector.app.espresso.tools.waitUntilViewVisible
import im.vector.app.features.MainActivity
import im.vector.app.features.home.HomeActivity
import im.vector.app.ui.robot.ElementRobot
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.random.Random
class VerifySessionNavigationTest : VerificationTestBase() {
var existingSession: Session? = null
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Before
fun createSessionWithCrossSigning() {
val matrix = getMatrixInstance()
val userName = "foobar_${Random.nextLong()}"
existingSession = createAccountAndSync(matrix, userName, password, true)
runTest {
existingSession!!.cryptoService().crossSigningService()
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = existingSession!!.myUserId,
password = "password",
session = flowResponse.session
)
)
}
}
)
}
}
@After
fun cleanUp() {
runTest {
existingSession?.signOutService()?.signOut(true)
}
}
@Test
fun testStartThenCancelRequest() {
val userId: String = existingSession!!.myUserId
loginAndClickVerifyToast(userId)
val otherRequest = deferredRequestUntil(existingSession!!) {
true
}
// Send out a self verification request
Espresso.onView(ViewMatchers.withId(R.id.bottomSheetVerificationRecyclerView))
.perform(
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.verification_verify_with_another_device)),
ViewActions.click()
)
)
Espresso.onView(ViewMatchers.withId(R.id.bottomSheetVerificationRecyclerView))
.check(ViewAssertions.matches(ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.verification_request_was_sent))))
val txId = runBlockingTest {
otherRequest.await().transactionId
}
// if we press back it should cancel
val otherGetCancelledRequest = deferredRequestUntil(existingSession!!) {
it.transactionId == txId && it.state == EVerificationState.Cancelled
}
Espresso.pressBack()
// Should go back to main verification options
Espresso.onView(ViewMatchers.isRoot())
.perform(waitForView(ViewMatchers.withId(R.id.bottomSheetFragmentContainer)))
Espresso.onView(ViewMatchers.withId(R.id.bottomSheetVerificationRecyclerView))
.check(ViewAssertions.matches(ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.verification_verify_with_another_device))))
runBlockingTest {
otherGetCancelledRequest.await()
}
Espresso.pressBack()
waitUntilActivityVisible<HomeActivity> {
waitUntilViewVisible(ViewMatchers.withId(R.id.roomListContainer))
}
ElementRobot().signout(false)
}
}