mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-22 01:15:54 +03:00
First automated UI tests
This commit is contained in:
parent
a3570a69dd
commit
bc2c345e21
11 changed files with 1114 additions and 7 deletions
|
@ -126,7 +126,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
||||||
* @param request the request
|
* @param request the request
|
||||||
*/
|
*/
|
||||||
private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
|
private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
|
||||||
Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request")
|
Timber.v("## CRYPTO - GOSSIP sendOutgoingGossipingRequest() : Requesting keys $request")
|
||||||
|
|
||||||
val params = SendGossipRequestWorker.Params(
|
val params = SendGossipRequestWorker.Params(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
|
|
|
@ -372,6 +372,7 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
|
override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
|
||||||
|
Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null} ")
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||||
xSignMasterPrivateKey = msk
|
xSignMasterPrivateKey = msk
|
||||||
|
@ -407,6 +408,7 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun storeMSKPrivateKey(msk: String?) {
|
override fun storeMSKPrivateKey(msk: String?) {
|
||||||
|
Timber.v("## CRYPTO | *** storeMSKPrivateKey ${msk != null} ")
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||||
xSignMasterPrivateKey = msk
|
xSignMasterPrivateKey = msk
|
||||||
|
@ -415,6 +417,7 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun storeSSKPrivateKey(ssk: String?) {
|
override fun storeSSKPrivateKey(ssk: String?) {
|
||||||
|
Timber.v("## CRYPTO | *** storeSSKPrivateKey ${ssk != null} ")
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||||
xSignSelfSignedPrivateKey = ssk
|
xSignSelfSignedPrivateKey = ssk
|
||||||
|
@ -423,6 +426,7 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun storeUSKPrivateKey(usk: String?) {
|
override fun storeUSKPrivateKey(usk: String?) {
|
||||||
|
Timber.v("## CRYPTO | *** storeUSKPrivateKey ${usk != null} ")
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||||
xSignUserPrivateKey = usk
|
xSignUserPrivateKey = usk
|
||||||
|
|
|
@ -172,6 +172,22 @@ android {
|
||||||
output.versionCodeOverride = variant.versionCode * 10 + baseAbiVersionCode
|
output.versionCodeOverride = variant.versionCode * 10 + baseAbiVersionCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The following argument makes the Android Test Orchestrator run its
|
||||||
|
// "pm clear" command after each test invocation. This command ensures
|
||||||
|
// that the app's state is completely cleared between tests.
|
||||||
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
// Disables animations during instrumented tests you run from the command line…
|
||||||
|
// This property does not affect tests that you run using Android Studio.”
|
||||||
|
animationsDisabled = true
|
||||||
|
|
||||||
|
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
@ -428,11 +444,12 @@ dependencies {
|
||||||
// Activate when you want to check for leaks, from time to time.
|
// Activate when you want to check for leaks, from time to time.
|
||||||
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
|
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test:core:1.2.0'
|
androidTestImplementation 'androidx.test:core:1.3.0'
|
||||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
|
||||||
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
|
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||||
androidTestImplementation "androidx.arch.core:core-testing:$arch_version"
|
androidTestImplementation "androidx.arch.core:core-testing:$arch_version"
|
||||||
// Plant Timber tree for test
|
// Plant Timber tree for test
|
||||||
|
|
205
vector/src/androidTest/java/im/vector/app/ExpressoExt.kt
Normal file
205
vector/src/androidTest/java/im/vector/app/ExpressoExt.kt
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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 android.app.Activity
|
||||||
|
import android.view.View
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.test.espresso.IdlingRegistry
|
||||||
|
import androidx.test.espresso.IdlingResource
|
||||||
|
import androidx.test.espresso.PerformException
|
||||||
|
import androidx.test.espresso.UiController
|
||||||
|
import androidx.test.espresso.ViewAction
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
|
import androidx.test.espresso.util.HumanReadables
|
||||||
|
import androidx.test.espresso.util.TreeIterables
|
||||||
|
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||||
|
import androidx.test.runner.lifecycle.ActivityLifecycleCallback
|
||||||
|
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
|
||||||
|
import androidx.test.runner.lifecycle.Stage
|
||||||
|
import org.hamcrest.Matcher
|
||||||
|
import org.hamcrest.Matchers
|
||||||
|
import org.hamcrest.StringDescription
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||||
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
|
object EspressoHelper {
|
||||||
|
fun getCurrentActivity(): Activity? {
|
||||||
|
var currentActivity: Activity? = null
|
||||||
|
getInstrumentation().runOnMainSync { run { currentActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0) } }
|
||||||
|
return currentActivity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun waitForView(viewMatcher: Matcher<View>, timeout: Long = 10000, waitForDisplayed: Boolean = true): ViewAction {
|
||||||
|
return object : ViewAction {
|
||||||
|
override fun getConstraints(): Matcher<View> {
|
||||||
|
return Matchers.any(View::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDescription(): String {
|
||||||
|
val matcherDescription = StringDescription()
|
||||||
|
viewMatcher.describeTo(matcherDescription)
|
||||||
|
return "wait for a specific view <$matcherDescription> to be ${if (waitForDisplayed) "displayed" else "not displayed during $timeout millis."}"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun perform(uiController: UiController, view: View) {
|
||||||
|
System.out.println("*** waitForView 1 $view")
|
||||||
|
uiController.loopMainThreadUntilIdle()
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
val endTime = startTime + timeout
|
||||||
|
val visibleMatcher = isDisplayed()
|
||||||
|
|
||||||
|
do {
|
||||||
|
System.out.println("*** waitForView loop $view end:$endTime currrent:${System.currentTimeMillis()}")
|
||||||
|
val viewVisible = TreeIterables.breadthFirstViewTraversal(view)
|
||||||
|
.any { viewMatcher.matches(it) && visibleMatcher.matches(it) }
|
||||||
|
|
||||||
|
System.out.println("*** waitForView loop viewVisible:$viewVisible")
|
||||||
|
if (viewVisible == waitForDisplayed) return
|
||||||
|
System.out.println("*** waitForView loop loopMainThreadForAtLeast...")
|
||||||
|
uiController.loopMainThreadForAtLeast(50)
|
||||||
|
System.out.println("*** waitForView loop ...loopMainThreadForAtLeast")
|
||||||
|
} while (System.currentTimeMillis() < endTime)
|
||||||
|
|
||||||
|
System.out.println("*** waitForView timeout $view")
|
||||||
|
// Timeout happens.
|
||||||
|
throw PerformException.Builder()
|
||||||
|
.withActionDescription(this.description)
|
||||||
|
.withViewDescription(HumanReadables.describe(view))
|
||||||
|
.withCause(TimeoutException())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initialSyncIdlingResource(session: Session): IdlingResource {
|
||||||
|
val res = object : IdlingResource, Observer<SyncState> {
|
||||||
|
private var callback: IdlingResource.ResourceCallback? = null
|
||||||
|
|
||||||
|
override fun getName() = "InitialSyncIdlingResource for ${session.myUserId}"
|
||||||
|
|
||||||
|
override fun isIdleNow(): Boolean {
|
||||||
|
val isIdle = session.hasAlreadySynced()
|
||||||
|
return isIdle
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
|
||||||
|
this.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChanged(t: SyncState?) {
|
||||||
|
val isIdle = session.hasAlreadySynced()
|
||||||
|
if (isIdle) {
|
||||||
|
callback?.onTransitionToIdle()
|
||||||
|
session.getSyncStateLive().removeObserver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runOnUiThread {
|
||||||
|
session.getSyncStateLive().observeForever(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
fun activityIdlingResource(activityClass: Class<*>): IdlingResource {
|
||||||
|
val res = object : IdlingResource, ActivityLifecycleCallback {
|
||||||
|
private var callback: IdlingResource.ResourceCallback? = null
|
||||||
|
|
||||||
|
var hasResumed = false
|
||||||
|
private var currentActivity : Activity? = null
|
||||||
|
|
||||||
|
val uniqTS = System.currentTimeMillis()
|
||||||
|
override fun getName() = "activityIdlingResource_${activityClass.name}_$uniqTS"
|
||||||
|
|
||||||
|
override fun isIdleNow(): Boolean {
|
||||||
|
val currentActivity = currentActivity ?: ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0)
|
||||||
|
|
||||||
|
val isIdle = hasResumed || currentActivity?.javaClass?.let { activityClass.isAssignableFrom(it) } ?: false
|
||||||
|
System.out.println("*** [$name] isIdleNow activityIdlingResource $currentActivity isIdle:$isIdle")
|
||||||
|
return isIdle
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
|
||||||
|
System.out.println("*** [$name] registerIdleTransitionCallback $callback")
|
||||||
|
this.callback = callback
|
||||||
|
// if (hasResumed) callback?.onTransitionToIdle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityLifecycleChanged(activity: Activity?, stage: Stage?) {
|
||||||
|
System.out.println("*** [$name] onActivityLifecycleChanged $activity $stage")
|
||||||
|
currentActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0)
|
||||||
|
val isIdle = currentActivity?.javaClass?.let { activityClass.isAssignableFrom(it) } ?: false
|
||||||
|
System.out.println("*** [$name] onActivityLifecycleChanged $currentActivity isIdle:$isIdle")
|
||||||
|
if (isIdle) {
|
||||||
|
hasResumed = true
|
||||||
|
System.out.println("*** [$name] onActivityLifecycleChanged callback: $callback")
|
||||||
|
callback?.onTransitionToIdle()
|
||||||
|
ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(res)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withIdlingResource(idlingResource: IdlingResource, block: (() -> Unit)) {
|
||||||
|
System.out.println("*** withIdlingResource register")
|
||||||
|
IdlingRegistry.getInstance().register(idlingResource)
|
||||||
|
block.invoke()
|
||||||
|
System.out.println("*** withIdlingResource unregister")
|
||||||
|
IdlingRegistry.getInstance().unregister(idlingResource)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun allSecretsKnownIdling(session: Session): IdlingResource {
|
||||||
|
val res = object : IdlingResource, Observer<Optional<PrivateKeysInfo>> {
|
||||||
|
private var callback: IdlingResource.ResourceCallback? = null
|
||||||
|
|
||||||
|
var privateKeysInfo: PrivateKeysInfo? = session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()
|
||||||
|
override fun getName() = "AllSecretsKnownIdling_${session.myUserId}"
|
||||||
|
|
||||||
|
override fun isIdleNow(): Boolean {
|
||||||
|
System.out.println("*** [$name]/isIdleNow allSecretsKnownIdling ${privateKeysInfo?.allKnown()}")
|
||||||
|
return privateKeysInfo?.allKnown() == true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
|
||||||
|
this.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChanged(t: Optional<PrivateKeysInfo>?) {
|
||||||
|
System.out.println("*** [$name] allSecretsKnownIdling ${t?.getOrNull()}")
|
||||||
|
privateKeysInfo = t?.getOrNull()
|
||||||
|
if (t?.getOrNull()?.allKnown() == true) {
|
||||||
|
session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().removeObserver(this)
|
||||||
|
callback?.onTransitionToIdle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runOnUiThread {
|
||||||
|
session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().observeForever(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
120
vector/src/androidTest/java/im/vector/app/RegistrationTest.kt
Normal file
120
vector/src/androidTest/java/im/vector/app/RegistrationTest.kt
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||||
|
import androidx.test.espresso.action.ViewActions.typeText
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
|
||||||
|
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.features.MainActivity
|
||||||
|
import im.vector.app.features.home.HomeActivity
|
||||||
|
import org.hamcrest.CoreMatchers.not
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@LargeTest
|
||||||
|
class RegistrationTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val activityRule = ActivityScenarioRule(MainActivity::class.java)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun simpleRegister() {
|
||||||
|
val userId: String = "UiAutoTest_${System.currentTimeMillis()}"
|
||||||
|
val password: String = "password"
|
||||||
|
val homeServerUrl: String = "http://10.0.2.2:8080"
|
||||||
|
|
||||||
|
// Check splashcreen is there
|
||||||
|
onView(withId(R.id.loginSplashSubmit))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
.check(matches(withText(R.string.login_splash_submit)))
|
||||||
|
|
||||||
|
// Click on get started
|
||||||
|
onView(withId(R.id.loginSplashSubmit))
|
||||||
|
.perform(click())
|
||||||
|
|
||||||
|
// Check that home server options are showned
|
||||||
|
onView(withId(R.id.loginServerTitle))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
.check(matches(withText(R.string.login_server_title)))
|
||||||
|
|
||||||
|
// Chose custom server
|
||||||
|
onView(withId(R.id.loginServerChoiceOther))
|
||||||
|
.perform(click())
|
||||||
|
|
||||||
|
// Enter local synapse
|
||||||
|
onView((withId(R.id.loginServerUrlFormHomeServerUrl)))
|
||||||
|
.perform(typeText(homeServerUrl))
|
||||||
|
|
||||||
|
// Click on continue
|
||||||
|
onView(withId(R.id.loginServerUrlFormSubmit))
|
||||||
|
.check(matches(isEnabled()))
|
||||||
|
.perform(closeSoftKeyboard(), click())
|
||||||
|
|
||||||
|
// Click on the signup button
|
||||||
|
onView(withId(R.id.loginSignupSigninSubmit))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
.perform(click())
|
||||||
|
|
||||||
|
// Ensure password flow supported
|
||||||
|
onView(withId(R.id.loginField))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.passwordField))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
|
||||||
|
// Ensure user id
|
||||||
|
onView((withId(R.id.loginField)))
|
||||||
|
.perform(typeText(userId))
|
||||||
|
|
||||||
|
// Ensure login button not yet enabled
|
||||||
|
onView(withId(R.id.loginSubmit))
|
||||||
|
.check(matches(not(isEnabled())))
|
||||||
|
|
||||||
|
// Ensure password
|
||||||
|
onView((withId(R.id.passwordField)))
|
||||||
|
.perform(typeText(password))
|
||||||
|
|
||||||
|
// Submit
|
||||||
|
onView(withId(R.id.loginSubmit))
|
||||||
|
.check(matches(isEnabled()))
|
||||||
|
.perform(closeSoftKeyboard(), click())
|
||||||
|
|
||||||
|
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
|
||||||
|
onView(withId(R.id.roomListContainer))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
val activity = EspressoHelper.getCurrentActivity()!!
|
||||||
|
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
|
||||||
|
|
||||||
|
// Wait for initial sync and check room list is there
|
||||||
|
withIdlingResource(initialSyncIdlingResource(uiSession)) {
|
||||||
|
onView(withId(R.id.roomListContainer))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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 android.view.View;
|
||||||
|
|
||||||
|
import androidx.test.espresso.UiController;
|
||||||
|
import androidx.test.espresso.ViewAction;
|
||||||
|
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||||
|
|
||||||
|
public class SleepViewAction {
|
||||||
|
|
||||||
|
public static ViewAction sleep(final long millis) {
|
||||||
|
return new ViewAction() {
|
||||||
|
@Override
|
||||||
|
public Matcher<View> getConstraints() {
|
||||||
|
return isRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Wait for at least " + millis + " millis";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void perform(final UiController uiController, final View view) {
|
||||||
|
uiController.loopMainThreadUntilIdle();
|
||||||
|
uiController.loopMainThreadForAtLeast(millis);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.annotation.CallSuper
|
||||||
|
import junit.framework.TestCase.fail
|
||||||
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple implementation of MatrixCallback, which count down the CountDownLatch on each API callback
|
||||||
|
* @param onlySuccessful true to fail if an error occurs. This is the default behavior
|
||||||
|
* @param <T>
|
||||||
|
*/
|
||||||
|
open class TestMatrixCallback<T>(private val countDownLatch: CountDownLatch,
|
||||||
|
private val onlySuccessful: Boolean = true) : MatrixCallback<T> {
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onSuccess(data: T) {
|
||||||
|
countDownLatch.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
Timber.e(failure, "TestApiCallback")
|
||||||
|
|
||||||
|
if (onlySuccessful) {
|
||||||
|
fail("onFailure " + failure.localizedMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
countDownLatch.countDown()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,225 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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 android.net.Uri
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.test.espresso.Espresso
|
||||||
|
import androidx.test.espresso.action.ViewActions
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
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.data.LoginFlowResult
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
abstract class VerificationTestBase {
|
||||||
|
|
||||||
|
val password = "password"
|
||||||
|
val homeServerUrl: String = "http://10.0.2.2:8080"
|
||||||
|
|
||||||
|
fun doLogin(homeServerUrl: String, userId: String, password: String) {
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_splash_submit)))
|
||||||
|
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
|
||||||
|
.perform(ViewActions.click())
|
||||||
|
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginServerTitle))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_server_title)))
|
||||||
|
|
||||||
|
// Chose custom server
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginServerChoiceOther))
|
||||||
|
.perform(ViewActions.click())
|
||||||
|
|
||||||
|
// Enter local synapse
|
||||||
|
Espresso.onView((ViewMatchers.withId(R.id.loginServerUrlFormHomeServerUrl)))
|
||||||
|
.perform(ViewActions.typeText(homeServerUrl))
|
||||||
|
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginServerUrlFormSubmit))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
|
||||||
|
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
|
||||||
|
|
||||||
|
// Click on the signin button
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginSignupSigninSignIn))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||||
|
.perform(ViewActions.click())
|
||||||
|
|
||||||
|
// Ensure password flow supported
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginField))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.passwordField))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||||
|
|
||||||
|
Espresso.onView((ViewMatchers.withId(R.id.loginField)))
|
||||||
|
.perform(ViewActions.typeText(userId))
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
|
||||||
|
.check(ViewAssertions.matches(CoreMatchers.not(ViewMatchers.isEnabled())))
|
||||||
|
|
||||||
|
Espresso.onView((ViewMatchers.withId(R.id.passwordField)))
|
||||||
|
.perform(ViewActions.typeText(password))
|
||||||
|
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
|
||||||
|
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAccount(userId: String = "UiAutoTest", password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") {
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_splash_submit)))
|
||||||
|
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
|
||||||
|
.perform(ViewActions.click())
|
||||||
|
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginServerTitle))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_server_title)))
|
||||||
|
|
||||||
|
// Chose custom server
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginServerChoiceOther))
|
||||||
|
.perform(ViewActions.click())
|
||||||
|
|
||||||
|
// Enter local synapse
|
||||||
|
Espresso.onView((ViewMatchers.withId(R.id.loginServerUrlFormHomeServerUrl)))
|
||||||
|
.perform(ViewActions.typeText(homeServerUrl))
|
||||||
|
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginServerUrlFormSubmit))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
|
||||||
|
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
|
||||||
|
|
||||||
|
// Click on the signup button
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginSignupSigninSubmit))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||||
|
.perform(ViewActions.click())
|
||||||
|
|
||||||
|
// Ensure password flow supported
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginField))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.passwordField))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||||
|
|
||||||
|
Espresso.onView((ViewMatchers.withId(R.id.loginField)))
|
||||||
|
.perform(ViewActions.typeText(userId))
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
|
||||||
|
.check(ViewAssertions.matches(CoreMatchers.not(ViewMatchers.isEnabled())))
|
||||||
|
|
||||||
|
Espresso.onView((ViewMatchers.withId(R.id.passwordField)))
|
||||||
|
.perform(ViewActions.typeText(password))
|
||||||
|
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
|
||||||
|
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
|
||||||
|
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.homeDrawerFragmentContainer))
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createAccountAndSync(matrix: Matrix, userName: String,
|
||||||
|
password: String,
|
||||||
|
withInitialSync: Boolean): Session {
|
||||||
|
val hs = createHomeServerConfig()
|
||||||
|
|
||||||
|
doSync<LoginFlowResult> {
|
||||||
|
matrix.authenticationService()
|
||||||
|
.getLoginFlow(hs, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
doSync<RegistrationResult> {
|
||||||
|
matrix.authenticationService()
|
||||||
|
.getRegistrationWizard()
|
||||||
|
.createAccount(userName, password, null, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preform dummy step
|
||||||
|
val registrationResult = doSync<RegistrationResult> {
|
||||||
|
matrix.authenticationService()
|
||||||
|
.getRegistrationWizard()
|
||||||
|
.dummy(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.assertTrue(registrationResult is RegistrationResult.Success)
|
||||||
|
val session = (registrationResult as RegistrationResult.Success).session
|
||||||
|
if (withInitialSync) {
|
||||||
|
syncSession(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createHomeServerConfig(): HomeServerConnectionConfig {
|
||||||
|
return HomeServerConnectionConfig.Builder()
|
||||||
|
.withHomeServerUri(Uri.parse(homeServerUrl))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform a method with a MatrixCallback to a synchronous method
|
||||||
|
inline fun <reified T> doSync(block: (MatrixCallback<T>) -> Unit): T {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
var result: T? = null
|
||||||
|
|
||||||
|
val callback = object : TestMatrixCallback<T>(lock) {
|
||||||
|
override fun onSuccess(data: T) {
|
||||||
|
result = data
|
||||||
|
super.onSuccess(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
block.invoke(callback)
|
||||||
|
|
||||||
|
lock.await(20_000, TimeUnit.MILLISECONDS)
|
||||||
|
|
||||||
|
Assert.assertNotNull(result)
|
||||||
|
return result!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncSession(session: Session) {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.Main) { session.open() }
|
||||||
|
|
||||||
|
session.startSync(true)
|
||||||
|
|
||||||
|
val syncLiveData = runBlocking(Dispatchers.Main) {
|
||||||
|
session.getSyncStateLive()
|
||||||
|
}
|
||||||
|
val syncObserver = object : Observer<SyncState> {
|
||||||
|
override fun onChanged(t: SyncState?) {
|
||||||
|
if (session.hasAlreadySynced()) {
|
||||||
|
lock.countDown()
|
||||||
|
syncLiveData.removeObserver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GlobalScope.launch(Dispatchers.Main) { syncLiveData.observeForever(syncObserver) }
|
||||||
|
|
||||||
|
lock.await(20_000, TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,272 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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 android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.IdlingRegistry
|
||||||
|
import androidx.test.espresso.IdlingResource
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||||
|
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 androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import im.vector.app.features.MainActivity
|
||||||
|
import im.vector.app.features.home.HomeActivity
|
||||||
|
import org.hamcrest.CoreMatchers.not
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.matrix.android.sdk.api.Matrix
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
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.VerificationService
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@LargeTest
|
||||||
|
class VerifySessionInteractiveTest : VerificationTestBase() {
|
||||||
|
|
||||||
|
var existingSession: Session? = null
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val activityRule = ActivityScenarioRule(MainActivity::class.java)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun createSessionWithCrossSigning() {
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
val matrix = Matrix.getInstance(context)
|
||||||
|
val userName = "foobar_${System.currentTimeMillis()}"
|
||||||
|
existingSession = createAccountAndSync(matrix, userName, password, true)
|
||||||
|
doSync<Unit> {
|
||||||
|
existingSession!!.cryptoService().crossSigningService()
|
||||||
|
.initializeCrossSigning(UserPasswordAuth(
|
||||||
|
user = existingSession!!.myUserId,
|
||||||
|
password = "password"
|
||||||
|
), it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkVerifyPopup() {
|
||||||
|
val userId: String = existingSession!!.myUserId
|
||||||
|
|
||||||
|
doLogin(homeServerUrl, userId, password)
|
||||||
|
|
||||||
|
// Thread.sleep(6000)
|
||||||
|
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
|
||||||
|
onView(withId(R.id.roomListContainer))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
.perform(closeSoftKeyboard())
|
||||||
|
}
|
||||||
|
|
||||||
|
val activity = EspressoHelper.getCurrentActivity()!!
|
||||||
|
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
|
||||||
|
|
||||||
|
withIdlingResource(initialSyncIdlingResource(uiSession)) {
|
||||||
|
onView(withId(R.id.roomListContainer))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// onView(withId(com.tapadoo.alerter.R.id.llAlertBackground))
|
||||||
|
// .perform(click())
|
||||||
|
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)))
|
||||||
|
// .check()
|
||||||
|
// onView(withId(R.id.bottomSheetFragmentContainer))
|
||||||
|
// .check(matches(isDisplayed()))
|
||||||
|
|
||||||
|
// onView(isRoot()).perform(SleepViewAction.sleep(2000))
|
||||||
|
|
||||||
|
onView(withText(R.string.use_latest_app))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
|
||||||
|
// 4S is not setup so passphrase option should be hidden
|
||||||
|
onView(withId(R.id.bottomSheetFragmentContainer))
|
||||||
|
.check(matches(not(hasDescendant(withText(R.string.verification_cannot_access_other_session)))))
|
||||||
|
|
||||||
|
val request = existingSession!!.cryptoService().verificationService().requestKeyVerification(
|
||||||
|
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
|
||||||
|
existingSession!!.myUserId,
|
||||||
|
listOf(uiSession.sessionParams.deviceId!!)
|
||||||
|
)
|
||||||
|
|
||||||
|
val transactionId = request.transactionId!!
|
||||||
|
val sasReadyIdle = verificationStateIdleResource(transactionId, VerificationTxState.ShortCodeReady, uiSession)
|
||||||
|
val otherSessionSasReadyIdle = verificationStateIdleResource(transactionId, VerificationTxState.ShortCodeReady, existingSession!!)
|
||||||
|
|
||||||
|
onView(isRoot()).perform(SleepViewAction.sleep(1000))
|
||||||
|
|
||||||
|
// Assert QR code option is there and available
|
||||||
|
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||||
|
.check(matches(hasDescendant(withText(R.string.verification_scan_their_code))))
|
||||||
|
|
||||||
|
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||||
|
.check(matches(hasDescendant(withId(R.id.itemVerificationQrCodeImage))))
|
||||||
|
|
||||||
|
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||||
|
.perform(
|
||||||
|
actionOnItem<RecyclerView.ViewHolder>(
|
||||||
|
hasDescendant(withText(R.string.verification_scan_emoji_title)),
|
||||||
|
click()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val firstSessionTr = existingSession!!.cryptoService().verificationService().getExistingTransaction(
|
||||||
|
existingSession!!.myUserId,
|
||||||
|
transactionId
|
||||||
|
) as SasVerificationTransaction
|
||||||
|
|
||||||
|
IdlingRegistry.getInstance().register(sasReadyIdle)
|
||||||
|
IdlingRegistry.getInstance().register(otherSessionSasReadyIdle)
|
||||||
|
onView(isRoot()).perform(SleepViewAction.sleep(300))
|
||||||
|
// will only execute when Idle is ready
|
||||||
|
val expectedEmojis = firstSessionTr.getEmojiCodeRepresentation()
|
||||||
|
val targets = listOf(R.id.emoji0, R.id.emoji1, R.id.emoji2, R.id.emoji3, R.id.emoji4, R.id.emoji5, R.id.emoji6)
|
||||||
|
targets.forEachIndexed { index, res ->
|
||||||
|
onView(withId(res))
|
||||||
|
.check(
|
||||||
|
matches(hasDescendant(withText(expectedEmojis[index].nameResId)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IdlingRegistry.getInstance().unregister(sasReadyIdle)
|
||||||
|
IdlingRegistry.getInstance().unregister(otherSessionSasReadyIdle)
|
||||||
|
|
||||||
|
val verificationSuccessIdle =
|
||||||
|
verificationStateIdleResource(transactionId, VerificationTxState.Verified, uiSession)
|
||||||
|
|
||||||
|
// CLICK ON THEY MATCH
|
||||||
|
|
||||||
|
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||||
|
.perform(
|
||||||
|
actionOnItem<RecyclerView.ViewHolder>(
|
||||||
|
hasDescendant(withText(R.string.verification_sas_match)),
|
||||||
|
click()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
firstSessionTr.userHasVerifiedShortCode()
|
||||||
|
|
||||||
|
onView(isRoot()).perform(SleepViewAction.sleep(1000))
|
||||||
|
|
||||||
|
withIdlingResource(verificationSuccessIdle) {
|
||||||
|
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||||
|
.check(
|
||||||
|
matches(hasDescendant(withText(R.string.verification_conclusion_ok_self_notice)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a bit before done (to delay a bit sending of secrets to let other have time
|
||||||
|
// to mark as verified :/
|
||||||
|
Thread.sleep(5_000)
|
||||||
|
// Click on done
|
||||||
|
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||||
|
.perform(
|
||||||
|
actionOnItem<RecyclerView.ViewHolder>(
|
||||||
|
hasDescendant(withText(R.string.done)),
|
||||||
|
click()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wait until local secrets are known (gossip)
|
||||||
|
withIdlingResource(allSecretsKnownIdling(uiSession)) {
|
||||||
|
onView(withId(R.id.groupToolbarAvatarImageView))
|
||||||
|
.perform(click())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: VerificationTxState, session: Session): IdlingResource {
|
||||||
|
val idle = object : IdlingResource, VerificationService.Listener {
|
||||||
|
private var callback: IdlingResource.ResourceCallback? = null
|
||||||
|
|
||||||
|
private var currentState: VerificationTxState? = null
|
||||||
|
|
||||||
|
override fun getName() = "verificationSuccessIdle"
|
||||||
|
|
||||||
|
override fun isIdleNow(): Boolean {
|
||||||
|
return currentState == checkForState
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
|
||||||
|
this.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(state: VerificationTxState) {
|
||||||
|
currentState = state
|
||||||
|
if (state == checkForState) {
|
||||||
|
session.cryptoService().verificationService().removeListener(this)
|
||||||
|
callback?.onTransitionToIdle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a transaction is created, either by the user or initiated by the other user.
|
||||||
|
*/
|
||||||
|
override fun transactionCreated(tx: VerificationTransaction) {
|
||||||
|
if (tx.transactionId == transactionId) update(tx.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a transaction is updated. You may be interested to track the state of the VerificationTransaction.
|
||||||
|
*/
|
||||||
|
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||||
|
if (tx.transactionId == transactionId) update(tx.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.cryptoService().verificationService().addListener(idle)
|
||||||
|
return idle
|
||||||
|
}
|
||||||
|
|
||||||
|
object UITestVerificationUtils
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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 android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||||
|
import androidx.test.espresso.action.ViewActions.typeText
|
||||||
|
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 androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.features.MainActivity
|
||||||
|
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
|
||||||
|
import im.vector.app.features.crypto.recover.BootstrapCrossSigningTask
|
||||||
|
import im.vector.app.features.crypto.recover.Params
|
||||||
|
import im.vector.app.features.home.HomeActivity
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.matrix.android.sdk.api.Matrix
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@LargeTest
|
||||||
|
class VerifySessionPassphraseTest : VerificationTestBase() {
|
||||||
|
|
||||||
|
var existingSession: Session? = null
|
||||||
|
val passphrase = "person woman camera tv"
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val activityRule = ActivityScenarioRule(MainActivity::class.java)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun createSessionWithCrossSigningAnd4S() {
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
val matrix = Matrix.getInstance(context)
|
||||||
|
val userName = "foobar_${System.currentTimeMillis()}"
|
||||||
|
existingSession = createAccountAndSync(matrix, userName, password, true)
|
||||||
|
doSync<Unit> {
|
||||||
|
existingSession!!.cryptoService().crossSigningService()
|
||||||
|
.initializeCrossSigning(UserPasswordAuth(
|
||||||
|
user = existingSession!!.myUserId,
|
||||||
|
password = "password"
|
||||||
|
), it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val task = BootstrapCrossSigningTask(existingSession!!, StringProvider(context.resources))
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
task.execute(Params(
|
||||||
|
userPasswordAuth = UserPasswordAuth(password = password),
|
||||||
|
passphrase = passphrase
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkVerifyWithPassphrase() {
|
||||||
|
val userId: String = existingSession!!.myUserId
|
||||||
|
|
||||||
|
doLogin(homeServerUrl, userId, password)
|
||||||
|
|
||||||
|
// Thread.sleep(6000)
|
||||||
|
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
|
||||||
|
onView(withId(R.id.roomListContainer))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
.perform(closeSoftKeyboard())
|
||||||
|
}
|
||||||
|
|
||||||
|
val activity = EspressoHelper.getCurrentActivity()!!
|
||||||
|
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
|
||||||
|
|
||||||
|
withIdlingResource(initialSyncIdlingResource(uiSession)) {
|
||||||
|
onView(withId(R.id.roomListContainer))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS IS THE ONLY WAY I FOUND TO CLICK ON ALERTERS... :(
|
||||||
|
// Cannot wait for view because of alerter animation? ...
|
||||||
|
Thread.sleep(6000)
|
||||||
|
val popup = activity.findViewById<View>(com.tapadoo.alerter.R.id.llAlertBackground)
|
||||||
|
activity.runOnUiThread {
|
||||||
|
popup.performClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
onView(withId(R.id.bottomSheetFragmentContainer))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
|
||||||
|
onView(isRoot()).perform(SleepViewAction.sleep(2000))
|
||||||
|
|
||||||
|
onView(withText(R.string.use_latest_app))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
|
||||||
|
// 4S is not setup so passphrase option should be hidden
|
||||||
|
onView(withId(R.id.bottomSheetFragmentContainer))
|
||||||
|
.check(matches(hasDescendant(withText(R.string.verification_cannot_access_other_session))))
|
||||||
|
|
||||||
|
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||||
|
.perform(
|
||||||
|
actionOnItem<RecyclerView.ViewHolder>(
|
||||||
|
hasDescendant(withText(R.string.verification_cannot_access_other_session)),
|
||||||
|
click()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
withIdlingResource(activityIdlingResource(SharedSecureStorageActivity::class.java)) {
|
||||||
|
onView(withId(R.id.ssss__root)).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
onView((withId(R.id.ssss_passphrase_enter_edittext)))
|
||||||
|
.perform(typeText(passphrase))
|
||||||
|
|
||||||
|
onView((withId(R.id.ssss_passphrase_submit)))
|
||||||
|
.perform(click())
|
||||||
|
|
||||||
|
System.out.println("*** passphrase 1")
|
||||||
|
|
||||||
|
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
|
||||||
|
System.out.println("*** passphrase 1.1")
|
||||||
|
onView(withId(R.id.bottomSheetVerificationRecyclerView))
|
||||||
|
.check(
|
||||||
|
matches(hasDescendant(withText(R.string.verification_conclusion_ok_self_notice)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("*** passphrase 2")
|
||||||
|
// check that all secrets are known?
|
||||||
|
assert(uiSession.cryptoService().crossSigningService().canCrossSign())
|
||||||
|
assert(uiSession.cryptoService().crossSigningService().allPrivateKeysKnown())
|
||||||
|
|
||||||
|
Thread.sleep(10_000)
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import android.app.Activity
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.provider.Settings
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import com.tapadoo.alerter.Alerter
|
import com.tapadoo.alerter.Alerter
|
||||||
|
@ -172,6 +173,10 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
|
||||||
private fun showAlert(alert: VectorAlert, activity: Activity, animate: Boolean = true) {
|
private fun showAlert(alert: VectorAlert, activity: Activity, animate: Boolean = true) {
|
||||||
clearLightStatusBar()
|
clearLightStatusBar()
|
||||||
|
|
||||||
|
val systemAnimationDurationDisabled = Settings.Global.getFloat(
|
||||||
|
activity.contentResolver,
|
||||||
|
Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f
|
||||||
|
|
||||||
alert.weakCurrentActivity = WeakReference(activity)
|
alert.weakCurrentActivity = WeakReference(activity)
|
||||||
val alerter = if (alert is VerificationVectorAlert) Alerter.create(activity, R.layout.alerter_verification_layout)
|
val alerter = if (alert is VerificationVectorAlert) Alerter.create(activity, R.layout.alerter_verification_layout)
|
||||||
else Alerter.create(activity)
|
else Alerter.create(activity)
|
||||||
|
@ -187,7 +192,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.apply {
|
.apply {
|
||||||
if (!animate) {
|
if (systemAnimationDurationDisabled || !animate) {
|
||||||
setEnterAnimation(R.anim.anim_alerter_no_anim)
|
setEnterAnimation(R.anim.anim_alerter_no_anim)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,6 +242,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
|
||||||
setBackgroundColorRes(alert.colorRes ?: R.color.notification_accent_color)
|
setBackgroundColorRes(alert.colorRes ?: R.color.notification_accent_color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.enableIconPulse(!systemAnimationDurationDisabled)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue