DavResourceTest.kt 20.9 KB
Newer Older
Ricki Hirner's avatar
Ricki Hirner committed
1
/*
Ricki Hirner's avatar
Ricki Hirner committed
2 3 4
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
Ricki Hirner's avatar
Ricki Hirner committed
5 6
 */

7 8
package at.bitfire.dav4android

9
import at.bitfire.dav4android.exception.DavException
10 11
import at.bitfire.dav4android.exception.HttpException
import at.bitfire.dav4android.exception.PreconditionFailedException
12 13 14 15
import at.bitfire.dav4android.property.DisplayName
import at.bitfire.dav4android.property.GetContentType
import at.bitfire.dav4android.property.GetETag
import at.bitfire.dav4android.property.ResourceType
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import java.net.HttpURLConnection

class DavResourceTest {

    private val sampleText = "SAMPLE RESPONSE"

    private val httpClient = OkHttpClient.Builder()
Ricki Hirner's avatar
Ricki Hirner committed
32
            .followRedirects(false)
33 34
            .build()
    private val mockServer = MockWebServer()
35

Ricki Hirner's avatar
Ricki Hirner committed
36

Ricki Hirner's avatar
Ricki Hirner committed
37
    @Before
38 39
    fun startServer() {
        mockServer.start()
Ricki Hirner's avatar
Ricki Hirner committed
40 41
    }

Ricki Hirner's avatar
Ricki Hirner committed
42
    @After
43 44
    fun stopServer() {
        mockServer.shutdown()
Ricki Hirner's avatar
Ricki Hirner committed
45 46
    }

47
    private fun sampleUrl() = mockServer.url("/dav/")
48 49


Ricki Hirner's avatar
Ricki Hirner committed
50
    @Test
51 52 53
    fun testOptions() {
        val url = sampleUrl()
        val dav = DavResource(httpClient, url)
54

55
        mockServer.enqueue(MockResponse()
56
                .setResponseCode(HttpURLConnection.HTTP_OK)
57 58 59 60 61 62
                .setHeader("DAV", "  1,  2 ,3,hyperactive-access"))
        val response = dav.options()
        assertTrue(response.capabilities.contains("1"))
        assertTrue(response.capabilities.contains("2"))
        assertTrue(response.capabilities.contains("3"))
        assertTrue(response.capabilities.contains("hyperactive-access"))
63 64 65

        mockServer.enqueue(MockResponse()
                .setResponseCode(HttpURLConnection.HTTP_OK))
66
        assertTrue(dav.options().capabilities.isEmpty())
67 68
    }

Ricki Hirner's avatar
Ricki Hirner committed
69
    @Test
70 71 72
    fun testGet() {
        val url = sampleUrl()
        val dav = DavResource(httpClient, url)
73 74 75 76

        /* POSITIVE TEST CASES */

        // 200 OK
77
        mockServer.enqueue(MockResponse()
78 79 80
                .setResponseCode(HttpURLConnection.HTTP_OK)
                .setHeader("ETag", "W/\"My Weak ETag\"")
                .setHeader("Content-Type", "application/x-test-result")
81
                .setBody(sampleText))
82 83 84 85 86
        dav.get("*/*").use { responseOK ->
            assertEquals(sampleText, responseOK.body!!.string())
            assertEquals("My Weak ETag", responseOK[GetETag::class.java]?.eTag)
            assertEquals("application/x-test-result", responseOK[GetContentType::class.java]?.type)
        }
87

88 89 90 91
        var rq = mockServer.takeRequest()
        assertEquals("GET", rq.method)
        assertEquals(url.encodedPath(), rq.path)
        assertEquals("*/*", rq.getHeader("Accept"))
92 93

        // 302 Moved Temporarily + 200 OK
94
        mockServer.enqueue(MockResponse()
95 96
                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
                .setHeader("Location", "/target")
97 98
                .setBody("This resource was moved."))
        mockServer.enqueue(MockResponse()
99 100
                .setResponseCode(HttpURLConnection.HTTP_OK)
                .setHeader("ETag", "\"StrongETag\"")
101
                .setBody(sampleText))
102 103 104 105
        dav.get("*/*").use { response302 ->
            assertEquals(sampleText, response302.body!!.string())
            assertEquals("StrongETag", response302[GetETag::class.java]?.eTag)
        }
106

107 108 109 110
        mockServer.takeRequest()
        rq = mockServer.takeRequest()
        assertEquals("GET", rq.method)
        assertEquals("/target", rq.path)
111 112

        // 200 OK without ETag in response
113
        mockServer.enqueue(MockResponse()
114
                .setResponseCode(HttpURLConnection.HTTP_OK)
115
                .setBody(sampleText))
116 117 118
        dav.get("*/*").use {
            assertNull(it[GetETag::class.java])
        }
119 120
    }

Ricki Hirner's avatar
Ricki Hirner committed
121
    @Test
122 123 124
    fun testPut() {
        val url = sampleUrl()
        val dav = DavResource(httpClient, url)
125 126 127 128

        /* POSITIVE TEST CASES */

        // no preconditions, 201 Created
129
        mockServer.enqueue(MockResponse()
130
                .setResponseCode(HttpURLConnection.HTTP_CREATED)
131
                .setHeader("ETag", "W/\"Weak PUT ETag\""))
132 133 134 135
        dav.put(RequestBody.create(MediaType.parse("text/plain"), sampleText), null, false).use {
            assertEquals("Weak PUT ETag", it[GetETag::class.java]?.eTag)
            assertEquals(it.url, dav.location)
        }
136

137 138 139 140 141
        var rq = mockServer.takeRequest()
        assertEquals("PUT", rq.method)
        assertEquals(url.encodedPath(), rq.path)
        assertNull(rq.getHeader("If-Match"))
        assertNull(rq.getHeader("If-None-Match"))
142 143

        // precondition: If-None-Match, 301 Moved Permanently + 204 No Content, no ETag in response
144
        mockServer.enqueue(MockResponse()
145
                .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
146 147 148
                .setHeader("Location", "/target"))
        mockServer.enqueue(MockResponse()
                .setResponseCode(HttpURLConnection.HTTP_NO_CONTENT))
149 150 151 152
        dav.put(RequestBody.create(MediaType.parse("text/plain"), sampleText), null, true).use {
            assertEquals(url.resolve("/target"), it.url)
            assertNull("Weak PUT ETag", it[GetETag::class.java]?.eTag)
        }
153 154 155 156 157

        mockServer.takeRequest()
        rq = mockServer.takeRequest()
        assertEquals("PUT", rq.method)
        assertEquals("*", rq.getHeader("If-None-Match"))
158 159

        // precondition: If-Match, 412 Precondition Failed
160 161
        mockServer.enqueue(MockResponse()
                .setResponseCode(HttpURLConnection.HTTP_PRECON_FAILED))
162 163 164 165
        try {
            dav.put(RequestBody.create(MediaType.parse("text/plain"), sampleText), "ExistingETag", false).close()
            fail("Expected PreconditionFailedException")
        } catch(e: PreconditionFailedException) {}
166 167 168
        rq = mockServer.takeRequest()
        assertEquals("\"ExistingETag\"", rq.getHeader("If-Match"))
        assertNull(rq.getHeader("If-None-Match"))
169 170
    }

Ricki Hirner's avatar
Ricki Hirner committed
171
    @Test
172 173 174
    fun testDelete() {
        val url = sampleUrl()
        val dav = DavResource(httpClient, url)
175 176 177 178

        /* POSITIVE TEST CASES */

        // no preconditions, 204 No Content
179 180
        mockServer.enqueue(MockResponse()
                .setResponseCode(HttpURLConnection.HTTP_NO_CONTENT))
181
        dav.delete(null).close()
182

183 184 185 186
        var rq = mockServer.takeRequest()
        assertEquals("DELETE", rq.method)
        assertEquals(url.encodedPath(), rq.path)
        assertNull(rq.getHeader("If-Match"))
187 188

        // precondition: If-Match, 200 OK
189
        mockServer.enqueue(MockResponse()
190
                .setResponseCode(HttpURLConnection.HTTP_OK)
191
                .setBody("Resource has been deleted."))
192
        dav.delete("DeleteOnlyThisETag").close()
193

194 195
        rq = mockServer.takeRequest()
        assertEquals("\"DeleteOnlyThisETag\"", rq.getHeader("If-Match"))
196

197
        // 302 Moved Temporarily
198
        mockServer.enqueue(MockResponse()
199 200
                .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
                .setHeader("Location", "/new-location")
201 202 203
        )
        mockServer.enqueue(MockResponse()
                .setResponseCode(HttpURLConnection.HTTP_OK))
204
        dav.delete(null).close()
205

206 207
        /* NEGATIVE TEST CASES */

208
        // 207 multi-status (e.g. single resource couldn't be deleted when DELETEing a collection)
209 210
        mockServer.enqueue(MockResponse()
                .setResponseCode(207))
211
        try {
212 213 214
            dav.delete(null).close()
            fail("Expected HttpException")
        } catch(e: HttpException) {}
215
    }
Ricki Hirner's avatar
Ricki Hirner committed
216

Ricki Hirner's avatar
Ricki Hirner committed
217
    @Test
218 219 220
    fun testPropfindAndMultiStatus() {
        val url = sampleUrl()
        val dav = DavResource(httpClient, url)
221 222 223

        /*** NEGATIVE TESTS ***/

224
        // test for non-multi-status responses:
225
        // * 500 Internal Server Error
226
        mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_INTERNAL_ERROR))
227
        try {
228
            dav.propfind(0, ResourceType.NAME).close()
229
            fail("Expected HttpException")
230
        } catch(e: HttpException) {}
231
        // * 200 OK (instead of 207 Multi-Status)
232
        mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK))
233
        try {
234 235 236
            dav.propfind(0, ResourceType.NAME).close()
            fail("Expected DavException")
        } catch(e: DavException) {}
237

238
        // test for invalid multi-status responses:
239
        // * non-XML response
240
        mockServer.enqueue(MockResponse()
241 242
                .setResponseCode(207)
                .setHeader("Content-Type", "text/html")
243
                .setBody("<html></html>"))
244
        try {
245 246 247
            dav.propfind(0, ResourceType.NAME).close()
            fail("Expected DavException")
        } catch(e: DavException) {}
Ricki Hirner's avatar
Ricki Hirner committed
248

249
        // * malformed XML response
250
        mockServer.enqueue(MockResponse()
251 252
                .setResponseCode(207)
                .setHeader("Content-Type", "application/xml; charset=utf-8")
253
                .setBody("<malformed-xml>"))
254
        try {
255 256 257
            dav.propfind(0, ResourceType.NAME).close()
            fail("Expected DavException")
        } catch(e: DavException) {}
258 259

        // * response without <multistatus> root element
260
        mockServer.enqueue(MockResponse()
261 262
                .setResponseCode(207)
                .setHeader("Content-Type", "application/xml; charset=utf-8")
263
                .setBody("<test></test>"))
264
        try {
265 266 267
            dav.propfind(0, ResourceType.NAME).close()
            fail("Expected DavException")
        } catch(e: DavException) {}
268

269
        // * multi-status response with invalid <status> in <response>
270
        mockServer.enqueue(MockResponse()
271 272 273 274 275 276 277
                .setResponseCode(207)
                .setHeader("Content-Type", "application/xml; charset=utf-8")
                .setBody("<multistatus xmlns='DAV:'>" +
                         "  <response>" +
                         "    <href>/dav</href>" +
                         "    <status>Invalid Status Line</status>" +
                         "  </response>" +
278
                         "</multistatus>"))
279
        try {
280
            dav.propfind(0, ResourceType.NAME).close()
281
            fail("Expected HttpException")
282
        } catch(e: HttpException) {}
283 284

        // * multi-status response with <response>/<status> element indicating failure
285
        mockServer.enqueue(MockResponse()
286 287 288 289 290 291 292
                .setResponseCode(207)
                .setHeader("Content-Type", "application/xml; charset=utf-8")
                .setBody("<multistatus xmlns='DAV:'>" +
                         "  <response>" +
                         "    <href>/dav</href>" +
                         "    <status>HTTP/1.1 403 Forbidden</status>" +
                         "  </response>" +
293
                         "</multistatus>"))
294
        try {
295 296
            dav.propfind(0, ResourceType.NAME)
            fail("Expected HttpException")
297
        } catch(e: HttpException) {}
298

299
        // * multi-status response with invalid <status> in <propstat>
300
        mockServer.enqueue(MockResponse()
301 302 303 304 305 306 307 308 309 310 311 312
                .setResponseCode(207)
                .setHeader("Content-Type", "application/xml; charset=utf-8")
                .setBody("<multistatus xmlns='DAV:'>" +
                        "  <response>" +
                        "    <href>/dav</href>" +
                        "    <propstat>" +
                        "      <prop>" +
                        "        <resourcetype><collection/></resourcetype>" +
                        "      </prop>" +
                        "      <status>Invalid Status Line</status>" +
                        "    </propstat>" +
                        "  </response>" +
313
                        "</multistatus>"))
314 315 316
        dav.propfind(0, ResourceType.NAME).use {
            assertNull(it[ResourceType::class.java])
        }
317 318


319 320 321
        /*** POSITIVE TESTS ***/

        // multi-status response without <response> elements
322
        mockServer.enqueue(MockResponse()
323 324
                .setResponseCode(207)
                .setHeader("Content-Type", "application/xml; charset=utf-8")
325
                .setBody("<multistatus xmlns='DAV:'></multistatus>"))
326 327 328 329
        dav.propfind(0, ResourceType.NAME).use {
            assertEquals(0, it.properties.size)
            assertEquals(0, it.members.size)
        }
330 331

        // multi-status response with <response>/<status> element indicating success
332
        mockServer.enqueue(MockResponse()
333 334 335 336 337 338 339
                .setResponseCode(207)
                .setHeader("Content-Type", "application/xml; charset=utf-8")
                .setBody("<multistatus xmlns='DAV:'>" +
                        "  <response>" +
                        "    <href>/dav</href>" +
                        "    <status>HTTP/1.1 200 OK</status>" +
                        "  </response>" +
340
                        "</multistatus>"))
341 342 343 344
        dav.propfind(0, ResourceType.NAME).use {
            assertEquals(0, it.properties.size)
            assertEquals(0, it.members.size)
        }
345 346

        // multi-status response with <response>/<propstat> element
347
        mockServer.enqueue(MockResponse()
348 349 350 351 352
                .setResponseCode(207)
                .setHeader("Content-Type", "application/xml; charset=utf-8")
                .setBody("<multistatus xmlns='DAV:'>" +
                         "  <response>" +
                         "    <href>/dav</href>" +
353 354 355 356 357 358 359 360
                         "    <propstat>" +
                         "      <prop>" +
                         "        <resourcetype>" +
                         "        </resourcetype>" +
                         "        <displayname>My DAV Collection</displayname>" +
                         "      </prop>" +
                         "      <status>HTTP/1.1 200 OK</status>" +
                         "    </propstat>" +
361
                         "  </response>" +
362
                         "</multistatus>"))
363 364 365 366
        dav.propfind(0, ResourceType.NAME, DisplayName.NAME).use {
            assertEquals("My DAV Collection", it[DisplayName::class.java]?.displayName)
            assertEquals(0, it.members.size)
        }
367

368
        // multi-status response for collection with several members; incomplete (not all <resourcetype>s listed)
369
        mockServer.enqueue(MockResponse()
370 371 372
                .setResponseCode(207)
                .setHeader("Content-Type", "application/xml; charset=utf-8")
                .setBody("<multistatus xmlns='DAV:'>" +
Ricki Hirner's avatar
Ricki Hirner committed
373
                        "  <response>" +
374
                        "    <href>" + url.toString() + "</href>" +
Ricki Hirner's avatar
Ricki Hirner committed
375 376
                        "    <propstat>" +
                        "      <prop>" +
377
                        "        <resourcetype><collection/></resourcetype>" +
Ricki Hirner's avatar
Ricki Hirner committed
378 379 380 381 382
                        "        <displayname>My DAV Collection</displayname>" +
                        "      </prop>" +
                        "      <status>HTTP/1.1 200 OK</status>" +
                        "    </propstat>" +
                        "  </response>" +
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
                        "  <response>" +
                        "    <href>/dav/subcollection</href>" +
                        "    <propstat>" +
                        "      <prop>" +
                        "        <resourcetype><collection/></resourcetype>" +
                        "        <displayname>A Subfolder</displayname>" +
                        "      </prop>" +
                        "      <status>HTTP/1.1 200 OK</status>" +
                        "    </propstat>" +
                        "  </response>" +
                        "  <response>" +
                        "    <href>/dav/uid@host:file</href>" +
                        "    <propstat>" +
                        "      <prop>" +
                        "        <displayname>Absolute path with @ and :</displayname>" +
                        "      </prop>" +
                        "      <status>HTTP/1.1 200 OK</status>" +
                        "    </propstat>" +
                        "  </response>" +
                        "  <response>" +
                        "    <href>relative-uid@host.file</href>" +
                        "    <propstat>" +
                        "      <prop>" +
                        "        <displayname>Relative path with @</displayname>" +
                        "      </prop>" +
                        "      <status>HTTP/1.1 200 OK</status>" +
                        "    </propstat>" +
                        "  </response>" +
                        "  <response>" +
                        "    <href>relative:colon.vcf</href>" +
                        "    <propstat>" +
                        "      <prop>" +
                        "        <displayname>Relative path with colon</displayname>" +
                        "      </prop>" +
                        "      <status>HTTP/1.1 200 OK</status>" +
                        "    </propstat>" +
                        "  </response>" +
                        "  <response>" +
                        "    <href>/something-very/else</href>" +
                        "    <propstat>" +
                        "      <prop>" +
                        "        <displayname>Not requested</displayname>" +
                        "      </prop>" +
                        "      <status>HTTP/1.1 200 OK</status>" +
                        "    </propstat>" +
                        "  </response>" +
429
                        "</multistatus>"))
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
        dav.propfind(1, ResourceType.NAME, DisplayName.NAME).use { response ->
            assertEquals(4, response.members.size)
            val ok = BooleanArray(4)
            for (member in response.members)
                when (member.url) {
                    url.resolve("/dav/subcollection/") -> {
                        assertTrue(member[ResourceType::class.java]!!.types.contains(ResourceType.COLLECTION))
                        assertEquals("A Subfolder", member[DisplayName::class.java]?.displayName)
                        ok[0] = true
                    }
                    url.resolve("/dav/uid@host:file") -> {
                        assertEquals("Absolute path with @ and :", member[DisplayName::class.java]?.displayName)
                        ok[1] = true
                    }
                    url.resolve("/dav/relative-uid@host.file") -> {
                        assertEquals("Relative path with @", member[DisplayName::class.java]?.displayName)
                        ok[2] = true
                    }
                    url.resolve("/dav/relative:colon.vcf") -> {
                        assertEquals("Relative path with colon", member[DisplayName::class.java]?.displayName)
                        ok[3] = true
                    }
452
                }
453 454
            assertTrue(ok.all { it })
        }
455 456 457 458

        /*** SPECIAL CASES ***/

        // same property is sent as 200 OK and 404 Not Found in same <response> (seen in iCloud)
459
        mockServer.enqueue(MockResponse()
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478
                .setResponseCode(207)
                .setHeader("Content-Type", "application/xml; charset=utf-8")
                .setBody("<multistatus xmlns='DAV:'>" +
                        "  <response>" +
                        "    <href>" + url.toString() + "</href>" +
                        "    <propstat>" +
                        "      <prop>" +
                        "        <resourcetype><collection/></resourcetype>" +
                        "        <displayname>My DAV Collection</displayname>" +
                        "      </prop>" +
                        "      <status>HTTP/1.1 200 OK</status>" +
                        "    </propstat>" +
                        "    <propstat>" +
                        "      <prop>" +
                        "        <resourcetype/>" +
                        "      </prop>" +
                        "      <status>HTTP/1.1 404 Not Found</status>" +
                        "    </propstat>" +
                        "  </response>" +
479
                        "</multistatus>"))
480 481 482 483
        dav.propfind(0, ResourceType.NAME, DisplayName.NAME).use {
            assertTrue(it[ResourceType::class.java]!!.types.contains(ResourceType.COLLECTION))
            assertEquals("My DAV Collection", it[DisplayName::class.java]?.displayName)
        }
484 485

        // multi-status response with <propstat> that doesn't contain <status> (=> assume 200 OK)
486
        mockServer.enqueue(MockResponse()
487 488 489 490 491 492 493 494 495 496 497
                .setResponseCode(207)
                .setHeader("Content-Type", "application/xml; charset=utf-8")
                .setBody("<multistatus xmlns='DAV:'>" +
                        "  <response>" +
                        "    <href>/dav</href>" +
                        "    <propstat>" +
                        "      <prop>" +
                        "        <displayname>Without Status</displayname>" +
                        "      </prop>" +
                        "    </propstat>" +
                        "  </response>" +
498
                        "</multistatus>"))
499 500 501
        dav.propfind(0, DisplayName.NAME).use {
            assertEquals("Without Status", it[DisplayName::class.java]?.displayName)
        }
502
    }
Ricki Hirner's avatar
Ricki Hirner committed
503

Ricki Hirner's avatar
Ricki Hirner committed
504
}