From d51b62d6cfd30db9031c7b233bfa83589025fba3 Mon Sep 17 00:00:00 2001 From: Victor Andreasson Date: Sat, 24 Aug 2024 13:34:19 +0200 Subject: [PATCH] Add new tests and convert some repo tests to local JVM tests Create a generic test suite for SyncRepo implementations in the "shared-test" library. This allows writing implementation-agnostic test cases which all implementations must pass. The challenge in writing "abstract" tests for SyncRepo is that most implementations can be tested with local unit tests, whereas DocumentRepo requires instrumented tests, since DocumentFile is a central class, and it's an Android class. --- .github/workflows/test.yaml | 48 +- app/build.gradle | 18 +- .../java/com/orgzly/android/OrgzlyTest.java | 2 - .../java/com/orgzly/android/TestUtils.java | 9 + .../orgzly/android/espresso/SyncingTest.java | 159 ------- .../android/repos/DataRepositoryTest.java | 1 + .../android/repos/DirectoryRepoTest.java | 1 - .../orgzly/android/repos/DocumentRepoTest.kt | 203 +++++++++ .../orgzly/android/repos/DropboxRepoTest.java | 103 +---- .../com/orgzly/android/repos/SyncTest.java | 266 ++++++++++- .../orgzly/android/repos/DropboxRepoTest.kt | 135 ++++++ .../com/orgzly/android/repos/GitRepoTest.kt | 137 ++++++ .../orgzly/android/repos/WebdavRepoTest.kt | 138 ++++++ shared-test/.gitignore | 1 + shared-test/build.gradle | 51 +++ shared-test/proguard-rules.pro | 21 + shared-test/src/main/AndroidManifest.xml | 17 + .../com/orgzly/android/repos/SyncRepoTest.kt | 422 ++++++++++++++++++ 18 files changed, 1451 insertions(+), 281 deletions(-) create mode 100644 app/src/androidTest/java/com/orgzly/android/repos/DocumentRepoTest.kt create mode 100644 app/src/test/java/com/orgzly/android/repos/DropboxRepoTest.kt create mode 100644 app/src/test/java/com/orgzly/android/repos/GitRepoTest.kt create mode 100644 app/src/test/java/com/orgzly/android/repos/WebdavRepoTest.kt create mode 100644 shared-test/.gitignore create mode 100644 shared-test/build.gradle create mode 100644 shared-test/proguard-rules.pro create mode 100644 shared-test/src/main/AndroidManifest.xml create mode 100644 shared-test/src/main/java/com/orgzly/android/repos/SyncRepoTest.kt diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index bb51aa23d..fc5b1a396 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,7 +22,41 @@ on: workflow_dispatch: jobs: - test: + + localUnitTests: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Gradle build Fdroid + run: ./gradlew assembleFdroidDebug + + - name: Gradle test Fdroid + run: ./gradlew testFdroidDebugUnitTest + + - name: Add Dropbox API credentials (for DropboxRepo tests) + shell: bash + run: | + echo "dropbox.refresh_token = \"${{ secrets.DROPBOX_REFRESH_TOKEN }}\"" >> app.properties + echo "dropbox.app_key = \"${{ secrets.DROPBOX_APP_KEY }}\"" >> app.properties + + - name: Gradle test Premium + run: ./gradlew testPremiumDebugUnitTest + + instrumentedTests: runs-on: ubuntu-latest strategy: matrix: @@ -39,8 +73,14 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - name: AVD cache uses: actions/cache@v4 @@ -80,7 +120,7 @@ jobs: echo "dropbox.refresh_token = \"${{ secrets.DROPBOX_REFRESH_TOKEN }}\"" >> app.properties echo "dropbox.app_key = \"${{ secrets.DROPBOX_APP_KEY }}\"" >> app.properties - - name: Run tests + - name: Run instrumented tests uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} @@ -91,4 +131,4 @@ jobs: disable-spellchecker: true profile: Nexus 6 # Tests should use the build which includes Dropbox code. - script: ./gradlew connected${{matrix.flavor}}DebugAndroidTest --no-watch-fs --build-cache --info + script: ./gradlew --no-configuration-cache connected${{matrix.flavor}}DebugAndroidTest --no-watch-fs --build-cache --info diff --git a/app/build.gradle b/app/build.gradle index 27beb38f4..4ed725498 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,9 +38,11 @@ android { viewBinding true } -// testOptions { -// execution 'ANDROIDX_TEST_ORCHESTRATOR' -// } + testOptions { + unitTests { + includeAndroidResources = true + } + } buildTypes { release { @@ -149,7 +151,15 @@ dependencies { implementation "androidx.work:work-runtime-ktx:$versions.android_workmanager" + testImplementation "androidx.test:runner:$versions.android_test" + testImplementation "androidx.test:rules:$versions.android_test" + testImplementation "androidx.test.ext:junit:$versions.android_test_ext_junit" testImplementation "junit:junit:$versions.junit" + testImplementation 'org.robolectric:robolectric:4.13' + testImplementation "io.github.atetzner:webdav-embedded-server:0.2.1" + testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.8.2") + testImplementation "org.mockito:mockito-core:2.28.2" + testImplementation "org.mockito.kotlin:mockito-kotlin:5.1.0" // AndroidX Test androidTestImplementation "androidx.test.espresso:espresso-core:$versions.android_test_espresso" @@ -209,6 +219,8 @@ dependencies { implementation("androidx.biometric:biometric-ktx:$versions.biometric_ktx") { because 'Protect SSH key with biometric prompt' } + testImplementation(project(":shared-test")) + androidTestImplementation(project(":shared-test")) } repositories { diff --git a/app/src/androidTest/java/com/orgzly/android/OrgzlyTest.java b/app/src/androidTest/java/com/orgzly/android/OrgzlyTest.java index dea5ce2e8..42c7256ae 100644 --- a/app/src/androidTest/java/com/orgzly/android/OrgzlyTest.java +++ b/app/src/androidTest/java/com/orgzly/android/OrgzlyTest.java @@ -4,7 +4,6 @@ import android.Manifest; import android.app.Activity; -import android.app.UiAutomation; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; @@ -20,7 +19,6 @@ import com.orgzly.android.repos.RepoFactory; import com.orgzly.android.util.UserTimeFormatter; import com.orgzly.org.datetime.OrgDateTime; -import com.orgzly.test.BuildConfig; import org.junit.After; import org.junit.Before; diff --git a/app/src/androidTest/java/com/orgzly/android/TestUtils.java b/app/src/androidTest/java/com/orgzly/android/TestUtils.java index 07eb863a3..0da43deb3 100644 --- a/app/src/androidTest/java/com/orgzly/android/TestUtils.java +++ b/app/src/androidTest/java/com/orgzly/android/TestUtils.java @@ -46,12 +46,21 @@ public SyncRepo repoInstance(RepoType type, String url) { return dataRepository.getRepoInstance(13, type, url); } + public SyncRepo repoInstance(RepoType type, String url, Long id) { + return dataRepository.getRepoInstance(id, type, url); + } + public Repo setupRepo(RepoType type, String url) { long id = dataRepository.createRepo(new RepoWithProps(new Repo(0, type, url))); return dataRepository.getRepo(id); } + public Repo setupRepo(RepoType type, String url, Map props) { + long id = dataRepository.createRepo(new RepoWithProps(new Repo(0, type, url), props)); + return dataRepository.getRepo(id); + } + public void deleteRepo(String url) { Repo repo = dataRepository.getRepo(url); if (repo != null) { diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/SyncingTest.java b/app/src/androidTest/java/com/orgzly/android/espresso/SyncingTest.java index 8a6d65fe3..6fa4d738a 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/SyncingTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/SyncingTest.java @@ -74,29 +74,6 @@ public void testRunSync() { sync(); } - @Test - public void testForceLoadingBookWithLink() { - Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); - testUtils.setupRook(repo, "mock://repo-a/booky.org", "New content", "abc", 1234567890000L); - testUtils.setupBook("booky", "First book used for testing\n* Note A"); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("booky"), isDisplayed())).perform(longClick()); - contextualToolbarOverflowMenu().perform(click()); - onView(withText(R.string.books_context_menu_item_set_link)).perform(click()); - onView(withText("mock://repo-a")).perform(click()); - - onView(allOf(withText("booky"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_load)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - onBook(0, R.id.item_book_last_action) - .check(matches((withText(containsString(context.getString(R.string.force_loaded_from_uri, "mock://repo-a/booky.org")))))); - - onView(allOf(withText("booky"), isDisplayed())).perform(click()); - onView(allOf(withId(R.id.item_preface_text_view), withText("New content"))) - .check(matches(isDisplayed())); - } - @Test public void testAutoSyncIsTriggeredAfterCreatingNote() { Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); @@ -169,64 +146,6 @@ public void nonLinkedBookCannotBeMadeOutOfSync() { onBook(0, R.id.item_book_sync_needed_icon).check(matches(not(isDisplayed()))); } - @Test - public void testForceLoadingBookWithNoLinkNoRepos() { - testUtils.setupBook("booky", "First book used for testing\n* Note A"); - testUtils.setupBook("book-two", "Second book used for testing\n* Note 1\n* Note 2"); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("booky"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_load)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - onSnackbar().check(matches(withText(endsWith(context.getString(R.string.message_book_has_no_link))))); - } - - @Test - public void testForceLoadingBookWithNoLinkSingleRepo() { - testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); - testUtils.setupBook("booky", "First book used for testing\n* Note A"); - testUtils.setupBook("book-two", "Second book used for testing\n* Note 1\n* Note 2"); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("booky"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_load)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - onSnackbar().check(matches(withText(endsWith(context.getString(R.string.message_book_has_no_link))))); - } - - /* Books view was returning multiple entries for the same book, due to duplicates in encodings - * table. The last statement in this method will fail if there are multiple books matching. - */ - @Test - public void testForceLoadingMultipleTimes() { - Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); - testUtils.setupRook(repo, "mock://repo-a/book-one.org", "New content", "abc", 1234567890000L); - testUtils.setupBook("book-one", "First book used for testing\n* Note A"); - testUtils.setupBook("book-two", "Second book used for testing\n* Note 1\n* Note 2"); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("book-one"), isDisplayed())).perform(longClick()); - contextualToolbarOverflowMenu().perform(click()); - onView(withText(R.string.books_context_menu_item_set_link)).perform(click()); - onView(withText("mock://repo-a")).perform(click()); - - onView(allOf(withText("book-one"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_load)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - - onBook(0, R.id.item_book_last_action) - .check(matches(withText(endsWith( - context.getString(R.string.force_loaded_from_uri, "mock://repo-a/book-one.org"))))); - - onView(allOf(withText("book-one"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_load)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - - onBook(0, R.id.item_book_last_action) - .check(matches(withText(endsWith( - context.getString(R.string.force_loaded_from_uri, "mock://repo-a/book-one.org"))))); - } - /* * Book is left with out-of-sync icon when it's modified, then force-loaded. * This is because book's mtime was not being updated and was greater then remote book's mtime. @@ -296,69 +215,6 @@ public void testForceLoadingMultipleBooks() { onNoteInBook(1, R.id.item_head_title_view).check(matches(withText("Note 1"))); } - @Test - public void testForceSavingBookWithNoLinkAndMultipleRepos() { - testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); - testUtils.setupRepo(RepoType.MOCK, "mock://repo-b"); - testUtils.setupBook("book-one", "First book used for testing\n* Note A"); - testUtils.setupBook("book-two", "Second book used for testing\n* Note 1\n* Note 2"); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("book-one"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_save)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - - onBook(0, R.id.item_book_last_action) - .check(matches(withText(endsWith( - context.getString(R.string.force_saving_failed, context.getString(R.string.multiple_repos)))))); - - } - - @Test - public void testForceSavingBookWithNoLinkNoRepos() { - testUtils.setupBook("book-one", "First book used for testing\n* Note A"); - testUtils.setupBook("book-two", "Second book used for testing\n* Note 1\n* Note 2"); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("book-one"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_save)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - onBook(0, R.id.item_book_last_action) - .check(matches(withText(endsWith( - context.getString(R.string.force_saving_failed, context.getString(R.string.no_repos)))))); - } - - @Test - public void testForceSavingBookWithNoLinkSingleRepo() { - testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); - testUtils.setupBook("book-one", "First book used for testing\n* Note A"); - testUtils.setupBook("book-two", "Second book used for testing\n* Note 1\n* Note 2"); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("book-one"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_save)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - - onBook(0, R.id.item_book_last_action) - .check(matches(withText(endsWith( - context.getString(R.string.force_saved_to_uri, "mock://repo-a/book-one.org"))))); - } - - @Test - public void testForceSavingBookWithLink() { - Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); - testUtils.setupBook("booky", "First book used for testing\n* Note A", repo); - scenario = ActivityScenario.launch(MainActivity.class); - - onView(allOf(withText("booky"), isDisplayed())).perform(longClick()); - onView(withId(R.id.books_context_menu_force_save)).perform(click()); - onView(withText(R.string.overwrite)).perform(click()); - - onBook(0, R.id.item_book_last_action) - .check(matches(withText(endsWith( - context.getString(R.string.force_saved_to_uri, "mock://repo-a/booky.org"))))); - } - @Test public void testForceSavingMultipleBooks() { Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); @@ -784,21 +640,6 @@ public void testSettingLinkForLoadedOrgTxtBook() { onBook(0, R.id.item_book_synced_url).check(matches(allOf(withText("mock://repo-a/booky.org.txt"), isDisplayed()))); } - @Test - public void testSpaceSeparatedBookName() { - Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); - testUtils.setupRook(repo, "mock://repo-a/Book%20Name.org", "", "1abcdef", 1400067155); - - scenario = ActivityScenario.launch(MainActivity.class); - - sync(); - - onBook(0, R.id.item_book_synced_url) - .check(matches(allOf(withText("mock://repo-a/Book%20Name.org"), isDisplayed()))); - onBook(0, R.id.item_book_last_action) - .check(matches(allOf(withText(endsWith("Loaded from mock://repo-a/Book%20Name.org")), isDisplayed()))); - } - @Test public void testRenameModifiedBook() { testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java b/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java index d0546380c..3db2b9425 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java @@ -73,6 +73,7 @@ public void testLoadRook() throws IOException { assertEquals("remote-book-1", BookName.fromRook(book.getSyncedTo()).getName()); assertEquals("0abcdef", book.getSyncedTo().getRevision()); assertEquals(1400067156000L, book.getSyncedTo().getMtime()); + assertEquals(repo.getUrl(), vrook.getRepoUri().toString()); } @Test diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java b/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java index a26ae2a5c..8a56286b1 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java @@ -118,7 +118,6 @@ public void testListDownloadsDirectory() throws IOException { assertNotNull(repo.getBooks()); } - // TODO: Do the same for dropbox repo @Test public void testRenameBook() { BookView bookView; diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DocumentRepoTest.kt b/app/src/androidTest/java/com/orgzly/android/repos/DocumentRepoTest.kt new file mode 100644 index 000000000..6ee38a981 --- /dev/null +++ b/app/src/androidTest/java/com/orgzly/android/repos/DocumentRepoTest.kt @@ -0,0 +1,203 @@ +package com.orgzly.android.repos + +import android.net.Uri +import android.os.Build +import android.os.SystemClock +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiSelector +import com.orgzly.R +import com.orgzly.android.OrgzlyTest +import com.orgzly.android.db.entity.Repo +import com.orgzly.android.espresso.util.EspressoUtils +import com.orgzly.android.ui.repos.ReposActivity +import org.hamcrest.core.AllOf +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.io.IOException + +class DocumentRepoTest : SyncRepoTest, OrgzlyTest() { + + private lateinit var documentTreeSegment: String + private lateinit var repo: Repo + private lateinit var syncRepo: SyncRepo + private lateinit var repoDirectory: DocumentFile + + @Before + override fun setUp() { + super.setUp() + setupDocumentRepo() + } + + @After + override fun tearDown() { + super.tearDown() + for (file in repoDirectory.listFiles()) { + file.delete() + } + } + + @Test + override fun testGetBooks_singleOrgFile() { + SyncRepoTest.testGetBooks_singleOrgFile(repoDirectory, syncRepo) + } + + @Test + override fun testGetBooks_singleFileInSubfolder() { + SyncRepoTest.testGetBooks_singleFileInSubfolder(repoDirectory, syncRepo) + } + + @Test + override fun testGetBooks_allFilesAreIgnored() { + SyncRepoTest.testGetBooks_allFilesAreIgnored(repoDirectory, syncRepo) + } + + @Test + override fun testGetBooks_specificFileInSubfolderIsIgnored() { + SyncRepoTest.testGetBooks_specificFileInSubfolderIsIgnored(repoDirectory, syncRepo) + } + + @Test + override fun testGetBooks_specificFileIsUnignored() { + SyncRepoTest.testGetBooks_specificFileIsUnignored(repoDirectory, syncRepo) + } + + @Test + override fun testGetBooks_ignoredExtensions() { + SyncRepoTest.testGetBooks_ignoredExtensions(repoDirectory, syncRepo) + } + + @Test + override fun testStoreBook_expectedUri() { + SyncRepoTest.testStoreBook_expectedUri(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsRetrieveBook() { + SyncRepoTest.testStoreBook_producesSameUriAsRetrieveBook(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsGetBooks() { + SyncRepoTest.testStoreBook_producesSameUriAsGetBooks(repoDirectory, syncRepo) + } + + @Test + override fun testStoreBook_inSubfolder() { + SyncRepoTest.testStoreBook_inSubfolder(repoDirectory, syncRepo) + } + + @Test + override fun testRenameBook_expectedUri() { + SyncRepoTest.testRenameBook_expectedUri(syncRepo) + } + + @Test(expected = IOException::class) + override fun testRenameBook_repoFileAlreadyExists() { + SyncRepoTest.testRenameBook_repoFileAlreadyExists(repoDirectory, syncRepo) + } + + @Test + override fun testRenameBook_fromRootToSubfolder() { + SyncRepoTest.testRenameBook_fromRootToSubfolder(syncRepo) + } + + @Test + override fun testRenameBook_fromSubfolderToRoot() { + SyncRepoTest.testRenameBook_fromSubfolderToRoot(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderSameLeafName() { + SyncRepoTest.testRenameBook_newSubfolderSameLeafName(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderAndLeafName() { + SyncRepoTest.testRenameBook_newSubfolderAndLeafName(syncRepo) + } + + @Test + override fun testRenameBook_sameSubfolderNewLeafName() { + SyncRepoTest.testRenameBook_sameSubfolderNewLeafName(syncRepo) + } + + private fun setupDocumentRepo(extraDir: String? = null) { + val repoDirName = SyncRepoTest.repoDirName + documentTreeSegment = if (Build.VERSION.SDK_INT < 30) { + "/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2F$repoDirName%2F" + } else { + "/document/primary%3A$repoDirName%2F" + } + var treeDocumentFileUrl = if (Build.VERSION.SDK_INT < 30) { + "content://com.android.providers.downloads.documents/tree/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2F$repoDirName" + } else { + "content://com.android.externalstorage.documents/tree/primary%3A$repoDirName" + } + if (extraDir != null) { + treeDocumentFileUrl = "$treeDocumentFileUrl%2F" + Uri.encode(extraDir) + } + repoDirectory = DocumentFile.fromTreeUri(context, treeDocumentFileUrl.toUri())!! + repo = if (!repoDirectory.exists()) { + if (extraDir != null) { + setupDocumentRepoInUi(extraDir) + } else { + setupDocumentRepoInUi(repoDirName) + } + dataRepository.getRepos()[0] + } else { + testUtils.setupRepo(RepoType.DOCUMENT, treeDocumentFileUrl) + } + syncRepo = testUtils.repoInstance(RepoType.DOCUMENT, repo.url, repo.id) + Assert.assertEquals(treeDocumentFileUrl, repo.url) + } + + /** + * Note that this solution only works the first time the tests are run on any given virtual + * device. On the second run, the file picker will start in a different folder, resulting in + * a different repo URL, making some tests fail. If you are running locally, you must work + * around this by wiping the device's data between test suite runs. + */ + private fun setupDocumentRepoInUi(repoDirName: String) { + ActivityScenario.launch(ReposActivity::class.java).use { + Espresso.onView(ViewMatchers.withId(R.id.activity_repos_directory)) + .perform(ViewActions.click()) + Espresso.onView(ViewMatchers.withId(R.id.activity_repo_directory_browse_button)) + .perform(ViewActions.click()) + SystemClock.sleep(500) + // In Android file browser (Espresso cannot be used): + val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + if (Build.VERSION.SDK_INT < 30) { + // Older system file picker UI + mDevice.findObject(UiSelector().description("More options")).click() + SystemClock.sleep(300) + mDevice.findObject(UiSelector().text("New folder")).click() + SystemClock.sleep(500) + mDevice.findObject(UiSelector().text("Folder name")).text = repoDirName + mDevice.findObject(UiSelector().text("OK")).click() + mDevice.findObject(UiSelector().textContains("ALLOW ACCESS TO")).click() + mDevice.findObject(UiSelector().text("ALLOW")).click() + } else { + mDevice.findObject(UiSelector().description("New folder")).click() + SystemClock.sleep(500) + mDevice.findObject(UiSelector().text("Folder name")).text = repoDirName + mDevice.findObject(UiSelector().text("OK")).click() + mDevice.findObject(UiSelector().text("USE THIS FOLDER")).click() + mDevice.findObject(UiSelector().text("ALLOW")).click() + } + // Back in Orgzly: + SystemClock.sleep(500) + Espresso.onView(ViewMatchers.isRoot()).perform(EspressoUtils.waitId(R.id.fab, 5000)) + Espresso.onView(AllOf.allOf(ViewMatchers.withId(R.id.fab), ViewMatchers.isDisplayed())) + .perform(ViewActions.click()) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java b/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java index 6a85e56a5..35d5c7c3a 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java @@ -42,112 +42,11 @@ public void testUrl() { } @Test - public void testSyncingUrlWithTrailingSlash() throws IOException { + public void testSyncingUrlWithTrailingSlash() { testUtils.setupRepo(RepoType.DROPBOX, randomUrl() + "/"); assertNotNull(testUtils.sync()); } - @Test - public void testRenameBook() throws IOException { - BookView bookView; - String repoUriString = testUtils.repoInstance(RepoType.DROPBOX, randomUrl()).getUri().toString(); - - testUtils.setupRepo(RepoType.DROPBOX, repoUriString); - testUtils.setupBook("booky", ""); - - testUtils.sync(); - bookView = dataRepository.getBookView("booky"); - - assertEquals(repoUriString, bookView.getLinkRepo().getUrl()); - assertEquals(repoUriString, bookView.getSyncedTo().getRepoUri().toString()); - assertEquals(repoUriString + "/booky.org", bookView.getSyncedTo().getUri().toString()); - - dataRepository.renameBook(bookView, "booky-renamed"); - bookView = dataRepository.getBookView("booky-renamed"); - - assertEquals(repoUriString, bookView.getLinkRepo().getUrl()); - assertEquals(repoUriString, bookView.getSyncedTo().getRepoUri().toString()); - assertEquals(repoUriString + "/booky-renamed.org", bookView.getSyncedTo().getUri().toString()); - } - - @Test - public void testIgnoreRulePreventsLinkingBook() throws Exception { - Uri repoUri = testUtils.repoInstance(RepoType.DROPBOX, randomUrl()).getUri(); - testUtils.setupRepo(RepoType.DROPBOX, repoUri.toString()); - uploadFileToRepo(repoUri, RepoIgnoreNode.IGNORE_FILE, "*.org"); - testUtils.setupBook("booky", ""); - exceptionRule.expect(IOException.class); - exceptionRule.expectMessage("matches a rule in .orgzlyignore"); - testUtils.syncOrThrow(); - } - - @Test - public void testIgnoreRulePreventsLoadingBook() throws Exception { - SyncRepo repo = testUtils.repoInstance(RepoType.DROPBOX, randomUrl()); - testUtils.setupRepo(RepoType.DROPBOX, repo.getUri().toString()); - - // Create two .org files - uploadFileToRepo(repo.getUri(), "ignored.org", "1 2 3"); - uploadFileToRepo(repo.getUri(), "notignored.org", "1 2 3"); - // Create .orgzlyignore - uploadFileToRepo(repo.getUri(), RepoIgnoreNode.IGNORE_FILE, "ignored.org"); - testUtils.sync(); - - List bookViews = dataRepository.getBooks(); - assertEquals(1, bookViews.size()); - assertEquals("notignored", bookViews.get(0).getBook().getName()); - } - - @Test - public void testIgnoreRulePreventsRenamingBook() throws Exception { - BookView bookView; - Uri repoUri = testUtils.repoInstance(RepoType.DROPBOX, randomUrl()).getUri(); - testUtils.setupRepo(RepoType.DROPBOX, repoUri.toString()); - uploadFileToRepo(repoUri, RepoIgnoreNode.IGNORE_FILE, "badname*"); - testUtils.setupBook("goodname", ""); - testUtils.sync(); - bookView = dataRepository.getBookView("goodname"); - dataRepository.renameBook(bookView, "badname"); - bookView = dataRepository.getBooks().get(0); - assertTrue( - bookView.getBook() - .getLastAction() - .toString() - .contains("matches a rule in .orgzlyignore") - ); - } - - @Test - public void testDropboxFileRename() throws IOException { - SyncRepo repo = testUtils.repoInstance(RepoType.DROPBOX, randomUrl()); - - assertNotNull(repo); - assertEquals(0, repo.getBooks().size()); - - File file = File.createTempFile("notebook.", ".org"); - MiscUtils.writeStringToFile("1 2 3", file); - - VersionedRook vrook = repo.storeBook(file, file.getName()); - - file.delete(); - - assertEquals(1, repo.getBooks().size()); - - repo.renameBook(vrook.getUri(), "notebook-renamed"); - - assertEquals(1, repo.getBooks().size()); - assertEquals(repo.getUri() + "/notebook-renamed.org", repo.getBooks().get(0).getUri().toString()); - assertEquals("notebook-renamed.org", BookName.fromRook(repo.getBooks().get(0)).getRepoRelativePath()); - } - - private void uploadFileToRepo(Uri repoUri, String fileName, String fileContents) throws IOException { - DropboxClient client = new DropboxClient(App.getAppContext(), 0); - File tmpFile = File.createTempFile("abc", null); - MiscUtils.writeStringToFile(fileContents, tmpFile); - client.upload(tmpFile, repoUri, fileName); - tmpFile.delete(); - } - private String randomUrl() { return "dropbox:"+ DROPBOX_TEST_DIR + "/" + UUID.randomUUID().toString(); } diff --git a/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java b/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java index 31fa1aaca..2fc5f4cac 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java @@ -1,7 +1,24 @@ package com.orgzly.android.repos; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.orgzly.android.espresso.util.EspressoUtils.onBook; +import static org.hamcrest.Matchers.allOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + import android.net.Uri; +import android.os.Build; +import com.orgzly.R; +import com.orgzly.android.BookFormat; import com.orgzly.android.BookName; import com.orgzly.android.LocalStorage; import com.orgzly.android.OrgzlyTest; @@ -15,21 +32,17 @@ import com.orgzly.android.util.MiscUtils; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import java.io.File; import java.io.IOException; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - public class SyncTest extends OrgzlyTest { private static final String TAG = SyncTest.class.getName(); @@ -38,6 +51,9 @@ public void setUp() throws Exception { super.setUp(); } + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + @Test public void testOrgRange() { Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); @@ -226,13 +242,19 @@ public void testOnlyBookWithLink() { } @Test - public void testOnlyBookWithoutLinkAndOneRepo() { - testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + public void testOnlyBookWithoutLinkAndOneRepo() throws IOException { + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); testUtils.setupBook("book-1", "Content"); testUtils.sync(); BookView book = dataRepository.getBooks().get(0); assertEquals(BookSyncStatus.ONLY_BOOK_WITHOUT_LINK_AND_ONE_REPO.toString(), book.getBook().getSyncStatus()); + assertEquals(context.getString(R.string.sync_status_saved, repo.getUrl()), + book.getBook().getLastAction().getMessage()); + assertEquals(repo.getUrl(), book.getLinkRepo().getUrl()); + SyncRepo syncRepo = testUtils.repoInstance(RepoType.MOCK, repo.getUrl()); + assertEquals(1, syncRepo.getBooks().size()); + assertEquals(syncRepo.getBooks().get(0).toString(), book.getSyncedTo().toString()); } @Test @@ -437,7 +459,7 @@ public void testDirectoryFileRename() throws IOException { } @Test - public void testRenameSyncedBook() { + public void testRenameSyncedBook() throws IOException { testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); testUtils.setupBook("Booky", "1 2 3"); @@ -455,6 +477,30 @@ public void testRenameSyncedBook() { assertEquals("mock://repo-a", renamedBook.getLinkRepo().getUrl()); assertEquals("mock://repo-a", renamedBook.getSyncedTo().getRepoUri().toString()); assertEquals("mock://repo-a/BookyRenamed.org", renamedBook.getSyncedTo().getUri().toString()); + assertEquals("1 2 3\n\n", dataRepository.getBookContent("BookyRenamed", BookFormat.ORG)); + } + + @Test + public void testRenameBookToNameWithSpace() throws IOException { + testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + testUtils.setupBook("Booky", "1 2 3"); + + testUtils.sync(); + + BookView book = dataRepository.getBookView("Booky"); + + assertEquals("mock://repo-a/Booky.org", book.getSyncedTo().getUri().toString()); + + dataRepository.renameBook(book, "Booky Renamed"); + + BookView renamedBook = dataRepository.getBookView("Booky Renamed"); + + assertNotNull(renamedBook); + assertEquals("mock://repo-a", renamedBook.getLinkRepo().getUrl()); + assertEquals("mock://repo-a", renamedBook.getSyncedTo().getRepoUri().toString()); + assertEquals("mock://repo-a/Booky%20Renamed.org", + renamedBook.getSyncedTo().getUri().toString()); + assertEquals("1 2 3\n\n", dataRepository.getBookContent("Booky Renamed", BookFormat.ORG)); } @Test @@ -490,6 +536,84 @@ public void testRenameSyncedBookWithDifferentLink() throws IOException { assertEquals("mock://repo-a/Booky.org", book.getSyncedTo().getUri().toString()); } + @Test + public void testRenameBookToExistingBookName() { + testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + testUtils.setupBook("a", ""); + testUtils.setupBook("b", ""); + assertEquals(2, dataRepository.getBooks().size()); + dataRepository.renameBook(dataRepository.getBookView("a"), "b"); + assertTrue(dataRepository.getBook("a") + .getLastAction() + .getMessage() + .contains("Renaming failed: Notebook b already exists") + ); + } + + @Test + public void testIgnoreRulePreventsRenamingBook() { + assumeTrue(Build.VERSION.SDK_INT >= 26); + String ignoreRules = "bad name*\n"; + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + + // Add ignore rules using repo properties (N.B. MockRepo-specific solution) + Map repoPropsMap = new HashMap<>(); + repoPropsMap.put(MockRepo.IGNORE_RULES_PREF_KEY, ignoreRules); + RepoWithProps repoWithProps = new RepoWithProps(repo, repoPropsMap); + dataRepository.updateRepo(repoWithProps); + + // Create book and sync it + testUtils.setupBook("good name", ""); + testUtils.sync(); + BookView bookView = dataRepository.getBookView("good name"); + + dataRepository.renameBook(bookView, "bad name"); + bookView = dataRepository.getBooks().get(0); + assertTrue(bookView.getBook() + .getLastAction() + .toString() + .contains("matches a rule in .orgzlyignore")); + } + + @Test + public void testIgnoreRulePreventsLinkingBook() throws Exception { + assumeTrue(Build.VERSION.SDK_INT >= 26); + String ignoreRules = "*.org\n"; + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + + // Add ignore rules using repo properties (N.B. MockRepo-specific solution) + Map repoPropsMap = new HashMap<>(); + repoPropsMap.put(MockRepo.IGNORE_RULES_PREF_KEY, ignoreRules); + RepoWithProps repoWithProps = new RepoWithProps(repo, repoPropsMap); + dataRepository.updateRepo(repoWithProps); + + // Create book and sync it + testUtils.setupBook("booky", ""); + exceptionRule.expect(IOException.class); + exceptionRule.expectMessage("matches a rule in .orgzlyignore"); + testUtils.syncOrThrow(); + } + + + /** + * Ensures that file names and book names are not parsed/created differently during + * force-loading. + */ + @Test + public void testForceLoadBookInSubfolder() { + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + BookView bookView = testUtils.setupBook("a folder/a book", "content"); + testUtils.sync(); + var books = dataRepository.getBooks(); + assertEquals(1, books.size()); + assertEquals("a folder/a book", books.get(0).getBook().getName()); + dataRepository.forceLoadBook(bookView.getBook().getId()); + books = dataRepository.getBooks(); + assertEquals(1, books.size()); + // Check that the name has not changed + assertEquals("a folder/a book", books.get(0).getBook().getName()); + } + /** * We remove the local book's' syncedTo attribute and repository link when its remote file * has been deleted, to make it easier to ascertain the book's state during subsequent sync @@ -533,4 +657,126 @@ public void testBookStatusAfterMultipleSyncsFollowingRemoteFileDeletion() throws assertNull(book.getLinkRepo()); assertEquals(BookSyncStatus.BOOK_WITH_PREVIOUS_ERROR_AND_NO_LINK.toString(), book.getBook().getSyncStatus()); } + + @Test + public void testSpaceSeparatedBookName() { + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + testUtils.setupRook(repo, "mock://repo-a/Book%20Name.org", "", "1abcdef", 1400067155); + + testUtils.sync(); + + BookView bookView = dataRepository.getBooks().get(0); + assertNotNull(bookView.getSyncedTo()); + assertEquals("Book Name", bookView.getBook().getName()); + assertEquals("mock://repo-a/Book%20Name.org", bookView.getSyncedTo().getUri().toString()); + assertEquals("Loaded from mock://repo-a/Book%20Name.org", + bookView.getBook().getLastAction().getMessage()); + } + + @Test + public void testForceLoadingBookWithLink() throws IOException { + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + testUtils.setupRook(repo, "mock://repo-a/booky.org", "New content", "abc", 1234567890000L); + Book book = testUtils.setupBook("booky", "First book used for testing\n* Note A").getBook(); + dataRepository.setLink(book.getId(), repo); + dataRepository.forceLoadBook(book.getId()); + + assertEquals(context.getString(R.string.force_loaded_from_uri, "mock://repo-a/booky.org") + , dataRepository.getBook(book.getName()).getLastAction().getMessage()); + assertEquals("New content\n\n", dataRepository.getBookContent("booky", BookFormat.ORG)); + } + + /** + * To ensure that book names are not parsed/constructed differently during force load + */ + @Test + public void testForceLoadBookWithSpaceInName() { + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + testUtils.setupRook(repo, "mock://repo-a/Book%20Name.org", "", "1abcdef", 1400067155); + + testUtils.sync(); + + BookView bookView = dataRepository.getBooks().get(0); + assertEquals("Book Name", bookView.getBook().getName()); + + dataRepository.forceLoadBook(bookView.getBook().getId()); + assertEquals("Book Name", dataRepository.getBooks().get(0).getBook().getName()); + } + + @Test + public void testForceLoadingBookWithNoLinkNoRepos() { + BookView book = testUtils.setupBook("booky", "First book used for testing\n* Note A"); + + exceptionRule.expect(IOException.class); + exceptionRule.expectMessage(context.getString(R.string.message_book_has_no_link)); + dataRepository.forceLoadBook(book.getBook().getId()); + } + + @Test + public void testForceLoadingBookWithNoLinkSingleRepo() { + testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + BookView book = testUtils.setupBook("booky", "First book used for testing\n* Note A"); + + exceptionRule.expect(IOException.class); + exceptionRule.expectMessage(context.getString(R.string.message_book_has_no_link)); + dataRepository.forceLoadBook(book.getBook().getId()); + } + + /* Books view was returning multiple entries for the same book, due to duplicates in encodings + * table. The last statement in this method will fail if there are multiple books matching. + */ + @Test + public void testForceLoadingMultipleTimes() { + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + testUtils.setupRook(repo, "mock://repo-a/book-one.org", "New content", "abc", 1234567890000L); + Book book = testUtils.setupBook("book-one", "First book used for testing\n* Note A").getBook(); + dataRepository.setLink(book.getId(), repo); + dataRepository.forceLoadBook(book.getId()); + assertEquals( + context.getString(R.string.force_loaded_from_uri, "mock://repo-a/book-one.org"), + dataRepository.getBook(book.getId()).getLastAction().getMessage() + ); + dataRepository.forceLoadBook(book.getId()); + assertEquals( + context.getString(R.string.force_loaded_from_uri, "mock://repo-a/book-one.org"), + dataRepository.getBook(book.getId()).getLastAction().getMessage() + ); + } + + @Test + public void testForceSavingBookWithNoLinkAndMultipleRepos() { + testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + testUtils.setupRepo(RepoType.MOCK, "mock://repo-b"); + Book book = testUtils.setupBook("book-one", "First book used for testing\n* Note A").getBook(); + exceptionRule.expect(IOException.class); + exceptionRule.expectMessage(context.getString(R.string.force_saving_failed, context.getString(R.string.multiple_repos))); + dataRepository.forceSaveBook(book.getId()); + } + + @Test + public void testForceSavingBookWithNoLinkNoRepos() { + Book book = testUtils.setupBook("book-one", "First book used for testing\n* Note A").getBook(); + exceptionRule.expect(IOException.class); + exceptionRule.expectMessage(context.getString(R.string.force_saving_failed, context.getString(R.string.no_repos))); + dataRepository.forceSaveBook(book.getId()); + } + + @Test + public void testForceSavingBookWithNoLinkSingleRepo() { + testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + Book book = testUtils.setupBook("book-one", "First book used for testing\n* Note A").getBook(); + dataRepository.forceSaveBook(book.getId()); + assertEquals(context.getString(R.string.force_saved_to_uri, "mock://repo-a/book-one.org") + , dataRepository.getBook(book.getId()).getLastAction().getMessage()); + } + + @Test + public void testForceSavingBookWithLink() { + Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); + Book book = testUtils.setupBook("booky", "First book used for testing\n* Note A", repo).getBook(); + dataRepository.setLink(book.getId(), repo); + dataRepository.forceSaveBook(book.getId()); + assertEquals(context.getString(R.string.force_saved_to_uri, "mock://repo-a/booky.org") + , dataRepository.getBook(book.getId()).getLastAction().getMessage()); + } } diff --git a/app/src/test/java/com/orgzly/android/repos/DropboxRepoTest.kt b/app/src/test/java/com/orgzly/android/repos/DropboxRepoTest.kt new file mode 100644 index 000000000..1b49db0b4 --- /dev/null +++ b/app/src/test/java/com/orgzly/android/repos/DropboxRepoTest.kt @@ -0,0 +1,135 @@ +package com.orgzly.android.repos + +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.orgzly.BuildConfig +import com.orgzly.android.db.entity.Repo +import com.orgzly.android.prefs.AppPreferences +import org.json.JSONObject +import org.junit.After +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException +import java.util.UUID + +@RunWith(AndroidJUnit4::class) +class DropboxRepoTest : SyncRepoTest { + + private lateinit var syncRepo: SyncRepo + private lateinit var client: DropboxClient + + @Before + fun setup() { + assumeTrue(BuildConfig.DROPBOX_APP_KEY.isNotEmpty()) + assumeTrue(BuildConfig.DROPBOX_REFRESH_TOKEN.isNotEmpty()) + val mockSerializedDbxCredential = JSONObject() + mockSerializedDbxCredential.put("access_token", "dummy") + mockSerializedDbxCredential.put("expires_at", System.currentTimeMillis()) + mockSerializedDbxCredential.put("refresh_token", BuildConfig.DROPBOX_REFRESH_TOKEN) + mockSerializedDbxCredential.put("app_key", BuildConfig.DROPBOX_APP_KEY) + AppPreferences.dropboxSerializedCredential( + ApplicationProvider.getApplicationContext(), + mockSerializedDbxCredential.toString() + ) + val repo = Repo(0, RepoType.DROPBOX, "dropbox:/${SyncRepoTest.repoDirName}/" + UUID.randomUUID().toString()) + val repoPropsMap = HashMap() + val repoWithProps = RepoWithProps(repo, repoPropsMap) + syncRepo = DropboxRepo(repoWithProps, ApplicationProvider.getApplicationContext()) + client = DropboxClient(ApplicationProvider.getApplicationContext(), repo.id) + } + + @After + fun tearDown() { + if (this::syncRepo.isInitialized) { + val dropboxRepo = syncRepo as DropboxRepo + dropboxRepo.deleteDirectory(syncRepo.uri) + } + } + + @Test + override fun testGetBooks_singleOrgFile() { + SyncRepoTest.testGetBooks_singleOrgFile(client, syncRepo) + } + + @Test + override fun testGetBooks_singleFileInSubfolder() { + SyncRepoTest.testGetBooks_singleFileInSubfolder(client, syncRepo) + } + + @Test + override fun testGetBooks_allFilesAreIgnored() { + SyncRepoTest.testGetBooks_allFilesAreIgnored(client, syncRepo) + } + + @Test + override fun testGetBooks_specificFileInSubfolderIsIgnored() { + SyncRepoTest.testGetBooks_specificFileInSubfolderIsIgnored(client, syncRepo) + } + + @Test + override fun testGetBooks_specificFileIsUnignored() { + SyncRepoTest.testGetBooks_specificFileIsUnignored(client, syncRepo) + } + + @Test + override fun testGetBooks_ignoredExtensions() { + SyncRepoTest.testGetBooks_ignoredExtensions(client, syncRepo) + } + + @Test + override fun testStoreBook_expectedUri() { + SyncRepoTest.testStoreBook_expectedUri(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsRetrieveBook() { + SyncRepoTest.testStoreBook_producesSameUriAsRetrieveBook(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsGetBooks() { + SyncRepoTest.testStoreBook_producesSameUriAsGetBooks(client, syncRepo) + } + + @Test + override fun testStoreBook_inSubfolder() { + SyncRepoTest.testStoreBook_inSubfolder(client, syncRepo) + } + + @Test + override fun testRenameBook_expectedUri() { + SyncRepoTest.testRenameBook_expectedUri(syncRepo) + } + + @Test(expected = IOException::class) + override fun testRenameBook_repoFileAlreadyExists() { + SyncRepoTest.testRenameBook_repoFileAlreadyExists(client, syncRepo) + } + + @Test + override fun testRenameBook_fromRootToSubfolder() { + SyncRepoTest.testRenameBook_fromRootToSubfolder(syncRepo) + } + + @Test + override fun testRenameBook_fromSubfolderToRoot() { + SyncRepoTest.testRenameBook_fromSubfolderToRoot(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderSameLeafName() { + SyncRepoTest.testRenameBook_newSubfolderSameLeafName(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderAndLeafName() { + SyncRepoTest.testRenameBook_newSubfolderAndLeafName(syncRepo) + } + + @Test + override fun testRenameBook_sameSubfolderNewLeafName() { + SyncRepoTest.testRenameBook_sameSubfolderNewLeafName(syncRepo) + } +} diff --git a/app/src/test/java/com/orgzly/android/repos/GitRepoTest.kt b/app/src/test/java/com/orgzly/android/repos/GitRepoTest.kt new file mode 100644 index 000000000..883be5b03 --- /dev/null +++ b/app/src/test/java/com/orgzly/android/repos/GitRepoTest.kt @@ -0,0 +1,137 @@ +package com.orgzly.android.repos + +import android.content.Context +import androidx.core.net.toUri +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.orgzly.android.db.entity.Repo +import com.orgzly.android.git.GitFileSynchronizer +import com.orgzly.android.git.GitPreferencesFromRepoPrefs +import com.orgzly.android.prefs.AppPreferences +import com.orgzly.android.prefs.RepoPreferences +import org.eclipse.jgit.api.Git +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File +import java.io.IOException +import kotlin.io.path.createTempDirectory + +@RunWith(AndroidJUnit4::class) +class GitRepoTest : SyncRepoTest { + + private lateinit var gitWorkingTree: File + private lateinit var bareRepoDir: File + private lateinit var gitFileSynchronizer: GitFileSynchronizer + private lateinit var syncRepo: SyncRepo + private val context: Context = ApplicationProvider.getApplicationContext() + + @Before + fun setup() { + bareRepoDir = createTempDirectory().toFile() + Git.init().setBare(true).setDirectory(bareRepoDir).call() + AppPreferences.gitIsEnabled(context, true) + val repo = Repo(0, RepoType.GIT, "file://$bareRepoDir") + val repoPreferences = RepoPreferences(context, repo.id, repo.url.toUri()) + val gitPreferences = GitPreferencesFromRepoPrefs(repoPreferences) + gitWorkingTree = File(gitPreferences.repositoryFilepath()) + gitWorkingTree.mkdirs() + val git = GitRepo.ensureRepositoryExists(gitPreferences, true, null) + gitFileSynchronizer = GitFileSynchronizer(git, gitPreferences) + val repoPropsMap = HashMap() + val repoWithProps = RepoWithProps(repo, repoPropsMap) + syncRepo = GitRepo.getInstance(repoWithProps, context) + } + + @After + fun tearDown() { + gitWorkingTree.deleteRecursively() + bareRepoDir.deleteRecursively() + } + + @Test + override fun testGetBooks_singleOrgFile() { + SyncRepoTest.testGetBooks_singleOrgFile(gitWorkingTree, syncRepo) + } + + @Test + override fun testGetBooks_singleFileInSubfolder() { + SyncRepoTest.testGetBooks_singleFileInSubfolder(gitWorkingTree, syncRepo) + } + + @Test + override fun testGetBooks_allFilesAreIgnored() { + SyncRepoTest.testGetBooks_allFilesAreIgnored(gitWorkingTree, syncRepo) + } + + @Test + override fun testGetBooks_specificFileInSubfolderIsIgnored() { + SyncRepoTest.testGetBooks_specificFileInSubfolderIsIgnored(gitWorkingTree, syncRepo) + } + + @Test + override fun testGetBooks_specificFileIsUnignored() { + SyncRepoTest.testGetBooks_specificFileIsUnignored(gitWorkingTree, syncRepo) + } + + @Test + override fun testGetBooks_ignoredExtensions() { + SyncRepoTest.testGetBooks_ignoredExtensions(gitWorkingTree, syncRepo) + } + + @Test + override fun testStoreBook_expectedUri() { + SyncRepoTest.testStoreBook_expectedUri(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsRetrieveBook() { + SyncRepoTest.testStoreBook_producesSameUriAsRetrieveBook(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsGetBooks() { + SyncRepoTest.testStoreBook_producesSameUriAsGetBooks(gitWorkingTree, syncRepo) + } + + @Test + override fun testStoreBook_inSubfolder() { + SyncRepoTest.testStoreBook_inSubfolder(gitWorkingTree, syncRepo) + } + + @Test + override fun testRenameBook_expectedUri() { + SyncRepoTest.testRenameBook_expectedUri(syncRepo) + } + + @Test(expected = IOException::class) + override fun testRenameBook_repoFileAlreadyExists() { + SyncRepoTest.testRenameBook_repoFileAlreadyExists(gitWorkingTree, syncRepo) + } + + @Test + override fun testRenameBook_fromRootToSubfolder() { + SyncRepoTest.testRenameBook_fromRootToSubfolder(syncRepo) + } + + @Test + override fun testRenameBook_fromSubfolderToRoot() { + SyncRepoTest.testRenameBook_fromSubfolderToRoot(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderSameLeafName() { + SyncRepoTest.testRenameBook_newSubfolderSameLeafName(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderAndLeafName() { + SyncRepoTest.testRenameBook_newSubfolderAndLeafName(syncRepo) + } + + @Test + override fun testRenameBook_sameSubfolderNewLeafName() { + SyncRepoTest.testRenameBook_sameSubfolderNewLeafName(syncRepo) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/orgzly/android/repos/WebdavRepoTest.kt b/app/src/test/java/com/orgzly/android/repos/WebdavRepoTest.kt new file mode 100644 index 000000000..273e1b752 --- /dev/null +++ b/app/src/test/java/com/orgzly/android/repos/WebdavRepoTest.kt @@ -0,0 +1,138 @@ +package com.orgzly.android.repos + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.orgzly.android.db.entity.Repo +import com.orgzly.android.repos.WebdavRepo.Companion.PASSWORD_PREF_KEY +import com.orgzly.android.repos.WebdavRepo.Companion.USERNAME_PREF_KEY +import io.github.atetzner.webdav.server.MiltonWebDAVFileServer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File +import java.io.IOException + + +@RunWith(AndroidJUnit4::class) +class WebdavRepoTest : SyncRepoTest { + + private val serverUrl = "http://localhost:8081" + + private lateinit var serverRootDir: File + private lateinit var localServer: MiltonWebDAVFileServer + private lateinit var syncRepo: SyncRepo + private lateinit var tmpFile: File + + @Before + fun setup() { + serverRootDir = java.nio.file.Files.createTempDirectory("orgzly-webdav-test-").toFile() + localServer = MiltonWebDAVFileServer(serverRootDir) + localServer.userCredentials["user"] = "secret" + localServer.start() + val repo = Repo(0, RepoType.WEBDAV, serverUrl) + val repoPropsMap = HashMap() + repoPropsMap[USERNAME_PREF_KEY] = "user" + repoPropsMap[PASSWORD_PREF_KEY] = "secret" + val repoWithProps = RepoWithProps(repo, repoPropsMap) + syncRepo = WebdavRepo.getInstance(repoWithProps) + assertEquals(serverUrl, repo.url) + tmpFile = kotlin.io.path.createTempFile().toFile() + } + + @After + fun tearDown() { + tmpFile.delete() + if (this::localServer.isInitialized) { + localServer.stop() + } + if (this::serverRootDir.isInitialized) { + serverRootDir.deleteRecursively() + } + } + + @Test + override fun testGetBooks_singleOrgFile() { + SyncRepoTest.testGetBooks_singleOrgFile(serverRootDir, syncRepo) + } + + @Test + override fun testGetBooks_singleFileInSubfolder() { + SyncRepoTest.testGetBooks_singleFileInSubfolder(serverRootDir, syncRepo) + } + + @Test + override fun testGetBooks_allFilesAreIgnored() { + SyncRepoTest.testGetBooks_allFilesAreIgnored(serverRootDir, syncRepo) + } + + @Test + override fun testGetBooks_specificFileInSubfolderIsIgnored() { + SyncRepoTest.testGetBooks_specificFileInSubfolderIsIgnored(serverRootDir, syncRepo) + } + + @Test + override fun testGetBooks_specificFileIsUnignored() { + SyncRepoTest.testGetBooks_specificFileIsUnignored(serverRootDir, syncRepo) + } + + @Test + override fun testGetBooks_ignoredExtensions() { + SyncRepoTest.testGetBooks_ignoredExtensions(serverRootDir, syncRepo) + } + + @Test + override fun testStoreBook_expectedUri() { + SyncRepoTest.testStoreBook_expectedUri(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsRetrieveBook() { + SyncRepoTest.testStoreBook_producesSameUriAsRetrieveBook(syncRepo) + } + + @Test + override fun testStoreBook_producesSameUriAsGetBooks() { + SyncRepoTest.testStoreBook_producesSameUriAsGetBooks(serverRootDir, syncRepo) + } + + @Test + override fun testStoreBook_inSubfolder() { + SyncRepoTest.testStoreBook_inSubfolder(serverRootDir, syncRepo) + } + + @Test + override fun testRenameBook_expectedUri() { + SyncRepoTest.testRenameBook_expectedUri(syncRepo) + } + + @Test(expected = IOException::class) + override fun testRenameBook_repoFileAlreadyExists() { + SyncRepoTest.testRenameBook_repoFileAlreadyExists(serverRootDir, syncRepo) + } + + @Test + override fun testRenameBook_fromRootToSubfolder() { + SyncRepoTest.testRenameBook_fromRootToSubfolder(syncRepo) + } + + @Test + override fun testRenameBook_fromSubfolderToRoot() { + SyncRepoTest.testRenameBook_fromSubfolderToRoot(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderSameLeafName() { + SyncRepoTest.testRenameBook_newSubfolderSameLeafName(syncRepo) + } + + @Test + override fun testRenameBook_newSubfolderAndLeafName() { + SyncRepoTest.testRenameBook_newSubfolderAndLeafName(syncRepo) + } + + @Test + override fun testRenameBook_sameSubfolderNewLeafName() { + SyncRepoTest.testRenameBook_sameSubfolderNewLeafName(syncRepo) + } +} \ No newline at end of file diff --git a/shared-test/.gitignore b/shared-test/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/shared-test/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/shared-test/build.gradle b/shared-test/build.gradle new file mode 100644 index 000000000..2554bd91f --- /dev/null +++ b/shared-test/build.gradle @@ -0,0 +1,51 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +android { + namespace = "com.orgzly.shared.test" + compileSdk 33 + + defaultConfig { + minSdk 21 + buildConfigField "String", "DROPBOX_APP_KEY", gradle.ext.appProperties.getProperty("dropbox.app_key", '""') + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + buildConfigField "String", "DROPBOX_REFRESH_TOKEN", gradle.ext.appProperties.getProperty("dropbox.refresh_token", '""') + } + } + flavorDimensions "store" + productFlavors { + premium { + dimension "store" + } + + fdroid { + dimension "store" + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = 17 + } + + packagingOptions { + resources.merges.add("plugin.properties") + } +} + +dependencies { + implementation project(":app") + implementation "junit:junit:$versions.junit" + implementation 'androidx.documentfile:documentfile:1.0.1' + implementation "org.eclipse.jgit:org.eclipse.jgit:$versions.jgit" +} diff --git a/shared-test/proguard-rules.pro b/shared-test/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/shared-test/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/shared-test/src/main/AndroidManifest.xml b/shared-test/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a522a4c23 --- /dev/null +++ b/shared-test/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/shared-test/src/main/java/com/orgzly/android/repos/SyncRepoTest.kt b/shared-test/src/main/java/com/orgzly/android/repos/SyncRepoTest.kt new file mode 100644 index 000000000..385910575 --- /dev/null +++ b/shared-test/src/main/java/com/orgzly/android/repos/SyncRepoTest.kt @@ -0,0 +1,422 @@ +package com.orgzly.android.repos + +import android.annotation.SuppressLint +import android.net.Uri +import android.os.Build +import androidx.documentfile.provider.DocumentFile +import com.orgzly.android.BookName +import com.orgzly.android.util.MiscUtils +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.storage.file.FileRepositoryBuilder +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import java.io.File +import java.io.IOException + +@SuppressLint("NewApi") +interface SyncRepoTest { + + fun testGetBooks_singleOrgFile() + fun testGetBooks_singleFileInSubfolder() + fun testGetBooks_allFilesAreIgnored() + fun testGetBooks_specificFileInSubfolderIsIgnored() + fun testGetBooks_specificFileIsUnignored() + fun testGetBooks_ignoredExtensions() + fun testStoreBook_expectedUri() + fun testStoreBook_producesSameUriAsRetrieveBook() + fun testStoreBook_producesSameUriAsGetBooks() + fun testStoreBook_inSubfolder() + fun testRenameBook_expectedUri() + fun testRenameBook_repoFileAlreadyExists() + fun testRenameBook_fromRootToSubfolder() + fun testRenameBook_fromSubfolderToRoot() + fun testRenameBook_newSubfolderSameLeafName() + fun testRenameBook_newSubfolderAndLeafName() + fun testRenameBook_sameSubfolderNewLeafName() + + companion object { + + const val repoDirName = "orgzly-android-test" + private var treeDocumentFileExtraSegment = if (Build.VERSION.SDK_INT < 30) { + "/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2F$repoDirName%2F" + } else { + "/document/primary%3A$repoDirName%2F" + } + + fun testGetBooks_singleOrgFile(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val fileContent = "\n\n...\n\n" + val fileName = "Book one.org" + val expectedRookUri = writeFileToRepo(fileContent, syncRepo, repoManipulationPoint, fileName) + + // When + val books = syncRepo.books + val retrieveBookDestinationFile = kotlin.io.path.createTempFile().toFile() + syncRepo.retrieveBook(fileName, retrieveBookDestinationFile) + + // Then + assertEquals(1, books.size) + assertEquals(expectedRookUri, books[0].uri.toString()) + assertEquals(fileContent, retrieveBookDestinationFile.readText()) + assertEquals(fileName, BookName.getRepoRelativePath(syncRepo.uri, books[0].uri)) + } + + fun testGetBooks_singleFileInSubfolder(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val repoFilePath = "Folder/Book one.org" + val fileContent = "\n\n...\n\n" + val expectedRookUri = writeFileToRepo(fileContent, syncRepo, repoManipulationPoint, "Book one.org", "Folder") + + // When + val books = syncRepo.books + val retrieveBookDestinationFile = kotlin.io.path.createTempFile().toFile() + syncRepo.retrieveBook(repoFilePath, retrieveBookDestinationFile) + + // Then + assertEquals(1, books.size) + assertEquals(expectedRookUri, books[0].uri.toString()) + assertEquals(repoFilePath, BookName.getRepoRelativePath(syncRepo.uri, books[0].uri)) + assertEquals(fileContent, retrieveBookDestinationFile.readText()) + } + + fun testGetBooks_allFilesAreIgnored(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val ignoreFileContent = "*\n" + writeFileToRepo("...", syncRepo, repoManipulationPoint, "book one.org", "folder") + writeFileToRepo(ignoreFileContent, syncRepo, repoManipulationPoint, RepoIgnoreNode.IGNORE_FILE) + // When + val books = syncRepo.books + // Then + assertEquals(0, books.size) + } + + fun testGetBooks_specificFileInSubfolderIsIgnored(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val ignoreFileContent = "folder/book one.org\n" + writeFileToRepo("...", syncRepo, repoManipulationPoint, "book one.org", "folder") + writeFileToRepo(ignoreFileContent, syncRepo, repoManipulationPoint, RepoIgnoreNode.IGNORE_FILE) + // When + val books = syncRepo.books + // Then + assertEquals(0, books.size) + } + fun testGetBooks_specificFileIsUnignored(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val folderName = "My Folder" + val fileName = "My file.org" + val ignoreFileContent = "folder/**\n!$folderName/$fileName\n" + writeFileToRepo("...", syncRepo, repoManipulationPoint, fileName, folderName) + writeFileToRepo(ignoreFileContent, syncRepo, repoManipulationPoint, RepoIgnoreNode.IGNORE_FILE) + // When + val books = syncRepo.books + // Then + assertEquals(1, books.size) + } + + fun testGetBooks_ignoredExtensions(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val testBookContent = "\n\n...\n\n" + for (fileName in arrayOf("file one.txt", "file two.o", "file three.org")) { + writeFileToRepo(testBookContent, syncRepo, repoManipulationPoint, fileName) + } + // When + val books = syncRepo.books + // Then + assertEquals(1, books.size.toLong()) + assertEquals("file three", BookName.fromRepoRelativePath(BookName.getRepoRelativePath(syncRepo.uri, books[0].uri)).name) + } + + fun testStoreBook_expectedUri(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + MiscUtils.writeStringToFile("...", tmpFile) + // When + val vrook = syncRepo.storeBook(tmpFile, "Book one.org") + tmpFile.delete() + // Then + val expectedRookUri = when (syncRepo) { + is GitRepo -> "/Book one.org" + is DocumentRepo -> syncRepo.uri.toString() + treeDocumentFileExtraSegment + "Book%20one.org" + else -> syncRepo.uri.toString() + "/Book%20one.org" + } + assertEquals(expectedRookUri, vrook.uri.toString()) + } + + fun testStoreBook_producesSameUriAsRetrieveBook(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + val repositoryPath = "a folder/a book.org" + MiscUtils.writeStringToFile("...", tmpFile) + // When + val storedRook = syncRepo.storeBook(tmpFile, repositoryPath) + val retrievedBook = syncRepo.retrieveBook(repositoryPath, tmpFile) + tmpFile.delete() + // Then + assertEquals(retrievedBook.uri, storedRook.uri) + } + + fun testStoreBook_producesSameUriAsGetBooks(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + val folderName = "A folder" + val fileName = "A book.org" + writeFileToRepo("...", syncRepo, repoManipulationPoint, fileName, folderName) + // When + val gottenBook = syncRepo.books[0] + MiscUtils.writeStringToFile("......", tmpFile) // N.B. Different content to ensure the repo file is actually changed + val storedRook = syncRepo.storeBook(tmpFile, "$folderName/$fileName") + tmpFile.delete() + // Then + assertEquals(gottenBook.uri, storedRook.uri) + } + + fun testStoreBook_inSubfolder(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + val repositoryPath = "A folder/A book.org" + val testBookContent = "\n\n...\n\n" + MiscUtils.writeStringToFile(testBookContent, tmpFile) + // When + syncRepo.storeBook(tmpFile, repositoryPath) + tmpFile.delete() + // Then + when (syncRepo) { + is WebdavRepo -> { + repoManipulationPoint as File + val subFolder = File(repoManipulationPoint, "A folder") + assertTrue(subFolder.exists()) + val bookFile = File(subFolder, "A book.org") + assertTrue(bookFile.exists()) + assertEquals(testBookContent, bookFile.readText()) + } + is GitRepo -> { + repoManipulationPoint as File + val git = Git( + FileRepositoryBuilder() + .addCeilingDirectory(repoManipulationPoint) + .findGitDir(repoManipulationPoint) + .build() + ) + git.pull().call() + val subFolder = File(repoManipulationPoint, "A folder") + assertTrue(subFolder.exists()) + val bookFile = File(subFolder, "A book.org") + assertTrue(bookFile.exists()) + assertEquals(testBookContent, bookFile.readText()) + } + is DocumentRepo -> { + repoManipulationPoint as DocumentFile + val subFolder = repoManipulationPoint.findFile("A folder") + assertTrue(subFolder!!.exists()) + assertTrue(subFolder.isDirectory) + val bookFile = subFolder.findFile("A book.org") + assertTrue(bookFile!!.exists()) + assertEquals(testBookContent, MiscUtils.readStringFromDocumentFile(bookFile)) + } + is DropboxRepo -> { + // Not really much to assert here; we don't really care how Dropbox implements things, + // as long as URLs work as expected. + repoManipulationPoint as DropboxClient + val retrievedFile = kotlin.io.path.createTempFile().toFile() + repoManipulationPoint.download(syncRepo.uri, repositoryPath, retrievedFile) + assertEquals(testBookContent, retrievedFile.readText()) + } + } + } + + fun testRenameBook_expectedUri(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + val oldFileName = "Original book.org" + val newBookName = "Renamed book" + val testBookContent = "\n\n...\n\n" + MiscUtils.writeStringToFile(testBookContent, tmpFile) + // When + val originalVrook = syncRepo.storeBook(tmpFile, oldFileName) + tmpFile.delete() + syncRepo.renameBook(originalVrook.uri, newBookName) + // Then + val renamedVrook = syncRepo.books[0] + val expectedRookUri = when (syncRepo) { + is GitRepo -> "/Renamed book.org" + is DocumentRepo -> syncRepo.uri.toString() + treeDocumentFileExtraSegment + "Renamed%20book.org" + else -> syncRepo.uri.toString() + "/Renamed%20book.org" + } + assertEquals(expectedRookUri, renamedVrook.uri.toString()) + } + + fun testRenameBook_repoFileAlreadyExists(repoManipulationPoint: Any, syncRepo: SyncRepo) { + // Given + for (fileName in arrayOf("Original.org", "Renamed.org")) { + writeFileToRepo("...", syncRepo, repoManipulationPoint, fileName) + } + val retrievedBookFile = kotlin.io.path.createTempFile().toFile() + // When + val originalRook = syncRepo.retrieveBook("Original.org", retrievedBookFile) + try { + syncRepo.renameBook(originalRook.uri, "Renamed") + } catch (e: IOException) { + // Then + assertTrue(e.message!!.contains("Renamed.org already exists")) + throw e + } finally { + retrievedBookFile.delete() + } + } + + fun testRenameBook_fromRootToSubfolder(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + MiscUtils.writeStringToFile("...", tmpFile) + // When + val originalRook = syncRepo.storeBook(tmpFile, "Original book.org") + tmpFile.delete() + val renamedRook = syncRepo.renameBook(originalRook.uri, "A folder/Renamed book") + // Then + val expectedRookUri = when (syncRepo) { + is GitRepo -> "/A folder/Renamed book.org" + is DocumentRepo -> syncRepo.uri.toString() + treeDocumentFileExtraSegment + "A%20folder%2FRenamed%20book.org" + else -> syncRepo.uri.toString() + "/A%20folder/Renamed%20book.org" + } + assertEquals(expectedRookUri, renamedRook.uri.toString()) + } + + fun testRenameBook_fromSubfolderToRoot(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + MiscUtils.writeStringToFile("...", tmpFile) + // When + val originalRook = syncRepo.storeBook(tmpFile, "A folder/Original book.org") + tmpFile.delete() + val renamedRook = syncRepo.renameBook(originalRook.uri, "Renamed book") + // Then + val expectedRookUri = when (syncRepo) { + is GitRepo -> "/Renamed book.org" + is DocumentRepo -> syncRepo.uri.toString() + treeDocumentFileExtraSegment + "Renamed%20book.org" + else -> syncRepo.uri.toString() + "/Renamed%20book.org" + } + assertEquals(expectedRookUri, renamedRook.uri.toString()) + } + + fun testRenameBook_newSubfolderSameLeafName(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + MiscUtils.writeStringToFile("...", tmpFile) + // When + val originalRook = syncRepo.storeBook(tmpFile, "Old folder/Original book.org") + tmpFile.delete() + val renamedRook = syncRepo.renameBook(originalRook.uri, "New folder/Original book") + // Then + val expectedRookUri = when (syncRepo) { + is GitRepo -> "/New folder/Original book.org" + is DocumentRepo -> syncRepo.uri.toString() + treeDocumentFileExtraSegment + "New%20folder%2FOriginal%20book.org" + else -> syncRepo.uri.toString() + "/New%20folder/Original%20book.org" + } + assertEquals(expectedRookUri, renamedRook.uri.toString()) + } + + fun testRenameBook_newSubfolderAndLeafName(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + MiscUtils.writeStringToFile("...", tmpFile) + // When + val originalRook = syncRepo.storeBook(tmpFile, "old folder/Original book.org") + tmpFile.delete() + val renamedRook = syncRepo.renameBook(originalRook.uri, "new folder/New book") + // Then + val expectedRookUri = when (syncRepo) { + is GitRepo -> "/new folder/New book.org" + is DocumentRepo -> syncRepo.uri.toString() + treeDocumentFileExtraSegment + "new%20folder%2FNew%20book.org" + else -> syncRepo.uri.toString() + "/new%20folder/New%20book.org" + } + assertEquals(expectedRookUri, renamedRook.uri.toString()) + } + + fun testRenameBook_sameSubfolderNewLeafName(syncRepo: SyncRepo) { + // Given + val tmpFile = kotlin.io.path.createTempFile().toFile() + MiscUtils.writeStringToFile("...", tmpFile) + // When + val originalRook = syncRepo.storeBook(tmpFile, "old folder/Original book.org") + tmpFile.delete() + val renamedRook = syncRepo.renameBook(originalRook.uri, "old folder/New book") + // Then + val expectedRookUri = when (syncRepo) { + is GitRepo -> "/old folder/New book.org" + is DocumentRepo -> syncRepo.uri.toString() + treeDocumentFileExtraSegment + "old%20folder%2FNew%20book.org" + else -> syncRepo.uri.toString() + "/old%20folder/New%20book.org" + } + assertEquals(expectedRookUri, renamedRook.uri.toString()) + } + + private fun writeFileToRepo( + content: String, + repo: SyncRepo, + repoManipulationPoint: Any, + fileName: String, + folderName: String? = null + ): String { + var expectedRookUri = repo.uri.toString() + "/" + Uri.encode(fileName) + when (repo) { + is WebdavRepo -> { + var targetDir = repoManipulationPoint as File + if (folderName != null) { + targetDir = File(targetDir.absolutePath + "/$folderName") + targetDir.mkdir() + expectedRookUri = repo.uri.toString() + "/" + Uri.encode("$folderName/$fileName", "/") + } + val remoteBookFile = File(targetDir.absolutePath + "/$fileName") + MiscUtils.writeStringToFile(content, remoteBookFile) + } + is GitRepo -> { + expectedRookUri = "/$fileName" + var targetDir = repoManipulationPoint as File + if (folderName != null) { + expectedRookUri = "/$folderName/$fileName" + targetDir = File(targetDir.absolutePath + "/$folderName") + targetDir.mkdir() + } + MiscUtils.writeStringToFile( + content, + File(targetDir.absolutePath + "/$fileName") + ) + updateGitRepo(repoManipulationPoint) + } + is DocumentRepo -> { + expectedRookUri = repo.uri.toString() + treeDocumentFileExtraSegment + Uri.encode(fileName) + var targetDir = repoManipulationPoint as DocumentFile + if (folderName != null) { + targetDir = targetDir.createDirectory(folderName)!! + expectedRookUri = repo.uri.toString() + treeDocumentFileExtraSegment + Uri.encode("$folderName/$fileName") + } + MiscUtils.writeStringToDocumentFile(content, fileName, targetDir.uri) + } + is DropboxRepo -> { + repoManipulationPoint as DropboxClient + val tmpFile = kotlin.io.path.createTempFile().toFile() + MiscUtils.writeStringToFile(content, tmpFile) + var targetPath = fileName + if (folderName != null) { + targetPath = "$folderName/$fileName" + expectedRookUri = repo.uri.toString() + "/" + Uri.encode("$folderName/$fileName", "/") + } + repoManipulationPoint.upload(tmpFile, repo.uri, targetPath) + tmpFile.delete() + } + } + return expectedRookUri + } + + private fun updateGitRepo(workdir: File) { + val git = Git( + FileRepositoryBuilder() + .addCeilingDirectory(workdir) + .findGitDir(workdir) + .build() + ) + git.add().addFilepattern(".").call() + git.commit().setMessage("").call() + git.push().call() + } + } +} \ No newline at end of file