Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,826 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
* @author Juan Carlos Garrote Gascón
* @author Jorge Aguado Recio
*
* Copyright (C) 2024 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
* <p>
* 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.authentication
import android.accounts.AccountManager
import android.accounts.AccountManager.KEY_ACCOUNT_NAME
import android.accounts.AccountManager.KEY_ACCOUNT_TYPE
import android.app.Activity.RESULT_OK
import android.app.Instrumentation
import android.content.Context
import android.content.Intent
import androidx.lifecycle.MutableLiveData
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.closeSoftKeyboard
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import eu.qsfera.android.R
import eu.qsfera.android.domain.exceptions.NoNetworkConnectionException
import eu.qsfera.android.domain.exceptions.QSferaVersionNotSupportedException
import eu.qsfera.android.domain.exceptions.ServerNotReachableException
import eu.qsfera.android.domain.exceptions.UnauthorizedException
import eu.qsfera.android.domain.server.model.ServerInfo
import eu.qsfera.android.domain.utils.Event
import eu.qsfera.android.extensions.parseError
import eu.qsfera.android.presentation.authentication.ACTION_UPDATE_EXPIRED_TOKEN
import eu.qsfera.android.presentation.authentication.ACTION_UPDATE_TOKEN
import eu.qsfera.android.presentation.authentication.AuthenticationViewModel
import eu.qsfera.android.presentation.authentication.BASIC_TOKEN_TYPE
import eu.qsfera.android.presentation.authentication.EXTRA_ACCOUNT
import eu.qsfera.android.presentation.authentication.EXTRA_ACTION
import eu.qsfera.android.presentation.authentication.KEY_AUTH_TOKEN_TYPE
import eu.qsfera.android.presentation.authentication.LoginActivity
import eu.qsfera.android.presentation.authentication.OAUTH_TOKEN_TYPE
import eu.qsfera.android.presentation.authentication.oauth.OAuthViewModel
import eu.qsfera.android.presentation.common.UIResult
import eu.qsfera.android.presentation.settings.SettingsActivity
import eu.qsfera.android.presentation.settings.SettingsViewModel
import eu.qsfera.android.providers.ContextProvider
import eu.qsfera.android.providers.MdmProvider
import eu.qsfera.android.testutil.OC_ACCOUNT
import eu.qsfera.android.testutil.OC_AUTH_TOKEN_TYPE
import eu.qsfera.android.testutil.OC_BASIC_PASSWORD
import eu.qsfera.android.testutil.OC_BASIC_USERNAME
import eu.qsfera.android.testutil.OC_INSECURE_SERVER_INFO_BASIC_AUTH
import eu.qsfera.android.testutil.OC_SECURE_SERVER_INFO_BASIC_AUTH
import eu.qsfera.android.testutil.OC_SECURE_SERVER_INFO_BEARER_AUTH
import eu.qsfera.android.utils.CONFIGURATION_SERVER_URL
import eu.qsfera.android.utils.CONFIGURATION_SERVER_URL_INPUT_VISIBILITY
import eu.qsfera.android.utils.NO_MDM_RESTRICTION_YET
import eu.qsfera.android.utils.matchers.assertVisibility
import eu.qsfera.android.utils.matchers.isDisplayed
import eu.qsfera.android.utils.matchers.isEnabled
import eu.qsfera.android.utils.matchers.isFocusable
import eu.qsfera.android.utils.matchers.withText
import eu.qsfera.android.utils.mockIntentToComponent
import eu.qsfera.android.utils.replaceText
import eu.qsfera.android.utils.scrollAndClick
import eu.qsfera.android.utils.typeText
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.verify
import org.hamcrest.Matchers.allOf
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
class LoginActivityTest {
private lateinit var activityScenario: ActivityScenario<LoginActivity>
private lateinit var authenticationViewModel: AuthenticationViewModel
private lateinit var oauthViewModel: OAuthViewModel
private lateinit var settingsViewModel: SettingsViewModel
private lateinit var ocContextProvider: ContextProvider
private lateinit var mdmProvider: MdmProvider
private lateinit var context: Context
private lateinit var loginResultLiveData: MutableLiveData<Event<UIResult<String>>>
private lateinit var serverInfoLiveData: MutableLiveData<Event<UIResult<ServerInfo>>>
private lateinit var supportsOauth2LiveData: MutableLiveData<Event<UIResult<Boolean>>>
private lateinit var baseUrlLiveData: MutableLiveData<Event<UIResult<String>>>
private lateinit var accountDiscoveryLiveData: MutableLiveData<Event<UIResult<Unit>>>
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
authenticationViewModel = mockk(relaxed = true)
oauthViewModel = mockk(relaxed = true)
settingsViewModel = mockk(relaxUnitFun = true)
ocContextProvider = mockk(relaxed = true)
mdmProvider = mockk(relaxed = true)
val accountManager = mockk<AccountManager>(relaxed = true)
every { accountManager.getUserData(any(), any()) } returns null
every { accountManager.getPassword(any()) } returns null
mockkStatic(AccountManager::class)
every { AccountManager.get(any()) } returns accountManager
loginResultLiveData = MutableLiveData()
serverInfoLiveData = MutableLiveData()
supportsOauth2LiveData = MutableLiveData()
baseUrlLiveData = MutableLiveData()
accountDiscoveryLiveData = MutableLiveData()
every { authenticationViewModel.loginResult } returns loginResultLiveData
every { authenticationViewModel.serverInfo } returns serverInfoLiveData
every { authenticationViewModel.supportsOAuth2 } returns supportsOauth2LiveData
every { authenticationViewModel.baseUrl } returns baseUrlLiveData
every { authenticationViewModel.accountDiscovery } returns accountDiscoveryLiveData
every { settingsViewModel.isThereAttachedAccount() } returns false
stopKoin()
startKoin {
context
allowOverride(override = true)
modules(
module {
viewModel {
authenticationViewModel
}
viewModel {
oauthViewModel
}
viewModel {
settingsViewModel
}
factory {
ocContextProvider
}
factory {
mdmProvider
}
}
)
}
}
@After
fun tearDown() {
unmockkAll()
}
private fun launchTest(
showServerUrlInput: Boolean = true,
serverUrl: String = "",
showLoginBackGroundImage: Boolean = true,
showWelcomeLink: Boolean = true,
accountType: String = "qsfera",
loginWelcomeText: String = "",
webfingerLookupServer: String = "",
intent: Intent? = null
) {
every { mdmProvider.getBrandingBoolean(CONFIGURATION_SERVER_URL_INPUT_VISIBILITY, R.bool.show_server_url_input) } returns showServerUrlInput
every { mdmProvider.getBrandingString(CONFIGURATION_SERVER_URL, R.string.server_url) } returns serverUrl
every { mdmProvider.getBrandingString(NO_MDM_RESTRICTION_YET, R.string.webfinger_lookup_server) } returns webfingerLookupServer
every { ocContextProvider.getBoolean(R.bool.use_login_background_image) } returns showLoginBackGroundImage
every { ocContextProvider.getBoolean(R.bool.show_welcome_link) } returns showWelcomeLink
every { ocContextProvider.getString(R.string.account_type) } returns accountType
every { ocContextProvider.getString(R.string.login_welcome_text) } returns loginWelcomeText
every { ocContextProvider.getString(R.string.app_name) } returns BRANDED_APP_NAME
activityScenario = if (intent == null) {
ActivityScenario.launch(LoginActivity::class.java)
} else {
ActivityScenario.launch(intent)
}
}
@Test
fun initialViewStatus_notBrandedOptions() {
launchTest()
assertViewsDisplayed()
assertWebfingerFlowDisplayed(webfingerEnabled = false)
}
@Test
fun initialViewStatus_brandedOptions_webfinger() {
launchTest(webfingerLookupServer = OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
assertWebfingerFlowDisplayed(webfingerEnabled = true)
}
@Test
fun initialViewStatus_brandedOptions_serverInfoInSetup() {
launchTest(showServerUrlInput = false, serverUrl = OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
assertViewsDisplayed(
showHostUrlFrame = false,
showHostUrlInput = false,
showCenteredRefreshButton = true,
showEmbeddedCheckServerButton = false
)
}
@Test
fun initialViewStatus_brandedOptions_serverInfoInSetup_connectionFails() {
launchTest(showServerUrlInput = false, serverUrl = OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
serverInfoLiveData.postValue(Event(UIResult.Error(NoNetworkConnectionException())))
R.id.centeredRefreshButton.isDisplayed(true)
R.id.centeredRefreshButton.scrollAndClick()
verify(exactly = 1) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) }
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
R.id.centeredRefreshButton.isDisplayed(false)
}
@Test
fun initialViewStatus_brandedOptions_dontUseLoginBackgroundImage() {
launchTest(showLoginBackGroundImage = false)
assertViewsDisplayed(showLoginBackGroundImage = false)
}
@Test
fun initialViewStatus_brandedOptions_dontShowWelcomeLink() {
launchTest(showWelcomeLink = false)
assertViewsDisplayed(showWelcomeLink = false)
}
@Test
fun initialViewStatus_brandedOptions_customWelcomeText() {
launchTest(showWelcomeLink = true, loginWelcomeText = CUSTOM_WELCOME_TEXT)
assertViewsDisplayed(showWelcomeLink = true)
R.id.welcome_link.withText(CUSTOM_WELCOME_TEXT)
}
@Test
fun initialViewStatus_brandedOptions_defaultWelcomeText() {
launchTest(showWelcomeLink = true, loginWelcomeText = "")
assertViewsDisplayed(showWelcomeLink = true)
R.id.welcome_link.withText(String.format(ocContextProvider.getString(R.string.auth_register), BRANDED_APP_NAME))
}
@Test
fun checkServerInfo_clickButton_callGetServerInfo() {
launchTest()
R.id.hostUrlInput.typeText(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
R.id.embeddedCheckServerButton.scrollAndClick()
verify(exactly = 1) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) }
}
@Test
fun checkServerInfo_clickLogo_callGetServerInfo() {
launchTest()
R.id.hostUrlInput.typeText(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
R.id.thumbnail.scrollAndClick()
verify(exactly = 1) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) }
}
@Test
fun checkServerInfo_isLoading_show() {
launchTest()
serverInfoLiveData.postValue(Event(UIResult.Loading()))
with(R.id.server_status_text) {
isDisplayed(true)
withText(R.string.auth_testing_connection)
}
}
@Test
fun checkServerInfo_isSuccess_updateUrlInput() {
launchTest()
R.id.hostUrlInput.typeText("demo.qsfera.eu")
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
R.id.hostUrlInput.withText(SECURE_SERVER_INFO_BASIC.baseUrl)
}
@Test
fun checkServerInfo_isSuccess_Secure() {
launchTest()
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
with(R.id.server_status_text) {
isDisplayed(true)
assertVisibility(Visibility.VISIBLE)
withText(R.string.auth_secure_connection)
}
}
@Test
fun checkServerInfo_isSuccess_NotSecure() {
launchTest()
serverInfoLiveData.postValue(Event(UIResult.Success(INSECURE_SERVER_INFO_BASIC)))
onView(withText(R.string.insecure_http_url_title_dialog)).check(matches(isDisplayed()))
onView(withText(R.string.insecure_http_url_message_dialog)).check(matches(isDisplayed()))
onView(withText(R.string.insecure_http_url_continue_button)).inRoot(isDialog()).check(matches(isDisplayed())).perform(click())
with(R.id.server_status_text) {
isDisplayed(true)
assertVisibility(Visibility.VISIBLE)
withText(R.string.auth_connection_established)
}
}
@Test
fun checkServerInfo_isSuccess_Basic() {
launchTest()
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
checkBasicFieldsVisibility(loginButtonShouldBeVisible = false)
}
@Test
fun checkServerInfo_isSuccess_Bearer() {
Intents.init()
launchTest()
avoidOpeningChromeCustomTab()
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BEARER)))
checkBearerFieldsVisibility()
Intents.release()
}
@Test
fun checkServerInfo_isSuccess_basicModifyUrlInput() {
launchTest()
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
checkBasicFieldsVisibility()
R.id.account_username.typeText(OC_BASIC_USERNAME)
R.id.account_password.typeText(OC_BASIC_PASSWORD)
R.id.hostUrlInput.typeText("anything")
with(R.id.account_username) {
withText("")
assertVisibility(Visibility.GONE)
}
with(R.id.account_password) {
withText("")
assertVisibility(Visibility.GONE)
}
R.id.loginButton.assertVisibility(Visibility.GONE)
}
@Test
fun checkServerInfo_isSuccess_bearerModifyUrlInput() {
Intents.init()
launchTest()
avoidOpeningChromeCustomTab()
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BEARER)))
checkBearerFieldsVisibility()
R.id.hostUrlInput.typeText("anything")
R.id.auth_status_text.assertVisibility(Visibility.GONE)
Intents.release()
}
@Test
fun checkServerInfo_isError_emptyUrl() {
launchTest()
R.id.hostUrlInput.typeText("")
R.id.embeddedCheckServerButton.scrollAndClick()
with(R.id.server_status_text) {
isDisplayed(true)
assertVisibility(Visibility.VISIBLE)
withText(R.string.auth_can_not_auth_against_server)
}
verify(exactly = 0) { authenticationViewModel.getServerInfo(any()) }
}
@Test
fun checkServerInfo_isError_qsferaVersionNotSupported() {
launchTest()
serverInfoLiveData.postValue(Event(UIResult.Error(QSferaVersionNotSupportedException())))
with(R.id.server_status_text) {
isDisplayed(true)
assertVisibility(Visibility.VISIBLE)
withText(R.string.server_not_supported)
}
}
@Test
fun checkServerInfo_isError_noNetworkConnection() {
launchTest()
serverInfoLiveData.postValue(Event(UIResult.Error(NoNetworkConnectionException())))
with(R.id.server_status_text) {
isDisplayed(true)
assertVisibility(Visibility.VISIBLE)
withText(R.string.error_no_network_connection)
}
}
@Test
fun checkServerInfo_isError_otherExceptions() {
launchTest()
serverInfoLiveData.postValue(Event(UIResult.Error(ServerNotReachableException())))
with(R.id.server_status_text) {
isDisplayed(true)
assertVisibility(Visibility.VISIBLE)
withText(R.string.network_host_not_available)
}
}
@Ignore
@Test
fun loginBasic_callLoginBasic() {
launchTest()
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
R.id.account_username.typeText(OC_BASIC_USERNAME)
R.id.account_password.typeText(OC_BASIC_PASSWORD)
with(R.id.loginButton) {
isDisplayed(true)
scrollAndClick()
}
verify(exactly = 1) { authenticationViewModel.loginBasic(OC_BASIC_USERNAME, OC_BASIC_PASSWORD, null) }
}
@Ignore
@Test
fun loginBasic_callLoginBasic_trimUsername() {
launchTest()
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
R.id.account_username.typeText(" $OC_BASIC_USERNAME ")
R.id.account_password.typeText(OC_BASIC_PASSWORD)
with(R.id.loginButton) {
isDisplayed(true)
scrollAndClick()
}
verify(exactly = 1) { authenticationViewModel.loginBasic(OC_BASIC_USERNAME, OC_BASIC_PASSWORD, null) }
}
@Test
fun loginBasic_showOrHideFields() {
launchTest()
serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
R.id.account_username.typeText(OC_BASIC_USERNAME)
R.id.loginButton.isDisplayed(false)
R.id.account_password.typeText(OC_BASIC_PASSWORD)
R.id.loginButton.isDisplayed(true)
R.id.account_username.replaceText("")
R.id.loginButton.isDisplayed(false)
}
@Test
fun login_isLoading() {
launchTest()
loginResultLiveData.postValue(Event(UIResult.Loading()))
with(R.id.auth_status_text) {
withText(R.string.auth_trying_to_login)
isDisplayed(true)
assertVisibility(Visibility.VISIBLE)
}
}
@Ignore
@Test
fun login_isSuccess_finishResultCode() {
launchTest()
loginResultLiveData.postValue(Event(UIResult.Success(data = "Account_name")))
accountDiscoveryLiveData.postValue(Event(UIResult.Success()))
assertEquals(activityScenario.result.resultCode, RESULT_OK)
val accountName: String? = activityScenario.result?.resultData?.extras?.getString(KEY_ACCOUNT_NAME)
val accountType: String? = activityScenario.result?.resultData?.extras?.getString(KEY_ACCOUNT_TYPE)
assertNotNull(accountName)
assertNotNull(accountType)
assertEquals("Account_name", accountName)
assertEquals("qsfera", accountType)
}
@Ignore
@Test
fun login_isSuccess_finishResultCodeBrandedAccountType() {
launchTest(accountType = "notQSfera")
loginResultLiveData.postValue(Event(UIResult.Success(data = "Account_name")))
accountDiscoveryLiveData.postValue(Event(UIResult.Success()))
assertEquals(activityScenario.result.resultCode, RESULT_OK)
val accountName: String? = activityScenario.result?.resultData?.extras?.getString(KEY_ACCOUNT_NAME)
val accountType: String? = activityScenario.result?.resultData?.extras?.getString(KEY_ACCOUNT_TYPE)
assertNotNull(accountName)
assertNotNull(accountType)
assertEquals("Account_name", accountName)
assertEquals("notQSfera", accountType)
}
@Test
fun login_isError_NoNetworkConnectionException() {
launchTest()
loginResultLiveData.postValue(Event(UIResult.Error(NoNetworkConnectionException())))
R.id.server_status_text.withText(R.string.error_no_network_connection)
checkBasicFieldsVisibility(fieldsShouldBeVisible = false)
}
@Test
fun login_isError_ServerNotReachableException() {
launchTest()
loginResultLiveData.postValue(Event(UIResult.Error(ServerNotReachableException())))
R.id.server_status_text.withText(R.string.error_no_network_connection)
checkBasicFieldsVisibility(fieldsShouldBeVisible = false)
}
@Test
fun login_isError_OtherException() {
launchTest()
val exception = UnauthorizedException()
loginResultLiveData.postValue(Event(UIResult.Error(exception)))
R.id.auth_status_text.withText(exception.parseError("", context.resources, true) as String)
}
@Test
fun intent_withSavedAccount_viewModelCalls() {
val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
}
launchTest(intent = intentWithAccount)
verify(exactly = 1) { authenticationViewModel.supportsOAuth2(OC_ACCOUNT.name) }
verify(exactly = 1) { authenticationViewModel.getBaseUrl(OC_ACCOUNT.name) }
}
@Test
fun supportsOAuth_isSuccess_actionUpdateExpiredTokenOAuth() {
val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
putExtra(EXTRA_ACTION, ACTION_UPDATE_EXPIRED_TOKEN)
putExtra(KEY_AUTH_TOKEN_TYPE, OAUTH_TOKEN_TYPE)
}
launchTest(intent = intentWithAccount)
supportsOauth2LiveData.postValue(Event(UIResult.Success(true)))
with(R.id.instructions_message) {
isDisplayed(true)
assertVisibility(Visibility.VISIBLE)
withText(context.getString(R.string.auth_expired_oauth_token_toast))
}
}
@Test
fun supportsOAuth_isSuccess_actionUpdateToken() {
val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
putExtra(EXTRA_ACTION, ACTION_UPDATE_TOKEN)
putExtra(KEY_AUTH_TOKEN_TYPE, OC_AUTH_TOKEN_TYPE)
}
launchTest(intent = intentWithAccount)
supportsOauth2LiveData.postValue(Event(UIResult.Success(false)))
R.id.instructions_message.assertVisibility(Visibility.GONE)
}
@Test
fun supportsOAuth_isSuccess_actionUpdateExpiredTokenBasic() {
val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
putExtra(EXTRA_ACTION, ACTION_UPDATE_EXPIRED_TOKEN)
putExtra(KEY_AUTH_TOKEN_TYPE, BASIC_TOKEN_TYPE)
}
launchTest(intent = intentWithAccount)
supportsOauth2LiveData.postValue(Event(UIResult.Success(false)))
with(R.id.instructions_message) {
isDisplayed(true)
assertVisibility(Visibility.VISIBLE)
withText(context.getString(R.string.auth_expired_basic_auth_toast))
}
}
@Test
fun getBaseUrl_isSuccess_updatesBaseUrl() {
val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
}
launchTest(intent = intentWithAccount)
baseUrlLiveData.postValue(Event(UIResult.Success(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)))
with(R.id.hostUrlInput) {
withText(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
assertVisibility(Visibility.VISIBLE)
isDisplayed(true)
isEnabled(false)
isFocusable(false)
}
verify(exactly = 0) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl) }
}
@Test
fun getBaseUrlAndActionNotCreate_isSuccess_updatesBaseUrl() {
val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
putExtra(EXTRA_ACTION, ACTION_UPDATE_EXPIRED_TOKEN)
}
launchTest(intent = intentWithAccount)
baseUrlLiveData.postValue(Event(UIResult.Success(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)))
with(R.id.hostUrlInput) {
withText(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
assertVisibility(Visibility.VISIBLE)
isDisplayed(true)
isEnabled(false)
isFocusable(false)
}
verify(exactly = 1) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl) }
}
@Test
fun settingsLink() {
Intents.init()
launchTest()
closeSoftKeyboard()
mockIntentToComponent(RESULT_OK, SettingsActivity::class.java.name)
onView(withId(R.id.settings_link)).perform(click())
intended(hasComponent(SettingsActivity::class.java.name))
Intents.release()
}
private fun avoidOpeningChromeCustomTab() {
Intents.intending(allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW)))
.respondWith(Instrumentation.ActivityResult(RESULT_OK, null))
}
private fun checkBasicFieldsVisibility(
fieldsShouldBeVisible: Boolean = true,
loginButtonShouldBeVisible: Boolean = false
) {
val visibilityMatcherFields = if (fieldsShouldBeVisible) Visibility.VISIBLE else Visibility.GONE
val visibilityMatcherLoginButton = if (loginButtonShouldBeVisible) Visibility.VISIBLE else Visibility.GONE
with(R.id.account_username) {
isDisplayed(fieldsShouldBeVisible)
assertVisibility(visibilityMatcherFields)
withText("")
}
with(R.id.account_password) {
isDisplayed(fieldsShouldBeVisible)
assertVisibility(visibilityMatcherFields)
withText("")
}
R.id.loginButton.assertVisibility(visibilityMatcherLoginButton)
R.id.auth_status_text.assertVisibility(visibilityMatcherLoginButton)
}
private fun checkBearerFieldsVisibility() {
R.id.account_username.assertVisibility(Visibility.GONE)
R.id.account_password.assertVisibility(Visibility.GONE)
R.id.auth_status_text.assertVisibility(Visibility.GONE)
with(R.id.server_status_text) {
isDisplayed(true)
assertVisibility(Visibility.VISIBLE)
}
}
private fun assertWebfingerFlowDisplayed(
webfingerEnabled: Boolean,
) {
R.id.webfinger_layout.isDisplayed(webfingerEnabled)
R.id.webfinger_username.isDisplayed(webfingerEnabled)
R.id.webfinger_button.isDisplayed(webfingerEnabled)
}
private fun assertViewsDisplayed(
showLoginBackGroundImage: Boolean = true,
showThumbnail: Boolean = true,
showCenteredRefreshButton: Boolean = false,
showInstructionsMessage: Boolean = false,
showHostUrlFrame: Boolean = true,
showHostUrlInput: Boolean = true,
showEmbeddedCheckServerButton: Boolean = true,
showEmbeddedRefreshButton: Boolean = false,
showServerStatusText: Boolean = false,
showAccountUsername: Boolean = false,
showAccountPassword: Boolean = false,
showAuthStatus: Boolean = false,
showLoginButton: Boolean = false,
showWelcomeLink: Boolean = true
) {
R.id.login_background_image.isDisplayed(displayed = showLoginBackGroundImage)
R.id.thumbnail.isDisplayed(displayed = showThumbnail)
R.id.centeredRefreshButton.isDisplayed(displayed = showCenteredRefreshButton)
R.id.instructions_message.isDisplayed(displayed = showInstructionsMessage)
R.id.hostUrlFrame.isDisplayed(displayed = showHostUrlFrame)
R.id.hostUrlInput.isDisplayed(displayed = showHostUrlInput)
R.id.embeddedCheckServerButton.isDisplayed(displayed = showEmbeddedCheckServerButton)
R.id.embeddedRefreshButton.isDisplayed(displayed = showEmbeddedRefreshButton)
R.id.server_status_text.isDisplayed(displayed = showServerStatusText)
R.id.account_username_container.isDisplayed(displayed = showAccountUsername)
R.id.account_username.isDisplayed(displayed = showAccountUsername)
R.id.account_password_container.isDisplayed(displayed = showAccountPassword)
R.id.account_password.isDisplayed(displayed = showAccountPassword)
R.id.auth_status_text.isDisplayed(displayed = showAuthStatus)
R.id.loginButton.isDisplayed(displayed = showLoginButton)
R.id.welcome_link.isDisplayed(displayed = showWelcomeLink)
}
companion object {
val SECURE_SERVER_INFO_BASIC = OC_SECURE_SERVER_INFO_BASIC_AUTH
val INSECURE_SERVER_INFO_BASIC = OC_INSECURE_SERVER_INFO_BASIC_AUTH
val SECURE_SERVER_INFO_BEARER = OC_SECURE_SERVER_INFO_BEARER_AUTH
private const val CUSTOM_WELCOME_TEXT = "Welcome to this test"
private const val BRANDED_APP_NAME = "BrandedAppName"
}
}
@@ -0,0 +1,97 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
*
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
* <p>
* 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.files
import android.os.Bundle
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragment
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withId
import eu.qsfera.android.R
import eu.qsfera.android.presentation.files.SortBottomSheetFragment
import eu.qsfera.android.presentation.files.SortOrder
import eu.qsfera.android.presentation.files.SortType
import eu.qsfera.android.utils.matchers.bsfItemWithIcon
import eu.qsfera.android.utils.matchers.bsfItemWithTitle
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.Before
import org.junit.Test
class SortBottomSheetFragmentTest {
private lateinit var fragmentScenario: FragmentScenario<SortBottomSheetFragment>
private val fragmentListener = mockk<SortBottomSheetFragment.SortDialogListener>()
@Before
fun setUp() {
val fragmentArgs = Bundle().apply {
putParcelable(SortBottomSheetFragment.ARG_SORT_TYPE, SortType.SORT_TYPE_BY_NAME)
putParcelable(SortBottomSheetFragment.ARG_SORT_ORDER, SortOrder.SORT_ORDER_ASCENDING)
}
fragmentScenario = launchFragment(fragmentArgs)
every { fragmentListener.onSortSelected(any()) } returns Unit
fragmentScenario.onFragment { it.sortDialogListener = fragmentListener }
}
@Test
fun test_initial_view() {
onView(withId(R.id.title))
.inRoot(RootMatchers.isDialog())
.check(matches(ViewMatchers.withText(R.string.actionbar_sort_title)))
.check(matches(ViewMatchers.hasTextColor(R.color.bottom_sheet_fragment_title_color)))
with(R.id.sort_by_name) {
bsfItemWithTitle(R.string.global_name, R.color.primary)
bsfItemWithIcon(R.drawable.ic_sort_by_name, R.color.primary)
}
with(R.id.sort_by_size) {
bsfItemWithTitle(R.string.global_size, R.color.bottom_sheet_fragment_item_color)
bsfItemWithIcon(R.drawable.ic_sort_by_size, R.color.bottom_sheet_fragment_item_color)
}
with(R.id.sort_by_date) {
bsfItemWithTitle(R.string.global_date, R.color.bottom_sheet_fragment_item_color)
bsfItemWithIcon(R.drawable.ic_sort_by_date, R.color.bottom_sheet_fragment_item_color)
}
}
@Test
fun test_sort_by_name_click() {
onView(withId(R.id.sort_by_name)).inRoot(RootMatchers.isDialog()).perform(ViewActions.click())
verify { fragmentListener.onSortSelected(SortType.SORT_TYPE_BY_NAME) }
}
@Test
fun test_sort_by_date_click() {
onView(withId(R.id.sort_by_date)).inRoot(RootMatchers.isDialog()).perform(ViewActions.click())
verify { fragmentListener.onSortSelected(SortType.SORT_TYPE_BY_DATE) }
}
@Test
fun test_sort_by_size_click() {
onView(withId(R.id.sort_by_size)).inRoot(RootMatchers.isDialog()).perform(ViewActions.click())
verify { fragmentListener.onSortSelected(SortType.SORT_TYPE_BY_SIZE) }
}
}
@@ -0,0 +1,216 @@
package eu.qsfera.android.files.details
import android.content.Context
import androidx.test.core.app.ActivityScenario.launch
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withId
import eu.qsfera.android.R
import eu.qsfera.android.domain.files.model.OCFileWithSyncInfo
import eu.qsfera.android.presentation.files.details.FileDetailsFragment
import eu.qsfera.android.presentation.files.details.FileDetailsViewModel
import eu.qsfera.android.presentation.files.operations.FileOperationsViewModel
import eu.qsfera.android.sharing.shares.ui.TestShareFileActivity
import eu.qsfera.android.testutil.OC_ACCOUNT
import eu.qsfera.android.testutil.OC_FILE
import eu.qsfera.android.testutil.OC_FILE_WITH_SYNC_INFO_AVAILABLE_OFFLINE
import eu.qsfera.android.testutil.OC_FILE_WITH_SYNC_INFO
import eu.qsfera.android.testutil.OC_FILE_WITH_SYNC_INFO_AND_SPACE
import eu.qsfera.android.testutil.OC_FILE_WITH_SYNC_INFO_AND_WITHOUT_PERSONAL_SPACE
import eu.qsfera.android.utils.DisplayUtils
import eu.qsfera.android.utils.matchers.assertVisibility
import eu.qsfera.android.utils.matchers.isDisplayed
import eu.qsfera.android.utils.matchers.withDrawable
import eu.qsfera.android.utils.matchers.withText
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
@Ignore
class FileDetailsFragmentTest {
private lateinit var fileDetailsViewModel: FileDetailsViewModel
private lateinit var fileOperationsViewModel: FileOperationsViewModel
private lateinit var context: Context
private var currentFile: MutableStateFlow<OCFileWithSyncInfo?> = MutableStateFlow(OC_FILE_WITH_SYNC_INFO_AND_SPACE)
private var currentFileWithoutPersonalSpace: MutableStateFlow<OCFileWithSyncInfo?> =
MutableStateFlow(OC_FILE_WITH_SYNC_INFO_AND_WITHOUT_PERSONAL_SPACE)
private var currentFileSyncInfo: MutableStateFlow<OCFileWithSyncInfo?> = MutableStateFlow(OC_FILE_WITH_SYNC_INFO)
private var currentFileAvailableOffline: MutableStateFlow<OCFileWithSyncInfo?> = MutableStateFlow(OC_FILE_WITH_SYNC_INFO_AVAILABLE_OFFLINE)
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
fileDetailsViewModel = mockk(relaxed = true)
fileOperationsViewModel = mockk(relaxed = true)
every { fileDetailsViewModel.currentFile } returns currentFile
stopKoin()
startKoin {
context
allowOverride(override = true)
modules(
module {
viewModel {
fileDetailsViewModel
}
viewModel {
fileOperationsViewModel
}
}
)
}
val fileDetailsFragment = FileDetailsFragment.newInstance(
OC_FILE,
OC_ACCOUNT,
syncFileAtOpen = false
)
launch(TestShareFileActivity::class.java).onActivity {
it.startFragment(fileDetailsFragment)
}
}
@Test
fun display_visibility_of_detail_view_when_it_is_displayed() {
assertViewsDisplayed()
}
@Test
fun show_space_personal_when_it_has_value() {
R.id.fdSpace.assertVisibility(ViewMatchers.Visibility.VISIBLE)
R.id.fdSpaceLabel.assertVisibility(ViewMatchers.Visibility.VISIBLE)
R.id.fdIconSpace.assertVisibility(ViewMatchers.Visibility.VISIBLE)
R.id.fdSpace.withText(R.string.bottom_nav_personal)
R.id.fdSpaceLabel.withText(R.string.space_label)
onView(withId(R.id.fdIconSpace))
.check(matches(withDrawable(R.drawable.ic_spaces)))
}
@Test
fun hide_space_when_it_has_no_value() {
every { fileDetailsViewModel.currentFile } returns currentFileSyncInfo
R.id.fdSpace.assertVisibility(ViewMatchers.Visibility.GONE)
R.id.fdSpaceLabel.assertVisibility(ViewMatchers.Visibility.GONE)
R.id.fdIconSpace.assertVisibility(ViewMatchers.Visibility.GONE)
}
@Test
fun show_space_not_personal_when_it_has_value() {
every { fileDetailsViewModel.currentFile } returns currentFileWithoutPersonalSpace
R.id.fdSpace.assertVisibility(ViewMatchers.Visibility.VISIBLE)
R.id.fdSpaceLabel.assertVisibility(ViewMatchers.Visibility.VISIBLE)
R.id.fdIconSpace.assertVisibility(ViewMatchers.Visibility.VISIBLE)
R.id.fdSpace.withText(currentFileWithoutPersonalSpace.value?.space?.name.toString())
R.id.fdSpaceLabel.withText(R.string.space_label)
onView(withId(R.id.fdIconSpace))
.check(matches(withDrawable(R.drawable.ic_spaces)))
}
@Test
fun show_last_sync_when_it_has_value() {
currentFile.value?.file?.lastSyncDateForData = 1212121212212
R.id.fdLastSync.assertVisibility(ViewMatchers.Visibility.VISIBLE)
R.id.fdLastSyncLabel.assertVisibility(ViewMatchers.Visibility.VISIBLE)
R.id.fdLastSyncLabel.withText(R.string.filedetails_last_sync)
R.id.fdLastSync.withText(DisplayUtils.unixTimeToHumanReadable(currentFile.value?.file?.lastSyncDateForData!!))
}
@Test
fun hide_last_sync_when_it_has_no_value() {
every { fileDetailsViewModel.currentFile } returns currentFile
R.id.fdLastSync.assertVisibility(ViewMatchers.Visibility.GONE)
R.id.fdLastSyncLabel.assertVisibility(ViewMatchers.Visibility.GONE)
}
@Test
fun verifyTests() {
R.id.fdCreatedLabel.withText(R.string.filedetails_created)
R.id.fdCreated.withText(DisplayUtils.unixTimeToHumanReadable(currentFile.value?.file?.creationTimestamp!!))
R.id.fdModifiedLabel.withText(R.string.filedetails_modified)
R.id.fdModified.withText(DisplayUtils.unixTimeToHumanReadable(currentFile.value?.file?.modificationTimestamp!!))
R.id.fdPathLabel.withText(R.string.ssl_validator_label_L)
R.id.fdPath.withText(currentFile.value?.file?.getParentRemotePath()!!)
R.id.fdname.withText(currentFile.value?.file?.fileName!!)
}
@Test
fun badge_available_offline_in_image_is_not_viewed_when_file_does_not_change_state() {
every { fileDetailsViewModel.currentFile } returns currentFileAvailableOffline
R.id.badgeDetailFile.assertVisibility(ViewMatchers.Visibility.VISIBLE)
onView(withId(R.id.badgeDetailFile))
.check(matches(withDrawable(R.drawable.offline_available_pin)))
}
@Test
fun show_badge_isAvailableLocally_in_image_when_file_change_state() {
currentFile.value?.file?.etagInConflict = "error"
R.id.badgeDetailFile.assertVisibility(ViewMatchers.Visibility.VISIBLE)
onView(withId(R.id.badgeDetailFile))
.check(matches(withDrawable(R.drawable.error_pin)))
}
private fun assertViewsDisplayed(
showImage: Boolean = true,
showFdName: Boolean = true,
showFdProgressText: Boolean = false,
showFdProgressBar: Boolean = false,
showFdCancelBtn: Boolean = false,
showDivider: Boolean = true,
showDivider2: Boolean = true,
showFdTypeLabel: Boolean = true,
showFdType: Boolean = true,
showFdSizeLabel: Boolean = true,
showFdSize: Boolean = true,
showFdModifiedLabel: Boolean = true,
showFdModified: Boolean = true,
showFdCreatedLabel: Boolean = true,
showFdCreated: Boolean = true,
showDivider3: Boolean = true,
showFdPathLabel: Boolean = true,
showFdPath: Boolean = true
) {
R.id.fdImageDetailFile.isDisplayed(displayed = showImage)
R.id.fdname.isDisplayed(displayed = showFdName)
R.id.fdProgressText.isDisplayed(displayed = showFdProgressText)
R.id.fdProgressBar.isDisplayed(displayed = showFdProgressBar)
R.id.fdCancelBtn.isDisplayed(displayed = showFdCancelBtn)
R.id.divider.isDisplayed(displayed = showDivider)
R.id.fdTypeLabel.isDisplayed(displayed = showFdTypeLabel)
R.id.fdType.isDisplayed(displayed = showFdType)
R.id.fdSizeLabel.isDisplayed(displayed = showFdSizeLabel)
R.id.fdSize.isDisplayed(displayed = showFdSize)
R.id.divider2.isDisplayed(displayed = showDivider2)
R.id.fdModifiedLabel.isDisplayed(displayed = showFdModifiedLabel)
R.id.fdModified.isDisplayed(displayed = showFdModified)
R.id.fdCreatedLabel.isDisplayed(displayed = showFdCreatedLabel)
R.id.fdCreated.isDisplayed(displayed = showFdCreated)
R.id.divider3.isDisplayed(displayed = showDivider3)
R.id.fdPathLabel.isDisplayed(displayed = showFdPathLabel)
R.id.fdPath.isDisplayed(displayed = showFdPath)
}
}
@@ -0,0 +1,106 @@
/**
* qsfera Android client application
*
* @author Fernando Sanz Velasco
* Copyright (C) 2021 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.logging
import androidx.test.core.app.ActivityScenario
import eu.qsfera.android.R
import eu.qsfera.android.presentation.logging.LogsListActivity
import eu.qsfera.android.presentation.logging.LogListViewModel
import eu.qsfera.android.utils.matchers.assertChildCount
import eu.qsfera.android.utils.matchers.isDisplayed
import eu.qsfera.android.utils.matchers.withText
import io.mockk.every
import io.mockk.mockk
import io.mockk.unmockkAll
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
import java.io.File
@Ignore
class LogsListActivityTest {
private lateinit var activityScenario: ActivityScenario<LogsListActivity>
private lateinit var logListViewModel: LogListViewModel
private fun launchTest(logs: List<File>) {
every { logListViewModel.getLogsFiles() } returns logs
activityScenario = ActivityScenario.launch(LogsListActivity::class.java)
}
@Before
fun setUp() {
logListViewModel = mockk(relaxed = true)
stopKoin()
startKoin {
allowOverride(override = true)
modules(
module {
viewModel {
logListViewModel
}
}
)
}
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun test_visibility_toolbar() {
launchTest(logs = emptyList())
R.id.toolbar_activity_logs_list.isDisplayed(true)
}
@Test
fun test_isRecyclerViewEmpty_show_label() {
launchTest(logs = emptyList())
R.id.logs_list_empty.isDisplayed(true)
R.id.list_empty_dataset_title.withText(R.string.prefs_log_no_logs_list_view)
R.id.list_empty_dataset_sub_title.withText(R.string.prefs_log_empty_subtitle)
R.id.recyclerView_activity_logs_list.isDisplayed(false)
}
@Test
fun test_isRecyclerViewNotEmpty_hide_label() {
launchTest(logs = listOf(File("path")))
R.id.logs_list_empty.isDisplayed(false)
R.id.recyclerView_activity_logs_list.isDisplayed(true)
}
@Test
fun test_childCount() {
launchTest(logs = listOf(File("qsfera.2021-01.01.log"), File("qsfera.2021-01-02.log")))
R.id.recyclerView_activity_logs_list.assertChildCount(2)
}
}
@@ -0,0 +1,300 @@
/**
* qsfera Android client application
*
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2021 ownCloud GmbH.
* <p>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
* <p>
* 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.settings
import android.content.ClipboardManager
import android.content.Context
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.preference.Preference
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import eu.qsfera.android.BuildConfig
import eu.qsfera.android.R
import eu.qsfera.android.presentation.releasenotes.ReleaseNotesActivity
import eu.qsfera.android.presentation.settings.privacypolicy.PrivacyPolicyActivity
import eu.qsfera.android.presentation.settings.SettingsFragment
import eu.qsfera.android.presentation.releasenotes.ReleaseNotesViewModel
import eu.qsfera.android.presentation.settings.more.SettingsMoreViewModel
import eu.qsfera.android.presentation.settings.SettingsViewModel
import eu.qsfera.android.utils.matchers.verifyPreference
import eu.qsfera.android.utils.releaseNotesList
import io.mockk.every
import io.mockk.mockk
import io.mockk.unmockkAll
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
class SettingsFragmentTest {
private lateinit var fragmentScenario: FragmentScenario<SettingsFragment>
private var subsectionSecurity: Preference? = null
private var subsectionLogging: Preference? = null
private var subsectionPictureUploads: Preference? = null
private var subsectionVideoUploads: Preference? = null
private var subsectionMore: Preference? = null
private var prefPrivacyPolicy: Preference? = null
private var subsectionWhatsNew: Preference? = null
private var prefAboutApp: Preference? = null
private lateinit var settingsViewModel: SettingsViewModel
private lateinit var moreViewModel: SettingsMoreViewModel
private lateinit var releaseNotesViewModel: ReleaseNotesViewModel
private lateinit var context: Context
private lateinit var version: String
@Before
fun setUp() {
context = InstrumentationRegistry.getInstrumentation().targetContext
settingsViewModel = mockk(relaxed = true)
moreViewModel = mockk(relaxed = true)
releaseNotesViewModel = mockk(relaxed = true)
stopKoin()
startKoin {
context
allowOverride(override = true)
modules(
module {
viewModel {
settingsViewModel
}
viewModel {
moreViewModel
}
viewModel {
releaseNotesViewModel
}
}
)
}
version = String.format(
context.getString(R.string.prefs_app_version_summary),
context.getString(R.string.app_name),
BuildConfig.BUILD_TYPE,
BuildConfig.VERSION_NAME,
BuildConfig.COMMIT_SHA1
)
Intents.init()
}
@After
fun tearDown() {
Intents.release()
unmockkAll()
}
private fun launchTest(
attachedAccount: Boolean,
moreSectionVisible: Boolean = true,
privacyPolicyEnabled: Boolean = true,
whatsNewSectionVisible: Boolean = true
) {
every { settingsViewModel.isThereAttachedAccount() } returns attachedAccount
every { moreViewModel.shouldMoreSectionBeVisible() } returns moreSectionVisible
every { moreViewModel.isPrivacyPolicyEnabled() } returns privacyPolicyEnabled
every { releaseNotesViewModel.shouldWhatsNewSectionBeVisible() } returns whatsNewSectionVisible
fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_qsfera)
fragmentScenario.onFragment { fragment ->
subsectionSecurity = fragment.findPreference(SUBSECTION_SECURITY)
subsectionLogging = fragment.findPreference(SUBSECTION_LOGGING)
subsectionPictureUploads = fragment.findPreference(SUBSECTION_PICTURE_UPLOADS)
subsectionVideoUploads = fragment.findPreference(SUBSECTION_VIDEO_UPLOADS)
subsectionMore = fragment.findPreference(SUBSECTION_MORE)
prefPrivacyPolicy = fragment.findPreference(PREFERENCE_PRIVACY_POLICY)
subsectionWhatsNew = fragment.findPreference(SUBSECTION_WHATSNEW)
prefAboutApp = fragment.findPreference(PREFERENCE_ABOUT_APP)
}
}
@Test
fun settingsViewCommon() {
launchTest(attachedAccount = false)
subsectionSecurity?.verifyPreference(
keyPref = SUBSECTION_SECURITY,
titlePref = context.getString(R.string.prefs_subsection_security),
summaryPref = context.getString(R.string.prefs_subsection_security_summary),
visible = true,
enabled = true
)
subsectionLogging?.verifyPreference(
keyPref = SUBSECTION_LOGGING,
titlePref = context.getString(R.string.prefs_subsection_logging),
summaryPref = context.getString(R.string.prefs_subsection_logging_summary),
visible = true,
enabled = true
)
subsectionMore?.verifyPreference(
keyPref = SUBSECTION_MORE,
titlePref = context.getString(R.string.prefs_subsection_more),
summaryPref = context.getString(R.string.prefs_subsection_more_summary),
visible = true,
enabled = true
)
prefPrivacyPolicy?.verifyPreference(
keyPref = PREFERENCE_PRIVACY_POLICY,
titlePref = context.getString(R.string.prefs_privacy_policy),
visible = true,
enabled = true
)
subsectionWhatsNew?.verifyPreference(
keyPref = SUBSECTION_WHATSNEW,
titlePref = context.getString(R.string.prefs_subsection_whatsnew),
visible = true,
enabled = true
)
prefAboutApp?.verifyPreference(
keyPref = PREFERENCE_ABOUT_APP,
titlePref = context.getString(R.string.prefs_app_version),
summaryPref = version,
visible = true,
enabled = true
)
}
@Test
fun settingsViewNoAccountAttached() {
launchTest(attachedAccount = false)
subsectionPictureUploads?.verifyPreference(
keyPref = SUBSECTION_PICTURE_UPLOADS,
titlePref = context.getString(R.string.prefs_subsection_picture_uploads),
summaryPref = context.getString(R.string.prefs_subsection_picture_uploads_summary),
visible = false
)
subsectionVideoUploads?.verifyPreference(
keyPref = SUBSECTION_VIDEO_UPLOADS,
titlePref = context.getString(R.string.prefs_subsection_video_uploads),
summaryPref = context.getString(R.string.prefs_subsection_video_uploads_summary),
visible = false
)
}
@Test
fun settingsViewAccountAttached() {
launchTest(attachedAccount = true)
subsectionPictureUploads?.verifyPreference(
keyPref = SUBSECTION_PICTURE_UPLOADS,
titlePref = context.getString(R.string.prefs_subsection_picture_uploads),
summaryPref = context.getString(R.string.prefs_subsection_picture_uploads_summary),
visible = true,
enabled = true
)
subsectionVideoUploads?.verifyPreference(
keyPref = SUBSECTION_VIDEO_UPLOADS,
titlePref = context.getString(R.string.prefs_subsection_video_uploads),
summaryPref = context.getString(R.string.prefs_subsection_video_uploads_summary),
visible = true,
enabled = true
)
}
@Test
fun settingsMoreSectionHidden() {
launchTest(attachedAccount = false, moreSectionVisible = false)
subsectionMore?.verifyPreference(
keyPref = SUBSECTION_MORE,
titlePref = context.getString(R.string.prefs_subsection_more),
summaryPref = context.getString(R.string.prefs_subsection_more_summary),
visible = false
)
}
@Test
fun settingsWhatsNewSectionHidden() {
launchTest(attachedAccount = false, whatsNewSectionVisible = false)
subsectionWhatsNew?.verifyPreference(
keyPref = SUBSECTION_WHATSNEW,
titlePref = context.getString(R.string.prefs_subsection_whatsnew),
visible = false
)
}
@Test
fun privacyPolicyOpensPrivacyPolicyActivity() {
launchTest(attachedAccount = false)
onView(withText(R.string.prefs_privacy_policy)).perform(click())
Intents.intended(IntentMatchers.hasComponent(PrivacyPolicyActivity::class.java.name))
}
@Ignore("Flaky test")
@Test
fun whatsnewOpensReleaseNotesActivity() {
launchTest(attachedAccount = false)
every { releaseNotesViewModel.getReleaseNotes() } returns releaseNotesList
onView(withText(R.string.prefs_subsection_whatsnew)).perform(click())
Intents.intended(IntentMatchers.hasComponent(ReleaseNotesActivity::class.java.name))
}
@Test
fun clickOnAppVersion() {
launchTest(attachedAccount = false)
onView(withText(R.string.prefs_app_version)).perform(click())
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
onView(withText(R.string.clipboard_text_copied)).check(matches(isEnabled()))
assertEquals(version, clipboard?.primaryClip?.getItemAt(0)?.coerceToText(context))
}
companion object {
private const val SUBSECTION_SECURITY = "security_subsection"
private const val SUBSECTION_LOGGING = "logging_subsection"
private const val SUBSECTION_PICTURE_UPLOADS = "picture_uploads_subsection"
private const val SUBSECTION_VIDEO_UPLOADS = "video_uploads_subsection"
private const val SUBSECTION_MORE = "more_subsection"
private const val PREFERENCE_PRIVACY_POLICY = "privacyPolicy"
private const val PREFERENCE_ABOUT_APP = "about_app"
private const val SUBSECTION_WHATSNEW = "whatsNew"
}
}
@@ -0,0 +1,111 @@
/**
* qsfera Android client application
*
* @author David Crespo Ríos
* Copyright (C) 2022 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.settings.advanced
import android.content.Context
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.preference.SwitchPreferenceCompat
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import eu.qsfera.android.R
import eu.qsfera.android.presentation.settings.advanced.SettingsAdvancedFragment
import eu.qsfera.android.presentation.settings.advanced.SettingsAdvancedFragment.Companion.PREF_SHOW_HIDDEN_FILES
import eu.qsfera.android.presentation.settings.advanced.SettingsAdvancedViewModel
import eu.qsfera.android.utils.matchers.verifyPreference
import io.mockk.every
import io.mockk.mockk
import io.mockk.unmockkAll
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
class SettingsAdvancedFragmentTest {
private lateinit var fragmentScenario: FragmentScenario<SettingsAdvancedFragment>
private var prefShowHiddenFiles: SwitchPreferenceCompat? = null
private lateinit var advancedViewModel: SettingsAdvancedViewModel
private lateinit var context: Context
@Before
fun setUp() {
context = InstrumentationRegistry.getInstrumentation().targetContext
advancedViewModel = mockk(relaxed = true)
stopKoin()
startKoin {
context
allowOverride(override = true)
modules(
module {
viewModel {
advancedViewModel
}
}
)
}
every { advancedViewModel.isHiddenFilesShown() } returns true
fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_qsfera)
fragmentScenario.onFragment { fragment ->
prefShowHiddenFiles = fragment.findPreference(PREF_SHOW_HIDDEN_FILES)
}
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun advancedView() {
assertNotNull(prefShowHiddenFiles)
prefShowHiddenFiles?.verifyPreference(
keyPref = PREF_SHOW_HIDDEN_FILES,
titlePref = context.getString(R.string.prefs_show_hidden_files),
visible = true,
enabled = true
)
}
@Ignore
@Test
fun disableShowHiddenFiles() {
prefShowHiddenFiles?.isChecked = advancedViewModel.isHiddenFilesShown()
onView(withText(context.getString(R.string.prefs_show_hidden_files))).perform(click())
prefShowHiddenFiles?.isChecked?.let { assertFalse(it) }
}
}
@@ -0,0 +1,209 @@
/**
* qsfera Android client application
*
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2021 ownCloud GmbH.
* <p>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
* <p>
* 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.settings.logs
import android.content.Context
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.preference.CheckBoxPreference
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import eu.qsfera.android.R
import eu.qsfera.android.presentation.logging.LogsListActivity
import eu.qsfera.android.presentation.settings.logging.SettingsLogsFragment
import eu.qsfera.android.presentation.logging.LogListViewModel
import eu.qsfera.android.presentation.settings.logging.SettingsLogsViewModel
import eu.qsfera.android.utils.matchers.verifyPreference
import io.mockk.every
import io.mockk.mockk
import io.mockk.unmockkAll
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
class SettingsLogsFragmentTest {
private lateinit var fragmentScenario: FragmentScenario<SettingsLogsFragment>
private lateinit var prefEnableLogging: SwitchPreferenceCompat
private lateinit var prefHttpLogs: CheckBoxPreference
private lateinit var prefLogsListActivity: Preference
private lateinit var logsViewModel: SettingsLogsViewModel
private lateinit var logListViewModel: LogListViewModel
private lateinit var context: Context
@Before
fun setUp() {
context = InstrumentationRegistry.getInstrumentation().targetContext
logsViewModel = mockk(relaxed = true)
logListViewModel = mockk(relaxed = true)
stopKoin()
startKoin {
context
allowOverride(override = true)
modules(
module {
viewModel {
logsViewModel
}
viewModel {
logListViewModel
}
}
)
}
Intents.init()
}
@After
fun tearDown() {
Intents.release()
PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit()
unmockkAll()
}
private fun launchTest(enabledLogging: Boolean) {
every { logsViewModel.isLoggingEnabled() } returns enabledLogging
fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_qsfera)
fragmentScenario.onFragment { fragment ->
prefEnableLogging = fragment.findPreference(SettingsLogsFragment.PREFERENCE_ENABLE_LOGGING)!!
prefHttpLogs = fragment.findPreference(SettingsLogsFragment.PREFERENCE_LOG_HTTP)!!
prefLogsListActivity = fragment.findPreference(SettingsLogsFragment.PREFERENCE_LOGS_LIST)!!
}
}
@Test
fun logsViewLoggingDisabled() {
launchTest(enabledLogging = false)
prefEnableLogging.verifyPreference(
keyPref = SettingsLogsFragment.PREFERENCE_ENABLE_LOGGING,
titlePref = context.getString(R.string.prefs_enable_logging),
summaryPref = context.getString(R.string.prefs_enable_logging_summary),
visible = true,
enabled = true
)
prefHttpLogs.verifyPreference(
keyPref = SettingsLogsFragment.PREFERENCE_LOG_HTTP,
titlePref = context.getString(R.string.prefs_http_logs),
visible = true,
enabled = false
)
prefLogsListActivity.verifyPreference(
keyPref = SettingsLogsFragment.PREFERENCE_LOGS_LIST,
titlePref = context.getString(R.string.prefs_log_open_logs_list_view),
visible = true,
enabled = true,
)
}
@Test
fun logsViewLoggingEnabled() {
launchTest(enabledLogging = true)
prefEnableLogging.verifyPreference(
keyPref = SettingsLogsFragment.PREFERENCE_ENABLE_LOGGING,
titlePref = context.getString(R.string.prefs_enable_logging),
summaryPref = context.getString(R.string.prefs_enable_logging_summary),
visible = true,
enabled = true
)
prefHttpLogs.verifyPreference(
keyPref = SettingsLogsFragment.PREFERENCE_LOG_HTTP,
titlePref = context.getString(R.string.prefs_http_logs),
visible = true,
enabled = true
)
prefLogsListActivity.verifyPreference(
keyPref = SettingsLogsFragment.PREFERENCE_LOGS_LIST,
titlePref = context.getString(R.string.prefs_log_open_logs_list_view),
visible = true,
enabled = true,
)
}
@Ignore
@Test
fun enableLoggingMakesSettingsEnable() {
launchTest(enabledLogging = false)
onView(withText(R.string.prefs_enable_logging)).perform(click())
assertTrue(prefHttpLogs.isEnabled)
}
@Test
fun disableLoggingMakesSettingsDisable() {
launchTest(enabledLogging = false)
onView(withText(R.string.prefs_enable_logging)).perform(click())
onView(withText(R.string.prefs_enable_logging)).perform(click())
assertFalse(prefHttpLogs.isEnabled)
assertTrue(prefLogsListActivity.isEnabled)
}
@Test
fun checkHttpLogs() {
launchTest(enabledLogging = true)
onView(withText(R.string.prefs_http_logs)).perform(click())
assertTrue(prefHttpLogs.isChecked)
}
@Test
fun disableLoggingMakesHttpLogsNotChecked() {
launchTest(enabledLogging = false)
onView(withText(R.string.prefs_enable_logging)).perform(click())
onView(withText(R.string.prefs_http_logs)).perform(click())
onView(withText(R.string.prefs_enable_logging)).perform(click())
assertFalse(prefHttpLogs.isChecked)
}
@Test
fun loggerOpen() {
launchTest(enabledLogging = true)
onView(withText(R.string.prefs_log_open_logs_list_view)).perform(click())
intended(hasComponent(LogsListActivity::class.java.name))
}
}
@@ -0,0 +1,365 @@
/**
* qsfera Android client application
*
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2023 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.settings.more
import android.content.Context
import android.content.Intent
import android.content.Intent.EXTRA_SUBJECT
import android.content.Intent.EXTRA_TEXT
import android.net.Uri
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.preference.Preference
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.matcher.IntentMatchers.hasData
import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
import androidx.test.espresso.intent.matcher.IntentMatchers.hasFlag
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import eu.qsfera.android.BuildConfig
import eu.qsfera.android.R
import eu.qsfera.android.presentation.settings.more.SettingsMoreFragment
import eu.qsfera.android.presentation.settings.more.SettingsMoreViewModel
import eu.qsfera.android.utils.matchers.verifyPreference
import eu.qsfera.android.utils.mockIntent
import io.mockk.every
import io.mockk.mockk
import io.mockk.unmockkAll
import org.hamcrest.Matchers.allOf
import org.junit.After
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
class SettingsMoreFragmentTest {
private lateinit var fragmentScenario: FragmentScenario<SettingsMoreFragment>
private var prefHelp: Preference? = null
private var prefSync: Preference? = null
private var prefAccessDocProvider: Preference? = null
private var prefRecommend: Preference? = null
private var prefFeedback: Preference? = null
private var prefImprint: Preference? = null
private lateinit var moreViewModel: SettingsMoreViewModel
private lateinit var context: Context
@Before
fun setUp() {
context = InstrumentationRegistry.getInstrumentation().targetContext
moreViewModel = mockk(relaxed = true)
stopKoin()
startKoin {
context
allowOverride(override = true)
modules(
module {
viewModel {
moreViewModel
}
}
)
}
Intents.init()
}
@After
fun tearDown() {
Intents.release()
unmockkAll()
}
private fun getPreference(key: String): Preference? {
var preference: Preference? = null
fragmentScenario.onFragment { fragment ->
preference = fragment.findPreference(key)
}
return preference
}
private fun launchTest(
helpEnabled: Boolean = true,
syncEnabled: Boolean = true,
docProviderAppEnabled: Boolean = true,
recommendEnabled: Boolean = true,
feedbackEnabled: Boolean = true,
imprintEnabled: Boolean = true
) {
every { moreViewModel.isHelpEnabled() } returns helpEnabled
every { moreViewModel.isSyncEnabled() } returns syncEnabled
every { moreViewModel.isDocProviderAppEnabled() } returns docProviderAppEnabled
every { moreViewModel.isRecommendEnabled() } returns recommendEnabled
every { moreViewModel.isFeedbackEnabled() } returns feedbackEnabled
every { moreViewModel.isImprintEnabled() } returns imprintEnabled
fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_qsfera)
}
@Test
fun moreView() {
launchTest()
prefHelp = getPreference(PREFERENCE_HELP)
assertNotNull(prefHelp)
prefHelp?.verifyPreference(
keyPref = PREFERENCE_HELP,
titlePref = context.getString(R.string.prefs_help),
visible = true,
enabled = true
)
prefSync = getPreference(PREFERENCE_SYNC_CALENDAR_CONTACTS)
assertNotNull(prefSync)
prefSync?.verifyPreference(
keyPref = PREFERENCE_SYNC_CALENDAR_CONTACTS,
titlePref = context.getString(R.string.prefs_sync_calendar_contacts),
summaryPref = context.getString(R.string.prefs_sync_calendar_contacts_summary),
visible = true,
enabled = true
)
prefAccessDocProvider = getPreference(PREFERENCE_ACCESS_DOCUMENT_PROVIDER)
assertNotNull(prefAccessDocProvider)
prefAccessDocProvider?.verifyPreference(
keyPref = PREFERENCE_ACCESS_DOCUMENT_PROVIDER,
titlePref = context.getString(R.string.prefs_access_document_provider),
summaryPref = context.getString(R.string.prefs_access_document_provider_summary),
visible = true,
enabled = true
)
prefRecommend = getPreference(PREFERENCE_RECOMMEND)
assertNotNull(prefRecommend)
prefRecommend?.verifyPreference(
keyPref = PREFERENCE_RECOMMEND,
titlePref = context.getString(R.string.prefs_recommend),
visible = true,
enabled = true
)
prefFeedback = getPreference(PREFERENCE_FEEDBACK)
assertNotNull(prefFeedback)
prefFeedback?.verifyPreference(
keyPref = PREFERENCE_FEEDBACK,
titlePref = context.getString(R.string.prefs_send_feedback),
visible = true,
enabled = true
)
prefImprint = getPreference(PREFERENCE_IMPRINT)
assertNotNull(prefImprint)
prefImprint?.verifyPreference(
keyPref = PREFERENCE_IMPRINT,
titlePref = context.getString(R.string.prefs_imprint),
visible = true,
enabled = true
)
}
@Test
fun helpNotEnabledView() {
launchTest(helpEnabled = false)
prefHelp = getPreference(PREFERENCE_HELP)
assertNull(prefHelp)
}
@Test
fun syncNotEnabledView() {
launchTest(syncEnabled = false)
prefSync = getPreference(PREFERENCE_SYNC_CALENDAR_CONTACTS)
assertNull(prefSync)
}
@Test
fun accessDocumentProviderNotEnabledView() {
launchTest(docProviderAppEnabled = false)
prefAccessDocProvider = getPreference(PREFERENCE_ACCESS_DOCUMENT_PROVIDER)
assertNull(prefAccessDocProvider)
}
@Test
fun recommendNotEnabledView() {
launchTest(recommendEnabled = false)
prefRecommend = getPreference(PREFERENCE_RECOMMEND)
assertNull(prefRecommend)
}
@Test
fun feedbackNotEnabledView() {
launchTest(feedbackEnabled = false)
prefFeedback = getPreference(PREFERENCE_FEEDBACK)
assertNull(prefFeedback)
}
@Test
fun imprintNotEnabledView() {
launchTest(imprintEnabled = false)
prefImprint = getPreference(PREFERENCE_IMPRINT)
assertNull(prefImprint)
}
@Ignore
@Test
fun helpOpensNotEmptyUrl() {
every { moreViewModel.getHelpUrl() } returns context.getString(R.string.url_help)
launchTest()
mockIntent(action = Intent.ACTION_VIEW)
onView(withText(R.string.prefs_help)).perform(click())
intended(hasData(context.getString(R.string.url_help)))
}
@Test
fun syncOpensNotEmptyUrl() {
every { moreViewModel.getSyncUrl() } returns context.getString(R.string.url_sync_calendar_contacts)
launchTest()
mockIntent(action = Intent.ACTION_VIEW)
onView(withText(R.string.prefs_sync_calendar_contacts)).perform(click())
intended(hasData(context.getString(R.string.url_sync_calendar_contacts)))
}
@Test
fun accessDocumentProviderOpensNotEmptyUrl() {
every { moreViewModel.getDocProviderAppUrl() } returns context.getString(R.string.url_document_provider_app)
launchTest()
mockIntent(action = Intent.ACTION_VIEW)
onView(withText(R.string.prefs_access_document_provider)).perform(click())
intended(hasData(context.getString(R.string.url_document_provider_app)))
}
@Test
fun recommendOpensSender() {
launchTest()
mockIntent(action = Intent.ACTION_SENDTO)
onView(withText(R.string.prefs_recommend)).perform(click())
// Delay needed since depending on the performance of the device where tests are executed,
// sender can interfere with the subsequent tests
Thread.sleep(1000)
intended(
allOf(
hasAction(Intent.ACTION_SENDTO), hasExtra(
EXTRA_SUBJECT, String.format(
context.getString(R.string.recommend_subject),
context.getString(R.string.app_name)
)
),
hasExtra(
EXTRA_TEXT,
String.format(
context.getString(R.string.recommend_text),
context.getString(R.string.app_name),
context.getString(R.string.url_app_download)
)
),
hasFlag(Intent.FLAG_ACTIVITY_NEW_TASK)
)
)
}
@Test
fun feedbackOpensSenderIfFeedbackMailExists() {
launchTest()
every { moreViewModel.getFeedbackMail() } returns FEEDBACK_MAIL
mockIntent(action = Intent.ACTION_SENDTO)
onView(withText(R.string.prefs_send_feedback)).perform(click())
// Delay needed since depending on the performance of the device where tests are executed,
// sender can interfere with the subsequent tests
Thread.sleep(1000)
intended(
allOf(
hasAction(Intent.ACTION_SENDTO),
hasExtra(
EXTRA_SUBJECT,
"Android v" + BuildConfig.VERSION_NAME + " - " + context.getText(R.string.prefs_feedback)
),
hasData(Uri.parse(FEEDBACK_MAIL)),
hasFlag(Intent.FLAG_ACTIVITY_NEW_TASK)
)
)
}
@Test
fun feedbackOpensAlertDialogIfFeedbackMailIsEmpty() {
launchTest()
every { moreViewModel.getFeedbackMail() } returns ""
onView(withText(R.string.prefs_send_feedback)).perform(click())
onView(withText(R.string.drawer_feedback)).check(ViewAssertions.matches(isDisplayed()))
}
@Test
fun imprintOpensUrl() {
every { moreViewModel.getImprintUrl() } returns "https://qsfera.eu/mobile"
launchTest()
mockIntent(action = Intent.ACTION_VIEW)
onView(withText(R.string.prefs_imprint)).perform(click())
intended(hasData("https://qsfera.eu/mobile"))
}
companion object {
private const val PREFERENCE_HELP = "help"
private const val PREFERENCE_SYNC_CALENDAR_CONTACTS = "syncCalendarContacts"
private const val PREFERENCE_ACCESS_DOCUMENT_PROVIDER = "accessDocumentProvider"
private const val PREFERENCE_RECOMMEND = "recommend"
private const val PREFERENCE_FEEDBACK = "feedback"
private const val PREFERENCE_IMPRINT = "imprint"
private const val FEEDBACK_MAIL = "mailto:mail@qsfera.eu"
}
}
@@ -0,0 +1,363 @@
/*
* qsfera Android client application
*
* @author Jesus Recio (@jesmrec)
* @author Christian Schabesberger (@theScrabi)
* @author Juan Carlos Garrote Gascón (@JuancaG05)
* @author David Crespo Ríos (@davcres)
*
* Copyright (C) 2021 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.settings.security
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.lifecycle.MutableLiveData
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import eu.qsfera.android.R
import eu.qsfera.android.db.PreferenceManager
import eu.qsfera.android.domain.utils.Event
import eu.qsfera.android.presentation.security.passcode.PassCodeActivity
import eu.qsfera.android.presentation.security.passcode.PasscodeAction
import eu.qsfera.android.presentation.security.passcode.PasscodeType
import eu.qsfera.android.presentation.security.passcode.Status
import eu.qsfera.android.presentation.security.biometric.BiometricViewModel
import eu.qsfera.android.presentation.security.passcode.PassCodeViewModel
import eu.qsfera.android.testutil.security.OC_PASSCODE_4_DIGITS
import eu.qsfera.android.utils.matchers.isDisplayed
import eu.qsfera.android.utils.matchers.withChildCountAndId
import io.mockk.every
import io.mockk.mockk
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
class PassCodeActivityTest {
private lateinit var activityScenario: ActivityScenario<PassCodeActivity>
private lateinit var context: Context
private lateinit var timeToUnlockLiveData: MutableLiveData<Event<String>>
private lateinit var finishTimeToUnlockLiveData: MutableLiveData<Event<Boolean>>
private lateinit var statusLiveData: MutableLiveData<Status>
private lateinit var passcodeLiveData: MutableLiveData<String>
private lateinit var passCodeViewModel: PassCodeViewModel
private lateinit var biometricViewModel: BiometricViewModel
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
passCodeViewModel = mockk(relaxed = true)
biometricViewModel = mockk(relaxed = true)
timeToUnlockLiveData = MutableLiveData()
finishTimeToUnlockLiveData = MutableLiveData()
statusLiveData = MutableLiveData()
passcodeLiveData = MutableLiveData()
stopKoin()
startKoin {
allowOverride(override = true)
context
modules(
module {
viewModel {
passCodeViewModel
}
viewModel {
biometricViewModel
}
}
)
}
every { passCodeViewModel.getPassCode() } returns OC_PASSCODE_4_DIGITS
every { passCodeViewModel.getNumberOfPassCodeDigits() } returns 4
every { passCodeViewModel.getNumberOfAttempts() } returns 0
every { passCodeViewModel.getTimeToUnlockLiveData } returns timeToUnlockLiveData
every { passCodeViewModel.getFinishedTimeToUnlockLiveData } returns finishTimeToUnlockLiveData
every { passCodeViewModel.status } returns statusLiveData
every { passCodeViewModel.passcode } returns passcodeLiveData
}
@After
fun tearDown() {
// Clean preferences
PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit()
}
@Test
fun passcodeCheckNotLockedView() {
// Open Activity in passcode check mode
openPasscodeActivity(PassCodeActivity.ACTION_CHECK)
with(R.id.header) {
isDisplayed(true)
withText(R.string.pass_code_enter_pass_code)
}
R.id.explanation.isDisplayed(false)
// Check if required amount of input fields are actually displayed
with(R.id.layout_code) {
isDisplayed(true)
withChildCountAndId(passCodeViewModel.getNumberOfPassCodeDigits(), R.id.passCodeEditText)
}
R.id.lock_time.isDisplayed(false)
R.id.numberKeyboard.isDisplayed(true)
R.id.key0.isDisplayed(true)
R.id.key1.isDisplayed(true)
R.id.key2.isDisplayed(true)
R.id.key3.isDisplayed(true)
R.id.key4.isDisplayed(true)
R.id.key5.isDisplayed(true)
R.id.key6.isDisplayed(true)
R.id.key7.isDisplayed(true)
R.id.key8.isDisplayed(true)
R.id.key9.isDisplayed(true)
R.id.backspaceBtn.isDisplayed(true)
R.id.biometricBtn.isDisplayed(false)
}
@Test
fun passcodeCheckLockedView() {
every { passCodeViewModel.getNumberOfAttempts() } returns 3
every { passCodeViewModel.getTimeToUnlockLeft() } returns 3000
timeToUnlockLiveData.postValue(Event("00:03"))
// Open Activity in passcode check mode
openPasscodeActivity(PassCodeActivity.ACTION_CHECK)
with(R.id.header) {
isDisplayed(true)
withText(R.string.pass_code_enter_pass_code)
}
R.id.explanation.isDisplayed(false)
// Check if required amount of input fields are actually displayed
with(R.id.layout_code) {
isDisplayed(true)
withChildCountAndId(passCodeViewModel.getNumberOfPassCodeDigits(), R.id.passCodeEditText)
}
R.id.lock_time.isDisplayed(true)
}
@Test
fun passcodeView() {
// Open Activity in passcode creation mode
openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
with(R.id.header) {
isDisplayed(true)
withText(R.string.pass_code_configure_your_pass_code)
}
with(R.id.explanation) {
isDisplayed(true)
withText(R.string.pass_code_configure_your_pass_code_explanation)
}
// Check if required amount of input fields are actually displayed
with(R.id.layout_code) {
isDisplayed(true)
withChildCountAndId(passCodeViewModel.getNumberOfPassCodeDigits(), R.id.passCodeEditText)
}
R.id.lock_time.isDisplayed(false)
R.id.error.isDisplayed(false)
}
@Test
fun firstTry() {
// Open Activity in passcode creation mode
openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.NO_CONFIRM))
with(R.id.header) {
isDisplayed(true)
withText(R.string.pass_code_reenter_your_pass_code)
}
onView(withText(R.string.pass_code_configure_your_pass_code)).check(doesNotExist())
R.id.error.isDisplayed(false)
}
@Ignore
@Test
fun secondTryCorrect() {
every { biometricViewModel.isBiometricLockAvailable() } returns true
// Open Activity in passcode creation mode
openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM))
// Click dialog's enable option
onView(withText(R.string.common_yes)).perform(click())
// Checking that the result returned is OK
assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
}
@Test
fun secondTryIncorrect() {
// Open Activity in passcode creation mode
openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.ERROR))
with(R.id.header) {
isDisplayed(true)
withText(R.string.pass_code_configure_your_pass_code)
}
with(R.id.explanation) {
isDisplayed(true)
withText(R.string.pass_code_configure_your_pass_code_explanation)
}
with(R.id.error) {
isDisplayed(true)
withText(R.string.pass_code_mismatch)
}
R.id.lock_time.isDisplayed(false)
}
@Test
fun deletePasscodeView() {
// Open Activity in passcode deletion mode
openPasscodeActivity(PassCodeActivity.ACTION_REMOVE)
with(R.id.header) {
isDisplayed(true)
withText(R.string.pass_code_remove_your_pass_code)
}
R.id.explanation.isDisplayed(false)
R.id.error.isDisplayed(false)
R.id.lock_time.isDisplayed(false)
}
@Ignore
@Test
fun deletePasscodeCorrect() {
// Open Activity in passcode deletion mode
openPasscodeActivity(PassCodeActivity.ACTION_REMOVE)
statusLiveData.postValue(Status(PasscodeAction.REMOVE, PasscodeType.OK))
assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
}
@Test
fun deletePasscodeIncorrect() {
// Open Activity in passcode deletion mode
openPasscodeActivity(PassCodeActivity.ACTION_REMOVE)
statusLiveData.postValue(Status(PasscodeAction.REMOVE, PasscodeType.ERROR))
with(R.id.header) {
isDisplayed(true)
withText(R.string.pass_code_enter_pass_code)
}
with(R.id.error) {
isDisplayed(true)
withText(R.string.pass_code_wrong)
}
R.id.explanation.isDisplayed(false)
R.id.lock_time.isDisplayed(false)
}
@Ignore
@Test
fun checkEnableBiometricDialogIsVisible() {
every { biometricViewModel.isBiometricLockAvailable() } returns true
// Open Activity in passcode creation mode
openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM))
onView(withText(R.string.biometric_dialog_title)).check(matches(isDisplayed()))
onView(withText(R.string.common_yes)).check(matches(isDisplayed()))
onView(withText(R.string.common_no)).check(matches(isDisplayed()))
}
@Ignore
@Test
fun checkEnableBiometricDialogYesOption() {
every { biometricViewModel.isBiometricLockAvailable() } returns true
// Open Activity in passcode creation mode
openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM))
onView(withText(R.string.common_yes)).perform(click())
// Checking that the result returned is OK
assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
}
@Ignore
@Test
fun checkEnableBiometricDialogNoOption() {
every { biometricViewModel.isBiometricLockAvailable() } returns true
// Open Activity in passcode creation mode
openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
statusLiveData.postValue(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM))
onView(withText(R.string.common_no)).perform(click())
// Checking that the result returned is OK
assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
}
private fun openPasscodeActivity(mode: String) {
val intent = Intent(context, PassCodeActivity::class.java).apply {
action = mode
}
activityScenario = ActivityScenario.launch(intent)
}
}
@@ -0,0 +1,127 @@
/*
* qsfera Android client application
*
* @author Jesus Recio (@jesmrec)
* @author Juan Carlos Garrote Gascón (@JuancaG05)
*
* Copyright (C) 2021 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.settings.security
import android.content.Context
import android.content.Intent
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import eu.qsfera.android.R
import eu.qsfera.android.db.PreferenceManager
import eu.qsfera.android.presentation.security.pattern.PatternActivity
import eu.qsfera.android.presentation.security.pattern.PatternViewModel
import eu.qsfera.android.testutil.security.OC_PATTERN
import eu.qsfera.android.utils.matchers.isDisplayed
import eu.qsfera.android.utils.matchers.withText
import io.mockk.mockk
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
class PatternActivityTest {
private lateinit var activityScenario: ActivityScenario<PatternActivity>
private lateinit var context: Context
private lateinit var patternViewModel: PatternViewModel
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
patternViewModel = mockk(relaxUnitFun = true)
stopKoin()
startKoin {
context
allowOverride(override = true)
modules(
module {
viewModel {
patternViewModel
}
}
)
}
}
@After
fun tearDown() {
// Clean preferences
PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit()
}
@Test
fun patternLockView() {
// Open Activity in pattern creation mode
openPatternActivity(PatternActivity.ACTION_REQUEST_WITH_RESULT)
with(R.id.header_pattern) {
isDisplayed(true)
withText(R.string.pattern_configure_pattern)
}
with(R.id.explanation_pattern) {
isDisplayed(true)
withText(R.string.pattern_configure_your_pattern_explanation)
}
R.id.pattern_lock_view.isDisplayed(true)
}
@Test
fun removePatternLock() {
// Save a pattern in Preferences
storePattern()
// Open Activity in pattern deletion mode
openPatternActivity(PatternActivity.ACTION_CHECK_WITH_RESULT)
with(R.id.header_pattern) {
isDisplayed(true)
withText(R.string.pattern_remove_pattern)
}
with(R.id.explanation_pattern) {
isDisplayed(true)
withText(R.string.pattern_no_longer_required)
}
}
private fun storePattern() {
val appPrefs = PreferenceManager.getDefaultSharedPreferences(context).edit()
appPrefs.apply {
putString(PatternActivity.PREFERENCE_PATTERN, OC_PATTERN)
putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, true)
apply()
}
}
private fun openPatternActivity(mode: String) {
val intent = Intent(context, PatternActivity::class.java).apply {
action = mode
}
activityScenario = ActivityScenario.launch(intent)
}
}
@@ -0,0 +1,499 @@
/**
* qsfera Android client application
*
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2021 ownCloud GmbH.
* <p>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
* <p>
* 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.settings.security
import android.app.Activity.RESULT_OK
import android.content.Context
import androidx.biometric.BiometricViewModel
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.preference.CheckBoxPreference
import androidx.preference.ListPreference
import androidx.preference.PreferenceManager
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import eu.qsfera.android.R
import eu.qsfera.android.presentation.security.biometric.BiometricActivity
import eu.qsfera.android.presentation.security.biometric.BiometricManager
import eu.qsfera.android.presentation.security.PREFERENCE_LOCK_TIMEOUT
import eu.qsfera.android.presentation.security.pattern.PatternActivity
import eu.qsfera.android.presentation.security.passcode.PassCodeActivity
import eu.qsfera.android.presentation.settings.security.SettingsSecurityFragment
import eu.qsfera.android.presentation.settings.security.SettingsSecurityFragment.Companion.PREFERENCE_LOCK_ACCESS_FROM_DOCUMENT_PROVIDER
import eu.qsfera.android.presentation.settings.security.SettingsSecurityViewModel
import eu.qsfera.android.testutil.security.OC_PATTERN
import eu.qsfera.android.utils.matchers.verifyPreference
import eu.qsfera.android.utils.mockIntent
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import org.hamcrest.Matchers.not
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
class SettingsSecurityFragmentTest {
private lateinit var fragmentScenario: FragmentScenario<SettingsSecurityFragment>
private lateinit var prefPasscode: CheckBoxPreference
private lateinit var prefPattern: CheckBoxPreference
private var prefBiometric: CheckBoxPreference? = null
private lateinit var prefLockApplication: ListPreference
private lateinit var prefLockAccessDocumentProvider: CheckBoxPreference
private lateinit var prefTouchesWithOtherVisibleWindows: CheckBoxPreference
private lateinit var securityViewModel: SettingsSecurityViewModel
private lateinit var biometricViewModel: BiometricViewModel
private lateinit var context: Context
@Before
fun setUp() {
context = InstrumentationRegistry.getInstrumentation().targetContext
securityViewModel = mockk(relaxed = true)
biometricViewModel = mockk(relaxed = true)
mockkObject(BiometricManager)
stopKoin()
startKoin {
context
allowOverride(override = true)
modules(
module {
viewModel {
securityViewModel
}
}
)
}
every { securityViewModel.isSecurityEnforcedEnabled() } returns false
every { securityViewModel.getBiometricsState() } returns false
every { securityViewModel.isLockDelayEnforcedEnabled() } returns false
Intents.init()
}
@After
fun tearDown() {
Intents.release()
PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit()
}
private fun launchTest(withBiometrics: Boolean = true) {
every { BiometricManager.isHardwareDetected() } returns withBiometrics
fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_qsfera)
fragmentScenario.onFragment { fragment ->
prefPasscode = fragment.findPreference(PassCodeActivity.PREFERENCE_SET_PASSCODE)!!
prefPattern = fragment.findPreference(PatternActivity.PREFERENCE_SET_PATTERN)!!
prefBiometric = fragment.findPreference(BiometricActivity.PREFERENCE_SET_BIOMETRIC)
prefLockApplication = fragment.findPreference(PREFERENCE_LOCK_TIMEOUT)!!
prefLockAccessDocumentProvider = fragment.findPreference(PREFERENCE_LOCK_ACCESS_FROM_DOCUMENT_PROVIDER)!!
prefTouchesWithOtherVisibleWindows =
fragment.findPreference(SettingsSecurityFragment.PREFERENCE_TOUCHES_WITH_OTHER_VISIBLE_WINDOWS)!!
}
}
private fun checkCommonPreferences() {
prefPasscode.verifyPreference(
keyPref = PassCodeActivity.PREFERENCE_SET_PASSCODE,
titlePref = context.getString(R.string.prefs_passcode),
visible = true,
enabled = true
)
assertFalse(prefPasscode.isChecked)
prefPattern.verifyPreference(
keyPref = PatternActivity.PREFERENCE_SET_PATTERN,
titlePref = context.getString(R.string.prefs_pattern),
visible = true,
enabled = true
)
assertFalse(prefPattern.isChecked)
prefLockApplication.verifyPreference(
keyPref = PREFERENCE_LOCK_TIMEOUT,
titlePref = context.getString(R.string.prefs_lock_application),
visible = true,
enabled = false
)
prefLockAccessDocumentProvider.verifyPreference(
keyPref = PREFERENCE_LOCK_ACCESS_FROM_DOCUMENT_PROVIDER,
titlePref = context.getString(R.string.prefs_lock_access_from_document_provider),
summaryPref = context.getString(R.string.prefs_lock_access_from_document_provider_summary),
visible = true,
enabled = true
)
assertFalse(prefLockAccessDocumentProvider.isChecked)
prefTouchesWithOtherVisibleWindows.verifyPreference(
keyPref = SettingsSecurityFragment.PREFERENCE_TOUCHES_WITH_OTHER_VISIBLE_WINDOWS,
titlePref = context.getString(R.string.prefs_touches_with_other_visible_windows),
summaryPref = context.getString(R.string.prefs_touches_with_other_visible_windows_summary),
visible = true,
enabled = true
)
assertFalse(prefTouchesWithOtherVisibleWindows.isChecked)
}
@Test
fun securityViewDeviceWithBiometrics() {
launchTest()
checkCommonPreferences()
assertNotNull(prefBiometric)
prefBiometric?.run {
verifyPreference(
keyPref = BiometricActivity.PREFERENCE_SET_BIOMETRIC,
titlePref = context.getString(R.string.prefs_biometric),
summaryPref = context.getString(R.string.prefs_biometric_summary),
visible = true,
enabled = false
)
assertFalse(isChecked)
}
}
@Test
fun securityViewDeviceWithNoBiometrics() {
launchTest(withBiometrics = false)
checkCommonPreferences()
assertNull(prefBiometric)
}
@Ignore
@Test
fun passcodeOpen() {
every { securityViewModel.isPatternSet() } returns false
launchTest()
mockIntent(RESULT_OK, PassCodeActivity.ACTION_CREATE)
onView(withText(R.string.prefs_passcode)).perform(click())
intended(hasComponent(PassCodeActivity::class.java.name))
}
@Test
fun patternOpen() {
every { securityViewModel.isPasscodeSet() } returns false
launchTest()
onView(withText(R.string.prefs_pattern)).perform(click())
intended(hasComponent(PatternActivity::class.java.name))
}
@Ignore
@Test
fun passcodeLockEnabledOk() {
every { securityViewModel.isPatternSet() } returns false
launchTest()
mockIntent(
action = PassCodeActivity.ACTION_CREATE
)
onView(withText(R.string.prefs_passcode)).perform(click())
assertTrue(prefPasscode.isChecked)
}
@Test
fun patternLockEnabledOk() {
every { securityViewModel.isPasscodeSet() } returns false
launchTest()
mockIntent(
extras = Pair(PatternActivity.PREFERENCE_PATTERN, OC_PATTERN),
action = PatternActivity.ACTION_REQUEST_WITH_RESULT
)
onView(withText(R.string.prefs_pattern)).perform(click())
assertTrue(prefPattern.isChecked)
}
@Ignore
@Test
fun enablePasscodeEnablesBiometricLockAndLockApplication() {
launchTest()
firstEnablePasscode()
onView(withText(R.string.prefs_biometric)).check(matches(isEnabled()))
assertTrue(prefBiometric!!.isEnabled)
assertFalse(prefBiometric!!.isChecked)
assertTrue(prefLockApplication.isEnabled)
}
@Test
fun enablePatternEnablesBiometricLockAndLockApplication() {
launchTest()
firstEnablePattern()
onView(withText(R.string.prefs_biometric)).check(matches(isEnabled()))
assertTrue(prefBiometric!!.isEnabled)
assertFalse(prefBiometric!!.isChecked)
assertTrue(prefLockApplication.isEnabled)
}
@Ignore
@Test
fun onlyOneMethodEnabledPattern() {
every { securityViewModel.isPatternSet() } returns true
launchTest()
firstEnablePattern()
onView(withText(R.string.prefs_passcode)).perform(click())
onView(withText(R.string.pattern_already_set)).check(matches(isEnabled()))
}
@Test
fun onlyOneMethodEnabledPasscode() {
every { securityViewModel.isPasscodeSet() } returns true
launchTest()
firstEnablePasscode()
onView(withText(R.string.prefs_pattern)).perform(click())
onView(withText(R.string.passcode_already_set)).check(matches(isEnabled()))
}
@Test
fun disablePasscodeOk() {
launchTest()
firstEnablePasscode()
mockIntent(
action = PassCodeActivity.ACTION_REMOVE
)
onView(withText(R.string.prefs_passcode)).perform(click())
assertFalse(prefPasscode.isChecked)
onView(withText(R.string.prefs_biometric)).check(matches(not(isEnabled())))
assertFalse(prefBiometric!!.isEnabled)
assertFalse(prefBiometric!!.isChecked)
assertFalse(prefLockApplication.isEnabled)
}
@Test
fun disablePatternOk() {
launchTest()
firstEnablePattern()
mockIntent(
action = PatternActivity.ACTION_CHECK_WITH_RESULT
)
onView(withText(R.string.prefs_pattern)).perform(click())
assertFalse(prefPattern.isChecked)
onView(withText(R.string.prefs_biometric)).check(matches(not(isEnabled())))
assertFalse(prefBiometric!!.isEnabled)
assertFalse(prefBiometric!!.isChecked)
assertFalse(prefLockApplication.isEnabled)
}
@Ignore
@Test
fun enableBiometricLockWithPasscodeEnabled() {
every { BiometricManager.hasEnrolledBiometric() } returns true
launchTest()
firstEnablePasscode()
onView(withText(R.string.prefs_biometric)).perform(click())
assertTrue(prefBiometric!!.isChecked)
}
@Test
fun enableBiometricLockWithPatternEnabled() {
every { BiometricManager.hasEnrolledBiometric() } returns true
launchTest()
firstEnablePattern()
onView(withText(R.string.prefs_biometric)).perform(click())
assertTrue(prefBiometric!!.isChecked)
}
@Ignore
@Test
fun enableBiometricLockNoEnrolledBiometric() {
every { BiometricManager.hasEnrolledBiometric() } returns false
launchTest()
firstEnablePasscode()
onView(withText(R.string.prefs_biometric)).perform(click())
assertFalse(prefBiometric!!.isChecked)
onView(withText(R.string.biometric_not_enrolled)).check(matches(isEnabled()))
}
@Test
fun disableBiometricLock() {
every { BiometricManager.hasEnrolledBiometric() } returns true
launchTest()
firstEnablePasscode()
onView(withText(R.string.prefs_biometric)).perform(click())
onView(withText(R.string.prefs_biometric)).perform(click())
assertFalse(prefBiometric!!.isChecked)
}
@Test
fun lockAccessFromDocumentProviderEnable() {
launchTest()
onView(withText(R.string.prefs_lock_access_from_document_provider)).perform(click())
assertTrue(prefLockAccessDocumentProvider.isChecked)
}
@Test
fun lockAccessFromDocumentProviderDisable() {
launchTest()
onView(withText(R.string.prefs_lock_access_from_document_provider)).perform(click())
onView(withText(R.string.prefs_lock_access_from_document_provider)).perform(click())
assertFalse(prefLockAccessDocumentProvider.isChecked)
}
@Test
fun touchesDialog() {
launchTest()
onView(withText(R.string.prefs_touches_with_other_visible_windows)).perform(click())
onView(withText(R.string.confirmation_touches_with_other_windows_title)).check(matches(isDisplayed()))
onView(withText(R.string.confirmation_touches_with_other_windows_message)).check(matches(isDisplayed()))
}
@Test
fun touchesEnable() {
launchTest()
onView(withText(R.string.prefs_touches_with_other_visible_windows)).perform(click())
onView(withText(R.string.common_yes)).perform(click())
assertTrue(prefTouchesWithOtherVisibleWindows.isChecked)
}
@Test
fun touchesRefuse() {
launchTest()
onView(withText(R.string.prefs_touches_with_other_visible_windows)).perform(click())
onView(withText(R.string.common_no)).perform(click())
assertFalse(prefTouchesWithOtherVisibleWindows.isChecked)
}
@Test
fun touchesDisable() {
launchTest()
onView(withText(R.string.prefs_touches_with_other_visible_windows)).perform(click())
onView(withText(R.string.common_yes)).perform(click())
onView(withText(R.string.prefs_touches_with_other_visible_windows)).perform(click())
assertFalse(prefTouchesWithOtherVisibleWindows.isChecked)
}
@Test
fun passcodeLockNotVisible() {
every { securityViewModel.isSecurityEnforcedEnabled() } returns true
launchTest()
assertFalse(prefPasscode.isVisible)
}
@Test
fun patternLockNotVisible() {
every { securityViewModel.isSecurityEnforcedEnabled() } returns true
launchTest()
assertFalse(prefPattern.isVisible)
}
@Test
fun passcodeLockVisible() {
launchTest()
assertTrue(prefPasscode.isVisible)
}
@Test
fun patternLockVisible() {
launchTest()
assertTrue(prefPattern.isVisible)
}
@Ignore
@Test
fun checkIfUserEnabledBiometricRecommendation() {
every { securityViewModel.getBiometricsState() } returns true
launchTest()
firstEnablePasscode()
assertTrue(prefBiometric!!.isChecked)
assertTrue(prefBiometric!!.isEnabled)
}
@Test
fun checkIfUserNotEnabledBiometricRecommendation() {
launchTest()
firstEnablePasscode()
assertFalse(prefBiometric!!.isChecked)
}
private fun firstEnablePasscode() {
every { securityViewModel.isPatternSet() } returns false
mockIntent(
action = PassCodeActivity.ACTION_CREATE
)
onView(withText(R.string.prefs_passcode)).perform(click())
}
private fun firstEnablePattern() {
every { securityViewModel.isPasscodeSet() } returns false
mockIntent(
action = PatternActivity.ACTION_REQUEST_WITH_RESULT
)
onView(withText(R.string.prefs_pattern)).perform(click())
}
}
@@ -0,0 +1,126 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.sharing.sharees.ui
import androidx.lifecycle.MutableLiveData
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import eu.qsfera.android.R
import eu.qsfera.android.domain.sharing.shares.model.OCShare
import eu.qsfera.android.domain.sharing.shares.model.ShareType
import eu.qsfera.android.domain.utils.Event
import eu.qsfera.android.presentation.common.UIResult
import eu.qsfera.android.presentation.sharing.sharees.SearchShareesFragment
import eu.qsfera.android.presentation.sharing.ShareViewModel
import eu.qsfera.android.sharing.shares.ui.TestShareFileActivity
import eu.qsfera.android.testutil.OC_SHARE
import io.mockk.every
import io.mockk.mockkClass
import org.hamcrest.CoreMatchers
import org.junit.Before
import org.junit.Test
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
class SearchShareesFragmentTest {
private val shareViewModel = mockkClass(ShareViewModel::class, relaxed = true)
private val sharesLiveData = MutableLiveData<Event<UIResult<List<OCShare>>>>()
@Before
fun setUp() {
every { shareViewModel.shares } returns sharesLiveData
stopKoin()
startKoin {
androidContext(ApplicationProvider.getApplicationContext())
allowOverride(override = true)
modules(
module {
viewModel {
shareViewModel
}
}
)
}
ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
val searchShareesFragment = SearchShareesFragment()
it.startFragment(searchShareesFragment)
}
}
@Test
fun showSearchBar() {
onView(withId(R.id.search_mag_icon)).check(matches(isDisplayed()))
onView(withId(R.id.search_plate)).check(matches(isDisplayed()))
}
@Test
fun showUserShares() {
sharesLiveData.postValue(
Event(
UIResult.Success(
listOf(
OC_SHARE.copy(sharedWithDisplayName = "Sheldon"),
OC_SHARE.copy(sharedWithDisplayName = "Penny")
)
)
)
)
onView(withText("Sheldon"))
.check(matches(isDisplayed()))
.check(matches(hasSibling(withId(R.id.unshareButton))))
.check(matches(hasSibling(withId(R.id.editShareButton))))
onView(withText("Penny")).check(matches(isDisplayed()))
}
@Test
fun showGroupShares() {
sharesLiveData.postValue(
Event(
UIResult.Success(
listOf(
OC_SHARE.copy(
shareType = ShareType.GROUP,
sharedWithDisplayName = "Friends"
)
)
)
)
)
onView(withText("Friends (group)"))
.check(matches(isDisplayed()))
.check(matches(hasSibling(withId(R.id.icon))))
onView(ViewMatchers.withTagValue(CoreMatchers.equalTo(R.drawable.ic_group))).check(matches(isDisplayed()))
}
}
@@ -0,0 +1,278 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.sharing.shares.ui
import androidx.lifecycle.MutableLiveData
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import eu.qsfera.android.R
import eu.qsfera.android.domain.sharing.shares.model.OCShare
import eu.qsfera.android.domain.utils.Event
import eu.qsfera.android.presentation.common.UIResult
import eu.qsfera.android.presentation.sharing.sharees.EditPrivateShareFragment
import eu.qsfera.android.presentation.sharing.ShareViewModel
import eu.qsfera.android.testutil.OC_ACCOUNT
import eu.qsfera.android.testutil.OC_FILE
import eu.qsfera.android.testutil.OC_FOLDER
import eu.qsfera.android.testutil.OC_SHARE
import eu.qsfera.android.utils.Permissions
import io.mockk.every
import io.mockk.mockk
import org.hamcrest.CoreMatchers.not
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
class EditPrivateShareFragmentTest {
private val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
private val defaultSharedWithDisplayName = "user"
private val shareViewModel = mockk<ShareViewModel>(relaxed = true)
private val privateShareAsLiveData = MutableLiveData<Event<UIResult<OCShare>>>()
private lateinit var activityScenario: ActivityScenario<TestShareFileActivity>
@Before
fun setUp() {
every { shareViewModel.privateShare } returns privateShareAsLiveData
every { shareViewModel.isResharingAllowed() } returns true
stopKoin()
startKoin {
androidContext(ApplicationProvider.getApplicationContext())
allowOverride(override = true)
modules(
module {
viewModel {
shareViewModel
}
}
)
}
}
@Test
fun showDialogTitle() {
loadEditPrivateShareFragment()
onView(withId(R.id.editShareTitle))
.check(
matches(
withText(
targetContext.getString(R.string.share_with_edit_title, defaultSharedWithDisplayName)
)
)
)
}
@Test
fun closeDialog() {
loadEditPrivateShareFragment()
onView(withId(R.id.closeButton)).perform(click())
activityScenario.onActivity { Assert.assertNull(it.getTestFragment()) }
}
@Test
fun showToggles() {
loadEditPrivateShareFragment()
onView(withId(R.id.canEditSwitch)).check(matches(isDisplayed()))
onView(withId(R.id.canShareSwitch)).check(matches(isDisplayed()))
}
@Test
fun showFileShareWithNoPermissions() {
loadEditPrivateShareFragment()
onView(withId(R.id.canEditSwitch)).check(matches(isNotChecked()))
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
}
@Test
fun showFileShareWithEditPermissions() {
loadEditPrivateShareFragment(permissions = Permissions.EDIT_PERMISSIONS.value)
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
}
@Test
fun showFileShareWithSharePermissions() {
loadEditPrivateShareFragment(permissions = Permissions.SHARE_PERMISSIONS.value)
onView(withId(R.id.canEditSwitch)).check(matches(isNotChecked()))
onView(withId(R.id.canShareSwitch)).check(matches(isChecked()))
}
@Test
fun showFileShareWithAllPermissions() {
loadEditPrivateShareFragment(permissions = Permissions.ALL_PERMISSIONS.value)
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
onView(withId(R.id.canShareSwitch)).check(matches(isChecked()))
}
@Test
fun showFolderShareWithCreatePermissions() {
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CREATE_PERMISSIONS.value)
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isChecked()))
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isNotChecked()))
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isNotChecked()))
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
}
@Test
fun showFolderShareWithCreateChangePermissions() {
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CREATE_CHANGE_PERMISSIONS.value)
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isChecked()))
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isChecked()))
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isNotChecked()))
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
}
@Test
fun showFolderShareWithCreateDeletePermissions() {
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CREATE_DELETE_PERMISSIONS.value)
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isChecked()))
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isNotChecked()))
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isChecked()))
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
}
@Test
fun showFolderShareWithCreateChangeDeletePermissions() {
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CREATE_CHANGE_DELETE_PERMISSIONS.value)
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isChecked()))
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isChecked()))
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isChecked()))
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
}
@Test
fun showFolderShareWithChangePermissions() {
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CHANGE_PERMISSIONS.value)
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isNotChecked()))
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isChecked()))
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isNotChecked()))
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
}
@Test
fun showFolderShareWithChangeDeletePermissions() {
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CHANGE_DELETE_PERMISSIONS.value)
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isNotChecked()))
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isChecked()))
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isChecked()))
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
}
@Test
fun showFolderShareWithDeletePermissions() {
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_DELETE_PERMISSIONS.value)
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isNotChecked()))
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isNotChecked()))
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isChecked()))
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
}
@Test
fun disableEditPermissionWithFile() {
loadEditPrivateShareFragment(permissions = Permissions.EDIT_PERMISSIONS.value)
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
onView(withId(R.id.canEditSwitch)).perform(click())
onView(withId(R.id.canEditSwitch)).check(matches(isNotChecked())) // "Can edit" changes
onView(withId(R.id.canEditCreateCheckBox)).check(matches(not(isDisplayed()))) // No suboptions
onView(withId(R.id.canEditChangeCheckBox)).check(matches(not(isDisplayed())))
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(not(isDisplayed())))
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked())) // "Can share" does not change
}
@Test
fun disableEditPermissionWithFolder() {
loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_PERMISSIONS.value)
onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
onView(withId(R.id.canEditCreateCheckBox)).check(matches(isDisplayed())) // Suboptions appear
onView(withId(R.id.canEditChangeCheckBox)).check(matches(isDisplayed()))
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isDisplayed()))
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
onView(withId(R.id.canEditSwitch)).perform(click())
onView(withId(R.id.canEditSwitch)).check(matches(isNotChecked())) // "Can edit" changes
onView(withId(R.id.canEditCreateCheckBox)).check(matches(not(isDisplayed()))) // Suboptions hidden
onView(withId(R.id.canEditChangeCheckBox)).check(matches(not(isDisplayed())))
onView(withId(R.id.canEditDeleteCheckBox)).check(matches(not(isDisplayed())))
onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked())) // "Can share" does not change
}
private fun loadEditPrivateShareFragment(
isFolder: Boolean = false,
permissions: Int = Permissions.READ_PERMISSIONS.value
) {
val shareToEdit = OC_SHARE.copy(
sharedWithDisplayName = defaultSharedWithDisplayName,
permissions = permissions
)
val sharedFile = if (isFolder) OC_FOLDER else OC_FILE
val editPrivateShareFragment = EditPrivateShareFragment.newInstance(
shareToEdit,
sharedFile,
OC_ACCOUNT
)
activityScenario = ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
it.startFragment(editPrivateShareFragment)
}
privateShareAsLiveData.postValue(
Event(
UIResult.Success(
OC_SHARE.copy(
shareWith = "user",
sharedWithDisplayName = "User",
path = "/Videos",
isFolder = isFolder,
permissions = permissions
)
)
)
)
}
}
@@ -0,0 +1,493 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.sharing.shares.ui
import android.text.InputType.TYPE_CLASS_TEXT
import android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
import androidx.lifecycle.MutableLiveData
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.scrollTo
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withInputType
import androidx.test.espresso.matcher.ViewMatchers.withText
import eu.qsfera.android.R
import eu.qsfera.android.domain.capabilities.model.CapabilityBooleanType
import eu.qsfera.android.domain.capabilities.model.OCCapability
import eu.qsfera.android.domain.utils.Event
import eu.qsfera.android.presentation.common.UIResult
import eu.qsfera.android.presentation.sharing.shares.PublicShareDialogFragment
import eu.qsfera.android.presentation.capabilities.CapabilityViewModel
import eu.qsfera.android.presentation.sharing.ShareViewModel
import eu.qsfera.android.testutil.OC_ACCOUNT
import eu.qsfera.android.testutil.OC_CAPABILITY
import eu.qsfera.android.testutil.OC_FILE
import eu.qsfera.android.testutil.OC_FOLDER
import eu.qsfera.android.utils.DateUtils
import io.mockk.every
import io.mockk.mockk
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Test
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
import java.text.SimpleDateFormat
import java.util.Date
class PublicShareCreationDialogFragmentTest {
private val capabilityViewModel = mockk<CapabilityViewModel>(relaxed = true)
private val capabilitiesLiveData = MutableLiveData<Event<UIResult<OCCapability>>>()
private val shareViewModel = mockk<ShareViewModel>(relaxed = true)
private val publicShareCreationStatus = MutableLiveData<Event<UIResult<Unit>>>()
@Before
fun setUp() {
every { capabilityViewModel.capabilities } returns capabilitiesLiveData
every { shareViewModel.publicShareCreationStatus } returns publicShareCreationStatus
stopKoin()
startKoin {
androidContext(ApplicationProvider.getApplicationContext())
allowOverride(override = true)
modules(
module {
viewModel {
capabilityViewModel
}
viewModel {
shareViewModel
}
}
)
}
}
@Test
fun showDialogTitle() {
loadPublicShareDialogFragment()
onView(withId(R.id.publicShareDialogTitle)).check(matches(withText(R.string.share_via_link_create_title)))
}
@Test
fun showMandatoryFields() {
loadPublicShareDialogFragment()
onView(withId(R.id.shareViaLinkNameSection)).check(matches(isDisplayed()))
onView(withId(R.id.shareViaLinkPasswordSection)).check(matches(isDisplayed()))
onView(withId(R.id.shareViaLinkExpirationSection)).check(matches(isDisplayed()))
}
@Test
fun showDialogButtons() {
loadPublicShareDialogFragment()
onView(withId(R.id.cancelButton)).check(matches(isDisplayed()))
onView(withId(R.id.saveButton)).check(matches(isDisplayed()))
}
@Test
fun showFolderAdditionalFields() {
loadPublicShareDialogFragment(
isFolder = true,
capabilities = OC_CAPABILITY.copy(
versionString = "10.0.1",
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE
)
)
onView(withId(R.id.shareViaLinkEditPermissionGroup)).check(matches(isDisplayed()))
}
@Test
fun showDefaultLinkName() {
loadPublicShareDialogFragment()
onView(withId(R.id.shareViaLinkNameValue)).check(matches(withText("DOC_12112018.jpg link")))
}
@Test
fun enablePasswordSwitch() {
loadPublicShareDialogFragment()
onView(withId(R.id.shareViaLinkPasswordSwitch)).perform(click())
onView(withId(R.id.shareViaLinkPasswordValue)).check(matches(isDisplayed()))
onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
}
@Test
fun checkPasswordNotVisible() {
loadPublicShareDialogFragment()
onView(withId(R.id.shareViaLinkPasswordSwitch)).perform(click())
onView(withId(R.id.shareViaLinkPasswordValue)).perform(typeText("supersecure"))
onView(withId(R.id.shareViaLinkPasswordValue)).check(
matches(
withInputType(
TYPE_CLASS_TEXT or TYPE_TEXT_VARIATION_PASSWORD
)
)
)
}
@Test
fun checkPasswordEnforced() {
loadPublicShareDialogFragment(
capabilities = OC_CAPABILITY.copy(
filesSharingPublicPasswordEnforced = CapabilityBooleanType.TRUE
)
)
onView(withId(R.id.shareViaLinkPasswordLabel)).check(
matches(withText(R.string.share_via_link_password_enforced_label))
)
onView(withId(R.id.shareViaLinkPasswordSwitch))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
onView(withId(R.id.shareViaLinkPasswordValue))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
}
@Test
fun checkExpireDateEnforced() {
loadPublicShareDialogFragment(
capabilities = OC_CAPABILITY.copy(
filesSharingPublicExpireDateEnforced = CapabilityBooleanType.TRUE
)
)
onView(withId(R.id.shareViaLinkExpirationLabel))
.check(matches(withText(R.string.share_via_link_expiration_date_enforced_label)))
onView(withId(R.id.shareViaLinkExpirationSwitch))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
onView(withId(R.id.shareViaLinkExpirationExplanationLabel))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
@Test
fun checkExpireDateNotEnforced() {
loadPublicShareDialogFragment(
capabilities = OC_CAPABILITY.copy(
filesSharingPublicExpireDateEnforced = CapabilityBooleanType.FALSE
)
)
onView(withId(R.id.shareViaLinkExpirationLabel))
.check(matches(withText(R.string.share_via_link_expiration_date_label)))
onView(withId(R.id.shareViaLinkExpirationSwitch))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
onView(withId(R.id.shareViaLinkExpirationExplanationLabel))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
}
@Test
fun enableExpirationSwitch() {
loadPublicShareDialogFragment()
onView(withId(R.id.shareViaLinkExpirationSwitch)).perform(click())
onView(withId(android.R.id.button1)).perform(click())
onView(withId(R.id.shareViaLinkExpirationValue))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
//TODO: check the date form the picker
}
@Test
fun cancelExpirationSwitch() {
loadPublicShareDialogFragment()
onView(withId(R.id.shareViaLinkExpirationSwitch)).perform(click())
onView(withId(android.R.id.button2)).perform(click())
onView(withId(R.id.shareViaLinkExpirationValue))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.INVISIBLE)))
}
@Test
fun showError() {
loadPublicShareDialogFragment()
onView(withId(R.id.saveButton)).perform(click())
publicShareCreationStatus.postValue(
Event(
UIResult.Error(
error = Throwable("It was not possible to share this file or folder")
)
)
)
onView(withId(R.id.public_link_error_message)).check(matches(isDisplayed()))
onView(withId(R.id.public_link_error_message)).check(
matches(
withText(R.string.share_link_file_error)
)
)
}
@Test
fun uploadPermissionsWithFolderDisplayed() {
loadPublicShareDialogFragment(
isFolder = true,
capabilities = OC_CAPABILITY.copy(
versionString = "10.1.1",
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
filesSharingPublicUpload = CapabilityBooleanType.TRUE
)
)
onView(withId(R.id.shareViaLinkEditPermissionGroup)).check(matches(isDisplayed()))
}
@Test
fun uploadPermissionsWithFolderNotDisplayed() {
loadPublicShareDialogFragment(
isFolder = true,
capabilities = OC_CAPABILITY.copy(
versionString = "10.1.1",
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
filesSharingPublicUpload = CapabilityBooleanType.FALSE
)
)
onView(withId(R.id.shareViaLinkEditPermissionGroup)).check(matches(not(isDisplayed())))
}
@Test
fun expirationDateDays() {
val daysToTest = 15
loadPublicShareDialogFragment(
capabilities = OC_CAPABILITY.copy(
versionString = "10.1.1",
filesSharingPublicExpireDateDays = daysToTest
)
)
val formattedDate = SimpleDateFormat.getDateInstance().format(
DateUtils.addDaysToDate(
Date(),
daysToTest
)
)
onView(withId(R.id.shareViaLinkExpirationSwitch))
.check(matches(isEnabled()))
onView(withId(R.id.shareViaLinkExpirationValue))
.check(matches(withText(formattedDate)))
}
@Test
fun passwordNotEnforced() {
loadPublicShareDialogFragment(
capabilities = OC_CAPABILITY.copy(
versionString = "10.1.1",
filesSharingPublicPasswordEnforced = CapabilityBooleanType.FALSE
)
)
onView(withId(R.id.shareViaLinkPasswordLabel))
.check(matches(withText(R.string.share_via_link_password_label)))
onView(withId(R.id.saveButton)).check(matches(isEnabled()))
}
@Test
fun passwordEnforced() {
loadPublicShareDialogFragment(
capabilities = OC_CAPABILITY.copy(
versionString = "10.1.1",
filesSharingPublicPasswordEnforced = CapabilityBooleanType.TRUE
)
)
onView(withId(R.id.shareViaLinkPasswordLabel))
.check(matches(withText(R.string.share_via_link_password_enforced_label)))
onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
}
@Test
fun passwordEnforcedReadOnlyFolders() {
loadPublicShareDialogFragment(
isFolder = true,
capabilities = OC_CAPABILITY.copy(
versionString = "10.1.1",
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
filesSharingPublicPasswordEnforcedReadOnly = CapabilityBooleanType.TRUE,
filesSharingPublicPasswordEnforced = CapabilityBooleanType.TRUE
)
)
onView(withId(R.id.shareViaLinkEditPermissionReadOnly)).perform(scrollTo())
onView(withId(R.id.shareViaLinkEditPermissionReadOnly)).check(matches(isDisplayed()))
onView(withId(R.id.shareViaLinkEditPermissionReadOnly)).perform(click())
onView(withId(R.id.shareViaLinkPasswordLabel))
.check(matches(withText(R.string.share_via_link_password_enforced_label)))
onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
}
@Test
fun passwordNotEnforcedReadOnlyFolders() {
loadPublicShareDialogFragment(
isFolder = true,
capabilities = OC_CAPABILITY.copy(
versionString = "10.1.1",
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
filesSharingPublicPasswordEnforcedReadOnly = CapabilityBooleanType.FALSE,
filesSharingPublicPasswordEnforced = CapabilityBooleanType.FALSE
)
)
onView(withId(R.id.shareViaLinkEditPermissionReadOnly)).check(matches(isDisplayed()))
onView(withId(R.id.shareViaLinkEditPermissionReadOnly)).perform(click())
onView(withId(R.id.shareViaLinkPasswordLabel))
.check(matches(withText(R.string.share_via_link_password_label)))
onView(withId(R.id.saveButton)).check(matches(isEnabled()))
}
@Test
fun passwordEnforcedReadWriteFolders() {
loadPublicShareDialogFragment(
isFolder = true,
capabilities = OC_CAPABILITY.copy(
versionString = "10.1.1",
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
filesSharingPublicPasswordEnforcedReadWrite = CapabilityBooleanType.TRUE,
filesSharingPublicPasswordEnforced = CapabilityBooleanType.TRUE
)
)
onView(withId(R.id.shareViaLinkEditPermissionReadAndWrite)).check(matches(isDisplayed()))
onView(withId(R.id.shareViaLinkEditPermissionReadAndWrite)).perform(click())
onView(withId(R.id.shareViaLinkPasswordLabel))
.check(matches(withText(R.string.share_via_link_password_enforced_label)))
onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
}
@Test
fun passwordNotEnforcedReadWriteFolders() {
loadPublicShareDialogFragment(
isFolder = true,
capabilities = OC_CAPABILITY.copy(
versionString = "10.1.1",
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
filesSharingPublicPasswordEnforcedReadWrite = CapabilityBooleanType.FALSE,
filesSharingPublicPasswordEnforced = CapabilityBooleanType.FALSE
)
)
onView(withId(R.id.shareViaLinkEditPermissionReadAndWrite)).check(matches(isDisplayed()))
onView(withId(R.id.shareViaLinkEditPermissionReadAndWrite)).perform(click())
onView(withId(R.id.shareViaLinkPasswordLabel))
.check(matches(withText(R.string.share_via_link_password_label)))
onView(withId(R.id.saveButton)).check(matches(isEnabled()))
}
@Test
fun passwordEnforcedUploadOnlyFolders() {
loadPublicShareDialogFragment(
isFolder = true,
capabilities = OC_CAPABILITY.copy(
versionString = "10.1.1",
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
filesSharingPublicPasswordEnforcedUploadOnly = CapabilityBooleanType.TRUE,
filesSharingPublicPasswordEnforced = CapabilityBooleanType.FALSE
)
)
onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).check(matches(isDisplayed()))
onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).perform(click())
onView(withId(R.id.shareViaLinkPasswordLabel))
.check(matches(withText(R.string.share_via_link_password_enforced_label)))
onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
}
@Test
fun passwordNotEnforcedUploadOnlyFolders() {
loadPublicShareDialogFragment(
isFolder = true,
capabilities = OC_CAPABILITY.copy(
versionString = "10.1.1",
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
filesSharingPublicPasswordEnforcedUploadOnly = CapabilityBooleanType.FALSE,
filesSharingPublicPasswordEnforced = CapabilityBooleanType.FALSE
)
)
onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).check(matches(isDisplayed()))
onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).perform(click())
onView(withId(R.id.shareViaLinkPasswordLabel))
.check(matches(withText(R.string.share_via_link_password_label)))
onView(withId(R.id.saveButton)).check(matches(isEnabled()))
}
@Test
fun passwordEnforcedClearErrorMessageIfSwitchesToNotEnforced() {
val commonError = "Common error"
//One permission with password enforced. Error is cleaned after switching permission
//to a non-forced one
loadPublicShareDialogFragment(
isFolder = true,
capabilities = OC_CAPABILITY.copy(
versionString = "10.1.1",
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
filesSharingPublicUpload = CapabilityBooleanType.TRUE,
filesSharingPublicPasswordEnforcedUploadOnly = CapabilityBooleanType.FALSE,
filesSharingPublicPasswordEnforcedReadOnly = CapabilityBooleanType.FALSE,
filesSharingPublicPasswordEnforced = CapabilityBooleanType.TRUE
)
)
onView(withId(R.id.saveButton)).perform(scrollTo())
onView(withId(R.id.saveButton)).perform(click())
publicShareCreationStatus.postValue(
Event(
UIResult.Error(
error = Throwable(commonError)
)
)
)
onView(withId(R.id.public_link_error_message)).perform(scrollTo())
onView(withText(commonError)).check(matches(isDisplayed()))
onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).perform(scrollTo(), click())
onView(withText(commonError)).check(matches(not(isDisplayed())))
onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
}
private fun loadPublicShareDialogFragment(
isFolder: Boolean = false,
capabilities: OCCapability = OC_CAPABILITY
) {
val file = if (isFolder) OC_FOLDER else OC_FILE
val publicShareDialogFragment = PublicShareDialogFragment.newInstanceToCreate(
file,
OC_ACCOUNT,
"DOC_12112018.jpg link"
)
ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
it.startFragment(publicShareDialogFragment)
}
capabilitiesLiveData.postValue(
Event(UIResult.Success(capabilities))
)
}
}
@@ -0,0 +1,145 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.sharing.shares.ui
import androidx.lifecycle.MutableLiveData
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withHint
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import eu.qsfera.android.R
import eu.qsfera.android.domain.capabilities.model.OCCapability
import eu.qsfera.android.domain.sharing.shares.model.ShareType
import eu.qsfera.android.domain.utils.Event
import eu.qsfera.android.lib.resources.shares.RemoteShare
import eu.qsfera.android.presentation.common.UIResult
import eu.qsfera.android.presentation.sharing.shares.PublicShareDialogFragment
import eu.qsfera.android.presentation.capabilities.CapabilityViewModel
import eu.qsfera.android.presentation.sharing.ShareViewModel
import eu.qsfera.android.testutil.OC_ACCOUNT
import eu.qsfera.android.testutil.OC_FILE
import eu.qsfera.android.testutil.OC_SHARE
import io.mockk.every
import io.mockk.mockk
import org.junit.Before
import org.junit.Test
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.GregorianCalendar
import java.util.TimeZone
class PublicShareEditionDialogFragmentTest {
private val capabilityViewModel = mockk<CapabilityViewModel>(relaxed = true)
private val capabilitiesLiveData = MutableLiveData<Event<UIResult<OCCapability>>>()
private val shareViewModel = mockk<ShareViewModel>(relaxed = true)
private val expirationDate = 1556575200000 // GMT: Monday, April 29, 2019 10:00:00 PM
@Before
fun setUp() {
every { capabilityViewModel.capabilities } returns capabilitiesLiveData
stopKoin()
startKoin {
androidContext(ApplicationProvider.getApplicationContext())
allowOverride(override = true)
modules(
module {
viewModel {
capabilityViewModel
}
viewModel {
shareViewModel
}
}
)
}
val publicShareDialogFragment = PublicShareDialogFragment.newInstanceToUpdate(
OC_FILE,
OC_ACCOUNT,
OC_SHARE.copy(
shareType = ShareType.PUBLIC_LINK,
shareWith = "user",
name = "Docs link",
permissions = RemoteShare.CREATE_PERMISSION_FLAG,
expirationDate = expirationDate,
isFolder = true
)
)
ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
it.startFragment(publicShareDialogFragment)
}
}
@Test
fun showEditionDialogTitle() {
onView(withId(R.id.publicShareDialogTitle)).check(matches(withText(R.string.share_via_link_edit_title)))
}
@Test
fun checkLinkNameSet() {
onView(withText(R.string.share_via_link_name_label)).check(matches(ViewMatchers.isDisplayed()))
onView(withId(R.id.shareViaLinkNameValue)).check(matches(withText("Docs link")))
}
@Test
fun checkUploadOnly() {
onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).check(matches(isChecked()))
}
@Test
fun checkPasswordSet() {
onView(withId(R.id.shareViaLinkPasswordLabel)).check(matches(withText(R.string.share_via_link_password_label)))
onView(withId(R.id.shareViaLinkPasswordSwitch)).check(matches(withEffectiveVisibility(VISIBLE)))
onView(withId(R.id.shareViaLinkPasswordValue)).check(matches(withEffectiveVisibility(VISIBLE)))
onView(withId(R.id.shareViaLinkPasswordValue)).check(matches(withHint(R.string.share_via_link_default_password)))
}
@Test
fun checkExpirationDateSet() {
val calendar = GregorianCalendar()
calendar.timeInMillis = expirationDate
val formatter: DateFormat = SimpleDateFormat.getDateInstance()
formatter.timeZone = TimeZone.getDefault()
val time = formatter.format(calendar.time)
onView(withId(R.id.shareViaLinkExpirationLabel)).check(matches(withText(R.string.share_via_link_expiration_date_label)))
onView(withId(R.id.shareViaLinkExpirationSwitch)).check(matches(withEffectiveVisibility(VISIBLE)))
onView(withId(R.id.shareViaLinkExpirationValue)).check(matches(withEffectiveVisibility(VISIBLE)))
onView(withId(R.id.shareViaLinkExpirationValue)).check(matches(withText(time)))
}
}
@@ -0,0 +1,320 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.sharing.shares.ui
import androidx.lifecycle.MutableLiveData
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withTagValue
import androidx.test.espresso.matcher.ViewMatchers.withText
import eu.qsfera.android.R
import eu.qsfera.android.domain.capabilities.model.CapabilityBooleanType
import eu.qsfera.android.domain.capabilities.model.OCCapability
import eu.qsfera.android.domain.sharing.shares.model.OCShare
import eu.qsfera.android.domain.sharing.shares.model.ShareType
import eu.qsfera.android.domain.utils.Event
import eu.qsfera.android.presentation.common.UIResult
import eu.qsfera.android.presentation.sharing.ShareFileFragment
import eu.qsfera.android.presentation.capabilities.CapabilityViewModel
import eu.qsfera.android.presentation.sharing.ShareViewModel
import eu.qsfera.android.testutil.OC_ACCOUNT
import eu.qsfera.android.testutil.OC_CAPABILITY
import eu.qsfera.android.testutil.OC_FILE
import eu.qsfera.android.testutil.OC_SHARE
import eu.qsfera.android.utils.matchers.assertVisibility
import eu.qsfera.android.utils.matchers.isDisplayed
import eu.qsfera.android.utils.matchers.withText
import io.mockk.every
import io.mockk.mockk
import org.hamcrest.CoreMatchers
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
@Ignore
class ShareFileFragmentTest {
private val capabilityViewModel = mockk<CapabilityViewModel>(relaxed = true)
private val capabilitiesLiveData = MutableLiveData<Event<UIResult<OCCapability>>>()
private val shareViewModel = mockk<ShareViewModel>(relaxed = true)
private val sharesLiveData = MutableLiveData<Event<UIResult<List<OCShare>>>>()
@Before
fun setUp() {
every { capabilityViewModel.capabilities } returns capabilitiesLiveData
every { shareViewModel.shares } returns sharesLiveData
stopKoin()
startKoin {
androidContext(ApplicationProvider.getApplicationContext())
allowOverride(override = true)
modules(
module {
viewModel {
capabilityViewModel
}
viewModel {
shareViewModel
}
}
)
}
}
@Test
fun showHeader() {
loadShareFileFragment()
onView(withId(R.id.shareFileName)).check(matches(withText(OC_FILE.fileName)))
}
@Test
fun fileSizeVisible() {
loadShareFileFragment()
R.id.shareFileSize.isDisplayed(displayed = true)
}
@Test
fun showPrivateLink() {
loadShareFileFragment()
R.id.getPrivateLinkButton.isDisplayed(displayed = true)
}
@Test
fun hidePrivateLink() {
loadShareFileFragment(capabilities = OC_CAPABILITY.copy(filesPrivateLinks = CapabilityBooleanType.FALSE))
R.id.getPrivateLinkButton.isDisplayed(displayed = false)
}
/******************************************************************************************************
******************************************* PRIVATE SHARES *******************************************
******************************************************************************************************/
private var userSharesList = listOf(
OC_SHARE.copy(sharedWithDisplayName = "Batman"),
OC_SHARE.copy(sharedWithDisplayName = "Joker")
)
private var groupSharesList = listOf(
OC_SHARE.copy(
shareType = ShareType.GROUP,
sharedWithDisplayName = "Suicide Squad"
),
OC_SHARE.copy(
shareType = ShareType.GROUP,
sharedWithDisplayName = "Avengers"
)
)
@Test
fun showUsersAndGroupsSectionTitle() {
loadShareFileFragment(shares = userSharesList)
onView(withText(R.string.share_with_user_section_title)).check(matches(isDisplayed()))
}
@Test
fun showNoPrivateShares() {
loadShareFileFragment(shares = listOf())
onView(withText(R.string.share_no_users)).check(matches(isDisplayed()))
}
@Test
fun showUserShares() {
loadShareFileFragment(shares = userSharesList)
onView(withText("Batman")).check(matches(isDisplayed()))
onView(withText("Batman")).check(matches(hasSibling(withId(R.id.unshareButton))))
.check(matches(isDisplayed()))
onView(withText("Batman")).check(matches(hasSibling(withId(R.id.editShareButton))))
.check(matches(isDisplayed()))
onView(withText("Joker")).check(matches(isDisplayed()))
}
@Test
fun showGroupShares() {
loadShareFileFragment(shares = listOf(groupSharesList.first()))
onView(withText("Suicide Squad (group)")).check(matches(isDisplayed()))
onView(withText("Suicide Squad (group)")).check(matches(hasSibling(withId(R.id.icon))))
.check(matches(isDisplayed()))
onView(withTagValue(CoreMatchers.equalTo(R.drawable.ic_group))).check(matches(isDisplayed()))
}
/******************************************************************************************************
******************************************* PUBLIC SHARES ********************************************
******************************************************************************************************/
private var publicShareList = listOf(
OC_SHARE.copy(
shareType = ShareType.PUBLIC_LINK,
path = "/Photos/image.jpg",
isFolder = false,
name = "Image link",
shareLink = "http://server:port/s/1"
),
OC_SHARE.copy(
shareType = ShareType.PUBLIC_LINK,
path = "/Photos/image.jpg",
isFolder = false,
name = "Image link 2",
shareLink = "http://server:port/s/2"
),
OC_SHARE.copy(
shareType = ShareType.PUBLIC_LINK,
path = "/Photos/image.jpg",
isFolder = false,
name = "Image link 3",
shareLink = "http://server:port/s/3"
)
)
@Test
fun showNoPublicShares() {
loadShareFileFragment(shares = listOf())
onView(withText(R.string.share_no_public_links)).check(matches(isDisplayed()))
}
@Test
fun showPublicShares() {
loadShareFileFragment(shares = publicShareList)
onView(withText("Image link")).check(matches(isDisplayed()))
onView(withText("Image link")).check(matches(hasSibling(withId(R.id.getPublicLinkButton))))
.check(matches(isDisplayed()))
onView(withText("Image link")).check(matches(hasSibling(withId(R.id.deletePublicLinkButton))))
.check(matches(isDisplayed()))
onView(withText("Image link")).check(matches(hasSibling(withId(R.id.editPublicLinkButton))))
.check(matches(isDisplayed()))
onView(withText("Image link 2")).check(matches(isDisplayed()))
onView(withText("Image link 3")).check(matches(isDisplayed()))
}
@Test
fun showPublicSharesSharingEnabled() {
loadShareFileFragment(
capabilities = OC_CAPABILITY.copy(filesSharingPublicEnabled = CapabilityBooleanType.TRUE),
shares = publicShareList
)
onView(withText("Image link")).check(matches(isDisplayed()))
onView(withText("Image link 2")).check(matches(isDisplayed()))
onView(withText("Image link 3")).check(matches(isDisplayed()))
}
@Test
fun hidePublicSharesSharingDisabled() {
loadShareFileFragment(
capabilities = OC_CAPABILITY.copy(filesSharingPublicEnabled = CapabilityBooleanType.FALSE),
shares = publicShareList
)
R.id.shareViaLinkSection.assertVisibility(ViewMatchers.Visibility.GONE)
}
@Test
fun createPublicShareMultipleCapability() {
loadShareFileFragment(
capabilities = OC_CAPABILITY.copy(
versionString = "10.1.1",
filesSharingPublicMultiple = CapabilityBooleanType.TRUE
),
shares = listOf(publicShareList[0])
)
R.id.addPublicLinkButton.assertVisibility(ViewMatchers.Visibility.VISIBLE)
}
@Test
fun cannotCreatePublicShareMultipleCapability() {
loadShareFileFragment(
capabilities = OC_CAPABILITY.copy(
versionString = "10.1.1",
filesSharingPublicMultiple = CapabilityBooleanType.FALSE
),
shares = listOf(publicShareList[0])
)
R.id.addPublicLinkButton.assertVisibility(ViewMatchers.Visibility.INVISIBLE)
}
@Test
fun cannotCreatePublicShareServerCapability() {
loadShareFileFragment(
capabilities = OC_CAPABILITY.copy(
versionString = "9.3.1"
),
shares = listOf(publicShareList[0])
)
R.id.addPublicLinkButton.assertVisibility(ViewMatchers.Visibility.INVISIBLE)
}
/******************************************************************************************************
*********************************************** COMMON ***********************************************
******************************************************************************************************/
@Test
fun hideSharesSharingApiDisabled() {
loadShareFileFragment(
capabilities = OC_CAPABILITY.copy(
filesSharingApiEnabled = CapabilityBooleanType.FALSE
)
)
R.id.shareWithUsersSection.assertVisibility(ViewMatchers.Visibility.GONE)
R.id.shareViaLinkSection.assertVisibility(ViewMatchers.Visibility.GONE)
}
@Test
fun showError() {
loadShareFileFragment(
sharesUIResult = UIResult.Error(
error = Throwable("It was not possible to retrieve the shares from the server")
)
)
com.google.android.material.R.id.snackbar_text.withText(R.string.get_shares_error)
}
private fun loadShareFileFragment(
capabilities: OCCapability = OC_CAPABILITY,
capabilitiesEvent: Event<UIResult<OCCapability>> = Event(UIResult.Success(capabilities)),
shares: List<OCShare> = listOf(OC_SHARE),
sharesUIResult: UIResult<List<OCShare>> = UIResult.Success(shares)
) {
val shareFileFragment = ShareFileFragment.newInstance(
OC_FILE,
OC_ACCOUNT
)
ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
it.startFragment(shareFileFragment)
}
capabilitiesLiveData.postValue(capabilitiesEvent)
sharesLiveData.postValue(Event(sharesUIResult))
}
}
@@ -0,0 +1,104 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* @author Jesus Recio Rincon
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.sharing.shares.ui
import androidx.lifecycle.MutableLiveData
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import eu.qsfera.android.R
import eu.qsfera.android.domain.capabilities.model.OCCapability
import eu.qsfera.android.domain.sharing.shares.model.OCShare
import eu.qsfera.android.domain.utils.Event
import eu.qsfera.android.presentation.common.UIResult
import eu.qsfera.android.presentation.sharing.ShareFileFragment
import eu.qsfera.android.presentation.capabilities.CapabilityViewModel
import eu.qsfera.android.presentation.sharing.ShareViewModel
import eu.qsfera.android.testutil.OC_ACCOUNT
import eu.qsfera.android.testutil.OC_CAPABILITY
import eu.qsfera.android.testutil.OC_FOLDER
import eu.qsfera.android.testutil.OC_SHARE
import io.mockk.every
import io.mockk.mockk
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Test
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
class ShareFolderFragmentTest {
private val capabilityViewModel = mockk<CapabilityViewModel>(relaxed = true)
private val capabilitiesLiveData = MutableLiveData<Event<UIResult<OCCapability>>>()
private val shareViewModel = mockk<ShareViewModel>(relaxed = true)
private val sharesLiveData = MutableLiveData<Event<UIResult<List<OCShare>>>>()
@Before
fun setUp() {
every { capabilityViewModel.capabilities } returns capabilitiesLiveData
every { shareViewModel.shares } returns sharesLiveData
stopKoin()
startKoin {
androidContext(ApplicationProvider.getApplicationContext())
allowOverride(override = true)
modules(
module {
viewModel {
capabilityViewModel
}
viewModel {
shareViewModel
}
}
)
}
val shareFileFragment = ShareFileFragment.newInstance(
OC_FOLDER.copy(privateLink = null),
OC_ACCOUNT
)
ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
it.startFragment(shareFileFragment)
}
capabilitiesLiveData.postValue(Event(UIResult.Success(OC_CAPABILITY)))
sharesLiveData.postValue(Event(UIResult.Success(listOf(OC_SHARE))))
}
@Test
fun folderSizeVisible() {
onView(withId(R.id.shareFileSize)).check(matches(not(isDisplayed())))
}
@Test
fun hidePrivateLink() {
onView(withId(R.id.getPrivateLinkButton)).check(matches(not(isDisplayed())))
}
}
@@ -0,0 +1,87 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
package eu.qsfera.android.sharing.shares.ui
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import eu.qsfera.android.R
import eu.qsfera.android.domain.files.model.OCFile
import eu.qsfera.android.domain.sharing.shares.model.OCShare
import eu.qsfera.android.presentation.sharing.ShareFragmentListener
import eu.qsfera.android.services.OperationsService
import eu.qsfera.android.testing.SingleFragmentActivity
import eu.qsfera.android.ui.fragment.FileFragment.ContainerActivity
import eu.qsfera.android.ui.helpers.FileOperationsHelper
class TestShareFileActivity : SingleFragmentActivity(), ShareFragmentListener, ContainerActivity {
fun startFragment(fragment: Fragment) {
supportFragmentManager.commit(allowStateLoss = true) {
add(R.id.container, fragment, TEST_FRAGMENT_TAG)
}
}
fun getTestFragment(): Fragment? = supportFragmentManager.findFragmentByTag(TEST_FRAGMENT_TAG)
override fun copyOrSendPrivateLink(file: OCFile) {
}
override fun deleteShare(remoteId: String) {
}
override fun showLoading() {
}
override fun dismissLoading() {
}
override fun showAddPublicShare(defaultLinkName: String) {
}
override fun showEditPublicShare(share: OCShare) {
}
override fun showRemoveShare(share: OCShare) {
}
override fun copyOrSendPublicLink(share: OCShare) {
}
override fun showSearchUsersAndGroups() {
}
override fun showEditPrivateShare(share: OCShare) {
}
companion object {
private const val TEST_FRAGMENT_TAG = "TEST FRAGMENT"
}
override fun getOperationsServiceBinder(): OperationsService.OperationsServiceBinder {
TODO("Not yet implemented")
}
override fun getFileOperationsHelper(): FileOperationsHelper {
TODO("Not yet implemented")
}
override fun showDetails(file: OCFile?) {
}
}
@@ -0,0 +1,117 @@
/**
* qsfera Android client application
*
* @author David Crespo Ríos
* Copyright (C) 2022 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.ui.activity
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import eu.qsfera.android.R
import eu.qsfera.android.presentation.releasenotes.ReleaseNotesActivity
import eu.qsfera.android.presentation.releasenotes.ReleaseNotesViewModel
import eu.qsfera.android.utils.click
import eu.qsfera.android.utils.matchers.assertChildCount
import eu.qsfera.android.utils.matchers.isDisplayed
import eu.qsfera.android.utils.matchers.withText
import eu.qsfera.android.utils.releaseNotesList
import io.mockk.every
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
class ReleaseNotesActivityTest {
private lateinit var activityScenario: ActivityScenario<ReleaseNotesActivity>
private lateinit var context: Context
private lateinit var releaseNotesViewModel: ReleaseNotesViewModel
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
releaseNotesViewModel = mockk(relaxed = true)
stopKoin()
startKoin {
context
allowOverride(override = true)
modules(
module {
viewModel {
releaseNotesViewModel
}
}
)
}
every { releaseNotesViewModel.getReleaseNotes() } returns releaseNotesList
val intent = Intent(context, ReleaseNotesActivity::class.java)
activityScenario = ActivityScenario.launch(intent)
}
@Test
fun releaseNotesView() {
val header = String.format(
context.getString(R.string.release_notes_header),
context.getString(R.string.app_name)
)
val footer = String.format(
context.getString(R.string.release_notes_footer),
context.getString(R.string.app_name)
)
with(R.id.txtHeader) {
isDisplayed(true)
withText(header)
}
R.id.releaseNotes.isDisplayed(true)
with(R.id.txtFooter) {
isDisplayed(true)
withText(footer)
}
R.id.btnProceed.isDisplayed(true)
}
@Ignore
@Test
fun releaseNotesProceedButton() {
R.id.btnProceed.click()
assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
}
@Ignore
@Test
fun test_childCount() {
R.id.releaseNotes.assertChildCount(3)
}
}
@@ -0,0 +1,42 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
*
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
* <p>
* 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.utils
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.scrollTo
import androidx.test.espresso.matcher.ViewMatchers.withId
fun Int.typeText(text: String) {
onView(withId(this)).perform(scrollTo(), ViewActions.typeText(text))
}
fun Int.replaceText(text: String) {
onView(withId(this)).perform(scrollTo(), ViewActions.replaceText(text))
}
fun Int.scrollAndClick() {
onView(withId(this)).perform(scrollTo(), ViewActions.click())
}
fun Int.click() {
onView(withId(this)).perform(ViewActions.click())
}
@@ -0,0 +1,58 @@
/**
* qsfera Android client application
*
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2021 ownCloud GmbH.
* <p>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
* <p>
* 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.utils
import android.app.Activity
import android.app.Instrumentation
import android.content.Intent
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
fun mockIntent(
extras: Pair<String, String>,
resultCode: Int = Activity.RESULT_OK,
action: String
) {
val result = Intent()
result.putExtra(extras.first, extras.second)
val intentResult = Instrumentation.ActivityResult(resultCode, result)
intending(hasAction(action)).respondWith(intentResult)
}
@JvmName("mockIntentNoExtras")
fun mockIntent(
resultCode: Int = Activity.RESULT_OK,
action: String
) {
val result = Intent()
val intentResult = Instrumentation.ActivityResult(resultCode, result)
intending(hasAction(action)).respondWith(intentResult)
}
fun mockIntentToComponent(
resultCode: Int = Activity.RESULT_OK,
packageName: String
) {
val result = Intent()
val intentResult = Instrumentation.ActivityResult(resultCode, result)
intending(hasComponent(packageName)).respondWith(intentResult)
}
@@ -0,0 +1,38 @@
/**
* qsfera Android client application
*
* Copyright (C) 2022 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.utils
import android.app.Application
import android.content.Context
import android.os.Build
import androidx.test.runner.AndroidJUnitRunner
import com.github.tmurakami.dexopener.DexOpener
/**
* We need to use DexOpener for executing instrumented tests on <P Android devices,
* as Mockk documentation suggests https://mockk.io/ANDROID.html
*/
class OCTestAndroidJUnitRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader, className: String, context: Context): Application {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
DexOpener.install(this)
}
return super.newApplication(cl, className, context)
}
}
@@ -0,0 +1,36 @@
/**
* qsfera Android client application
*
* @author David González Verdugo
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.utils
enum class Permissions(val value: Int) {
READ_PERMISSIONS(1),
EDIT_PERMISSIONS(3),
SHARE_PERMISSIONS(17),
ALL_PERMISSIONS(19),
// FOLDERS
EDIT_CREATE_PERMISSIONS(5),
EDIT_CREATE_CHANGE_PERMISSIONS(7),
EDIT_CREATE_DELETE_PERMISSIONS(13),
EDIT_CREATE_CHANGE_DELETE_PERMISSIONS(15),
EDIT_CHANGE_PERMISSIONS(3),
EDIT_CHANGE_DELETE_PERMISSIONS(11),
EDIT_DELETE_PERMISSIONS(9),
}
@@ -0,0 +1,42 @@
/**
* qsfera Android client application
*
* @author David Crespo Ríos
* Copyright (C) 2022 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.utils
import eu.qsfera.android.R
import eu.qsfera.android.presentation.releasenotes.ReleaseNote
import eu.qsfera.android.presentation.releasenotes.ReleaseNoteType
val releaseNotesList = listOf(
ReleaseNote(
title = R.string.release_notes_header,
subtitle = R.string.release_notes_footer,
type = ReleaseNoteType.BUGFIX
),
ReleaseNote(
title = R.string.release_notes_header,
subtitle = R.string.release_notes_footer,
type = ReleaseNoteType.BUGFIX
),
ReleaseNote(
title = R.string.release_notes_header,
subtitle = R.string.release_notes_footer,
type = ReleaseNoteType.ENHANCEMENT
)
)
@@ -0,0 +1,77 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
*
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
* <p>
* 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.utils.matchers
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.test.espresso.Espresso
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.BoundedMatcher
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withText
import eu.qsfera.android.R
import eu.qsfera.android.presentation.common.BottomSheetFragmentItemView
import org.hamcrest.Description
import org.hamcrest.Matcher
fun Int.bsfItemWithTitle(@StringRes title: Int, @ColorRes tintColor: Int?) {
Espresso.onView(ViewMatchers.withId(this)).inRoot(RootMatchers.isDialog())
.check(ViewAssertions.matches(withTitle(title, tintColor)))
}
fun Int.bsfItemWithIcon(@DrawableRes drawable: Int, @ColorRes tintColor: Int?) {
Espresso.onView(ViewMatchers.withId(this)).inRoot(RootMatchers.isDialog())
.check(ViewAssertions.matches(withIcon(drawable, tintColor)))
}
private fun withTitle(@StringRes title: Int, @ColorRes tintColor: Int?): Matcher<View> =
object : BoundedMatcher<View, BottomSheetFragmentItemView>(BottomSheetFragmentItemView::class.java) {
override fun describeTo(description: Description) {
description.appendText("BottomSheetFragmentItemView with text: $title")
tintColor?.let { description.appendText(" and tint color id: $tintColor") }
}
override fun matchesSafely(item: BottomSheetFragmentItemView): Boolean {
val itemTitleView = item.findViewById<TextView>(R.id.item_title)
val textMatches = withText(title).matches(itemTitleView)
val textColorMatches = tintColor?.let { withTextColor(tintColor).matches(itemTitleView) } ?: true
return textMatches && textColorMatches
}
}
private fun withIcon(@DrawableRes drawable: Int, @ColorRes tintColor: Int?): Matcher<View> =
object : BoundedMatcher<View, BottomSheetFragmentItemView>(BottomSheetFragmentItemView::class.java) {
override fun describeTo(description: Description) {
description.appendText("BottomSheetFragmentItemView with icon: $drawable")
tintColor?.let { description.appendText(" and tint color id: $tintColor") }
}
override fun matchesSafely(item: BottomSheetFragmentItemView): Boolean {
val itemIconView = item.findViewById<ImageView>(R.id.item_icon)
return withDrawable(drawable, tintColor).matches(itemIconView)
}
}
@@ -0,0 +1,62 @@
/**
* qsfera Android client application
*
* @author Christian Schabesberger
* Copyright (C) 2021 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.utils.matchers
import android.view.View
import android.view.ViewGroup
import androidx.test.espresso.matcher.BoundedMatcher
import org.hamcrest.Matcher
import org.hamcrest.TypeSafeMatcher
import org.hamcrest.Description
fun withChildViewCount(count: Int, childMatcher: Matcher<View>): Matcher<View> {
return object : BoundedMatcher<View, ViewGroup>(ViewGroup::class.java) {
override fun matchesSafely(viewGroup: ViewGroup): Boolean {
var matchCount = 0
for (i in 0 until viewGroup.childCount) {
if (childMatcher.matches(viewGroup.getChildAt(i))) {
matchCount++
}
}
return matchCount == count
}
override fun describeTo(description: Description?) {
description?.appendText("ViewGroup with child-count=$count and")
childMatcher.describeTo(description)
}
}
}
fun nthChildOf(parentMatcher: Matcher<View>, childPosition: Int): Matcher<View> {
return object : TypeSafeMatcher<View>() {
override fun matchesSafely(view: View): Boolean {
if (view.parent !is ViewGroup) {
return parentMatcher.matches(view.parent)
}
val group = view.parent as ViewGroup
return parentMatcher.matches(view.parent) && group.getChildAt(childPosition) == view
}
override fun describeTo(description: Description) {
description.appendText("with $childPosition child view of type parentMatcher")
}
}
}
@@ -0,0 +1,63 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
*
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
* <p>
* 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.utils.matchers
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageView
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import org.hamcrest.Description
import org.hamcrest.TypeSafeMatcher
fun withDrawable(
@DrawableRes id: Int,
@ColorRes tint: Int? = null,
tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_IN
) = object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("ImageView with drawable same as drawable with id $id")
tint?.let { description.appendText(", tint color id: $tint, mode: $tintMode") }
}
override fun matchesSafely(view: View): Boolean {
val context = view.context
val tintColor = tint?.toColor(context)
val expectedBitmap = context.getDrawable(id)?.tinted(tintColor, tintMode)?.toBitmap()
return view is ImageView && view.drawable.toBitmap().sameAs(expectedBitmap)
}
}
private fun Int.toColor(context: Context) = ContextCompat.getColor(context, this)
private fun Drawable.tinted(@ColorInt tintColor: Int? = null, tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_IN) =
apply {
setTintList(tintColor?.toColorStateList())
setTintMode(tintMode)
}
private fun Int.toColorStateList() = ColorStateList.valueOf(this)
@@ -0,0 +1,74 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
*
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
* <p>
* 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.utils.matchers
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasChildCount
import androidx.test.espresso.matcher.ViewMatchers.withId
import org.hamcrest.CoreMatchers
fun Int.isDisplayed(displayed: Boolean) {
val displayMatcher = if (displayed) ViewMatchers.isDisplayed() else CoreMatchers.not(ViewMatchers.isDisplayed())
onView(withId(this))
.check(matches(displayMatcher))
}
fun Int.isEnabled(enabled: Boolean) {
val enableMatcher = if (enabled) ViewMatchers.isEnabled() else CoreMatchers.not(ViewMatchers.isEnabled())
onView(withId(this))
.check(matches(enableMatcher))
}
fun Int.isFocusable(focusable: Boolean) {
val focusableMatcher = if (focusable) ViewMatchers.isFocusable() else CoreMatchers.not(ViewMatchers.isFocusable())
onView(withId(this))
.check(matches(focusableMatcher))
}
fun Int.withText(text: String) {
onView(withId(this))
.check(matches(ViewMatchers.withText(text)))
}
fun Int.withText(resourceId: Int) {
onView(withId(this))
.check(matches(ViewMatchers.withText(resourceId)))
}
fun Int.withChildCountAndId(count: Int, resourceId: Int) {
onView(withId(this))
.check(matches(withChildViewCount(count, withId(resourceId))))
}
fun Int.assertVisibility(visibility: ViewMatchers.Visibility) {
onView(withId(this))
.check(matches(ViewMatchers.withEffectiveVisibility(visibility)))
}
fun Int.assertChildCount(childs: Int) {
onView(withId(this))
.check(matches(hasChildCount(childs)))
}
@@ -0,0 +1,48 @@
/**
* qsfera Android client application
*
* @author Juan Carlos Garrote Gascón
*
* Copyright (C) 2021 ownCloud GmbH.
* <p>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
* <p>
* 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.utils.matchers
import androidx.preference.Preference
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.junit.Assert.assertEquals
fun Preference.verifyPreference(
keyPref: String,
titlePref: String,
summaryPref: String? = null,
visible: Boolean,
enabled: Boolean? = null
) {
if (visible) onView(withText(titlePref)).check(matches(isDisplayed()))
summaryPref?.let {
if (visible) onView(withText(it)).check(matches(isDisplayed()))
assertEquals(it, summary)
}
assertEquals(keyPref, key)
assertEquals(titlePref, title)
assertEquals(visible, isVisible)
enabled?.let {
assertEquals(enabled, isEnabled)
}
}
@@ -0,0 +1,43 @@
/**
* qsfera Android client application
*
* @author Abel García de Prada
*
* Copyright (C) 2020 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
* <p>
* 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.qsfera.android.utils.matchers
import android.view.View
import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.test.espresso.matcher.BoundedMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher
fun withTextColor(
@ColorRes textColor: Int
): Matcher<View> =
object : BoundedMatcher<View, TextView>(TextView::class.java) {
override fun describeTo(description: Description) {
description.appendText("TextView with text color: $textColor")
}
override fun matchesSafely(view: TextView): Boolean {
val expectedColor = ContextCompat.getColor(view.context, textColor)
val actualColor = view.currentTextColor
return actualColor == expectedColor
}
}