Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 ==============================================
4 changes: 2 additions & 2 deletions opencloudApp/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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() + "\""
Expand Down
2 changes: 1 addition & 1 deletion opencloudComLibrary/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <response> 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<android.content.Context>() }

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 =
"""<?xml version="1.0" encoding="utf-8"?>
<d:multistatus xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:response>
<d:href>$href</d:href>
<d:propstat>
<d:prop>
<d:getlastmodified>Mon, 23 Jun 2026 10:00:00 GMT</d:getlastmodified>
<d:getcontentlength>12345</d:getcontentlength>
<d:getcontenttype>image/png</d:getcontenttype>
<d:resourcetype/>
<d:getetag>"abc123"</d:getetag>
<oc:id>00000001ocidvalue</oc:id>
<oc:permissions>RDNVW</oc:permissions>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>
""".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)
}
}
14 changes: 14 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -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 =========================================
Loading