Skip to content
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
dokka = "2.0.0"
junit4 = "4.13.2"
kotlin = "2.2.0"
okhttpVersion = "4.12.0"
okhttpVersion = "5.1.0"
xpp3Version = "1.1.6"

[libraries]
junit4 = { module = "junit:junit", version.ref = "junit4" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttpVersion" }
okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttpVersion" }
okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver3", version.ref = "okhttpVersion" }
xpp3 = { module = "org.ogce:xpp3", version.ref = "xpp3Version" }

[plugins]
Expand Down
47 changes: 23 additions & 24 deletions src/main/kotlin/at/bitfire/dav4jvm/DavResource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -733,30 +733,29 @@ open class DavResource @JvmOverloads constructor(
if (response.code != HTTP_MULTISTATUS)
throw DavException("Expected 207 Multi-Status, got ${response.code} ${response.message}", httpResponse = response)

val body = response.body ?:
throw DavException("Received 207 Multi-Status without body", httpResponse = response)

body.contentType()?.let { mimeType ->
if (((mimeType.type != "application" && mimeType.type != "text")) || mimeType.subtype != "xml") {
/* Content-Type is not application/xml or text/xml although that is expected here.
Some broken servers return an XML response with some other MIME type. So we try to see
whether the response is maybe XML although the Content-Type is something else. */
try {
val firstBytes = ByteArray(XML_SIGNATURE.size)
body.source().peek().readFully(firstBytes)
if (XML_SIGNATURE.contentEquals(firstBytes)) {
logger.warning("Received 207 Multi-Status that seems to be XML but has MIME type $mimeType")

// response is OK, return and do not throw Exception below
return
response.peekBody(XML_SIGNATURE.size.toLong()).use { body ->
body.contentType()?.let { mimeType ->
if (((mimeType.type != "application" && mimeType.type != "text")) || mimeType.subtype != "xml") {
/* Content-Type is not application/xml or text/xml although that is expected here.
Some broken servers return an XML response with some other MIME type. So we try to see
whether the response is maybe XML although the Content-Type is something else. */
try {
response.peekBody(XML_SIGNATURE.size.toLong()).use { body ->
if (XML_SIGNATURE.contentEquals(body.bytes())) {
logger.warning("Received 207 Multi-Status that seems to be XML but has MIME type $mimeType")

// response is OK, return and do not throw Exception below
return
}
}
} catch (e: Exception) {
logger.log(Level.WARNING, "Couldn't scan for XML signature", e)
}
} catch (e: Exception) {
logger.log(Level.WARNING, "Couldn't scan for XML signature", e)
}

throw DavException("Received non-XML 207 Multi-Status", httpResponse = response)
}
} ?: logger.warning("Received 207 Multi-Status without Content-Type, assuming XML")
throw DavException("Received non-XML 207 Multi-Status", httpResponse = response)
}
} ?: logger.warning("Received 207 Multi-Status without Content-Type, assuming XML")
}
}


Expand All @@ -778,8 +777,8 @@ open class DavResource @JvmOverloads constructor(
protected fun processMultiStatus(response: Response, callback: MultiResponseCallback): List<Property> {
checkStatus(response)
assertMultiStatus(response)
response.body!!.use {
return processMultiStatus(it.charStream(), callback)
return response.body.use {
processMultiStatus(it.charStream(), callback)
}
}

Expand Down
10 changes: 5 additions & 5 deletions src/test/kotlin/at/bitfire/dav4jvm/DavCalendarTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@

package at.bitfire.dav4jvm

import mockwebserver3.MockResponse
import mockwebserver3.MockWebServer
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
Expand All @@ -33,14 +33,14 @@ class DavCalendarTest {

@After
fun stopServer() {
mockServer.shutdown()
mockServer.close()
}


@Test
fun calendarQuery_formatStartEnd() {
val cal = DavCalendar(httpClient, mockServer.url("/"))
mockServer.enqueue(MockResponse().setResponseCode(207).setBody("<multistatus xmlns=\"DAV:\"/>"))
mockServer.enqueue(MockResponse.Builder().code(207).body("<multistatus xmlns=\"DAV:\"/>").build())
cal.calendarQuery("VEVENT",
start = Instant.ofEpochSecond(784111777),
end = Instant.ofEpochSecond(1689324577)) { _, _ -> }
Expand All @@ -57,7 +57,7 @@ class DavCalendarTest {
"</CAL:comp-filter>" +
"</CAL:comp-filter>" +
"</CAL:filter>" +
"</CAL:calendar-query>", rq.body.readUtf8())
"</CAL:calendar-query>", rq.body?.utf8())
}

}
205 changes: 107 additions & 98 deletions src/test/kotlin/at/bitfire/dav4jvm/DavCollectionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,13 @@ import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.dav4jvm.property.webdav.GetETag
import at.bitfire.dav4jvm.property.webdav.NS_WEBDAV
import at.bitfire.dav4jvm.property.webdav.SyncToken
import mockwebserver3.MockResponse
import mockwebserver3.MockWebServer
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import java.net.HttpURLConnection
Expand All @@ -42,7 +39,7 @@ class DavCollectionTest {
fun startServer() = mockServer.start()

@After
fun stopServer() = mockServer.shutdown()
fun stopServer() = mockServer.close()


/**
Expand All @@ -53,58 +50,62 @@ class DavCollectionTest {
val url = sampleUrl()
val collection = DavCollection(httpClient, url)

mockServer.enqueue(MockResponse()
.setResponseCode(207)
mockServer.enqueue(
MockResponse.Builder()
.code(207)
.setHeader("Content-Type", "text/xml; charset=\"utf-8\"")
.setBody("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
" <D:multistatus xmlns:D=\"DAV:\">\n" +
" <D:response>\n" +
" <D:href\n" +
" >${sampleUrl()}test.doc</D:href>\n" +
" <D:propstat>\n" +
" <D:prop>\n" +
" <D:getetag>\"00001-abcd1\"</D:getetag>\n" +
" <R:bigbox xmlns:R=\"urn:ns.example.com:boxschema\">\n" +
" <R:BoxType>Box type A</R:BoxType>\n" +
" </R:bigbox>\n" +
" </D:prop>\n" +
" <D:status>HTTP/1.1 200 OK</D:status>\n" +
" </D:propstat>\n" +
" </D:response>\n" +
" <D:response>\n" +
" <D:href\n" +
" >${sampleUrl()}vcard.vcf</D:href>\n" +
" <D:propstat>\n" +
" <D:prop>\n" +
" <D:getetag>\"00002-abcd1\"</D:getetag>\n" +
" </D:prop>\n" +
" <D:status>HTTP/1.1 200 OK</D:status>\n" +
" </D:propstat>\n" +
" <D:propstat>\n" +
" <D:prop>\n" +
" <R:bigbox xmlns:R=\"urn:ns.example.com:boxschema\"/>\n" +
" </D:prop>\n" +
" <D:status>HTTP/1.1 404 Not Found</D:status>\n" +
" </D:propstat>\n" +
" </D:response>\n" +
" <D:response>\n" +
" <D:href\n" +
" >${sampleUrl()}calendar.ics</D:href>\n" +
" <D:propstat>\n" +
" <D:prop>\n" +
" <D:getetag>\"00003-abcd1\"</D:getetag>\n" +
" </D:prop>\n" +
" <D:status>HTTP/1.1 200 OK</D:status>\n" +
" </D:propstat>\n" +
" <D:propstat>\n" +
" <D:prop>\n" +
" <R:bigbox xmlns:R=\"urn:ns.example.com:boxschema\"/>\n" +
" </D:prop>\n" +
" <D:status>HTTP/1.1 404 Not Found</D:status>\n" +
" </D:propstat>\n" +
" </D:response>\n" +
" <D:sync-token>http://example.com/ns/sync/1234</D:sync-token>\n" +
" </D:multistatus>")
.body(
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
" <D:multistatus xmlns:D=\"DAV:\">\n" +
" <D:response>\n" +
" <D:href\n" +
" >${sampleUrl()}test.doc</D:href>\n" +
" <D:propstat>\n" +
" <D:prop>\n" +
" <D:getetag>\"00001-abcd1\"</D:getetag>\n" +
" <R:bigbox xmlns:R=\"urn:ns.example.com:boxschema\">\n" +
" <R:BoxType>Box type A</R:BoxType>\n" +
" </R:bigbox>\n" +
" </D:prop>\n" +
" <D:status>HTTP/1.1 200 OK</D:status>\n" +
" </D:propstat>\n" +
" </D:response>\n" +
" <D:response>\n" +
" <D:href\n" +
" >${sampleUrl()}vcard.vcf</D:href>\n" +
" <D:propstat>\n" +
" <D:prop>\n" +
" <D:getetag>\"00002-abcd1\"</D:getetag>\n" +
" </D:prop>\n" +
" <D:status>HTTP/1.1 200 OK</D:status>\n" +
" </D:propstat>\n" +
" <D:propstat>\n" +
" <D:prop>\n" +
" <R:bigbox xmlns:R=\"urn:ns.example.com:boxschema\"/>\n" +
" </D:prop>\n" +
" <D:status>HTTP/1.1 404 Not Found</D:status>\n" +
" </D:propstat>\n" +
" </D:response>\n" +
" <D:response>\n" +
" <D:href\n" +
" >${sampleUrl()}calendar.ics</D:href>\n" +
" <D:propstat>\n" +
" <D:prop>\n" +
" <D:getetag>\"00003-abcd1\"</D:getetag>\n" +
" </D:prop>\n" +
" <D:status>HTTP/1.1 200 OK</D:status>\n" +
" </D:propstat>\n" +
" <D:propstat>\n" +
" <D:prop>\n" +
" <R:bigbox xmlns:R=\"urn:ns.example.com:boxschema\"/>\n" +
" </D:prop>\n" +
" <D:status>HTTP/1.1 404 Not Found</D:status>\n" +
" </D:propstat>\n" +
" </D:response>\n" +
" <D:sync-token>http://example.com/ns/sync/1234</D:sync-token>\n" +
" </D:multistatus>"
)
.build()
)
var nrCalled = 0
val result = collection.reportChanges(null, false, null, GetETag.NAME) { response, relation ->
Expand Down Expand Up @@ -147,40 +148,44 @@ class DavCollectionTest {
val url = sampleUrl()
val collection = DavCollection(httpClient, url)

mockServer.enqueue(MockResponse()
.setResponseCode(207)
mockServer.enqueue(
MockResponse.Builder()
.code(207)
.setHeader("Content-Type", "text/xml; charset=\"utf-8\"")
.setBody("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
" <D:multistatus xmlns:D=\"DAV:\">\n" +
" <D:response>\n" +
" <D:href>${sampleUrl()}test.doc</D:href>\n" +
" <D:propstat>\n" +
" <D:prop>\n" +
" <D:getetag>\"00001-abcd1\"</D:getetag>\n" +
" </D:prop>\n" +
" <D:status>HTTP/1.1 200 OK</D:status>\n" +
" </D:propstat>\n" +
" </D:response>\n" +
" <D:response>\n" +
" <D:href>${sampleUrl()}vcard.vcf</D:href>\n" +
" <D:propstat>\n" +
" <D:prop>\n" +
" <D:getetag>\"00002-abcd1\"</D:getetag>\n" +
" </D:prop>\n" +
" <D:status>HTTP/1.1 200 OK</D:status>\n" +
" </D:propstat>\n" +
" </D:response>\n" +
" <D:response>\n" +
" <D:href>${sampleUrl()}removed.txt</D:href>\n" +
" <D:status>HTTP/1.1 404 Not Found</D:status>\n" +
" </D:response>" +
" <D:response>\n" +
" <D:href>${sampleUrl()}</D:href>\n" +
" <D:status>HTTP/1.1 507 Insufficient Storage</D:status>\n" +
" <D:error><D:number-of-matches-within-limits/></D:error>\n" +
" </D:response>" +
" <D:sync-token>http://example.com/ns/sync/1233</D:sync-token>\n" +
" </D:multistatus>")
.body(
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
" <D:multistatus xmlns:D=\"DAV:\">\n" +
" <D:response>\n" +
" <D:href>${sampleUrl()}test.doc</D:href>\n" +
" <D:propstat>\n" +
" <D:prop>\n" +
" <D:getetag>\"00001-abcd1\"</D:getetag>\n" +
" </D:prop>\n" +
" <D:status>HTTP/1.1 200 OK</D:status>\n" +
" </D:propstat>\n" +
" </D:response>\n" +
" <D:response>\n" +
" <D:href>${sampleUrl()}vcard.vcf</D:href>\n" +
" <D:propstat>\n" +
" <D:prop>\n" +
" <D:getetag>\"00002-abcd1\"</D:getetag>\n" +
" </D:prop>\n" +
" <D:status>HTTP/1.1 200 OK</D:status>\n" +
" </D:propstat>\n" +
" </D:response>\n" +
" <D:response>\n" +
" <D:href>${sampleUrl()}removed.txt</D:href>\n" +
" <D:status>HTTP/1.1 404 Not Found</D:status>\n" +
" </D:response>" +
" <D:response>\n" +
" <D:href>${sampleUrl()}</D:href>\n" +
" <D:status>HTTP/1.1 507 Insufficient Storage</D:status>\n" +
" <D:error><D:number-of-matches-within-limits/></D:error>\n" +
" </D:response>" +
" <D:sync-token>http://example.com/ns/sync/1233</D:sync-token>\n" +
" </D:multistatus>"
)
.build()
)
var nrCalled = 0
val result = collection.reportChanges(null, false, null, GetETag.NAME) { response, relation ->
Expand Down Expand Up @@ -227,13 +232,17 @@ class DavCollectionTest {
val url = sampleUrl()
val collection = DavCollection(httpClient, url)

mockServer.enqueue(MockResponse()
.setResponseCode(507)
mockServer.enqueue(
MockResponse.Builder()
.code(507)
.setHeader("Content-Type", "text/xml; charset=\"utf-8\"")
.setBody("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
" <D:error xmlns:D=\"DAV:\">\n" +
" <D:number-of-matches-within-limits/>\n" +
" </D:error>")
.body(
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
" <D:error xmlns:D=\"DAV:\">\n" +
" <D:number-of-matches-within-limits/>\n" +
" </D:error>"
)
.build()
)

try {
Expand All @@ -252,7 +261,7 @@ class DavCollectionTest {
val dav = DavCollection(httpClient, url)

// 201 Created
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_CREATED))
mockServer.enqueue(MockResponse.Builder().code(HttpURLConnection.HTTP_CREATED).build())
var called = false
dav.post(sampleText.toRequestBody("text/plain".toMediaType())) { response ->
assertEquals("POST", mockServer.takeRequest().method)
Expand Down
Loading
Loading