mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-24 07:46:07 +00:00
Merge pull request #464 from alexbakker/ui-tests
Add initial set of UI tests
This commit is contained in:
commit
0eb338a0bf
6 changed files with 387 additions and 12 deletions
62
.travis.yml
62
.travis.yml
|
@ -1,15 +1,55 @@
|
|||
language: android
|
||||
language: shell
|
||||
os: linux
|
||||
dist: bionic
|
||||
env:
|
||||
global:
|
||||
- ANDROID_HOME=/usr/local/android-sdk
|
||||
android:
|
||||
components:
|
||||
- android-29
|
||||
- build-tools-29.0.2
|
||||
- TERM=dumb
|
||||
- ABI=x86_64
|
||||
- EMU_FLAVOR=default
|
||||
- ADB_INSTALL_TIMEOUT=16
|
||||
- ANDROID_HOME=${HOME}/android
|
||||
- ANDROID_SDK_ROOT=${ANDROID_HOME}
|
||||
- TOOLS=${ANDROID_HOME}/cmdline-tools
|
||||
- PATH=${ANDROID_HOME}/emulator:${TOOLS}/tools/bin:${ANDROID_HOME}/platform-tools:${PATH}
|
||||
jobs:
|
||||
- API=21
|
||||
- API=23
|
||||
#- API=25 <- inconsistent
|
||||
- API=27
|
||||
- API=28
|
||||
#- API=29 <- emulator crashes
|
||||
#- API=R EMU_FLAVOR=google_apis <- too heavy, re-enable when AOSP image is available
|
||||
before_install:
|
||||
- mkdir -p $ANDROID_HOME/licenses
|
||||
- echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > $ANDROID_HOME/licenses/android-sdk-license
|
||||
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" >> $ANDROID_HOME/licenses/android-sdk-license
|
||||
- echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" >> $ANDROID_HOME/licenses/android-sdk-license
|
||||
- mkdir -p ${ANDROID_HOME}/licenses
|
||||
- echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > ${ANDROID_HOME}/licenses/android-sdk-license
|
||||
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" >> ${ANDROID_HOME}/licenses/android-sdk-license
|
||||
- echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" >> ${ANDROID_HOME}/licenses/android-sdk-license
|
||||
install:
|
||||
- mkdir -p ${TOOLS}
|
||||
- travis_retry wget https://dl.google.com/android/repository/commandlinetools-linux-6514223_latest.zip -O ${TOOLS}/tools.zip
|
||||
- unzip ${TOOLS}/tools.zip -d ${TOOLS}
|
||||
- yes | travis_retry sdkmanager "platform-tools" > /dev/null
|
||||
- yes | travis_retry sdkmanager "tools" > /dev/null
|
||||
- yes | travis_retry sdkmanager "build-tools;29.0.2" > /dev/null
|
||||
- yes | travis_retry sdkmanager "platforms;android-29" > /dev/null
|
||||
- yes | travis_retry sdkmanager "emulator" > /dev/null
|
||||
- yes | travis_retry sdkmanager "system-images;android-$API;$EMU_FLAVOR;$ABI" > /dev/null
|
||||
- travis_retry sudo apt-get -yq --no-install-suggests install qemu-kvm
|
||||
before_script:
|
||||
- sudo gpasswd -a $USER kvm
|
||||
script:
|
||||
- ./gradlew build test
|
||||
- ./gradlew assemble check
|
||||
- echo no | avdmanager create avd --force -n test -k "system-images;android-$API;$EMU_FLAVOR;$ABI" -d "Nexus 5X" -c 128M
|
||||
- sudo -E sudo -u $USER -E bash -c "${ANDROID_HOME}/emulator/emulator -verbose -avd test -gpu swiftshader_indirect -no-audio -no-boot-anim -no-snapshot -no-window -camera-back none -camera-front none -selinux permissive -memory 3072 &"
|
||||
- adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;'
|
||||
- adb shell settings put global window_animation_scale 0
|
||||
- adb shell settings put global transition_animation_scale 0
|
||||
- adb shell settings put global animator_duration_scale 0
|
||||
- adb reboot
|
||||
- adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;'
|
||||
- adb shell input keyevent 82 &
|
||||
- bash .travis/android-wait-for-launcher.sh
|
||||
- adb logcat -c
|
||||
- ./gradlew connectedCheck
|
||||
after_failure:
|
||||
- adb logcat -d
|
||||
|
|
33
.travis/android-wait-for-launcher.sh
Executable file
33
.travis/android-wait-for-launcher.sh
Executable file
|
@ -0,0 +1,33 @@
|
|||
#!/bin/bash
|
||||
|
||||
# source: https://gist.github.com/d4vidi/7862d60375b38f8970f824c4ce0ad2a9
|
||||
|
||||
echo ""
|
||||
echo "[Waiting for launcher to start]"
|
||||
LAUNCHER_READY=
|
||||
while [[ -z ${LAUNCHER_READY} ]]; do
|
||||
UI_FOCUS=`adb shell dumpsys activity activities 2>/dev/null | grep -i mResumedActivity`
|
||||
echo "(DEBUG) Current focus: ${UI_FOCUS}"
|
||||
|
||||
case $UI_FOCUS in
|
||||
*"Launcher"*)
|
||||
LAUNCHER_READY=true
|
||||
;;
|
||||
"")
|
||||
echo "Waiting for window service..."
|
||||
sleep 3
|
||||
;;
|
||||
*"Not Responding"*)
|
||||
echo "Detected an ANR! Dismissing..."
|
||||
adb shell input keyevent KEYCODE_DPAD_DOWN
|
||||
adb shell input keyevent KEYCODE_DPAD_DOWN
|
||||
adb shell input keyevent KEYCODE_ENTER
|
||||
;;
|
||||
*)
|
||||
echo "Waiting for launcher..."
|
||||
sleep 3
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Launcher is ready :-)"
|
|
@ -25,6 +25,9 @@ android {
|
|||
multiDexEnabled true
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
|
||||
|
||||
testInstrumentationRunner "com.beemdevelopment.aegis.AegisTestRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
|
@ -123,7 +126,7 @@ dependencies {
|
|||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
implementation "com.google.guava:guava:${guavaVersion}-android"
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.12.1'
|
||||
implementation 'com.google.zxing:core:3.4.0'
|
||||
implementation "com.mikepenz:iconics-core:3.2.5"
|
||||
|
@ -134,6 +137,16 @@ dependencies {
|
|||
implementation 'net.lingala.zip4j:zip4j:2.6.0'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
|
||||
androidTestImplementation 'junit:junit:4.13'
|
||||
androidTestUtil 'androidx.test:orchestrator:1.2.0'
|
||||
|
||||
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
|
||||
|
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
|
@ -16,6 +16,8 @@
|
|||
# public *;
|
||||
#}
|
||||
|
||||
-keepclasseswithmembers public class androidx.recyclerview.widget.RecyclerView { *; }
|
||||
|
||||
-keep class com.beemdevelopment.aegis.importers.** { *; }
|
||||
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.espresso.AmbiguousViewMatcherException;
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onData;
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
|
||||
import static androidx.test.espresso.action.ViewActions.clearText;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
import static androidx.test.espresso.action.ViewActions.pressBack;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static junit.framework.TestCase.assertNull;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.hamcrest.Matchers.anything;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class AegisTest {
|
||||
private static final String _password = "test";
|
||||
private static final String _groupName = "Test";
|
||||
|
||||
@Rule
|
||||
public final ActivityTestRule<MainActivity> activityRule = new ActivityTestRule<>(MainActivity.class);
|
||||
|
||||
@Test
|
||||
public void doOverallTest() {
|
||||
ViewInteraction next = onView(withId(R.id.next));
|
||||
next.perform(click());
|
||||
onView(withId(R.id.rb_password)).perform(click());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.text_password)).perform(typeText(_password), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(_password), closeSoftKeyboard());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.done)).perform(click());
|
||||
|
||||
VaultManager vault = getVault();
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
|
||||
|
||||
List<VaultEntry> entries = Arrays.asList(
|
||||
generateEntry(TotpInfo.class, "Frank", "Google"),
|
||||
generateEntry(HotpInfo.class, "John", "GitHub"),
|
||||
generateEntry(TotpInfo.class, "Alice", "Office 365"),
|
||||
generateEntry(SteamInfo.class, "Gaben", "Steam")
|
||||
);
|
||||
for (VaultEntry entry : entries) {
|
||||
addEntry(entry);
|
||||
}
|
||||
|
||||
List<VaultEntry> realEntries = new ArrayList<>(vault.getEntries());
|
||||
for (int i = 0; i < realEntries.size(); i++) {
|
||||
assertTrue(realEntries.get(i).equivalates(entries.get(i)));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, clickChildViewWithId(R.id.buttonRefresh)));
|
||||
}
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
|
||||
onView(withId(R.id.action_copy)).perform(click());
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
|
||||
onView(withId(R.id.action_edit)).perform(click());
|
||||
onView(withId(R.id.text_name)).perform(clearText(), typeText("Bob"), closeSoftKeyboard());
|
||||
onView(withId(R.id.spinner_group)).perform(click());
|
||||
onData(anything()).atPosition(1).perform(click());
|
||||
onView(withId(R.id.text_input)).perform(typeText(_groupName), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(isRoot()).perform(pressBack());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
changeSort(R.string.sort_alphabetically_name);
|
||||
changeSort(R.string.sort_alphabetically_name_reverse);
|
||||
changeSort(R.string.sort_alphabetically);
|
||||
changeSort(R.string.sort_alphabetically_reverse);
|
||||
changeSort(R.string.sort_custom);
|
||||
|
||||
changeFilter(_groupName);
|
||||
changeFilter(R.string.filter_ungrouped);
|
||||
changeFilter(R.string.all);
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
|
||||
onView(withId(R.id.action_share_qr)).perform(click());
|
||||
onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click());
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, longClick()));
|
||||
onView(withId(R.id.action_delete)).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
openContextualActionModeOverflowMenu();
|
||||
onView(withText(R.string.lock)).perform(click());
|
||||
onView(withId(R.id.text_password)).perform(typeText(_password), closeSoftKeyboard());
|
||||
onView(withId(R.id.button_decrypt)).perform(click());
|
||||
vault = getVault();
|
||||
|
||||
openContextualActionModeOverflowMenu();
|
||||
onView(withText(R.string.action_settings)).perform(click());
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), click()));
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
assertFalse(vault.isEncryptionEnabled());
|
||||
assertNull(vault.getCredentials());
|
||||
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), click()));
|
||||
onView(withId(R.id.text_password)).perform(typeText(_password), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(_password), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
|
||||
}
|
||||
|
||||
private void changeSort(@IdRes int resId) {
|
||||
onView(withId(R.id.action_sort)).perform(click());
|
||||
onView(withText(resId)).perform(click());
|
||||
}
|
||||
|
||||
private void changeFilter(String text) {
|
||||
openContextualActionModeOverflowMenu();
|
||||
onView(withText(R.string.filter)).perform(click());
|
||||
onView(withText(text)).perform(click());
|
||||
}
|
||||
|
||||
private void changeFilter(@IdRes int resId) {
|
||||
changeFilter(ApplicationProvider.getApplicationContext().getString(resId));
|
||||
}
|
||||
|
||||
private void addEntry(VaultEntry entry) {
|
||||
onView(withId(R.id.fab_expand_menu_button)).perform(click());
|
||||
onView(withId(R.id.fab_enter)).perform(click());
|
||||
|
||||
onView(withId(R.id.text_name)).perform(typeText(entry.getName()), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_issuer)).perform(typeText(entry.getIssuer()), closeSoftKeyboard());
|
||||
|
||||
if (entry.getInfo().getClass() != TotpInfo.class) {
|
||||
int i = entry.getInfo() instanceof HotpInfo ? 1 : 2;
|
||||
try {
|
||||
onView(withId(R.id.spinner_type)).perform(click());
|
||||
onData(anything()).atPosition(i).perform(click());
|
||||
} catch (AmbiguousViewMatcherException e) {
|
||||
// for some reason, clicking twice is sometimes necessary, otherwise the test fails on the next line
|
||||
onView(withId(R.id.spinner_type)).perform(click());
|
||||
onData(anything()).atPosition(i).perform(click());
|
||||
}
|
||||
if (entry.getInfo() instanceof HotpInfo) {
|
||||
onView(withId(R.id.text_counter)).perform(typeText("0"), closeSoftKeyboard());
|
||||
}
|
||||
if (entry.getInfo() instanceof SteamInfo) {
|
||||
onView(withId(R.id.text_digits)).perform(clearText(), typeText("5"), closeSoftKeyboard());
|
||||
}
|
||||
}
|
||||
|
||||
String secret = Base32.encode(entry.getInfo().getSecret());
|
||||
onView(withId(R.id.text_secret)).perform(typeText(secret), closeSoftKeyboard());
|
||||
|
||||
onView(withId(R.id.action_save)).perform(click());
|
||||
}
|
||||
|
||||
private <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer) {
|
||||
byte[] secret = CryptoUtils.generateRandomBytes(20);
|
||||
|
||||
OtpInfo info;
|
||||
try {
|
||||
info = type.getConstructor(byte[].class).newInstance(secret);
|
||||
} catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return new VaultEntry(info, name, issuer);
|
||||
}
|
||||
|
||||
private AegisApplication getApp() {
|
||||
return (AegisApplication) activityRule.getActivity().getApplication();
|
||||
}
|
||||
|
||||
private VaultManager getVault() {
|
||||
return getApp().getVaultManager();
|
||||
}
|
||||
|
||||
// source: https://stackoverflow.com/a/30338665
|
||||
private static ViewAction clickChildViewWithId(final int id) {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Click on a child view with specified id.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, View view) {
|
||||
View v = view.findViewById(id);
|
||||
v.performClick();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.runner.AndroidJUnitRunner;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class AegisTestRunner extends AndroidJUnitRunner {
|
||||
@Override
|
||||
public void callApplicationOnCreate(Application app) {
|
||||
Context context = app.getApplicationContext();
|
||||
|
||||
// clear internal storage so that there is no vault file
|
||||
clearDirectory(context.getFilesDir(), false);
|
||||
|
||||
// clear preferences so that the intro is started from MainActivity
|
||||
ApplicationProvider.getApplicationContext().getFilesDir();
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.clear()
|
||||
.apply();
|
||||
|
||||
super.callApplicationOnCreate(app);
|
||||
}
|
||||
|
||||
private static void clearDirectory(File dir, boolean deleteParent) {
|
||||
File[] files = dir.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
clearDirectory(file, true);
|
||||
} else {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deleteParent) {
|
||||
dir.delete();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue