Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,24 @@ package com.owncloud.android.ui.activity

import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentUris
import android.content.ContentValues
import android.net.Uri
import android.os.SystemClock
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.launchActivity
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.contrib.DrawerActions
import androidx.test.espresso.contrib.NavigationViewActions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.nextcloud.client.account.User
import com.google.android.material.navigation.NavigationView
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.account.UserAccountManagerImpl
import com.nextcloud.test.Flaky
Expand All @@ -34,16 +39,18 @@ import com.owncloud.android.R
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
import com.owncloud.android.lib.common.ExternalLinkType
import com.owncloud.android.lib.common.accounts.AccountUtils
import org.hamcrest.Matchers
import org.junit.Assert
import com.owncloud.android.ui.navigation.NavigatorActivity
import com.owncloud.android.ui.navigation.NavigatorScreen
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.anyOf
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import java.util.function.Supplier

class DrawerActivityIT : AbstractIT() {
@Rule
@JvmField
@get:Rule
val retryTestRule = RetryTestRule()

@get:Rule
Expand All @@ -52,62 +59,65 @@ class DrawerActivityIT : AbstractIT() {
@Test
@Flaky(reason = "Account switch relaunches FileDisplayActivity, which races with the drawer assertions")
fun switchAccountViaAccountList() {
// Switching accounts finishes and relaunches FileDisplayActivity (see
// FileDisplayActivity.handleRestartIntent). That self-relaunch is incompatible with
// ActivityScenario#close(), which cannot drive its tracked instance to DESTROYED, so the
// scenario is launched without auto-closing.
val scenario = launchActivity<FileDisplayActivity>()
lateinit var sut: FileDisplayActivity
scenario.onActivity { activity ->
sut = activity
}
scenario.onActivity { sut = it }

Assert.assertEquals(account1, sut.user.get().toPlatformAccount())
assertEquals(account1, sut.user.get().toPlatformAccount())

onView(ViewMatchers.withId(R.id.switch_account_button)).perform(ViewActions.click())
onView(
Matchers.anyOf(
ViewMatchers.withText(account2Name),
ViewMatchers.withText(
account2DisplayName
)
)
).perform(ViewActions.click())
onView(withId(R.id.switch_account_button)).perform(click())
onView(anyOf(withText(account2Name), withText(account2DisplayName))).perform(click())

Assert.assertEquals(account2, sut.user.get().toPlatformAccount())
assertEquals(account2, sut.user.get().toPlatformAccount())

onView(ViewMatchers.withId(R.id.switch_account_button)).perform(ViewActions.click())
onView(ViewMatchers.withText(account1?.name)).perform(ViewActions.click())
onView(withId(R.id.switch_account_button)).perform(click())
onView(withText(account1.name)).perform(click())
}

/**
* External link menu items get the item id [MENU_ITEM_EXTERNAL_LINK] + the link's local database id, and that
* id grows without bound because the table is cleared and refilled on every refresh. A link whose id has grown
* past the legacy 0-100 window must still open.
*/
@Test
fun externalLinkWithIdBeyondLegacyRangeIsOpened() {
insertExternalLink()
val linkId = insertExternalLink()
assertTrue(linkId > LEGACY_EXTERNAL_LINK_RANGE)
val menuItemId = MENU_ITEM_EXTERNAL_LINK + linkId

Intents.init()
try {
launchActivity<FileDisplayActivity>().use {
waitForIdleSync()
val intent = NavigatorActivity.intent(targetContext, NavigatorScreen.Community)
ActivityScenario.launch<NavigatorActivity>(intent).use { scenario ->
waitUntil { scenario.hasDrawerMenuItem(menuItemId) }

onView(ViewMatchers.withId(R.id.drawer_layout)).perform(DrawerActions.open())
onView(ViewMatchers.withId(R.id.nav_view))
.perform(NavigationViewActions.navigateTo(MENU_ITEM_EXTERNAL_LINK + EXTERNAL_LINK_ID))
onView(withId(R.id.drawer_layout)).perform(DrawerActions.open())
onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(menuItemId))

Intents.intended(IntentMatchers.hasComponent(ExternalSiteWebView::class.java.name))
Intents.intended(IntentMatchers.hasExtra(ExternalSiteWebView.EXTRA_URL, EXTERNAL_LINK_URL))
val externalSiteIntent = allOf(
hasComponent(ExternalSiteWebView::class.java.name),
hasExtra(ExternalSiteWebView.EXTRA_URL, EXTERNAL_LINK_URL)
)
waitUntil { Intents.getIntents().any(externalSiteIntent::matches) }
}
} finally {
Intents.release()
deleteExternalLink()
deleteExternalLink(linkId)
}
}

private fun insertExternalLink() {
private fun ActivityScenario<NavigatorActivity>.hasDrawerMenuItem(menuItemId: Int): Boolean {
var found = false
onActivity {
found = it.findViewById<NavigationView>(R.id.nav_view).menu.findItem(menuItemId) != null
}
return found
}

private fun waitUntil(condition: () -> Boolean) {
val deadline = SystemClock.elapsedRealtime() + WAIT_TIMEOUT_MILLIS
while (!condition()) {
check(SystemClock.elapsedRealtime() < deadline) { "Condition not met within $WAIT_TIMEOUT_MILLIS ms" }
SystemClock.sleep(WAIT_POLL_INTERVAL_MILLIS)
}
}

private fun insertExternalLink(): Int {
val values = ContentValues().apply {
put(ProviderTableMeta._ID, EXTERNAL_LINK_ID)
put(ProviderTableMeta.EXTERNAL_LINKS_ICON_URL, "")
Expand All @@ -119,86 +129,66 @@ class DrawerActivityIT : AbstractIT() {
}

val uri = targetContext.contentResolver.insert(ProviderTableMeta.CONTENT_URI_EXTERNAL_LINKS, values)
Assert.assertNotNull("External link could not be stored", uri)

// the whole point of the test is an id outside the legacy window, so fail loudly if it was not honoured
Assert.assertTrue(
"External link id must exceed the legacy window",
EXTERNAL_LINK_ID > LEGACY_EXTERNAL_LINK_RANGE
)
return ContentUris.parseId(requireNotNull(uri) { "External link could not be stored" }).toInt()
}

private fun deleteExternalLink() {
private fun deleteExternalLink(linkId: Int) {
targetContext.contentResolver.delete(
ProviderTableMeta.CONTENT_URI_EXTERNAL_LINKS,
"${ProviderTableMeta._ID} = ?",
arrayOf(EXTERNAL_LINK_ID.toString())
arrayOf(linkId.toString())
)
}

companion object {
// kept in sync with DrawerActivity, where both values are private
private const val MENU_ITEM_EXTERNAL_LINK = 111
private const val LEGACY_EXTERNAL_LINK_RANGE = 100

private const val EXTERNAL_LINK_ID = 501
private const val EXTERNAL_LINK_NAME = "High ID Test"
private const val EXTERNAL_LINK_URL = "https://nextcloud.com"

private var account1: Account? = null
private var user1: User? = null
private var account2: Account? = null
private var account2Name: String? = null
private var account2DisplayName: String? = null
private const val WAIT_TIMEOUT_MILLIS = 5_000L
private const val WAIT_POLL_INTERVAL_MILLIS = 100L

private const val SERVER_VERSION = "14.0.0.0"

private lateinit var account1: Account
private lateinit var account2: Account
private lateinit var account2Name: String
private lateinit var account2DisplayName: String

@JvmStatic
@BeforeClass
fun beforeClass() {
val arguments = InstrumentationRegistry.getArguments()
val baseUrl = Uri.parse(arguments.getString("TEST_SERVER_URL"))

val baseUrl = Uri.parse(InstrumentationRegistry.getArguments().getString("TEST_SERVER_URL"))
val platformAccountManager = AccountManager.get(targetContext)
val userAccountManager: UserAccountManager = UserAccountManagerImpl.fromContext(targetContext)

for (account in platformAccountManager.accounts) {
platformAccountManager.removeAccountExplicitly(account)
}
platformAccountManager.accounts.forEach(platformAccountManager::removeAccountExplicitly)

var loginName = "user1"
var password = "user1"

var temp = Account("$loginName@$baseUrl", MainApp.getAccountType(targetContext))
platformAccountManager.addAccountExplicitly(temp, password, null)
platformAccountManager.setUserData(
temp,
AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION,
UserAccountManager.ACCOUNT_VERSION.toString()
)
platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_VERSION, "14.0.0.0")
platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_BASE_URL, baseUrl.toString())
platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_USER_ID, loginName) // same as userId

account1 = userAccountManager.getAccountByName("$loginName@$baseUrl")
user1 = userAccountManager.getUser(account1!!.name)
.orElseThrow<IllegalAccessError?>(Supplier { IllegalAccessError() })

loginName = "user2"
password = "user2"

temp = Account("$loginName@$baseUrl", MainApp.getAccountType(targetContext))
platformAccountManager.addAccountExplicitly(temp, password, null)
platformAccountManager.setUserData(
temp,
AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION,
UserAccountManager.ACCOUNT_VERSION.toString()
)
platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_VERSION, "14.0.0.0")
platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_BASE_URL, baseUrl.toString())
platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_USER_ID, loginName) // same as userId

account2 = userAccountManager.getAccountByName("$loginName@$baseUrl")
account2Name = "$loginName@$baseUrl"
account1 = addAccount(platformAccountManager, "user1", baseUrl)
account2 = addAccount(platformAccountManager, "user2", baseUrl)
account2Name = account2.name
account2DisplayName = "User Two@$baseUrl"
}

private fun addAccount(platformAccountManager: AccountManager, loginName: String, baseUrl: Uri): Account {
val accountName = "$loginName@$baseUrl"
val account = Account(accountName, MainApp.getAccountType(targetContext))

platformAccountManager.run {
addAccountExplicitly(account, loginName, null)
setUserData(
account,
AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION,
UserAccountManager.ACCOUNT_VERSION.toString()
)
setUserData(account, AccountUtils.Constants.KEY_OC_VERSION, SERVER_VERSION)
setUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL, baseUrl.toString())
setUserData(account, AccountUtils.Constants.KEY_USER_ID, loginName)
}

return requireNotNull(UserAccountManagerImpl.fromContext(targetContext).getAccountByName(accountName))
}
}
}
Loading