diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
new file mode 100644
index 00000000..f138aaa1
--- /dev/null
+++ b/.github/workflows/e2e.yml
@@ -0,0 +1,88 @@
+on: [push]
+
+jobs:
+ setup_nextcloud:
+ runs-on: ubuntu-latest
+ name: Run e2e test
+ strategy:
+ fail-fast: false
+ matrix:
+ api-level: [ 28 ] #, 24, 25, 26, 27, 28, 29 ]
+ nextcloud-version: [ 'nextcloud:latest' ] #, 'nextcloud:stable', 'nextcloud:production' ]
+ services:
+ nextcloud:
+ image: ${{ matrix.nextcloud-version }}
+ env:
+ SQLITE_DATABASE: db.sqlite
+ NEXTCLOUD_ADMIN_USER: Test
+ NEXTCLOUD_ADMIN_PASSWORD: Test
+ NEXTCLOUD_TRUSTED_DOMAINS: 172.17.0.1
+ ports:
+ - 8080:80
+ options: >-
+ --health-cmd "curl GET 'http://Test:Test@localhost:80/ocs/v2.php/apps/serverinfo/api/v1/info' -f -H 'OCS-APIRequest: true' || exit 1"
+ --health-interval 1s
+ --health-timeout 2s
+ --health-retries 10
+ --health-start-period 3s
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ # TODO 172.17.0.1 is the hard coded IP address of the docker container. Make this more generic.
+ - name: Verify Nextcloud being present
+ run: |
+ curl -v -X GET 'http://Test:Test@172.17.0.1:8080/ocs/v2.php/cloud/capabilities?format=json' -H 'OCS-APIRequest: true' | jq
+
+ ##########################
+ # AVD CACHING START #
+ ##########################
+
+# - name: Gradle cache
+# uses: actions/cache@v2
+# with:
+# path: |
+# ~/.gradle/caches
+# ~/.gradle/wrapper
+# key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/buildSrc/**/*.kt') }}
+#
+# - name: AVD cache
+# uses: actions/cache@v2
+# id: avd-cache
+# with:
+# path: |
+# ~/.android/avd/*
+# ~/.android/adb*
+# key: avd-${{ matrix.api-level }}
+#
+# - name: Create AVD and generate snapshot for caching
+# if: steps.avd-cache.outputs.cache-hit != 'true'
+# uses: reactivecircus/android-emulator-runner@v2
+# with:
+# api-level: ${{ matrix.api-level }}
+# force-avd-creation: false
+# sdcard-path-or-size: sdcard
+# emulator-options: -gpu swiftshader_indirect -no-window -noaudio -no-boot-anim -camera-back none
+# disable-animations: true
+# script: echo "Generated AVD snapshot for caching."
+
+ ##########################
+ # AVD CACHING END #
+ ##########################
+
+ - name: Run e2e tests
+ uses: reactivecircus/android-emulator-runner@v2
+ with:
+ api-level: ${{ matrix.api-level }}
+ force-avd-creation: false
+ emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
+ disable-animations: true
+ script: |
+ adb shell pm uninstall -k --user 0 com.nextcloud.android.beta || true
+ wget -q https://download.nextcloud.com/android/dev/latest.apk
+ adb install latest.apk
+ adb shell pm grant com.nextcloud.android.beta android.permission.READ_EXTERNAL_STORAGE
+ adb logcat -c || true
+ adb logcat -s "E2E" -v color &
+ adb logcat *:I -v color &
+ ./gradlew :sample:connectedDebugAndroidTest
diff --git a/build.gradle b/build.gradle
index 3dfd1ec2..065b46bb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -22,6 +22,10 @@ buildscript {
}
}
+//plugins {
+// id "de.undercouch.download" version "5.4.0"
+//}
+
allprojects {
repositories {
google()
diff --git a/sample/build.gradle b/sample/build.gradle
index b323e854..ef75b9e4 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -4,7 +4,7 @@
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-
+
apply plugin: 'com.android.application'
android {
@@ -35,6 +35,34 @@ android {
}
}
+//task downloadNextcloudApk(type: Download) {
+// src 'https://download.nextcloud.com/android/dev/latest.apk'
+// dest new File(buildDir, 'latest.apk')
+// overwrite true
+//}
+//
+//task setupNextcloudEnvironment(dependsOn: downloadNextcloudApk) {
+// def bridge = AndroidDebugBridge.createBridge(android.adbExecutable.path, false, 10, TimeUnit.SECONDS)
+// doLast {
+// bridge.devices.each { device ->
+// println "Uninstall Nextcloud apk from ${device.name}"
+// device.uninstallPackage("com.nextcloud.android.beta")
+//
+// println "Install Nextcloud apk on ${device.name}"
+// device.installPackage(new File(buildDir, 'latest.apk').getAbsolutePath(), true)
+//
+// println "Grant permissions to Nextcloud"
+// device.executeShellCommand("pm grant com.nextcloud.android.beta android.permission.READ_EXTERNAL_STORAGE", NullOutputReceiver.receiver, 3, TimeUnit.SECONDS)
+// }
+// }
+//}
+//
+//tasks.whenTaskAdded { taskItem ->
+// if (taskItem.name.contains("connected") && taskItem.name.endsWith("AndroidTest")) {
+// taskItem.dependsOn setupNextcloudEnvironment
+// }
+//}
+
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.9.22"))
@@ -45,8 +73,6 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
- testImplementation 'junit:junit:4.13.2'
-
- androidTestImplementation 'androidx.test.ext:junit:1.1.5'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ androidTestImplementation 'androidx.test:runner:1.4.0'
+ androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}
\ No newline at end of file
diff --git a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java
new file mode 100644
index 00000000..f7e754ee
--- /dev/null
+++ b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java
@@ -0,0 +1,219 @@
+package com.nextcloud.android.sso.sample;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.uiautomator.Until.hasObject;
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.util.Log;
+import android.webkit.WebView;
+import android.widget.Button;
+import android.widget.EditText;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObjectNotFoundException;
+import androidx.test.uiautomator.UiSelector;
+
+import org.junit.Before;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ *
Setup
+ * CI / CD
+ * No manual configuration needs to be done because the setup already happens in the e2e.yml file.
+ * Local
+ *
+ * - Set {@link #CONFIG_SERVER_URL}, {@link #CONFIG_USERNAME}, {@link #CONFIG_PASSWORD} and {@link #CONFIG_DISPLAY_NAME}. The Nextcloud instance must exist and be reachable.
+ * - Remove any existing installation of the Nextcloud files app
+ * - Install the Dev-Version of the Nextcloud files app
+ * - Grant the
android.permission.READ_EXTERNAL_STORAGE permission to the Nextcloud files app
+ *
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class E2ETest {
+ private static final String CONFIG_SERVER_URL = "http://172.17.0.1:8080";
+ private static final String CONFIG_USERNAME = "Test";
+ private static final String CONFIG_DISPLAY_NAME = "Test";
+ private static final String CONFIG_PASSWORD = "Test";
+
+ private static final String TAG = "E2E";
+ private static final int TIMEOUT = 60_000;
+
+ private UiDevice mDevice;
+
+ private static final String APP_SAMPLE = BuildConfig.APPLICATION_ID;
+ // TODO This should be passed as argument
+ private static final String APP_NEXTCLOUD = "com.nextcloud.android.beta";
+
+ @Before
+ public void before() {
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ }
+
+ @Test
+ public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException, InterruptedException {
+ Log.i(TAG, "Configure Nextcloud account");
+
+ final var context = getInstrumentation().getContext();
+ final var packageManager = context.getPackageManager();
+ try {
+ packageManager.getPackageInfo(APP_NEXTCLOUD, 0);
+ Log.i(TAG, "Nextcloud APK is installed (checking on runtime)");
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Nextcloud APK is NOT installed (checking on runtime");
+ }
+
+ launch(APP_NEXTCLOUD);
+
+ final var loginButton = mDevice.findObject(new UiSelector().textContains("Log in"));
+ loginButton.waitForExists(TIMEOUT);
+ Log.d(TAG, "Login Button exists. Clicking on it…");
+ loginButton.click();
+ Log.d(TAG, "Login Button clicked.");
+
+ final var urlInput = mDevice.findObject(new UiSelector().focused(true));
+ urlInput.waitForExists(TIMEOUT);
+ Log.d(TAG, "URL input exists.");
+ Log.d(TAG, "Entering URL…");
+ urlInput.setText(CONFIG_SERVER_URL);
+ Log.d(TAG, "URL entered.");
+
+ Log.d(TAG, "Pressing enter…");
+ mDevice.pressEnter();
+ Log.d(TAG, "Enter pressed.");
+
+ final var webView = mDevice.findObject(new UiSelector().instance(0).className(WebView.class));
+ Log.d(TAG, "Waiting for WebView…");
+// mDevice.wait(findObject(By.clazz(WebView.class)), TIMEOUT);
+ webView.waitForExists(TIMEOUT);
+ Log.d(TAG, "WebView exists.");
+
+ final var webViewLoginButton = mDevice.findObject(new UiSelector()
+ .instance(0)
+ .className(Button.class));
+ Log.d(TAG, "Waiting for WebView Login Button…");
+ webViewLoginButton.waitForExists(TIMEOUT);
+ Log.d(TAG, "WebView Login Button exists. Clicking on it…");
+
+ // TODO Find better way to scroll the Login button to the visible area
+ // Log.d(TAG, "Scroll to bottom of WebView…");
+ // mDevice.findObject(By.clazz(WebView.class)).swipe(Direction.UP, 1f);
+ // Log.d(TAG, "Finished scrolling");
+ webViewLoginButton.dragTo(0, 0, 40);
+
+ webViewLoginButton.click();
+
+ final var usernameInput = mDevice.findObject(new UiSelector()
+ .instance(0)
+ .className(EditText.class));
+ Log.d(TAG, "Waiting for Username Input…");
+ usernameInput.waitForExists(TIMEOUT);
+ Log.d(TAG, "Username Input exists. Setting text…");
+ usernameInput.setText(CONFIG_USERNAME);
+ Log.d(TAG, "Username has been set.");
+
+ final var passwordInput = mDevice.findObject(new UiSelector()
+ .instance(1)
+ .className(EditText.class));
+ Log.d(TAG, "Waiting for Password Input…");
+ passwordInput.waitForExists(TIMEOUT);
+ Log.d(TAG, "Password Input exists. Setting text…");
+ passwordInput.setText(CONFIG_PASSWORD);
+
+ // mDevice.pressEnter();
+ final var webViewSubmitButton = mDevice.findObject(new UiSelector()
+ .instance(1) // First button is password visibility toggle
+ .className(Button.class));
+ Log.d(TAG, "Waiting for WebView Submit Button…");
+ webViewSubmitButton.waitForExists(TIMEOUT);
+ Log.d(TAG, "WebView Submit Button exists. Clicking on it…");
+ webViewSubmitButton.click();
+
+ webViewSubmitButton.waitUntilGone(TIMEOUT);
+
+ final var webViewGrantAccessButton = mDevice.findObject(new UiSelector()
+ .instance(0)
+ .className(Button.class));
+ Log.d(TAG, "Waiting for WebView Grant Access Button…");
+ webViewGrantAccessButton.waitForExists(TIMEOUT);
+ Log.d(TAG, "WebView Grant Access Button exists. Clicking on it…");
+ webViewGrantAccessButton.click();
+
+ webView.waitUntilGone(TIMEOUT);
+
+ mDevice.waitForIdle(TIMEOUT);
+
+ Log.d(TAG, "Wait for Nextcloud files app…");
+ Thread.sleep(3_000);
+ Log.d(TAG, "Finishing setup…");
+ }
+
+ @Test
+ public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundException, InterruptedException {
+ Log.i(TAG, "Import account into sample app");
+ launch(APP_SAMPLE);
+ final var WAIT = 3_000;
+
+ final var accountButton = mDevice.findObject(new UiSelector()
+ .instance(0)
+ .className(Button.class));
+ accountButton.waitForExists(TIMEOUT);
+ accountButton.click();
+
+ mDevice.waitForWindowUpdate(null, TIMEOUT);
+
+ final var radioAccount = mDevice.findObject(new UiSelector()
+ .clickable(true)
+ .instance(0));
+ radioAccount.waitForExists(TIMEOUT);
+ radioAccount.click();
+
+ Thread.sleep(WAIT);
+
+ final var okButton = mDevice.findObject(new UiSelector()
+ .textContains("OK"));
+ Log.d(TAG, "Waiting for OK Button…");
+ okButton.waitForExists(TIMEOUT);
+ Thread.sleep(WAIT);
+ Log.d(TAG, "OK Button exists. Clicking on it…");
+ okButton.click();
+ Log.d(TAG, "OK Button clicked");
+
+ Thread.sleep(WAIT);
+
+ final var allowButton = mDevice.findObject(new UiSelector()
+ .instance(1)
+ .className(Button.class));
+ Log.d(TAG, "Waiting for Allow Button…");
+ allowButton.waitForExists(TIMEOUT);
+ Log.d(TAG, "Allow Button exists. Clicking on it…");
+ allowButton.click();
+ Log.d(TAG, "Allow Button clicked");
+
+ Log.d(TAG, "Waiting for finished import…");
+ final var welcomeText = mDevice.findObject(new UiSelector().description("Filter"));
+ welcomeText.waitForExists(TIMEOUT);
+ Log.d(TAG, "Import finished.");
+
+ Log.i(TAG, "Verify successful import…");
+ final var expectedToContain = CONFIG_DISPLAY_NAME + " on Nextcloud";
+ final var result = mDevice.findObject(new UiSelector().textContains(expectedToContain));
+ result.waitForExists(TIMEOUT);
+ Log.i(TAG, "Expected UI to display '" + expectedToContain + "'. Found: '" + result.getText() + "'.");
+ }
+
+ private void launch(@NonNull String packageName) {
+ Log.d(TAG, "Launching " + packageName);
+ mDevice.pressHome();
+ final var context = getInstrumentation().getContext();
+ context.startActivity(context
+ .getPackageManager()
+ .getLaunchIntentForPackage(packageName)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK));
+ mDevice.wait(hasObject(By.pkg(packageName).depth(0)), TIMEOUT);
+ }
+}
diff --git a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/ExampleInstrumentedTest.java b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/ExampleInstrumentedTest.java
deleted file mode 100644
index 5bb98ff0..00000000
--- a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/ExampleInstrumentedTest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Nextcloud Android SingleSignOn Library
- *
- * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-FileCopyrightText: 2021 Stefan Niedermann
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-package com.nextcloud.android.sso.sample;
-
-import static org.junit.Assert.assertEquals;
-
-import android.content.Context;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * @see Testing documentation
- */
-@RunWith(AndroidJUnit4.class)
-public class ExampleInstrumentedTest {
- @Test
- public void useAppContext() {
- // Context of the app under test.
- Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
- assertEquals("com.nextcloud.android.sso.sample", appContext.getPackageName());
- }
-}
\ No newline at end of file
diff --git a/sample/src/test/java/com/nextcloud/android/sso/sample/ExampleUnitTest.java b/sample/src/test/java/com/nextcloud/android/sso/sample/ExampleUnitTest.java
deleted file mode 100644
index dbc01426..00000000
--- a/sample/src/test/java/com/nextcloud/android/sso/sample/ExampleUnitTest.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Nextcloud Android SingleSignOn Library
- *
- * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-FileCopyrightText: 2021 Stefan Niedermann
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-package com.nextcloud.android.sso.sample;
-
-import static org.junit.Assert.assertEquals;
-
-import org.junit.Test;
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * @see Testing documentation
- */
-public class ExampleUnitTest {
- @Test
- public void addition_isCorrect() {
- assertEquals(4, 2 + 2);
- }
-}
\ No newline at end of file