diff --git a/build.gradle b/build.gradle index 9a95bc3ce..e6c5b3194 100644 --- a/build.gradle +++ b/build.gradle @@ -167,3 +167,26 @@ def getBranchName() { def name = "git rev-parse --abbrev-ref HEAD".execute() return name.text.toString().trim() } + +// === LOCAL android-dav: run its unit tests as part of the app build ================= +// Composite builds only compile an included build's MAIN source, never its tests. When the +// includeBuild('../android-dav') block in settings.gradle is active, this wires android-dav's +// own ':test' task into the verification lifecycle of opencloudComLibrary (the module that +// consumes it), so `./gradlew :opencloudComLibrary:check`, `check` or `build` also run them. +// It is a no-op when that block is commented out (JitPack artifact in use). +def includedAndroidDav = gradle.includedBuilds.find { it.name == "android-dav" } +if (includedAndroidDav != null) { + tasks.register("androidDavTest") { + group = "verification" + description = "Runs the unit tests of the locally included android-dav build." + dependsOn includedAndroidDav.task(":test") + } + + gradle.projectsEvaluated { + def comLib = subprojects.find { it.name == "opencloudComLibrary" } + comLib?.tasks?.matching { it.name == "check" }?.configureEach { + dependsOn ":androidDavTest" + } + } +} +// === end LOCAL android-dav test wiring ============================================== diff --git a/opencloudApp/build.gradle b/opencloudApp/build.gradle index 1debe35a7..ab17823c0 100644 --- a/opencloudApp/build.gradle +++ b/opencloudApp/build.gradle @@ -118,8 +118,8 @@ android { testInstrumentationRunner "eu.opencloud.android.utils.OCTestAndroidJUnitRunner" - versionCode = 10 - versionName = "1.2.4" + versionCode = 11 + versionName = "1.2.5" buildConfigField "String", gitRemote, "\"" + getGitOriginRemote() + "\"" buildConfigField "String", commitSHA1, "\"" + getLatestGitHash() + "\"" diff --git a/opencloudComLibrary/build.gradle b/opencloudComLibrary/build.gradle index 8d50bd59c..5eedb8491 100644 --- a/opencloudComLibrary/build.gradle +++ b/opencloudComLibrary/build.gradle @@ -6,7 +6,7 @@ apply plugin: 'kotlin-parcelize' dependencies { api 'com.squareup.okhttp3:okhttp:4.9.2' implementation libs.kotlin.stdlib - api 'com.github.opencloud-eu:android-dav:oc_support_2.1.5' + api 'com.github.opencloud-eu:android-dav:oc_support_2.1.6' // Androidx implementation libs.androidx.core.ktx diff --git a/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/ReadRemoteFileOperationBracketTest.kt b/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/ReadRemoteFileOperationBracketTest.kt new file mode 100644 index 000000000..b584b2e6b --- /dev/null +++ b/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/ReadRemoteFileOperationBracketTest.kt @@ -0,0 +1,120 @@ +package eu.opencloud.android.lib.resources.files + +import android.accounts.Account +import android.accounts.AccountManager +import android.net.Uri +import androidx.test.core.app.ApplicationProvider +import eu.opencloud.android.lib.common.OpenCloudAccount +import eu.opencloud.android.lib.common.OpenCloudClient +import eu.opencloud.android.lib.common.accounts.AccountUtils +import eu.opencloud.android.lib.common.authentication.OpenCloudCredentialsFactory +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +/** + * Regression test for the NullPointerException reported in opencloud-eu/android#170 and #93: + * reading a file whose name contains square brackets (e.g. "[B004EXIHDQ].png") returned a + * 207 Multi-Status, but the single was misclassified as not-SELF (because + * UrlUtils.equals choked on "[" / "]") so PropfindMethod.root stayed null and + * ReadRemoteFileOperation crashed on `propFind.root!!`. + * + * The mock server deliberately echoes the href with *literal* brackets while the request URL + * percent-encodes them (%5B / %5D), which is exactly the encoding mismatch that used to make the + * old java.net.URI fallback throw and return `false`. + */ +@RunWith(RobolectricTestRunner::class) +class ReadRemoteFileOperationBracketTest { + + private lateinit var server: MockWebServer + private val context by lazy { ApplicationProvider.getApplicationContext() } + + private val accountType = "com.example" + private val userId = "user-123" + private val username = "user@example.com" + private val token = "TEST_TOKEN" + + @Before + fun setUp() { + server = MockWebServer() + server.start() + } + + @After + fun tearDown() { + server.shutdown() + } + + private fun newClient(): OpenCloudClient { + val base = server.url("/").toString().removeSuffix("/") + + val am = AccountManager.get(context) + val account = Account("$username@${Uri.parse(base).host}", accountType) + am.addAccountExplicitly(account, null, null) + am.setUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL, base) + am.setUserData(account, AccountUtils.Constants.KEY_ID, userId) + + val ocAccount = OpenCloudAccount(account, context) + val client = OpenCloudClient(ocAccount.baseUri, null, true, null, context) + client.account = ocAccount + client.credentials = OpenCloudCredentialsFactory.newBearerCredentials(username, token) + return client + } + + private fun multiStatusBody(href: String): String = + """ + + + $href + + + Mon, 23 Jun 2026 10:00:00 GMT + 12345 + image/png + + "abc123" + 00000001ocidvalue + RDNVW + + HTTP/1.1 200 OK + + + + """.trimIndent() + + @Test + fun readFileWithBracketsInNameDoesNotCrash() { + val client = newClient() + val remotePath = "/Test/Guards! Guards! [B004EXIHDQ].png" + + server.dispatcher = object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + // Simulate a server that returns the href with *literal* brackets, i.e. a different + // (but semantically identical) encoding than the request's %5B / %5D. + val hrefPath = request.path!! + .replace("%5B", "[") + .replace("%5D", "]") + return MockResponse() + .setResponseCode(207) // HTTP Multi-Status + .addHeader("Content-Type", "application/xml; charset=utf-8") + .setBody(multiStatusBody(hrefPath)) + } + } + + val result = ReadRemoteFileOperation(remotePath).execute(client) + + // Before the UrlUtils.equals fix this returned an error wrapping a NullPointerException. + assertTrue("Expected success but got ${result.code} / ${result.exception}", result.isSuccess) + assertNotNull(result.data) + assertEquals("/Test/Guards! Guards! [B004EXIHDQ].png", result.data.remotePath) + } +} diff --git a/settings.gradle b/settings.gradle index a8d400e00..55c462d83 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,15 @@ include ':opencloudApp', ':opencloudDomain', ':opencloudData', ':opencloudComLibrary', ':opencloudTestUtil' + +// === LOCAL android-dav composite build (for testing local android-dav fixes) =========== +// While this block is active, the `com.github.opencloud-eu:android-dav` dependency in +// opencloudComLibrary/build.gradle is transparently replaced by the local ../android-dav +// source (no need to touch that line). Comment this whole block out to go back to the +// published JitPack artifact. + +//includeBuild('../android-dav') { +// dependencySubstitution { +// substitute module('com.github.opencloud-eu:android-dav') using project(':') +// } +//} + +// === end LOCAL android-dav composite build =========================================