Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fc65c21
Add automated tests for password-protected ODT files
TomTasche Jul 13, 2025
d2fa8f9
Fix UI test for password-protected documents
TomTasche Jul 13, 2025
031efce
Make password UI test more robust for CI environment
TomTasche Jul 20, 2025
b5eefdf
Add CI test artifact uploads and improve test debugging
TomTasche Jul 20, 2025
16de141
Simplify logcat capture in CI tests
TomTasche Jul 20, 2025
834b174
Add debugging for password test CI failure
TomTasche Jul 20, 2025
344bb15
Add enhanced debugging for password test CI failures
TomTasche Jul 26, 2025
dbb74eb
Revert "Add enhanced debugging for password test CI failures"
TomTasche Jul 26, 2025
95317e5
Merge branch 'main' of github.com:opendocument-app/OpenDocument.droid…
TomTasche Jul 28, 2025
5630e53
Merge branch 'main' of github.com:opendocument-app/OpenDocument.droid…
andiwand Jul 28, 2025
ff86414
Merge branch 'add-password-protected-tests' of github.com:opendocumen…
TomTasche Aug 3, 2025
7a4ae23
Merge branch 'main' of github.com:opendocument-app/OpenDocument.droid…
TomTasche Aug 3, 2025
9b185a2
stuff
TomTasche Aug 3, 2025
bdd1adf
make test always fail
TomTasche Aug 3, 2025
9a08c14
Merge branch 'main' of github.com:opendocument-app/OpenDocument.droid…
TomTasche Aug 15, 2025
0b359d2
cleanup
TomTasche Aug 15, 2025
4177524
Update app/src/androidTest/java/at/tomtasche/reader/test/CoreTest.java
TomTasche Aug 15, 2025
1897b41
raise version, fix CoreTest
TomTasche Aug 15, 2025
221bc31
Merge branch 'add-password-protected-tests' of github.com:opendocumen…
TomTasche Aug 15, 2025
8317516
undo gradle upgrade (breaks fastlane)
TomTasche Aug 15, 2025
e5cd23f
Phase 1: Update safe dependencies
TomTasche Aug 16, 2025
dc7a5b2
Phase 2: Update dependencies with breaking changes
TomTasche Aug 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion .github/workflows/build_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,43 @@ jobs:
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: ./gradlew connectedCheck
script: |
# Clear logcat before tests
adb logcat -c

# Run tests
./gradlew connectedCheck

# Dump logcat after tests
adb logcat -d > logcat.txt

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.api-level }}
path: |
app/build/reports/androidTests/
app/build/outputs/androidTest-results/
if-no-files-found: warn

- name: Upload test logs
if: always()
uses: actions/upload-artifact@v4
with:
name: test-logs-${{ matrix.api-level }}
path: |
app/build/outputs/logs/
app/build/test-results/
if-no-files-found: warn

- name: Upload emulator logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: emulator-logs-${{ matrix.api-level }}
path: |
~/.android/avd/*.avd/config.ini
~/.android/avd/*.avd/*.log
logcat.txt
if-no-files-found: warn
10 changes: 5 additions & 5 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,18 @@ android {
}

dependencies {
implementation platform('com.google.firebase:firebase-bom:33.3.0')
implementation platform('com.google.firebase:firebase-bom:34.1.0')
implementation 'com.google.firebase:firebase-storage'
implementation 'com.google.firebase:firebase-auth'
implementation 'com.google.firebase:firebase-crashlytics-ndk'
implementation 'com.google.firebase:firebase-analytics'
implementation 'com.google.firebase:firebase-config'

implementation 'com.google.android.gms:play-services-ads:23.3.0'
implementation 'com.google.android.play:review:2.0.1'
implementation 'com.google.android.gms:play-services-ads:24.5.0'
implementation 'com.google.android.play:review:2.0.2'
implementation 'com.google.android.ump:user-messaging-platform:3.0.0'

implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.core:core:1.13.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.webkit:webkit:1.11.0'
Expand All @@ -159,7 +159,7 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1'
// espresso-idling-resource is used in main sourceSet as well. cannot be just androidTestImplementation
implementation 'androidx.test.espresso:espresso-idling-resource:3.6.1'
implementation 'androidx.annotation:annotation:1.8.2'
implementation 'androidx.annotation:annotation:1.9.1'
}

// Without removing .cxx dir on cleanup, double gradle clean is erroring out.
Expand Down
Binary file added app/src/androidTest/assets/password-test.odt
Binary file not shown.
60 changes: 59 additions & 1 deletion app/src/androidTest/java/at/tomtasche/reader/test/CoreTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
@RunWith(AndroidJUnit4.class)
public class CoreTest {
private File m_testFile;
private File m_passwordTestFile;

@Before
public void initializeCore() {
Expand All @@ -36,19 +37,26 @@ public void initializeCore() {
public void extractTestFile() throws IOException {
Context appCtx = InstrumentationRegistry.getInstrumentation().getTargetContext();
m_testFile = new File(appCtx.getCacheDir(), "test.odt");
m_passwordTestFile = new File(appCtx.getCacheDir(), "password-test.odt");

Context testCtx = InstrumentationRegistry.getInstrumentation().getContext();
AssetManager assetManager = testCtx.getAssets();
try (InputStream inputStream = assetManager.open("test.odt")) {
copy(inputStream, m_testFile);
}
try (InputStream inputStream = assetManager.open("password-test.odt")) {
copy(inputStream, m_passwordTestFile);
}
}

@After
public void cleanupTestFile() {
if (null != m_testFile) {
m_testFile.delete();
}
if (null != m_passwordTestFile) {
m_passwordTestFile.delete();
}
}

private static void copy(InputStream src, File dst) throws IOException {
Expand All @@ -70,8 +78,8 @@ public void test() {
CoreWrapper.CoreOptions coreOptions = new CoreWrapper.CoreOptions();
coreOptions.inputPath = m_testFile.getAbsolutePath();
coreOptions.outputPath = outputPath.getPath();
coreOptions.cachePath = cachePath.getPath();
coreOptions.editable = true;
coreOptions.cachePath = cachePath.getPath();

CoreWrapper.CoreResult coreResult = CoreWrapper.parse(coreOptions);
Assert.assertEquals(0, coreResult.errorCode);
Expand All @@ -82,6 +90,56 @@ public void test() {
String htmlDiff = "{\"modifiedText\":{\"3\":\"This is a simple test document to demonstrate the DocumentLoadewwwwr example!\"}}";

CoreWrapper.CoreResult result = CoreWrapper.backtranslate(coreOptions, htmlDiff);
Assert.assertEquals(0, result.errorCode);
}

@Test
public void testPasswordProtectedDocumentWithoutPassword() {
File cacheDir = InstrumentationRegistry.getInstrumentation().getTargetContext().getCacheDir();
File outputDir = new File(cacheDir, "output_password_test");
File cachePath = new File(cacheDir, "core_cache");

CoreWrapper.CoreOptions coreOptions = new CoreWrapper.CoreOptions();
coreOptions.inputPath = m_passwordTestFile.getAbsolutePath();
coreOptions.outputPath = outputDir.getPath();
coreOptions.editable = false;
coreOptions.cachePath = cachePath.getPath();

CoreWrapper.CoreResult coreResult = CoreWrapper.parse(coreOptions);
Assert.assertEquals(-2, coreResult.errorCode);
}

@Test
public void testPasswordProtectedDocumentWithWrongPassword() {
File cacheDir = InstrumentationRegistry.getInstrumentation().getTargetContext().getCacheDir();
File outputDir = new File(cacheDir, "output_password_test");
File cachePath = new File(cacheDir, "core_cache");

CoreWrapper.CoreOptions coreOptions = new CoreWrapper.CoreOptions();
coreOptions.inputPath = m_passwordTestFile.getAbsolutePath();
coreOptions.outputPath = outputDir.getPath();
coreOptions.password = "wrongpassword";
coreOptions.editable = false;
coreOptions.cachePath = cachePath.getPath();

CoreWrapper.CoreResult coreResult = CoreWrapper.parse(coreOptions);
Assert.assertEquals(-2, coreResult.errorCode);
}

@Test
public void testPasswordProtectedDocumentWithCorrectPassword() {
File cacheDir = InstrumentationRegistry.getInstrumentation().getTargetContext().getCacheDir();
File outputDir = new File(cacheDir, "output_password_test");
File cachePath = new File(cacheDir, "core_cache");

CoreWrapper.CoreOptions coreOptions = new CoreWrapper.CoreOptions();
coreOptions.inputPath = m_passwordTestFile.getAbsolutePath();
coreOptions.outputPath = outputDir.getPath();
coreOptions.password = "passwort";
coreOptions.editable = false;
coreOptions.cachePath = cachePath.getPath();

CoreWrapper.CoreResult coreResult = CoreWrapper.parse(coreOptions);
Assert.assertEquals(0, coreResult.errorCode);
}
}
113 changes: 103 additions & 10 deletions app/src/androidTest/java/at/tomtasche/reader/test/MainActivityTests.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package at.tomtasche.reader.test;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.clearText;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.equalTo;

import android.app.Activity;
import android.app.Instrumentation;
Expand All @@ -18,6 +23,7 @@
import android.content.res.AssetManager;
import android.net.Uri;
import android.util.ArrayMap;
import android.util.Log;

import androidx.core.content.FileProvider;
import androidx.test.espresso.IdlingRegistry;
Expand Down Expand Up @@ -55,12 +61,14 @@ public class MainActivityTests {

// Yes, this is ActivityTestRule instead of ActivityScenario, because ActivityScenario does not actually work.
// Issue ID may or may not be added later.
// Launch activity manually to ensure complete restart between tests
@Rule
public ActivityTestRule<MainActivity> mainActivityActivityTestRule = new ActivityTestRule<>(MainActivity.class);
public ActivityTestRule<MainActivity> mainActivityActivityTestRule = new ActivityTestRule<>(MainActivity.class, false, false);

@Before
public void setUp() {
MainActivity mainActivity = mainActivityActivityTestRule.getActivity();
// Launch a fresh activity for each test
MainActivity mainActivity = mainActivityActivityTestRule.launchActivity(null);

m_idlingResource = mainActivity.getOpenFileIdlingResource();
IdlingRegistry.getInstance().register(m_idlingResource);
Expand All @@ -70,15 +78,29 @@ public void setUp() {
mainActivity.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));

Intents.init();

// Log test setup for debugging
Log.d("MainActivityTests", "setUp() called for test: " + getClass().getName());
}

@After
public void tearDown() {
Log.d("MainActivityTests", "tearDown() called");

Intents.release();

if (null != m_idlingResource) {
IdlingRegistry.getInstance().unregister(m_idlingResource);
}

// Finish and wait for activity to be destroyed
MainActivity activity = mainActivityActivityTestRule.getActivity();
if (activity != null) {
mainActivityActivityTestRule.finishActivity();

// Use Instrumentation to wait until activity is destroyed
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
}

private static void copy(InputStream src, File dst) throws IOException {
Expand All @@ -104,7 +126,7 @@ public static void extractTestFiles() throws IOException {

AssetManager testAssetManager = instrumentation.getContext().getAssets();

for (String filename: new String[] {"test.odt", "dummy.pdf"}) {
for (String filename: new String[] {"test.odt", "dummy.pdf", "password-test.odt"}) {
File targetFile = new File(testDocumentsDir, filename);
try (InputStream inputStream = testAssetManager.open(filename)) {
copy(inputStream, targetFile);
Expand Down Expand Up @@ -135,21 +157,21 @@ public void testODT() {
);

onView(allOf(withId(R.id.menu_open), withContentDescription("Open document"), isDisplayed()))
.perform(click());
.perform(click());

// The menu item could be either Documents or Files.
onView(allOf(withId(android.R.id.text1), anyOf(withText("Documents"), withText("Files")), isDisplayed()))
.perform(click());

// next onView will be blocked until m_idlingResource is idle.
onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isEnabled()))
.withFailureHandler((error, viewMatcher) -> {
// fails on small screens, try again with overflow menu
onView(allOf(withContentDescription("More options"), isDisplayed())).perform(click());
.withFailureHandler((error, viewMatcher) -> {
// fails on small screens, try again with overflow menu
onView(allOf(withContentDescription("More options"), isDisplayed())).perform(click());

onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isDisplayed()))
.perform(click());
});
onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isDisplayed()))
.perform(click());
});
}

@Test
Expand Down Expand Up @@ -183,5 +205,76 @@ public void testPDF() {
onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isDisplayed()))
.perform(click());
});

try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

@Test
public void testPasswordProtectedODT() {
File testFile = s_testFiles.get("password-test.odt");
Assert.assertNotNull(testFile);

// Check if the file exists and is readable
Assert.assertTrue("Password test file does not exist: " + testFile.getAbsolutePath(), testFile.exists());
Assert.assertTrue("Password test file is not readable: " + testFile.getAbsolutePath(), testFile.canRead());

// Log file info for debugging CI issues
Log.d("MainActivityTests", "Password test file path: " + testFile.getAbsolutePath());
Log.d("MainActivityTests", "Password test file size: " + testFile.length());
Log.d("MainActivityTests", "All test files: " + s_testFiles.keySet());

// Double-check we're using the right file
Assert.assertEquals("password-test.odt file size mismatch", 12671L, testFile.length());

Context appCtx = InstrumentationRegistry.getInstrumentation().getTargetContext();
Uri testFileUri = FileProvider.getUriForFile(appCtx, appCtx.getPackageName() + ".provider", testFile);
Intents.intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(
new Instrumentation.ActivityResult(Activity.RESULT_OK,
new Intent()
.setData(testFileUri)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
)
);

onView(allOf(withId(R.id.menu_open), withContentDescription("Open document"), isDisplayed()))
.perform(click());

onView(allOf(withId(android.R.id.text1), anyOf(withText("Documents"), withText("Files")), isDisplayed()))
.perform(click());

// Wait for the password dialog to appear
onView(withText("This document is password-protected"))
.check(matches(isDisplayed()));

// Enter wrong password first
onView(withClassName(equalTo("android.widget.EditText")))
.perform(typeText("wrongpassword"));

onView(withId(android.R.id.button1))
.perform(click());

// Should show password dialog again for wrong password
onView(withText("This document is password-protected"))
.check(matches(isDisplayed()));

// Clear the text field and enter correct password
onView(withClassName(equalTo("android.widget.EditText")))
.perform(clearText(), typeText("passwort"));

onView(withId(android.R.id.button1))
.perform(click());

// Check if edit button becomes available (indicating successful load)
onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isEnabled()))
.withFailureHandler((error, viewMatcher) -> {
onView(allOf(withContentDescription("More options"), isDisplayed())).perform(click());

onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isDisplayed()))
.perform(click());
});
}
}
4 changes: 2 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto"
android:versionCode="192"
android:versionName="3.38"
android:versionCode="195"
android:versionName="3.40"
tools:ignore="GoogleAppIndexingWarning">

<uses-permission android:name="android.permission.INTERNET" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public static void initialize(Context context) {
public static class CoreOptions {
public boolean ooxml;
public boolean txt;
// TODO: remove
public boolean pdf;

public boolean editable;
Expand Down
Loading