Skip to content
Snippets Groups Projects
Commit eeb5a21c authored by Pierre Nicolas's avatar Pierre Nicolas :joy:
Browse files

tests: implement add contact tests

GitLab: #1692
Change-Id: I5538f100b0636dc09f46ff2575faa70c67b3b902
parent 18db91ac
No related branches found
No related tags found
No related merge requests found
/*
* Copyright (C) 2004-2024 Savoir-faire Linux Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package cx.ring.client.addcontact
import org.hamcrest.Matchers.allOf
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.supportsInputMethods
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 androidx.test.rule.GrantPermissionRule
import cx.ring.R
import cx.ring.assertOnView
import cx.ring.client.HomeActivity
import cx.ring.client.wizard.AccountUtils
import cx.ring.doOnView
import net.jami.model.Account
import net.jami.model.Conversation
import net.jami.model.Uri
import net.jami.utils.Log
import org.hamcrest.Matchers
import org.hamcrest.Matchers.not
import org.junit.FixMethodOrder
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
/**
* This test class tests the addition of a contact.
* Precondition: Should have access to the nameserver (https://ns-test.jami.net/).
*/
@LargeTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@RunWith(AndroidJUnit4::class)
class AddContact {
@Rule
@JvmField
var mActivityScenarioRule = ActivityScenarioRule(HomeActivity::class.java)
@get:Rule
val grantPermissionRule: GrantPermissionRule =
GrantPermissionRule.grant(android.Manifest.permission.POST_NOTIFICATIONS)
companion object {
@JvmStatic
private var accountsCreated = false
@JvmStatic // Account A will be the one sending the trust request.
private var accountA: Account? = null
@JvmStatic // Account B will be the one accepting the trust request.
private var accountB: Account? = null
@JvmStatic // Account C will be the one refusing the trust request.
private var accountC: Account? = null
@JvmStatic // Account D will be the one blocking the trust request.
private var accountD: Account? = null
private val TAG = AddContact::class.java.simpleName
}
/**
* This test MUST be the first one because it creates the accounts.
*/
@Test
fun a_setup() {
// Doing this in a test is not ideal.
// Ideally, it should be in an `@Before` method, but the problem with `@Before` is that
// it is executed on the same activity than the test, while we need to restart to see the
// accounts on the app.
// `@BeforeClass` could be used, but it does not have access to the activity.
Log.d(TAG, "Creating accounts ...")
mActivityScenarioRule.scenario.onActivity { activity ->
val accountList =
AccountUtils.createAccountAndRegister(activity.mAccountService, 4)
accountA = accountList[0]
accountB = accountList[1]
accountC = accountList[2]
accountD = accountList[3]
// Need delay to give time to accounts to register on DHT before sending trust request.
// Inferior delay will occasionally cause the trust request to fail.
Thread.sleep(10000)
Log.d(TAG, "Accounts created.")
// AccountB will be manually added.
// But let's skip UI for AccountC and AccountD.
val accountCUri = Uri.fromString(accountC!!.uri!!)
val fakeCConversation =
Conversation(accountA!!.accountId, accountCUri, Conversation.Mode.Request)
activity.mAccountService.sendTrustRequest(
fakeCConversation,
accountCUri
)
val accountDUri = Uri.fromString(accountD!!.uri!!)
val fakeDConversation =
Conversation(accountA!!.accountId, accountDUri, Conversation.Mode.Request)
activity.mAccountService.sendTrustRequest(
fakeDConversation,
accountDUri
)
accountsCreated = true // To not redo account creation for every test.
}
}
@Test
fun b1_searchForContact_usingRegisteredName() {
// Move to account A
AccountNavigationUtils.moveToAccount(accountA!!.registeredName)
// Click on the search bar
onView(withId(R.id.search_bar)).perform(click())
// Type the username of account B
onView(allOf(supportsInputMethods(), isDescendantOfA(withId(R.id.search_view))))
.perform(typeText(accountB!!.registeredName))
// Click on the search result for account B
assertOnView(
allOf(withId(R.id.conv_participant), withText(accountB!!.registeredName)),
matches(isDisplayed())
)
}
@Test
fun b2_searchForContact_usingJamiId() {
// Click on the search bar
onView(withId(R.id.search_bar)).perform(click())
// Type the username of account B
onView(allOf(supportsInputMethods(), isDescendantOfA(withId(R.id.search_view))))
.perform(typeText(accountB!!.username))
// Click on the search result for account B
assertOnView(
allOf(withId(R.id.conv_participant), withText(accountB!!.registeredName)),
matches(isDisplayed())
)
}
@Test
fun b3_sendContactInvite() {
// Click on the search bar
onView(withId(R.id.search_bar)).perform(click())
// Type the username of account B
onView(allOf(supportsInputMethods(), isDescendantOfA(withId(R.id.search_view))))
.perform(typeText(accountB!!.registeredName))
// Click on the search result for account B
doOnView(
allOf(withId(R.id.conv_participant), withText(accountB!!.registeredName)),
click()
)
// Click on "add contact" button
onView(withId(R.id.unknownContactButton)).perform(click())
// Check that the contact has been invited
val contactInvitedString =
String.format(
InstrumentationRegistry.getInstrumentation().targetContext
.getString(R.string.conversation_contact_invited),
accountB!!.registeredName
)
assertOnView(
withText(Matchers.containsString(contactInvitedString)),
matches(isDisplayed())
)
}
@Test
fun c1_acceptContactInvite_hasInvitationReceivedBanner() {
// Move to account B
AccountNavigationUtils.moveToAccount(accountB!!.registeredName)
// Check that the contact invitation has been received
assertOnView(withId(R.id.invitation_received_label), matches(isDisplayed()))
}
@Test
fun c2_acceptContactInvite_hasOptions() {
// Open invitations
doOnView(allOf(withId(R.id.invitation_received_label), isDisplayed()), click())
// Click on invitation
onView(
allOf(
withId(R.id.conv_participant),
withText(accountA!!.registeredName.lowercase())
)
).perform(click())
// Check there is three options: Block, Refuse and Accept
assertOnView(withId(R.id.btnBlock), matches(isDisplayed()))
assertOnView(withId(R.id.btnRefuse), matches(isDisplayed()))
assertOnView(withId(R.id.btnAccept), matches(isDisplayed()))
}
@Test
fun c3_acceptContactInvite_accept() {
// Open invitations
doOnView(withId(R.id.invitation_received_label), click())
// Click on invitation
onView(
allOf(
withId(R.id.conv_participant),
withText(accountA!!.registeredName.lowercase())
)
).perform(click())
// Accept invitation
onView(withId(R.id.btnAccept)).perform(click())
// Check that the contact has been added
val contactInvitedString =
String.format(
InstrumentationRegistry.getInstrumentation().targetContext
.getString(R.string.conversation_contact_added),
accountB!!.registeredName.lowercase()
)
assertOnView(
withText(Matchers.containsString(contactInvitedString)),
matches(isDisplayed())
)
// Going back to invitation list
pressBack()
assertOnView(withId(R.id.confs_list), matches(hasDescendant(withId(R.id.conv_participant))))
}
@Test
fun c4_acceptContactInvite_InvitationReceivedBannerRemoved() {
// Check that the contact invitation has been removed
assertOnView(withId(R.id.invitation_received_label), matches(not(isDisplayed())))
}
@Test
fun d1_refuseContactInvite_refuse() {
// Move to account C
AccountNavigationUtils.moveToAccount(accountC!!.registeredName)
// Open invitations
doOnView(allOf(withId(R.id.invitation_received_label), isDisplayed()), click())
// Click on invitation
onView(
allOf(
withId(R.id.conv_participant),
isDisplayed(),
withText(accountA!!.registeredName.lowercase())
)
).perform(click())
// Refuse invitation
onView(withId(R.id.btnRefuse)).perform(click())
// Check conversation fragment is well closed
onView(withId(R.id.conversation_fragment)).check(doesNotExist())
// Check the smart list doesnt contain the contact
assertOnView(
withId(R.id.confs_list),
matches(not(hasDescendant(allOf(withId(R.id.conv_participant), isDisplayed()))))
)
}
@Test
fun d2_refuseContactInvite_InvitationReceivedBannerRemoved() {
// Check that the contact invitation has been removed
assertOnView(withId(R.id.invitation_received_label), matches(not(isDisplayed())))
}
@Test
fun e1_blockContactInvite_block() {
// Move to account D
AccountNavigationUtils.moveToAccount(accountD!!.registeredName)
// Open invitations
doOnView(withId(R.id.invitation_received_label), click())
// Click on invitation
onView(
allOf(
withId(R.id.conv_participant),
withText(accountA!!.registeredName.lowercase())
)
).perform(click())
// Refuse invitation
onView(withId(R.id.btnBlock)).perform(click())
// Check conversation fragment is well closed
onView(withId(R.id.conversation_fragment)).check(doesNotExist())
// Check the smart list doesnt contain the contact
assertOnView(
withId(R.id.confs_list),
matches(not(hasDescendant(withId(R.id.conv_participant))))
)
}
@Test
fun e2_blockContactInvite_InvitationReceivedBannerRemoved() {
// Check that the contact invitation has been removed
assertOnView(withId(R.id.invitation_received_label), matches(not(isDisplayed())))
}
@Test
fun e3_blockContactInvite_UserIsInBlockedList() {
// Click on search bar menu
onView(withId(R.id.menu_overflow)).perform(click())
// Click on account settings
// Don't know wht but doesn't work to select by ID.
val accountSettingString = InstrumentationRegistry.getInstrumentation().targetContext
.getString(R.string.menu_item_account_settings)
onView(allOf(withText(accountSettingString), isDisplayed())).perform(click())
onView(allOf(withId(R.id.settings_account), isDisplayed())).perform(click())
// Click on the block list
onView(withId(R.id.system_black_list_title)).perform(click())
// Check that the contact is in the blocked list
onView(withText(accountA!!.registeredName.lowercase())).check(matches(isDisplayed()))
}
/**
* This test MUST be the last one because it removes the accounts.
*/
@Test
fun z_tearDown() {
// Doing this in a test is not ideal.
// Ideally, it should be in an `@After` method, but the problem is that it is executed
// after each test and not only at the end.
// That is the same problem with Android Test Orchestrator which removes the application
// between each test and not only at the end.
// `@AfterClass` could be used (executed once), but it does not have access to the activity.
mActivityScenarioRule.scenario.onActivity { activity ->
AccountUtils.removeAllAccounts(accountService = activity.mAccountService)
}
}
}
object AccountNavigationUtils {
fun moveToAccount(username: String) {
val searchBarContentNavigationDescription = InstrumentationRegistry
.getInstrumentation().targetContext.getString(R.string.searchbar_navigation_account)
onView(ViewMatchers.withContentDescription(searchBarContentNavigationDescription))
.perform(click())
onView(withText(username.lowercase())).perform(click())
}
}
\ No newline at end of file
/*
* Copyright (C) 2004-2024 Savoir-faire Linux Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package cx.ring.client.wizard
import androidx.test.espresso.Espresso.onView
......@@ -12,6 +28,12 @@ import cx.ring.R
import cx.ring.assertOnView
import cx.ring.client.HomeActivity
import cx.ring.doOnView
import io.reactivex.rxjava3.core.Single
import net.jami.model.Account
import net.jami.model.AccountConfig
import net.jami.model.ConfigKey
import net.jami.services.AccountService
import net.jami.utils.Log
import org.hamcrest.Matchers.allOf
import org.junit.Before
import org.junit.FixMethodOrder
......@@ -19,6 +41,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import java.util.HashMap
@LargeTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
......@@ -30,22 +53,7 @@ class AccountCreation {
var mActivityScenarioRule = ActivityScenarioRule(HomeActivity::class.java)
@Before
fun moveToAccountCreation() {
mActivityScenarioRule.scenario.onActivity { activity -> // Set custom name server
activity.mAccountService.customNameServer = "https://ns-test.jami.net"
}
try {
val searchBarContentNavigationDescription = InstrumentationRegistry
.getInstrumentation().targetContext.getString(R.string.searchbar_navigation_account)
onView(withContentDescription(searchBarContentNavigationDescription)).perform(click())
val addAccountString = InstrumentationRegistry
.getInstrumentation().targetContext.getString(R.string.add_ring_account_title)
onView(withText(addAccountString)).perform(click())
Thread.sleep(5000)
} catch (_: Exception) {
}
}
fun moveToAccountCreation() = moveToWizard()
/**
* Checks if an account can be created by skipping all the steps.
......@@ -58,7 +66,10 @@ class AccountCreation {
* Skip others steps.
*/
@Test
fun accountCreation_SpecifyUsernameOnly() = createAccountWithUsername()
fun accountCreation_SpecifyUsernameOnly() {
val randomUsername = "JamiTest_" + System.currentTimeMillis()
createAccountWithUsername(randomUsername)
}
/**
* Checks if an account can be created by specifying a password only.
......@@ -88,7 +99,9 @@ class AccountCreation {
fun accountCreation_SpecifyUsernameAndPassword() {
onView(withId(R.id.ring_create_btn)).perform(scrollTo(), click())
specifyUsername()
val randomUsername = "JamiTest_" + System.currentTimeMillis()
onView(allOf(withId(R.id.input_username), isDisplayed()))
.perform(replaceText(randomUsername), closeSoftKeyboard())
doOnView(allOf(withId(R.id.create_account_username), isDisplayed(), isEnabled()), click())
......@@ -173,12 +186,6 @@ class AccountCreation {
)
}
private fun specifyUsername() {
val randomUsername = "JamiTest_" + System.currentTimeMillis()
onView(allOf(withId(R.id.input_username), isDisplayed()))
.perform(replaceText(randomUsername), closeSoftKeyboard())
}
/**
* Check what happens when writing a valid username.
* Assert that the create account button is enabled.
......@@ -187,7 +194,9 @@ class AccountCreation {
fun usernameSelection_ValidUsername() {
onView(allOf(withId(R.id.ring_create_btn), isDisplayed())).perform(scrollTo(), click())
specifyUsername()
val randomUsername = "JamiTest_" + System.currentTimeMillis()
onView(allOf(withId(R.id.input_username), isDisplayed()))
.perform(replaceText(randomUsername), closeSoftKeyboard())
assertOnView(
allOf(withId(R.id.create_account_username), isEnabled()),
......@@ -294,6 +303,19 @@ class AccountCreation {
)
}
@Test
fun z_tearDown() {
// Doing this in a test is not ideal.
// Ideally, it should be in an `@After` method, but the problem is that it is executed
// after each test and not only at the end.
// That is the same problem with Android Test Orchestrator which removes the application
// between each test and not only at the end.
// `@AfterClass` could be used (executed once), but it does not have access to the activity.
mActivityScenarioRule.scenario.onActivity { activity ->
AccountUtils.removeAllAccounts(accountService = activity.mAccountService)
}
}
private fun createDefaultAccount() {
onView(withId(R.id.ring_create_btn)).perform(scrollTo(), click())
......@@ -304,17 +326,6 @@ class AccountCreation {
onView(allOf(withId(R.id.skip_create_account), isDisplayed())).perform(click())
}
private fun createAccountWithUsername() {
onView(withId(R.id.ring_create_btn)).perform(scrollTo(), click())
specifyUsername()
doOnView(allOf(withId(R.id.create_account_username), isDisplayed(), isEnabled()), click())
onView(allOf(withId(R.id.create_account_password), isDisplayed())).perform(click())
onView(allOf(withId(R.id.skip_create_account), isDisplayed())).perform(click())
}
private fun specifyPassword() {
onView(allOf(withId(R.id.password), isDisplayed()))
......@@ -332,4 +343,110 @@ class AccountCreation {
onView(allOf(withText(noThanksSrc), isDisplayed())).perform(click())
}
}
private fun moveToWizard() {
mActivityScenarioRule.scenario.onActivity { activity -> // Set custom name server
activity.mAccountService.customNameServer = "https://ns-test.jami.net/"
}
try {
val searchBarContentNavigationDescription = InstrumentationRegistry
.getInstrumentation().targetContext.getString(R.string.searchbar_navigation_account)
onView(withContentDescription(searchBarContentNavigationDescription)).perform(click())
val addAccountString = InstrumentationRegistry
.getInstrumentation().targetContext.getString(R.string.add_ring_account_title)
onView(withText(addAccountString)).perform(click())
} catch (_: Exception) { // Already in the wizard ?
// Todo: Should check before exception where we are.
}
}
private fun createAccountWithUsername(username: String) {
onView(withId(R.id.ring_create_btn)).perform(scrollTo(), click())
onView(allOf(withId(R.id.input_username), isDisplayed()))
.perform(replaceText(username), closeSoftKeyboard())
doOnView(allOf(withId(R.id.create_account_username), isDisplayed(), isEnabled()), click())
onView(allOf(withId(R.id.create_account_password), isDisplayed())).perform(click())
onView(allOf(withId(R.id.skip_create_account), isDisplayed())).perform(click())
Log.d("devdebug", "Account created: $username")
}
}
object AccountUtils {
private val TAG = AccountUtils::class.java.simpleName
private const val NAME_SERVER_ADDRESS = "https://ns-test.jami.net"
/**
* Create n accounts and register them.
* This function is blocking.
*
* @param accountService The account service to use.
* @param count The number of accounts to create.
* @return The list of registered account names.
*/
fun createAccountAndRegister(accountService: AccountService, count: Int): List<Account> {
val baseUsername = "jamitest"
val time = System.currentTimeMillis()
val accountObservableList = (0..<count).map { accountCount ->
val username = "${baseUsername}_${time}_${accountCount}"
Log.d(TAG, "Account username: $username...")
accountService.getAccountTemplate(AccountConfig.ACCOUNT_TYPE_JAMI)
.map { accountDetails: HashMap<String, String> ->
accountDetails[ConfigKey.ACCOUNT_ALIAS.key] = "Jami account $accountCount"
accountDetails[ConfigKey.RINGNS_HOST.key] = NAME_SERVER_ADDRESS
accountDetails
}.flatMapObservable { details ->
Log.d(TAG, "Adding account ...")
accountService.addAccount(details)
}
.filter { account: Account ->
account.registrationState != AccountConfig.RegistrationState.INITIALIZING
}
.firstOrError()
.map { account: Account ->
Log.d(TAG, "Registering account ...")
accountService.registerName(
account, username, AccountService.ACCOUNT_SCHEME_PASSWORD, ""
)
account
}
}
// Wait for all accounts to be created.
val accountList: List<Account> =
Single.zip(accountObservableList) { it.filterIsInstance<Account>() }.blockingGet()
// Wait for all accounts to be registered.
Single.zip(
accountList.map {
accountService.getObservableAccount(it)
.filter { account: Account ->
account.registrationState == AccountConfig.RegistrationState.REGISTERED
}.firstOrError()
}
) { it }.blockingSubscribe()
return accountList
}
/**
* Remove all accounts.
*
* @param accountService The account service to use.
*/
fun removeAllAccounts(accountService: AccountService) {
accountService.observableAccountList.blockingFirst().forEach {
accountService.removeAccount(it.accountId)
}
}
}
\ No newline at end of file
......@@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/conversation_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
......
......@@ -89,8 +89,7 @@ enum class ConfigKey(val key: String, val isBool: Boolean = false) {
SRTP_KEY_EXCHANGE("SRTP.keyExchange"),
SRTP_ENCRYPTION_ALGO("SRTP.encryptionAlgorithm"),
SRTP_RTP_FALLBACK("SRTP.rtpFallback"),
RINGNS_ACCOUNT("RingNS.account"),
RINGNS_HOST("RingNS.host"),
RINGNS_HOST("RingNS.uri"),
DHT_PORT("DHT.port"),
DHT_PUBLIC_IN("DHT.PublicInCalls", true),
PROXY_ENABLED("Account.proxyEnabled", true),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment