Commit a3fd1ac9 authored by Ricki Hirner's avatar Ricki Hirner

Tests

parent 2a4b0da6
Pipeline #16125552 passed with stages
in 2 minutes 23 seconds
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.dav4android
import at.bitfire.dav4android.property.GetETag
import at.bitfire.dav4android.property.SyncToken
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class DavCollectionTest {
private val httpClient = OkHttpClient.Builder()
.followRedirects(false)
.build()
private val mockServer = MockWebServer()
private fun sampleUrl() = mockServer.url("/dav/")
@Before
fun startServer() = mockServer.start()
@After
fun stopServer() = mockServer.shutdown()
@Test
fun testSyncCollectionReport() {
val url = sampleUrl()
val collection = DavCollection(httpClient, url)
mockServer.enqueue(MockResponse()
.setResponseCode(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>")
)
collection.reportChanges(null, false, null, GetETag.NAME)
assertEquals(2, collection.members.size)
val members = collection.members.iterator()
val member1 = members.next()
assertEquals(sampleUrl().newBuilder().addPathSegment("test.doc").build(), member1.location)
assertEquals("00001-abcd1", member1.properties.get(GetETag::class.java)!!.eTag)
val member2 = members.next()
assertEquals(sampleUrl().newBuilder().addPathSegment("vcard.vcf").build(), member2.location)
assertEquals("00002-abcd1", member2.properties.get(GetETag::class.java)!!.eTag)
assertEquals(1, collection.removedMembers.size)
val removedMember = collection.removedMembers.first()
assertEquals(sampleUrl().newBuilder().addPathSegment("removed.txt").build(), removedMember.location)
assertTrue(collection.furtherResults)
assertEquals("http://example.com/ns/sync/1233", collection.properties[SyncToken::class.java]!!.token)
}
}
\ No newline at end of file
......@@ -8,12 +8,80 @@
package at.bitfire.dav4android
import at.bitfire.dav4android.property.SyncToken
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import java.io.StringWriter
import java.util.logging.Logger
open class DavCollection(
httpClient: OkHttpClient,
location: HttpUrl,
log: Logger = Constants.log
): DavResource(httpClient, location, log)
\ No newline at end of file
): DavResource(httpClient, location, log) {
/**
* Sends a REPORT sync-collection request. If a sync-token is returned, it will be made
* available in [properties].
*
* @param syncToken sync-token to be sent with the request
* @param infiniteDepth sync-level to be sent with the request: false = "1", true = "infinite"
* @param limit maximum number of results (may cause truncation)
* @param properties WebDAV properties to be requested
*/
fun reportChanges(syncToken: String?, infiniteDepth: Boolean, limit: Int?, vararg properties: Property.Name) {
/* <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
<!ELEMENT sync-token CDATA> <!-- Text MUST be a URI -->
<!ELEMENT sync-level CDATA> <!-- Text MUST be either "1" or "infinite" -->
<!ELEMENT limit (nresults) >
<!ELEMENT nresults (#PCDATA)> <!-- only digits -->
<!-- DAV:prop defined in RFC 4918, Section 14.18 -->
*/
val serializer = XmlUtils.newSerializer()
val writer = StringWriter()
serializer.setOutput(writer)
serializer.startDocument("UTF-8", null)
serializer.setPrefix("", XmlUtils.NS_WEBDAV)
serializer.startTag(XmlUtils.NS_WEBDAV, "sync-collection")
serializer.startTag(SyncToken.NAME.namespace, SyncToken.NAME.name)
syncToken?.let { serializer.text(it) }
serializer.endTag(SyncToken.NAME.namespace, SyncToken.NAME.name)
serializer.startTag(XmlUtils.NS_WEBDAV, "sync-level")
serializer.text(if (infiniteDepth) "infinite" else "1")
serializer.endTag(XmlUtils.NS_WEBDAV, "sync-level")
limit?.let { nresults ->
serializer.startTag(XmlUtils.NS_WEBDAV, "limit")
serializer.startTag(XmlUtils.NS_WEBDAV, "nresults")
serializer.text(nresults.toString())
serializer.endTag(XmlUtils.NS_WEBDAV, "nresults")
serializer.endTag(XmlUtils.NS_WEBDAV, "limit")
}
serializer.startTag(XmlUtils.NS_WEBDAV, "prop")
properties.forEach {
serializer.startTag(it.namespace, it.name)
serializer.endTag(it.namespace, it.name)
}
serializer.endTag(XmlUtils.NS_WEBDAV, "prop")
serializer.endTag(XmlUtils.NS_WEBDAV, "sync-collection")
serializer.endDocument()
val response = httpClient.newCall(Request.Builder()
.url(location)
.method("REPORT", RequestBody.create(MIME_XML, writer.toString()))
.header("Depth", "0")
.build()).execute()
checkStatus(response, false)
assertMultiStatus(response)
members.clear()
related.clear()
response.body()?.charStream()?.use { processMultiStatus(it) }
}
}
\ No newline at end of file
......@@ -12,6 +12,7 @@ import at.bitfire.dav4android.exception.*
import at.bitfire.dav4android.property.GetContentType
import at.bitfire.dav4android.property.GetETag
import at.bitfire.dav4android.property.ResourceType
import at.bitfire.dav4android.property.SyncToken
import okhttp3.*
import okhttp3.internal.http.StatusLine
import org.xmlpull.v1.XmlPullParser
......@@ -27,9 +28,9 @@ import java.util.logging.Logger
/**
* Represents a WebDAV resource at the given location.
* @param httpClient #{@link OkHttpClient} to access this object
* @param httpClient [OkHttpClient] to access this object
* @param location location of the WebDAV resource
* @param log #{@link Logger} which will be used for logging, or null for default
* @param log [Logger] which will be used for logging, or null for default
*/
open class DavResource @JvmOverloads constructor(
val httpClient: OkHttpClient,
......@@ -37,16 +38,27 @@ open class DavResource @JvmOverloads constructor(
val log: Logger = Constants.log
) {
val MIME_XML = MediaType.parse("application/xml; charset=utf-8")
val MAX_REDIRECTS = 5
companion object {
val MAX_REDIRECTS = 5
val MIME_XML = MediaType.parse("application/xml; charset=utf-8")
}
/** HTTP capabilities reported by an OPTIONS response */
val capabilities = mutableSetOf<String>()
/** WebDAV properties of this resource */
val properties = PropertyCollection()
/** whether a 507 Insufficient Storage was found in the response */
var furtherResults = false
/** members of this resource */
val members = mutableSetOf<DavResource>()
/** members of this resource with status 404 Not Found */
val removedMembers = mutableSetOf<DavResource>()
/** resources which have been found in the answer, although they aren't members of this resource */
val related = mutableSetOf<DavResource>()
init {
// Don't follow redirects (only useful for GET/POST).
// This means we have to handle 30x responses manually.
......@@ -75,7 +87,7 @@ open class DavResource @JvmOverloads constructor(
val response = httpClient.newCall(Request.Builder()
.method("OPTIONS", null)
.header("Content-Length", "0") // workaround for https://github.com/square/okhttp/issues/2892
.header("Content-Length", "0")
.url(location)
.build()).execute()
checkStatus(response, true)
......@@ -501,15 +513,29 @@ open class DavResource @JvmOverloads constructor(
log.log(Level.FINE, "Received <response> for $href", if (status != null) status else properties)
if (status != null)
// treat an HTTP error of a single response (i.e. requested resource or a member) like an HTTP error of the requested resource
checkStatus(status!!)
var removed = false
var insufficientStorage = false
status?.let {
/* Treat an HTTP error of a single response (i.e. requested resource or a member)
like an HTTP error of the requested resource.
Exceptions for RFC 6578 support:
- 507 Insufficient Storage on the requested resource means there are further results
- members with status 404 Not Found go into removedMembers instead of members
*/
when (it.code) {
404 -> removed = true
507 -> insufficientStorage = true
else -> checkStatus(it)
}
}
// Which resource does this <response> represent?
var target: DavResource? = null
if (UrlUtils.equals(UrlUtils.omitTrailingSlash(href), UrlUtils.omitTrailingSlash(location))) {
// it's about ourselves
if (UrlUtils.equals(UrlUtils.omitTrailingSlash(href), UrlUtils.omitTrailingSlash(location)) && !removed) {
// it's about ourselves (and not 404)
target = this
} else if (location.scheme() == href.scheme() && location.host() == href.host() && location.port() == href.port()) {
val locationSegments = location.pathSegments()
val hrefSegments = href.pathSegments()
......@@ -526,8 +552,12 @@ open class DavResource @JvmOverloads constructor(
if (hrefSegments.size > nBasePathSegments) {
val sameBasePath = (0 until nBasePathSegments).none { locationSegments[it] != hrefSegments[it] }
if (sameBasePath) {
// it's about a member
target = DavResource(httpClient, href, log)
members.add(target)
if (!removed)
members += target
else
removedMembers += target
}
}
}
......@@ -539,6 +569,7 @@ open class DavResource @JvmOverloads constructor(
}
// set properties for target
target.furtherResults = insufficientStorage
target.properties.merge(properties, true)
}
......@@ -548,9 +579,15 @@ open class DavResource @JvmOverloads constructor(
var eventType = parser.eventType
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
if (eventType == XmlPullParser.START_TAG && parser.depth == depth+1 &&
parser.namespace == XmlUtils.NS_WEBDAV && parser.name == "response")
parseMultiStatus_Response()
if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1 && parser.namespace == XmlUtils.NS_WEBDAV)
when (parser.name) {
"response" ->
parseMultiStatus_Response()
"sync-token" ->
XmlUtils.readText(parser)?.let { token ->
properties[SyncToken.NAME] = SyncToken(token)
}
}
eventType = parser.next()
}
}
......
......@@ -20,6 +20,7 @@ interface Property {
val namespace: String,
val name: String
) {
override fun equals(o: Any?): Boolean {
return if (o is Name)
namespace == o.namespace && name == o.name
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment