diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index bb51aa23d..39275c92b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,7 +22,44 @@ 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: 11 + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + with: + cmdline-tools-version: 9862592 + + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - 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,6 +76,12 @@ 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: 11 + - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 @@ -80,7 +123,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 +134,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/GitRepoTest.kt b/app/src/androidTest/java/com/orgzly/android/repos/GitRepoTest.kt deleted file mode 100644 index 3f4b1a260..000000000 --- a/app/src/androidTest/java/com/orgzly/android/repos/GitRepoTest.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.orgzly.android.repos - -import android.net.Uri -import android.os.Build -import androidx.core.net.toUri -import com.orgzly.R -import com.orgzly.android.OrgzlyTest -import com.orgzly.android.db.entity.BookView -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 com.orgzly.android.util.MiscUtils -import org.eclipse.jgit.api.Git -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Assume -import org.junit.Rule -import org.junit.Test -import org.junit.rules.ExpectedException -import java.io.File -import java.io.IOException -import java.nio.file.Path -import kotlin.io.path.createTempDirectory - -class GitRepoTest : OrgzlyTest() { - - private lateinit var bareRepoPath: Path - private lateinit var repoUri: Uri - private lateinit var gitPreferences: GitPreferencesFromRepoPrefs - private lateinit var workingtree: File - private lateinit var repoPreferences: RepoPreferences - private lateinit var repo: Repo - private lateinit var syncRepo: GitRepo - private lateinit var git: Git - private lateinit var synchronizer: GitFileSynchronizer - - @Rule - @JvmField - var exceptionRule: ExpectedException = ExpectedException.none() - - override fun setUp() { - super.setUp() - bareRepoPath = createTempDirectory() - Git.init().setBare(true).setDirectory(bareRepoPath.toFile()).call() - AppPreferences.gitIsEnabled(context, true) - repoUri = bareRepoPath.toFile().toUri() - repo = testUtils.setupRepo(RepoType.GIT, repoUri.toString()) - repoPreferences = RepoPreferences(context, repo.id, repoUri) - gitPreferences = GitPreferencesFromRepoPrefs(repoPreferences) - workingtree = File(gitPreferences.repositoryFilepath()) - workingtree.mkdirs() - git = GitRepo.ensureRepositoryExists(gitPreferences, true, null) - syncRepo = dataRepository.getRepoInstance(repo.id, RepoType.GIT, repo.url) as GitRepo - synchronizer = GitFileSynchronizer(git, gitPreferences) - } - - override fun tearDown() { - super.tearDown() - testUtils.deleteRepo(repo.url) - workingtree.deleteRecursively() - bareRepoPath.toFile()?.deleteRecursively() - } - - @Test - fun testSyncNewBookWithoutLinkAndOneRepo() { - testUtils.setupBook("book1", "book content") - testUtils.sync() - val bookView = dataRepository.getBooks()[0] - assertEquals(repoUri.toString(), bookView.linkRepo?.url) - assertEquals(1, syncRepo.books.size) - assertEquals(bookView.syncedTo.toString(), syncRepo.books[0].toString()) - assertEquals(context.getString(R.string.sync_status_saved, repo.url), bookView.book.lastAction!!.message) - assertEquals("/book1.org", bookView.syncedTo!!.uri.toString()) - } - - @Test - fun testIgnoredFilesInRepoAreNotLoaded() { - Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - // Create ignore file in working tree and commit - val ignoreFileContents = """ - ignoredbook.org - ignored-*.org - """.trimIndent() - addAndCommitIgnoreFile(ignoreFileContents) - // Add multiple files to repo - for (fileName in arrayOf("ignoredbook.org", "ignored-3.org", "notignored.org")) { - val tmpFile = File.createTempFile("orgzlytest", null) - MiscUtils.writeStringToFile("book content", tmpFile) - synchronizer.addAndCommitNewFile(tmpFile, fileName) - tmpFile.delete() - } - testUtils.sync() - assertEquals(1, syncRepo.books.size) - assertEquals(1, dataRepository.getBooks().size) - assertEquals("notignored", dataRepository.getBooks()[0].book.name) - } - - @Test - fun testUnIgnoredFilesInRepoAreLoaded() { - Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - // Create ignore file in working tree and commit - val ignoreFileContents = """ - *.org - !notignored.org - """.trimIndent() - addAndCommitIgnoreFile(ignoreFileContents) - // Add multiple files to repo - for (fileName in arrayOf("ignoredbook.org", "ignored-3.org", "notignored.org")) { - val tmpFile = File.createTempFile("orgzlytest", null) - MiscUtils.writeStringToFile("book content", tmpFile) - synchronizer.addAndCommitNewFile(tmpFile, fileName) - tmpFile.delete() - } - testUtils.sync() - assertEquals(1, syncRepo.books.size) - assertEquals(1, dataRepository.getBooks().size) - assertEquals("notignored", dataRepository.getBooks()[0].book.name) - } - - @Test - fun testIgnoreRulePreventsLinkingBook() { - Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - addAndCommitIgnoreFile("*.org") - testUtils.setupBook("booky", "") - exceptionRule.expect(IOException::class.java) - exceptionRule.expectMessage("matches a rule in .orgzlyignore") - testUtils.syncOrThrow() - } - - @Test - fun testIgnoreRulePreventsRenamingBook() { - Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - addAndCommitIgnoreFile("badname*") - testUtils.setupBook("goodname", "") - testUtils.sync() - var bookView: BookView? = dataRepository.getBookView("goodname") - dataRepository.renameBook(bookView!!, "badname") - bookView = dataRepository.getBooks()[0] - assertTrue( - bookView.book.lastAction.toString().contains("matches a rule in .orgzlyignore") - ) - } - - private fun addAndCommitIgnoreFile(contents: String) { - val tmpFile = File.createTempFile("orgzlytest", null) - MiscUtils.writeStringToFile(contents, tmpFile) - synchronizer.addAndCommitNewFile(tmpFile, RepoIgnoreNode.IGNORE_FILE) - tmpFile.delete() - } -} \ No newline at end of file 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/settings.gradle b/settings.gradle index 2a35ddd25..6af49b8d1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -34,3 +34,4 @@ if (gradle.ext.appProperties.org_java_directory?.trim()) { } include ':app' +include ':shared-test' 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..f933f5514 --- /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_11 + targetCompatibility JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = 11 + } + + 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