diff --git a/app/src/androidTest/java/com/nextcloud/client/ActivitiesFragmentIT.kt b/app/src/androidTest/java/com/nextcloud/client/ActivitiesFragmentIT.kt index 737f77c10169..ea7082a35810 100644 --- a/app/src/androidTest/java/com/nextcloud/client/ActivitiesFragmentIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/ActivitiesFragmentIT.kt @@ -99,7 +99,7 @@ class ActivitiesFragmentIT : AbstractIT() { fun showActivities() { val capability = OCCapability() capability.versionMayor = 20 - fileDataStorageManager.saveCapabilities(capability) + storageManager.saveCapabilities(capability) val date = GregorianCalendar() date.set(2005, 4, 17, 10, 35, 30) diff --git a/app/src/androidTest/java/com/nextcloud/client/SettingsActivityIT.kt b/app/src/androidTest/java/com/nextcloud/client/SettingsActivityIT.kt index 47b2f227648c..3f9ee4d48dff 100644 --- a/app/src/androidTest/java/com/nextcloud/client/SettingsActivityIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/SettingsActivityIT.kt @@ -14,7 +14,6 @@ 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.isRoot -import com.nextcloud.test.GrantStoragePermissionRule.Companion.grant import com.owncloud.android.AbstractIT import com.owncloud.android.datamodel.ArbitraryDataProviderImpl import com.owncloud.android.ui.activity.RequestCredentialsActivity @@ -22,17 +21,12 @@ import com.owncloud.android.ui.activity.SettingsActivity import com.owncloud.android.utils.EncryptionUtils import com.owncloud.android.utils.ScreenshotTest import org.junit.Assert -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule @Suppress("FunctionNaming") class SettingsActivityIT : AbstractIT() { private val testClassName = "com.nextcloud.client.SettingsActivityIT" - @get:Rule - var storagePermissionRule: TestRule = grant() - @Test @ScreenshotTest fun open() { diff --git a/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt b/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt index 2effe006f166..74014e79c4c4 100644 --- a/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt +++ b/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt @@ -25,14 +25,14 @@ class AssistantRepositoryTests : AbstractOnServerIT() { @Before fun setup() { - sut = AssistantRemoteRepositoryImpl(nextcloudClient, capability) + sut = AssistantRemoteRepositoryImpl(nextcloudClient, getCapability()) } @Test fun testGetTaskTypes() { testOnlyOnServer(NextcloudVersion.nextcloud_28) - if (capability.assistant.isFalse) { + if (getCapability().assistant.isFalse) { return } @@ -46,7 +46,7 @@ class AssistantRepositoryTests : AbstractOnServerIT() { fun testGetTaskList() { testOnlyOnServer(NextcloudVersion.nextcloud_28) - if (capability.assistant.isFalse) { + if (getCapability().assistant.isFalse) { return } @@ -60,7 +60,7 @@ class AssistantRepositoryTests : AbstractOnServerIT() { fun testCreateTask() { testOnlyOnServer(NextcloudVersion.nextcloud_28) - if (capability.assistant.isFalse) { + if (getCapability().assistant.isFalse) { return } @@ -83,7 +83,7 @@ class AssistantRepositoryTests : AbstractOnServerIT() { fun testDeleteTask() { testOnlyOnServer(NextcloudVersion.nextcloud_28) - if (capability.assistant.isFalse) { + if (getCapability().assistant.isFalse) { return } diff --git a/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt b/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt index 79197a12a034..fec6756ccf06 100644 --- a/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt @@ -101,7 +101,7 @@ class ContactsBackupIT : AbstractOnServerIT() { fail("folderPath cannot be empty") } - val folder = fileDataStorageManager.getFileByDecryptedRemotePath(folderPath) + val folder = storageManager.getFileByDecryptedRemotePath(folderPath) if (folder == null) { fail("folder cannot be null") } diff --git a/app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt b/app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt index e7dba3840397..eed81971fe52 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt @@ -21,7 +21,7 @@ import org.junit.Test @SinceServer(majorVersion = 30) class AutoRenameTests : AbstractOnServerIT() { - private var capability: OCCapability = fileDataStorageManager.getCapability(account.name) + private var capability: OCCapability = storageManager.getCapability(account.name) private val forbiddenFilenameExtension = "." private val forbiddenFilenameCharacter = ">" diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt index 8350bac7ee8c..83ae3e9110c6 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt @@ -26,7 +26,7 @@ class FileNameValidatorTests : AbstractOnServerIT() { private const val MAX_FILENAME_SIZE = 250 } - private var capability: OCCapability = fileDataStorageManager.getCapability(account.name) + private var capability: OCCapability = storageManager.getCapability(account.name) @Before fun setup() { diff --git a/app/src/androidTest/java/com/nextcloud/utils/OCCapabilityJsonToListTests.kt b/app/src/androidTest/java/com/nextcloud/utils/OCCapabilityJsonToListTests.kt index c8b94a3dbbd3..e41bbf5aee9f 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/OCCapabilityJsonToListTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/OCCapabilityJsonToListTests.kt @@ -19,7 +19,7 @@ import org.junit.Test @Suppress("MagicNumber") class OCCapabilityJsonToListTests : AbstractIT() { - private var capability: OCCapability = fileDataStorageManager.getCapability(account.name) + private var capability: OCCapability = storageManager.getCapability(account.name) // region Valid Input @Test diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java deleted file mode 100644 index 0640a8cee639..000000000000 --- a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java +++ /dev/null @@ -1,511 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2018 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.app.Activity; -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.os.Build; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.View; - -import com.facebook.testing.screenshot.Screenshot; -import com.facebook.testing.screenshot.internal.TestNameDetector; -import com.nextcloud.android.common.ui.theme.MaterialSchemes; -import com.nextcloud.android.common.ui.theme.MaterialSchemesImpl; -import com.nextcloud.client.account.User; -import com.nextcloud.client.account.UserAccountManager; -import com.nextcloud.client.account.UserAccountManagerImpl; -import com.nextcloud.utils.PowerManagementFactory; -import com.nextcloud.client.jobs.upload.FileUploadWorker; -import com.nextcloud.client.network.ConnectivityManagerFactory; -import com.nextcloud.client.network.ConnectivityService; -import com.nextcloud.client.preferences.AppPreferencesImpl; -import com.nextcloud.client.preferences.DarkMode; -import com.nextcloud.common.NextcloudClient; -import com.nextcloud.test.GrantStoragePermissionRule; -import com.nextcloud.test.RandomStringGenerator; -import com.owncloud.android.datamodel.ArbitraryDataProvider; -import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.UploadsStorageManager; -import com.owncloud.android.db.OCUpload; -import com.owncloud.android.files.services.NameCollisionPolicy; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.OwnCloudClientFactory; -import com.owncloud.android.lib.common.accounts.AccountUtils; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation; -import com.owncloud.android.lib.resources.status.CapabilityBooleanType; -import com.owncloud.android.lib.resources.status.GetCapabilitiesRemoteOperation; -import com.owncloud.android.lib.resources.status.OCCapability; -import com.owncloud.android.lib.resources.status.OwnCloudVersion; -import com.owncloud.android.operations.CreateFolderOperation; -import com.owncloud.android.operations.UploadFileOperation; -import com.owncloud.android.utils.FileStorageUtils; - -import org.apache.commons.io.FileUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.rules.TestRule; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Locale; -import java.util.Objects; -import java.util.Optional; - -import androidx.fragment.app.DialogFragment; -import androidx.test.espresso.contrib.DrawerActions; -import androidx.test.espresso.intent.rule.IntentsTestRule; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; -import androidx.test.runner.lifecycle.Stage; - -import static androidx.test.InstrumentationRegistry.getInstrumentation; -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID; -import static org.junit.Assert.assertTrue; -import static org.junit.Assume.assumeTrue; - -/** - * Common base for all integration tests. - */ -public abstract class AbstractIT { - @Rule - public final TestRule storagePermissionRule = GrantStoragePermissionRule.grant(); - - protected ConnectivityService connectivityServiceMock = ConnectivityManagerFactory.INSTANCE.getMock(); - - protected static OwnCloudClient client; - protected static NextcloudClient nextcloudClient; - protected static Account account; - protected static User user; - protected static Context targetContext; - protected static String DARK_MODE = ""; - protected static String COLOR = ""; - - protected Activity currentActivity; - - protected FileDataStorageManager fileDataStorageManager = - new FileDataStorageManager(user, targetContext.getContentResolver()); - - protected ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext); - - @BeforeClass - public static void beforeAll() { - try { - // clean up - targetContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - AccountManager platformAccountManager = AccountManager.get(targetContext); - - for (Account account : platformAccountManager.getAccounts()) { - if (account.type.equalsIgnoreCase(MainApp.getAccountType(targetContext))) { - platformAccountManager.removeAccountExplicitly(account); - } - } - - account = createAccount("test@https://nextcloud.localhost"); - user = getUser(account); - - client = OwnCloudClientFactory.createOwnCloudClient(account, targetContext); - nextcloudClient = OwnCloudClientFactory.createNextcloudClient(user, targetContext); - } catch (OperationCanceledException | - IOException | - AccountUtils.AccountNotFoundException | - AuthenticatorException e) { - throw new RuntimeException("Error setting up clients", e); - } - - Bundle arguments = androidx.test.platform.app.InstrumentationRegistry.getArguments(); - - // color - String colorParameter = arguments.getString("COLOR"); - if (!TextUtils.isEmpty(colorParameter)) { - FileDataStorageManager fileDataStorageManager = new FileDataStorageManager(user, - targetContext.getContentResolver()); - - String colorHex = null; - COLOR = colorParameter; - switch (colorParameter) { - case "red": - colorHex = "#7c0000"; - break; - - case "green": - colorHex = "#00ff00"; - break; - - case "white": - colorHex = "#ffffff"; - break; - - case "black": - colorHex = "#000000"; - break; - - case "lightgreen": - colorHex = "#aaff00"; - break; - - default: - break; - } - - OCCapability capability = fileDataStorageManager.getCapability(account.name); - capability.setGroupfolders(CapabilityBooleanType.TRUE); - - if (colorHex != null) { - capability.setServerColor(colorHex); - } - - fileDataStorageManager.saveCapabilities(capability); - } - - // dark / light - String darkModeParameter = arguments.getString("DARKMODE"); - - if (darkModeParameter != null) { - if ("dark".equalsIgnoreCase(darkModeParameter)) { - DARK_MODE = "dark"; - AppPreferencesImpl.fromContext(targetContext).setDarkThemeMode(DarkMode.DARK); - MainApp.setAppTheme(DarkMode.DARK); - } else { - DARK_MODE = "light"; - } - } - - if ("light".equalsIgnoreCase(DARK_MODE) && "blue".equalsIgnoreCase(COLOR)) { - // use already existing names - DARK_MODE = ""; - COLOR = ""; - } - } - - protected void testOnlyOnServer(OwnCloudVersion version) throws AccountUtils.AccountNotFoundException { - OCCapability ocCapability = getCapability(); - assumeTrue(ocCapability.getVersion().isNewerOrEqual(version)); - } - - protected OCCapability getCapability() throws AccountUtils.AccountNotFoundException { - NextcloudClient client = OwnCloudClientFactory.createNextcloudClient(user, targetContext); - - return new GetCapabilitiesRemoteOperation().execute(client).getResultData(); - } - - @Before - public void enableAccessibilityChecks() { - androidx.test.espresso.accessibility.AccessibilityChecks.enable().setRunChecksFromRootView(true); - } - - @After - public void after() { - fileDataStorageManager.removeLocalFiles(user, fileDataStorageManager); - fileDataStorageManager.deleteAllFiles(); - } - - protected FileDataStorageManager getStorageManager() { - return fileDataStorageManager; - } - - protected Account[] getAllAccounts() { - return AccountManager.get(targetContext).getAccounts(); - } - - protected static List createDummyFiles() throws IOException { - File tempPath = new File(FileStorageUtils.getTemporalPath(account.name)); - if (!tempPath.exists()) { - assertTrue(tempPath.mkdirs()); - } - - assertTrue(tempPath.exists()); - - return Arrays.asList( - createFile("empty.txt", 0), - createFile("nonEmpty.txt", 100), - createFile("chunkedFile.txt", 500000) - ); - } - - protected static File getDummyFile(String name) throws IOException { - File file = new File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + File.separator + name); - - if (file.exists()) { - return file; - } else if (name.endsWith("/")) { - file.mkdirs(); - return file; - } else { - return switch (name) { - case "empty.txt" -> createFile("empty.txt", 0); - case "nonEmpty.txt" -> createFile("nonEmpty.txt", 100); - case "chunkedFile.txt" -> createFile("chunkedFile.txt", 500000); - default -> createFile(name, 0); - }; - } - } - - public static File createFile(String name, int iteration) throws IOException { - File file = new File(FileStorageUtils.getTemporalPath(account.name) + File.separator + name); - if (!file.getParentFile().exists()) { - assertTrue(file.getParentFile().mkdirs()); - } - - file.createNewFile(); - - FileWriter writer = new FileWriter(file); - - for (int i = 0; i < iteration; i++) { - writer.write("123123123123123123123123123\n"); - } - writer.flush(); - writer.close(); - - return file; - } - - protected File getFile(String filename) throws IOException { - InputStream inputStream = getInstrumentation().getContext().getAssets().open(filename); - File temp = File.createTempFile("file", "file"); - FileUtils.copyInputStreamToFile(inputStream, temp); - - return temp; - } - - protected void waitForIdleSync() { - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - } - - public void onIdleSync(Runnable recipient) { - InstrumentationRegistry.getInstrumentation().waitForIdle(recipient); - } - - protected void openDrawer(IntentsTestRule activityRule) { - Activity sut = activityRule.launchActivity(null); - - shortSleep(); - - onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()); - - waitForIdleSync(); - - screenshot(sut); - } - - protected Activity getCurrentActivity() { - InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { - Collection resumedActivities = ActivityLifecycleMonitorRegistry - .getInstance() - .getActivitiesInStage(Stage.RESUMED); - - if (resumedActivities.iterator().hasNext()) { - currentActivity = resumedActivities.iterator().next(); - } - }); - - return currentActivity; - } - - protected static void shortSleep() { - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - protected void longSleep() { - try { - Thread.sleep(20000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - protected void sleep(int second) { - try { - Thread.sleep(1000L * second); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - public void createFolder(String remotePath) { - RemoteOperationResult check = new ExistenceCheckRemoteOperation(remotePath, false).execute(client); - - if (!check.isSuccess()) { - assertTrue(new CreateFolderOperation(remotePath, user, targetContext, getStorageManager()) - .execute(client) - .isSuccess()); - } - } - - public void uploadFile(File file, String remotePath) { - OCUpload ocUpload = new OCUpload(file.getAbsolutePath(), remotePath, account.name); - - uploadOCUpload(ocUpload); - } - - public void uploadOCUpload(OCUpload ocUpload) { - final var powerManagementServiceMock = PowerManagementFactory.getMock(); - UserAccountManager accountManager = UserAccountManagerImpl.fromContext(targetContext); - UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(accountManager, - targetContext.getContentResolver()); - - UploadFileOperation newUpload = new UploadFileOperation( - uploadsStorageManager, - connectivityServiceMock, - powerManagementServiceMock, - user, - null, - ocUpload, - NameCollisionPolicy.DEFAULT, - FileUploadWorker.LOCAL_BEHAVIOUR_COPY, - targetContext, - false, - false, - getStorageManager() - ); - newUpload.addRenameUploadListener(() -> { - // dummy - }); - - newUpload.setRemoteFolderToBeCreated(); - - var result = newUpload.execute(client); - assertTrue(result.getLogMessage(), result.isSuccess()); - } - - protected void enableRTL() { - Locale locale = new Locale("ar"); - Resources resources = InstrumentationRegistry.getInstrumentation().getTargetContext().getResources(); - Configuration config = resources.getConfiguration(); - config.setLocale(locale); - resources.updateConfiguration(config, null); - } - - protected void resetLocale() { - Locale locale = new Locale("en"); - Resources resources = InstrumentationRegistry.getInstrumentation().getTargetContext().getResources(); - Configuration config = resources.getConfiguration(); - config.setLocale(locale); - resources.updateConfiguration(config, null); - } - - protected void screenshot(View view) { - screenshot(view, ""); - } - - public void screenshotViaName(Activity activity, String name) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - Screenshot.snapActivity(activity).setName(name).record(); - } - } - - protected void screenshotViaName(View view, String name) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - Screenshot.snap(view).setName(name).record(); - } - } - - protected void screenshot(View view, String prefix) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - Screenshot.snap(view).setName(createName(prefix)).record(); - } - } - - protected void screenshot(Activity sut) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - Screenshot.snapActivity(sut).setName(createName()).record(); - } - } - - protected void screenshot(DialogFragment dialogFragment, String prefix) { - screenshot(Objects.requireNonNull(dialogFragment.requireDialog().getWindow()).getDecorView(), prefix); - } - - private String createName() { - return createName(""); - } - - public String createName(String name, String prefix) { - if (!TextUtils.isEmpty(prefix)) { - name = name + "_" + prefix; - } - - if (!DARK_MODE.isEmpty()) { - name = name + "_" + DARK_MODE; - } - - if (!COLOR.isEmpty()) { - name = name + "_" + COLOR; - } - - return name; - } - - private String createName(String prefix) { - String name = TestNameDetector.getTestClass() + "_" + TestNameDetector.getTestName(); - return createName(name, prefix); - } - - public static String getUserId(User user) { - return AccountManager.get(targetContext).getUserData(user.toPlatformAccount(), KEY_USER_ID); - } - - public String getRandomName() { - return getRandomName(5); - } - - public String getRandomName(int length) { - return RandomStringGenerator.make(length); - } - - protected static User getUser(Account account) { - Optional optionalUser = UserAccountManagerImpl.fromContext(targetContext).getUser(account.name); - return optionalUser.orElseThrow(IllegalAccessError::new); - } - - protected static Account createAccount(String name) { - AccountManager platformAccountManager = AccountManager.get(targetContext); - - Account temp = new Account(name, MainApp.getAccountType(targetContext)); - int atPos = name.lastIndexOf('@'); - platformAccountManager.addAccountExplicitly(temp, "password", null); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_BASE_URL, - name.substring(atPos + 1)); - platformAccountManager.setUserData(temp, KEY_USER_ID, name.substring(0, atPos)); - - Account account = UserAccountManagerImpl.fromContext(targetContext).getAccountByName(name); - if (Objects.equals(account.type, targetContext.getString(R.string.anonymous_account_type))) { - throw new RuntimeException("Could not get account with name " + name); - } - return account; - } - - protected static boolean removeAccount(Account account) { - return AccountManager.get(targetContext).removeAccountExplicitly(account); - } - - protected MaterialSchemes getMaterialSchemesForCurrentUser() { - return new MaterialSchemesImpl(R.color.primary, false); - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractIT.kt b/app/src/androidTest/java/com/owncloud/android/AbstractIT.kt new file mode 100644 index 000000000000..661d96d7db03 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/AbstractIT.kt @@ -0,0 +1,421 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android + +import android.accounts.Account +import android.accounts.AccountManager +import android.accounts.AuthenticatorException +import android.accounts.OperationCanceledException +import android.app.Activity +import android.app.Instrumentation +import android.content.Context +import android.os.Build +import android.view.View +import androidx.fragment.app.DialogFragment +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.accessibility.AccessibilityChecks +import androidx.test.espresso.contrib.DrawerActions +import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry +import androidx.test.runner.lifecycle.Stage +import com.facebook.testing.screenshot.RecordBuilder +import com.facebook.testing.screenshot.Screenshot +import com.facebook.testing.screenshot.internal.TestNameDetector +import com.nextcloud.android.common.ui.theme.MaterialSchemes +import com.nextcloud.client.account.User +import com.nextcloud.client.account.UserAccountManagerImpl +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.client.network.ConnectivityManagerFactory +import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.client.preferences.AppPreferencesImpl +import com.nextcloud.client.preferences.DarkMode +import com.nextcloud.common.NextcloudClient +import com.nextcloud.test.GrantStoragePermissionRule +import com.nextcloud.utils.PowerManagementFactory +import com.owncloud.android.datamodel.ArbitraryDataProvider +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.db.OCUpload +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.OwnCloudClientFactory +import com.owncloud.android.lib.common.accounts.AccountUtils +import com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_BASE_URL +import com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID +import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation +import com.owncloud.android.lib.resources.status.CapabilityBooleanType +import com.owncloud.android.lib.resources.status.GetCapabilitiesRemoteOperation +import com.owncloud.android.lib.resources.status.OCCapability +import com.owncloud.android.lib.resources.status.OwnCloudVersion +import com.owncloud.android.operations.CreateFolderOperation +import com.owncloud.android.operations.UploadFileOperation +import com.owncloud.android.utils.FileStorageUtils +import org.apache.commons.io.FileUtils +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Rule +import org.junit.rules.TestRule +import java.io.File +import java.io.IOException +import java.util.Locale + +@Suppress("DEPRECATION") +abstract class AbstractIT { + @get:Rule + val storagePermissionRule: TestRule = GrantStoragePermissionRule.grant() + + protected val connectivityServiceMock: ConnectivityService = ConnectivityManagerFactory.mock + + private var currentActivity: Activity? = null + + protected val storageManager = FileDataStorageManager(user, targetContext.contentResolver) + + protected val arbitraryDataProvider: ArbitraryDataProvider = ArbitraryDataProviderImpl(targetContext) + + protected val materialSchemesForCurrentUser: MaterialSchemes + get() = MaterialSchemes.fromColor(R.color.primary) + + @Before + fun enableAccessibilityChecks() { + AccessibilityChecks.enable().setRunChecksFromRootView(true) + } + + @After + open fun after() { + storageManager.removeLocalFiles(user, storageManager) + storageManager.deleteAllFiles() + } + + @Throws(AccountUtils.AccountNotFoundException::class) + protected fun testOnlyOnServer(version: OwnCloudVersion) { + assumeTrue(getCapability().version.isNewerOrEqual(version)) + } + + @Throws(AccountUtils.AccountNotFoundException::class) + protected fun getCapability(): OCCapability { + val client = OwnCloudClientFactory.createNextcloudClient(user, targetContext) + return GetCapabilitiesRemoteOperation().execute(client).resultData + } + + @Throws(IOException::class) + protected fun getFile(filename: String): File = instrumentation.context.assets.open(filename).use { asset -> + File.createTempFile("file", "file").also { FileUtils.copyInputStreamToFile(asset, it) } + } + + protected fun waitForIdleSync() { + instrumentation.waitForIdleSync() + } + + fun onIdleSync(recipient: Runnable) { + instrumentation.waitForIdle(recipient) + } + + protected fun openDrawer(activityRule: IntentsTestRule<*>) { + val sut = activityRule.launchActivity(null) + shortSleep() + onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()) + waitForIdleSync() + screenshot(sut) + } + + protected fun getCurrentActivity(): Activity? { + instrumentation.runOnMainSync { + ActivityLifecycleMonitorRegistry.getInstance() + .getActivitiesInStage(Stage.RESUMED) + .firstOrNull() + ?.let { currentActivity = it } + } + return currentActivity + } + + protected fun longSleep() = sleepIgnoringInterrupt(LONG_SLEEP_MILLIS) + + fun createFolder(remotePath: String) { + if (ExistenceCheckRemoteOperation(remotePath, false).execute(client).isSuccess) return + + val result = CreateFolderOperation(remotePath, user, targetContext, storageManager).execute(client) + assertTrue(result.isSuccess) + } + + fun uploadFile(file: File, remotePath: String) { + uploadOCUpload(OCUpload(file.absolutePath, remotePath, account.name)) + } + + open fun uploadOCUpload(ocUpload: OCUpload) { + executeUpload(ocUpload, NameCollisionPolicy.DEFAULT, FileUploadWorker.LOCAL_BEHAVIOUR_COPY) + } + + protected fun executeUpload(ocUpload: OCUpload, nameCollisionPolicy: NameCollisionPolicy, localBehaviour: Int) { + val uploadsStorageManager = UploadsStorageManager( + UserAccountManagerImpl.fromContext(targetContext), + targetContext.contentResolver + ) + + val existingFile: OCFile? = null + val onWifiOnly = false + val whileChargingOnly = false + + val operation = UploadFileOperation( + uploadsStorageManager, + connectivityServiceMock, + PowerManagementFactory.mock, + user, + existingFile, + ocUpload, + nameCollisionPolicy, + localBehaviour, + targetContext, + onWifiOnly, + whileChargingOnly, + storageManager + ).apply { + addRenameUploadListener { } + setRemoteFolderToBeCreated() + } + + val result = operation.execute(client) + assertTrue(result.logMessage, result.isSuccess) + } + + protected fun enableRTL() = applyLocale(Locale.forLanguageTag("ar")) + + protected fun resetLocale() = applyLocale(Locale.forLanguageTag("en")) + + @Suppress("DEPRECATION") + private fun applyLocale(locale: Locale) { + val resources = instrumentation.targetContext.resources + val config = resources.configuration.apply { setLocale(locale) } + resources.updateConfiguration(config, null) + } + + fun screenshotViaName(activity: Activity?, name: String) = + recordScreenshot { Screenshot.snapActivity(activity).setName(name) } + + protected fun screenshotViaName(view: View?, name: String) = recordScreenshot { + Screenshot.snap(view).setName(name) + } + + @JvmOverloads + protected fun screenshot(view: View?, prefix: String = "") = + recordScreenshot { Screenshot.snap(view).setName(createName(prefix)) } + + protected fun screenshot(sut: Activity?) = recordScreenshot { Screenshot.snapActivity(sut).setName(createName()) } + + protected fun screenshot(dialogFragment: DialogFragment, prefix: String) = + screenshot(requireNotNull(dialogFragment.requireDialog().window).decorView, prefix) + + private inline fun recordScreenshot(snap: () -> RecordBuilder) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) return + snap().record() + } + + fun createName(name: String, prefix: String): String { + val suffixes = listOf(prefix, darkMode, color).filter { it.isNotEmpty() } + return (listOf(name) + suffixes).joinToString("_") + } + + private fun createName(prefix: String = ""): String = + createName("${TestNameDetector.getTestClass()}_${TestNameDetector.getTestName()}", prefix) + + companion object { + private const val TEST_ACCOUNT_NAME = "test@https://nextcloud.localhost" + private const val TEST_ACCOUNT_PASSWORD = "password" + private const val DUMMY_FILE_LINE = "123123123123123123123123123\n" + private const val SHORT_SLEEP_MILLIS = 2_000L + private const val LONG_SLEEP_MILLIS = 20_000L + private const val DARK = "dark" + private const val LIGHT = "light" + private const val DEFAULT_COLOR = "blue" + + private val DUMMY_FILE_LINE_COUNTS = linkedMapOf( + "empty.txt" to 0, + "nonEmpty.txt" to 100, + "chunkedFile.txt" to 500_000 + ) + + private val SERVER_COLORS = mapOf( + "red" to "#7c0000", + "green" to "#00ff00", + "white" to "#ffffff", + "black" to "#000000", + "lightgreen" to "#aaff00" + ) + + private val CLIENT_SETUP_FAILURES = listOf( + OperationCanceledException::class.java, + IOException::class.java, + AccountUtils.AccountNotFoundException::class.java, + AuthenticatorException::class.java + ) + + private val instrumentation: Instrumentation + get() = InstrumentationRegistry.getInstrumentation() + + @JvmStatic + protected lateinit var client: OwnCloudClient + + @JvmStatic + protected lateinit var nextcloudClient: NextcloudClient + + @JvmStatic + protected lateinit var account: Account + + @JvmStatic + protected lateinit var user: User + + @JvmStatic + protected lateinit var targetContext: Context + + private var darkMode = "" + private var color = "" + + @BeforeClass + @JvmStatic + fun beforeAll() { + targetContext = instrumentation.targetContext + removeAccountsOfType(MainApp.getAccountType(targetContext)) + + rethrowClientSetupFailure { + account = createAccount(TEST_ACCOUNT_NAME) + user = getUser(account) + client = OwnCloudClientFactory.createOwnCloudClient(account, targetContext) + nextcloudClient = OwnCloudClientFactory.createNextcloudClient(user, targetContext) + } + + val arguments = InstrumentationRegistry.getArguments() + arguments.getString("COLOR")?.takeIf { it.isNotEmpty() }?.let(::applyServerColor) + arguments.getString("DARKMODE")?.let(::applyDarkMode) + + if (LIGHT.equals(darkMode, ignoreCase = true) && DEFAULT_COLOR.equals(color, ignoreCase = true)) { + darkMode = "" + color = "" + } + } + + private fun applyServerColor(colorArgument: String) { + color = colorArgument + val fileDataStorageManager = FileDataStorageManager(user, targetContext.contentResolver) + val capability = fileDataStorageManager.getCapability(account.name).apply { + groupfolders = CapabilityBooleanType.TRUE + SERVER_COLORS[colorArgument]?.let { serverColor = it } + } + fileDataStorageManager.saveCapabilities(capability) + } + + private fun applyDarkMode(darkModeArgument: String) { + if (!DARK.equals(darkModeArgument, ignoreCase = true)) { + darkMode = LIGHT + return + } + + darkMode = DARK + AppPreferencesImpl.fromContext(targetContext).setDarkThemeMode(DarkMode.DARK) + MainApp.setAppTheme(DarkMode.DARK) + } + + @JvmStatic + protected fun rethrowClientSetupFailure(setup: () -> T): T = try { + setup() + } catch (e: Exception) { + if (CLIENT_SETUP_FAILURES.none { it.isInstance(e) }) throw e + throw RuntimeException("Error setting up clients", e) + } + + @JvmStatic + protected fun removeAccountsOfType(accountType: String) = with(AccountManager.get(targetContext)) { + accounts + .filter { it.type.equals(accountType, ignoreCase = true) } + .forEach(this::removeAccountExplicitly) + } + + @JvmStatic + @Throws(IOException::class) + protected fun createDummyFiles(): List { + val tempPath = File(FileStorageUtils.getTemporalPath(account.name)) + if (!tempPath.exists()) assertTrue(tempPath.mkdirs()) + assertTrue(tempPath.exists()) + + return DUMMY_FILE_LINE_COUNTS.map { (name, lineCount) -> createFile(name, lineCount) } + } + + @JvmStatic + @Throws(IOException::class) + protected fun getDummyFile(name: String): File { + val file = File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext), name) + return when { + file.exists() -> file + name.endsWith("/") -> file.apply { mkdirs() } + else -> createFile(name, DUMMY_FILE_LINE_COUNTS.getOrElse(name) { 0 }) + } + } + + @JvmStatic + @Throws(IOException::class) + fun createFile(name: String, iteration: Int): File { + val file = File(FileStorageUtils.getTemporalPath(account.name), name) + val parent = requireNotNull(file.parentFile) + if (!parent.exists()) assertTrue(parent.mkdirs()) + + file.bufferedWriter().use { writer -> repeat(iteration) { writer.write(DUMMY_FILE_LINE) } } + return file + } + + @JvmStatic + protected fun shortSleep() = sleepIgnoringInterrupt(SHORT_SLEEP_MILLIS) + + @JvmStatic + protected fun sleepIgnoringInterrupt(millis: Long) { + try { + Thread.sleep(millis) + } catch (e: InterruptedException) { + e.printStackTrace() + } + } + + @JvmStatic + fun getUserId(user: User): String = requireNotNull( + AccountManager.get(targetContext).getUserData(user.toPlatformAccount(), KEY_USER_ID) + ) { "No user id stored for ${user.accountName}" } + + @JvmStatic + protected fun getUser(account: Account): User = + UserAccountManagerImpl.fromContext(targetContext).getUser(account.name).orElseThrow { IllegalAccessError() } + + @JvmStatic + protected fun createAccount(name: String): Account { + val platformAccount = Account(name, MainApp.getAccountType(targetContext)) + + AccountManager.get(targetContext).run { + addAccountExplicitly(platformAccount, TEST_ACCOUNT_PASSWORD, null) + setUserData(platformAccount, KEY_OC_BASE_URL, name.substringAfterLast('@')) + setUserData(platformAccount, KEY_USER_ID, name.substringBeforeLast('@')) + } + + return getRegisteredAccount(name) + } + + @JvmStatic + protected fun getRegisteredAccount(name: String): Account { + val account = UserAccountManagerImpl.fromContext(targetContext).getAccountByName(name) + if (account.type == targetContext.getString(R.string.anonymous_account_type)) { + throw RuntimeException("Could not get account with name $name") + } + return account + } + + @JvmStatic + protected fun removeAccount(account: Account): Boolean = + AccountManager.get(targetContext).removeAccountExplicitly(account) + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java deleted file mode 100644 index e7e1e2b05bf2..000000000000 --- a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.net.Uri; -import android.os.Bundle; - -import com.nextcloud.client.account.User; -import com.nextcloud.client.account.UserAccountManager; -import com.nextcloud.client.account.UserAccountManagerImpl; -import com.nextcloud.utils.PowerManagementFactory; -import com.nextcloud.client.jobs.upload.FileUploadWorker; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.datamodel.UploadsStorageManager; -import com.owncloud.android.db.OCUpload; -import com.owncloud.android.files.services.NameCollisionPolicy; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.OwnCloudClientFactory; -import com.owncloud.android.lib.common.accounts.AccountUtils; -import com.owncloud.android.lib.resources.e2ee.ToggleEncryptionRemoteOperation; -import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation; -import com.owncloud.android.lib.resources.files.RemoveFileRemoteOperation; -import com.owncloud.android.lib.resources.files.model.RemoteFile; -import com.owncloud.android.operations.RefreshFolderOperation; -import com.owncloud.android.operations.UploadFileOperation; -import com.owncloud.android.utils.MimeType; - -import org.apache.commons.httpclient.HttpStatus; -import org.apache.commons.httpclient.methods.GetMethod; -import org.junit.After; -import org.junit.Assert; -import org.junit.BeforeClass; - -import java.io.File; -import java.io.IOException; -import java.util.Objects; -import java.util.Optional; - -import androidx.test.platform.app.InstrumentationRegistry; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/// Common base for all integration tests requiring a server connection. -/// ATTENTION: Deletes ALL files of the test user on the server after each test run. -/// So you MUST use a dedicated test user. -/// Uses server, user and password given as `testInstrumentationRunnerArgument` -/// - TEST_SERVER_URL -/// - TEST_SERVER_USERNAME -/// - TEST_SERVER_PASSWORD -/// These are supplied via build.gradle, which takes them from gradle.properties. -/// So look in the latter file to set to your own server & test user. -public abstract class AbstractOnServerIT extends AbstractIT { - @BeforeClass - public static void beforeAll() { - try { - // clean up - targetContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - AccountManager platformAccountManager = AccountManager.get(targetContext); - - for (Account account : platformAccountManager.getAccounts()) { - if (account.type.equalsIgnoreCase("nextcloud")) { - platformAccountManager.removeAccountExplicitly(account); - } - } - - Bundle arguments = androidx.test.platform.app.InstrumentationRegistry.getArguments(); - - Uri baseUrl = Uri.parse(arguments.getString("TEST_SERVER_URL")); - String loginName = arguments.getString("TEST_SERVER_USERNAME"); - String password = arguments.getString("TEST_SERVER_PASSWORD"); - - Account temp = new Account(loginName + "@" + baseUrl, MainApp.getAccountType(targetContext)); - UserAccountManager accountManager = UserAccountManagerImpl.fromContext(targetContext); - if (!accountManager.exists(temp)) { - platformAccountManager.addAccountExplicitly(temp, password, null); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION, - Integer.toString(UserAccountManager.ACCOUNT_VERSION)); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_VERSION, "14.0.0.0"); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_BASE_URL, baseUrl.toString()); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_USER_ID, loginName); // same as userId - } - - final UserAccountManager userAccountManager = UserAccountManagerImpl.fromContext(targetContext); - account = userAccountManager.getAccountByName(loginName + "@" + baseUrl); - - if (Objects.equals(account.type, targetContext.getString(R.string.anonymous_account_type))) { - throw new RuntimeException("Could not get account with name " + loginName + "@" + baseUrl); - } - - Optional optionalUser = userAccountManager.getUser(account.name); - user = optionalUser.orElseThrow(IllegalAccessError::new); - - client = OwnCloudClientFactory.createOwnCloudClient(account, targetContext); - nextcloudClient = OwnCloudClientFactory.createNextcloudClient(user, targetContext); - - createDummyFiles(); - - waitForServer(client, baseUrl); - - // deleteAllFilesOnServer(); // makes sure that no file/folder is in root - - } catch (OperationCanceledException | - IOException | - AccountUtils.AccountNotFoundException | - AuthenticatorException e) { - throw new RuntimeException("Error setting up clients", e); - } - } - - @After - public void after() { - deleteAllFilesOnServer(); - - super.after(); - } - - private static boolean isFolder(RemoteFile file) { - // TODO: should probably move to RemoteFile class - return MimeType.DIRECTORY.equals(file.getMimeType()) || MimeType.WEBDAV_FOLDER.equals(file.getMimeType()); - } - - public static void deleteAllFilesOnServer() { - var result = new ReadFolderRemoteOperation("/").execute(client); - assertTrue(result.getLogMessage(targetContext), result.isSuccess()); - - for (Object object : result.getData()) { - RemoteFile remoteFile = (RemoteFile) object; - - if (!Objects.equals(remoteFile.getRemotePath(), "/")) { - if (remoteFile.isEncrypted()) { - ToggleEncryptionRemoteOperation operation = new ToggleEncryptionRemoteOperation(remoteFile.getLocalId(), - remoteFile.getRemotePath(), - false); - - boolean operationResult = operation - .execute(client) - .isSuccess(); - - if (!operationResult && isFolder(remoteFile)) { - // Deleting encrypted folder is not possible due to bug - // https://github.com/nextcloud/end_to_end_encryption/issues/421 - // Toggling encryption also fails, when the folder is not empty. So we ignore this folder - continue; - } - - assertTrue(operationResult); - } - - if (remoteFile.getPermissions().contains("M")) { - // skip Teamfolders - continue; - } - - boolean removeResult = false; - for (int i = 0; i < 5; i++) { - removeResult = new RemoveFileRemoteOperation(remoteFile.getRemotePath()) - .execute(client) - .isSuccess(); - - if (removeResult) { - break; - } - - shortSleep(); - } - - assertTrue("Remove of " + remoteFile.getRemotePath() + " failed", removeResult); - } - } - } - - private static void waitForServer(OwnCloudClient client, Uri baseUrl) { - GetMethod get = new GetMethod(baseUrl + "/status.php"); - - try { - int i = 0; - while (client.executeMethod(get) != HttpStatus.SC_OK && i < 3) { - System.out.println("wait…"); - Thread.sleep(60 * 1000); - i++; - } - - if (i == 3) { - Assert.fail("Server not ready!"); - } - - } catch (IOException | InterruptedException e) { - e.printStackTrace(); - } - } - - public void uploadOCUpload(OCUpload ocUpload) { - uploadOCUpload(ocUpload, FileUploadWorker.LOCAL_BEHAVIOUR_COPY); - } - - public void uploadOCUpload(OCUpload ocUpload, int localBehaviour) { - final var powerManagementServiceMock = PowerManagementFactory.getMock(); - UserAccountManager accountManager = UserAccountManagerImpl.fromContext(targetContext); - UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(accountManager, - targetContext.getContentResolver()); - - UploadFileOperation newUpload = new UploadFileOperation( - uploadsStorageManager, - connectivityServiceMock, - powerManagementServiceMock, - user, - null, - ocUpload, - NameCollisionPolicy.OVERWRITE, - localBehaviour, - targetContext, - false, - false, - getStorageManager() - ); - newUpload.addRenameUploadListener(() -> { - // dummy - }); - - newUpload.setRemoteFolderToBeCreated(); - - var result = newUpload.execute(client); - assertTrue(result.getLogMessage(), result.isSuccess()); - - OCFile parentFolder = getStorageManager() - .getFileByEncryptedRemotePath(new File(ocUpload.getRemotePath()).getParent() + "/"); - String uploadedFileName = new File(ocUpload.getRemotePath()).getName(); - OCFile uploadedFile = getStorageManager(). - getFileByDecryptedRemotePath(parentFolder.getDecryptedRemotePath() + uploadedFileName); - - assertNotNull(uploadedFile); - assertNotNull(uploadedFile.getRemoteId()); - assertNotNull(uploadedFile.getPermissions()); - - if (localBehaviour == FileUploadWorker.LOCAL_BEHAVIOUR_COPY || - localBehaviour == FileUploadWorker.LOCAL_BEHAVIOUR_MOVE) { - assertTrue(new File(uploadedFile.getStoragePath()).exists()); - } - } - - protected void refreshFolder(String path) { - assertTrue(new RefreshFolderOperation(getStorageManager().getFileByEncryptedRemotePath(path), - System.currentTimeMillis(), - false, - false, - getStorageManager(), - user, - targetContext - ).execute(client).isSuccess()); - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.kt b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.kt new file mode 100644 index 000000000000..9ddaa63b2922 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.kt @@ -0,0 +1,203 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android + +import android.accounts.Account +import android.accounts.AccountManager +import android.net.Uri +import android.os.Bundle +import androidx.test.platform.app.InstrumentationRegistry +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.account.UserAccountManagerImpl +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.owncloud.android.db.OCUpload +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.OwnCloudClientFactory +import com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION +import com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_BASE_URL +import com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_VERSION +import com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID +import com.owncloud.android.lib.resources.e2ee.ToggleEncryptionRemoteOperation +import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation +import com.owncloud.android.lib.resources.files.RemoveFileRemoteOperation +import com.owncloud.android.lib.resources.files.model.RemoteFile +import com.owncloud.android.operations.RefreshFolderOperation +import com.owncloud.android.utils.FileStorageUtils +import com.owncloud.android.utils.MimeType +import org.apache.commons.httpclient.HttpStatus +import org.apache.commons.httpclient.methods.GetMethod +import org.junit.After +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.BeforeClass +import java.io.File +import java.io.IOException + +@Suppress("DEPRECATION") +abstract class AbstractOnServerIT : AbstractIT() { + @After + override fun after() { + deleteAllFilesOnServer() + super.after() + } + + override fun uploadOCUpload(ocUpload: OCUpload) { + uploadOCUpload(ocUpload, FileUploadWorker.LOCAL_BEHAVIOUR_COPY) + } + + fun uploadOCUpload(ocUpload: OCUpload, localBehaviour: Int) { + executeUpload(ocUpload, NameCollisionPolicy.OVERWRITE, localBehaviour) + + val remotePath = ocUpload.remotePath + val parentPath = FileStorageUtils.getParentPath(remotePath) + ?: throw AssertionError("$remotePath has no parent folder") + val parentFolder = storageManager.getFileByEncryptedRemotePath(parentPath) + ?: throw AssertionError("Parent folder $parentPath not found") + val uploadedFile = storageManager.getFileByDecryptedRemotePath( + parentFolder.decryptedRemotePath + File(remotePath).name + ) ?: throw AssertionError("Uploaded file $remotePath not found") + + assertNotNull(uploadedFile.remoteId) + assertNotNull(uploadedFile.permissions) + + if (localBehaviour in LOCAL_COPY_BEHAVIOURS) { + assertTrue(File(uploadedFile.storagePath).exists()) + } + } + + protected fun refreshFolder(path: String) { + val operation = RefreshFolderOperation( + storageManager.getFileByEncryptedRemotePath(path), + System.currentTimeMillis(), + false, + false, + storageManager, + user, + targetContext + ) + assertTrue(operation.execute(client).isSuccess) + } + + companion object { + private const val SERVER_ACCOUNT_TYPE = "nextcloud" + private const val SERVER_VERSION = "14.0.0.0" + private const val ROOT_PATH = "/" + private const val TEAM_FOLDER_PERMISSION = "M" + private const val REMOVE_ATTEMPTS = 5 + private const val SERVER_READY_RETRIES = 3 + private const val SERVER_READY_RETRY_DELAY_MILLIS = 60_000L + + private val FOLDER_MIME_TYPES = setOf(MimeType.DIRECTORY, MimeType.WEBDAV_FOLDER) + private val LOCAL_COPY_BEHAVIOURS = setOf( + FileUploadWorker.LOCAL_BEHAVIOUR_COPY, + FileUploadWorker.LOCAL_BEHAVIOUR_MOVE + ) + + @BeforeClass + @JvmStatic + fun beforeAll() { + rethrowClientSetupFailure { + targetContext = InstrumentationRegistry.getInstrumentation().targetContext + removeAccountsOfType(SERVER_ACCOUNT_TYPE) + + val arguments = InstrumentationRegistry.getArguments() + val baseUrl = Uri.parse(arguments.requireString("TEST_SERVER_URL")) + val loginName = arguments.requireString("TEST_SERVER_USERNAME") + val password = arguments.requireString("TEST_SERVER_PASSWORD") + val accountName = "$loginName@$baseUrl" + + addServerAccountIfMissing(accountName, loginName, password, baseUrl) + + account = getRegisteredAccount(accountName) + user = getUser(account) + client = OwnCloudClientFactory.createOwnCloudClient(account, targetContext) + nextcloudClient = OwnCloudClientFactory.createNextcloudClient(user, targetContext) + + createDummyFiles() + + waitForServer(client, baseUrl) + } + } + + private fun addServerAccountIfMissing(accountName: String, loginName: String, password: String, baseUrl: Uri) { + val platformAccount = Account(accountName, MainApp.getAccountType(targetContext)) + if (UserAccountManagerImpl.fromContext(targetContext).exists(platformAccount)) return + + AccountManager.get(targetContext).run { + addAccountExplicitly(platformAccount, password, null) + setUserData(platformAccount, KEY_OC_ACCOUNT_VERSION, UserAccountManager.ACCOUNT_VERSION.toString()) + setUserData(platformAccount, KEY_OC_VERSION, SERVER_VERSION) + setUserData(platformAccount, KEY_OC_BASE_URL, baseUrl.toString()) + setUserData(platformAccount, KEY_USER_ID, loginName) + } + } + + private fun isFolder(file: RemoteFile): Boolean = file.mimeType in FOLDER_MIME_TYPES + + private fun isTeamFolder(file: RemoteFile): Boolean = file.permissions?.contains(TEAM_FOLDER_PERMISSION) == true + + private fun deleteAllFilesOnServer() { + val result = ReadFolderRemoteOperation(ROOT_PATH).execute(client) + assertTrue(result.getLogMessage(targetContext), result.isSuccess) + + result.data + .filterIsInstance() + .filterNot { it.remotePath == ROOT_PATH } + .forEach(::deleteRemoteFile) + } + + private fun deleteRemoteFile(remoteFile: RemoteFile) { + if (remoteFile.isEncrypted) { + val isEncryptionDisabled = disableEncryption(remoteFile) + + if (!isEncryptionDisabled && isFolder(remoteFile)) return + + assertTrue(isEncryptionDisabled) + } + + if (isTeamFolder(remoteFile)) return + + assertTrue("Remove of ${remoteFile.remotePath} failed", removeWithRetry(remoteFile)) + } + + private fun disableEncryption(remoteFile: RemoteFile): Boolean = + ToggleEncryptionRemoteOperation(remoteFile.localId, remoteFile.remotePath, false) + .execute(client) + .isSuccess + + private fun removeWithRetry(remoteFile: RemoteFile): Boolean { + repeat(REMOVE_ATTEMPTS) { + if (RemoveFileRemoteOperation(remoteFile.remotePath).execute(client).isSuccess) return true + shortSleep() + } + return false + } + + private fun waitForServer(client: OwnCloudClient, baseUrl: Uri) { + val statusRequest = GetMethod("$baseUrl/status.php") + + repeat(SERVER_READY_RETRIES) { + if (isServerReady(client, statusRequest)) return + println("wait…") + sleepIgnoringInterrupt(SERVER_READY_RETRY_DELAY_MILLIS) + } + + assertTrue("Server not ready!", isServerReady(client, statusRequest)) + } + + private fun isServerReady(client: OwnCloudClient, statusRequest: GetMethod): Boolean = try { + client.executeMethod(statusRequest) == HttpStatus.SC_OK + } catch (e: IOException) { + e.printStackTrace() + false + } + + private fun Bundle.requireString(key: String): String = + requireNotNull(getString(key)) { "Missing instrumentation argument $key" } + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/DownloadIT.kt b/app/src/androidTest/java/com/owncloud/android/DownloadIT.kt index 7298142d89df..ef0f25871873 100644 --- a/app/src/androidTest/java/com/owncloud/android/DownloadIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/DownloadIT.kt @@ -70,8 +70,8 @@ class DownloadIT : AbstractOnServerIT() { refreshFolder(FOLDER) - var file1 = fileDataStorageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty.txt") - var file2 = fileDataStorageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty2.txt") + var file1 = storageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty.txt") + var file2 = storageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty2.txt") if (file1 == null) { fail("file 1 cannot be null") @@ -88,8 +88,8 @@ class DownloadIT : AbstractOnServerIT() { Assert.assertTrue(operation2Result.isSuccess) refreshFolder(FOLDER) - file1 = fileDataStorageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty.txt") - file2 = fileDataStorageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty2.txt") + file1 = storageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty.txt") + file2 = storageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty2.txt") verifyDownload(file1, file2) } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivityIT.kt index 51e534bdb5ff..cb501ccb594f 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivityIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivityIT.kt @@ -26,7 +26,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import com.facebook.testing.screenshot.internal.TestNameDetector import com.nextcloud.client.preferences.AppPreferencesImpl import com.nextcloud.test.Flaky -import com.nextcloud.test.GrantStoragePermissionRule import com.nextcloud.test.withSelectedText import com.nextcloud.utils.extensions.removeFileExtension import com.owncloud.android.AbstractIT @@ -37,16 +36,11 @@ import org.hamcrest.Matchers.not import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import java.io.File class ReceiveExternalFilesActivityIT : AbstractIT() { - @get:Rule - var storagePermissionRule: TestRule = GrantStoragePermissionRule.grant() - lateinit var mainFolder: OCFile lateinit var subFolder: OCFile lateinit var existingImageFile: OCFile @@ -57,15 +51,15 @@ class ReceiveExternalFilesActivityIT : AbstractIT() { mainFolder = OCFile("/folder/").apply { permissions = OCFile.PERMISSION_CAN_CREATE_FILE_AND_FOLDER setFolder() - fileDataStorageManager.saveNewFile(this) + storageManager.saveNewFile(this) } subFolder = OCFile("${mainFolder.remotePath}sub folder/").apply { permissions = OCFile.PERMISSION_CAN_CREATE_FILE_AND_FOLDER setFolder() - fileDataStorageManager.saveNewFile(this) + storageManager.saveNewFile(this) } existingImageFile = OCFile("${mainFolder.remotePath}Existing Image File.jpg").apply { - fileDataStorageManager.saveNewFile(this) + storageManager.saveNewFile(this) } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/UploadFilesActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/activity/UploadFilesActivityIT.kt index d07c9ba2a7e3..9f33e62fa2fc 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/UploadFilesActivityIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/activity/UploadFilesActivityIT.kt @@ -14,23 +14,17 @@ 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.isRoot -import com.nextcloud.test.GrantStoragePermissionRule.Companion.grant import com.owncloud.android.AbstractIT import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.ScreenshotTest import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule import java.io.File class UploadFilesActivityIT : AbstractIT() { private val testClassName = "com.owncloud.android.ui.activity.UploadFilesActivityIT" - @get:Rule - var storagePermissionRule: TestRule = grant() - private val directories = listOf("A", "B", "C", "D") .map { File("${FileStorageUtils.getTemporalPath(account.name)}${File.separator}$it") } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailFragmentStaticServerIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailFragmentStaticServerIT.kt index 7119f9933e4d..0a737e1df326 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailFragmentStaticServerIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailFragmentStaticServerIT.kt @@ -35,7 +35,7 @@ class FileDetailFragmentStaticServerIT : AbstractIT() { private val oCFile: OCFile = OCFile("/").apply { storagePath = file.absolutePath fileId = 12 - fileDataStorageManager.saveFile(this) + storageManager.saveFile(this) } @Test diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt index 40dca83c77b3..6c98b4646de6 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt @@ -75,7 +75,7 @@ class FileDetailSharingFragmentIT : AbstractIT() { remoteId = "00000001" parentId = activity.storageManager.getFileByEncryptedRemotePath("/").fileId permissions = OCFile.PERMISSION_CAN_RESHARE - fileDataStorageManager.saveFile(this) + storageManager.saveFile(this) } folder = OCFile("/test").apply { diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt index 5cfcf18e1b7f..a20d875786ff 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt @@ -23,7 +23,6 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.nextcloud.test.Flaky -import com.nextcloud.test.GrantStoragePermissionRule.Companion.grant import com.nextcloud.test.TestActivity import com.owncloud.android.AbstractIT import com.owncloud.android.R @@ -40,16 +39,11 @@ import org.hamcrest.Matchers.not import org.junit.After import org.junit.Assert import org.junit.Before -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule class OCFileListFragmentStaticServerIT : AbstractIT() { private val testClassName = "com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT" - @get:Rule - var storagePermissionRule: TestRule = grant() - @Before fun initIntentRecording() { Intents.init() diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt index a2cd480eb26e..e75c7912f7f7 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt @@ -14,7 +14,6 @@ import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isRoot -import com.nextcloud.test.GrantStoragePermissionRule.Companion.grant import com.nextcloud.test.TestActivity import com.owncloud.android.AbstractIT import com.owncloud.android.datamodel.OCFile @@ -26,9 +25,7 @@ import com.owncloud.android.utils.ScreenshotTest import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule internal class SharedListFragmentIT : AbstractIT() { private val testClassName = "com.owncloud.android.ui.fragment.SharedListFragmentIT" @@ -43,9 +40,6 @@ internal class SharedListFragmentIT : AbstractIT() { IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) } - @get:Rule - var storagePermissionRule: TestRule = grant() - @Test @ScreenshotTest fun showSharedFiles() { diff --git a/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewMediaActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewMediaActivityIT.kt index a5965f6474bc..c423c177fd5f 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewMediaActivityIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewMediaActivityIT.kt @@ -80,7 +80,7 @@ class PreviewMediaActivityIT : AbstractOnServerIT() { } uploadOCUpload(ocUpload) - fileDataStorageManager.getFileByDecryptedRemotePath(folder + pngFile.name)!! + storageManager.getFileByDecryptedRemotePath(folder + pngFile.name)!! } } diff --git a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt index 1aa2367ff87b..bd2b5bb60626 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt @@ -965,7 +965,7 @@ class EncryptionUtilsV2IT : EncryptionIT() { getUserId(user), EncryptionTestIT.privateKey, folder, - fileDataStorageManager, + storageManager, client, decryptedFolderMetadata1.metadata.counter, signature,