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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user