diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 2d09cd547..c9c855a28 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -228,6 +228,7 @@ dependencies {
androidTestImplementation("androidx.test:rules:${Versions.androidx_test}")
androidTestImplementation("androidx.test.ext:junit:1.1.2")
androidTestImplementation("androidx.annotation:annotation:1.1.0")
+ androidTestImplementation("com.squareup.okhttp3:mockwebserver:${Versions.okhttp}")
testImplementation("junit:junit:4.13.1")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2")
diff --git a/app/src/androidTest/java/org/tasks/caldav/CaldavSynchronizerTest.kt b/app/src/androidTest/java/org/tasks/caldav/CaldavSynchronizerTest.kt
new file mode 100644
index 000000000..cfafc97c0
--- /dev/null
+++ b/app/src/androidTest/java/org/tasks/caldav/CaldavSynchronizerTest.kt
@@ -0,0 +1,219 @@
+package org.tasks.caldav
+
+import com.natpryce.makeiteasy.MakeItEasy.with
+import com.todoroo.astrid.helper.UUIDHelper
+import dagger.hilt.android.testing.HiltAndroidTest
+import dagger.hilt.android.testing.UninstallModules
+import kotlinx.coroutines.runBlocking
+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 org.tasks.R
+import org.tasks.data.CaldavAccount
+import org.tasks.data.CaldavCalendar
+import org.tasks.data.CaldavDao
+import org.tasks.data.TaskDao
+import org.tasks.injection.InjectingTestCase
+import org.tasks.injection.ProductionModule
+import org.tasks.makers.CaldavTaskMaker.CALENDAR
+import org.tasks.makers.CaldavTaskMaker.ETAG
+import org.tasks.makers.CaldavTaskMaker.OBJECT
+import org.tasks.makers.CaldavTaskMaker.newCaldavTask
+import org.tasks.preferences.Preferences
+import org.tasks.security.KeyStoreEncryption
+import javax.inject.Inject
+
+@UninstallModules(ProductionModule::class)
+@HiltAndroidTest
+class CaldavSynchronizerTest : InjectingTestCase() {
+ @Inject lateinit var synchronizer: CaldavSynchronizer
+ @Inject lateinit var encryption: KeyStoreEncryption
+ @Inject lateinit var preferences: Preferences
+ @Inject lateinit var caldavDao: CaldavDao
+ @Inject lateinit var taskDao: TaskDao
+ private val server = MockWebServer()
+ lateinit var account: CaldavAccount
+
+ @Before
+ override fun setUp() = runBlocking {
+ super.setUp()
+ preferences.setBoolean(R.string.p_debug_pro, true)
+ server.start()
+ account = CaldavAccount().apply {
+ uuid = UUIDHelper.newUUID()
+ username = "username"
+ password = encryption.encrypt("password")
+ url = server.url("/remote.php/dav/calendars/user1/").toString()
+ id = caldavDao.insert(this)
+ }
+ }
+
+ @After
+ fun after() = server.shutdown()
+
+ @Test
+ fun setMessageOnError() = runBlocking {
+ enqueueFailure(500)
+
+ synchronizer.sync(account)
+
+ assertEquals("HTTP 500 Server Error", caldavDao.getAccounts().first().error)
+ }
+
+ @Test
+ fun dontFetchCalendarIfCtagMatches() = runBlocking {
+ caldavDao.insert(CaldavCalendar().apply {
+ account = this@CaldavSynchronizerTest.account.uuid
+ ctag = "http://sabre.io/ns/sync/1"
+ url = "${this@CaldavSynchronizerTest.account.url}test-shared/"
+ })
+ enqueue(OC_SHARE_PROPFIND)
+ enqueueFailure()
+
+ synchronizer.sync(account)
+
+ assertFalse(caldavDao.getAccountByUuid(account.uuid!!)!!.hasError)
+ }
+
+ @Test
+ fun dontFetchTaskIfEtagMatches() = runBlocking {
+ val calendar = CaldavCalendar().apply {
+ account = this@CaldavSynchronizerTest.account.uuid
+ uuid = UUIDHelper.newUUID()
+ url = "${this@CaldavSynchronizerTest.account.url}test-shared/"
+ caldavDao.insert(this)
+ }
+ caldavDao.insert(newCaldavTask(
+ with(OBJECT, "3164728546640386952.ics"),
+ with(ETAG, "43b3ffaac5131880e4dd07a79adba82a"),
+ with(CALENDAR, calendar.uuid)
+ ))
+ enqueue(OC_SHARE_PROPFIND, OC_SHARE_REPORT)
+ enqueueFailure()
+
+ synchronizer.sync(account)
+
+ assertFalse(caldavDao.getAccountByUuid(account.uuid!!)!!.hasError)
+ }
+
+ @Test
+ fun syncNewTask() = runBlocking {
+ enqueue(OC_SHARE_PROPFIND, OC_SHARE_REPORT, OC_SHARE_TASK)
+
+ synchronizer.sync(account)
+
+ val calendar = caldavDao.getCalendars().takeIf { it.size == 1 }!!.first()
+ val caldavTask = caldavDao.getTaskByRemoteId(calendar.uuid!!, "3164728546640386952")!!
+ assertEquals("Test task", taskDao.fetch(caldavTask.task)!!.title)
+ }
+
+ private fun enqueue(vararg responses: String) = responses.forEach {
+ server.enqueue(
+ MockResponse()
+ .setResponseCode(207)
+ .setHeader("Content-Type", "text/xml; charset=\"utf-8\"")
+ .setBody(it)
+ )
+ }
+
+ private fun enqueueFailure(code: Int = 500) =
+ server.enqueue(MockResponse().setResponseCode(code))
+
+ companion object {
+ private val OC_SHARE_PROPFIND = """
+
+
+
+ /remote.php/dav/calendars/user1/test-shared/
+
+
+
+
+
+
+ Test shared
+
+
+
+ http://sabre.io/ns/sync/1
+ #0082c9
+ http://sabre.io/ns/sync/1
+ principals/users/user1
+
+
+ principal:principals/users/user2
+ user2
+
+
+
+
+
+
+
+ HTTP/1.1 200 OK
+
+
+
+
+
+
+ HTTP/1.1 404 Not Found
+
+
+
+ """.trimIndent()
+
+ private val OC_SHARE_REPORT = """
+
+
+
+ /remote.php/dav/calendars/user1/test-shared/3164728546640386952.ics
+
+
+ "43b3ffaac5131880e4dd07a79adba82a"
+
+ HTTP/1.1 200 OK
+
+
+
+ """.trimIndent()
+
+ private val OC_SHARE_TASK = """
+
+
+
+ /remote.php/dav/calendars/user1/test-shared/3164728546640386952.ics
+
+
+ text/calendar; charset=utf-8; component=vtodo
+ "43b3ffaac5131880e4dd07a79adba82a"
+ BEGIN:VCALENDAR
+ VERSION:2.0
+ PRODID:+//IDN tasks.org//android-110500//EN
+ BEGIN:VTODO
+ DTSTAMP:20210223T154147Z
+ UID:3164728546640386952
+ CREATED:20210223T154134Z
+ LAST-MODIFIED:20210223T154140Z
+ SUMMARY:Test task
+ PRIORITY:9
+ END:VTODO
+ END:VCALENDAR
+
+ HTTP/1.1 200 OK
+
+
+
+
+
+ HTTP/1.1 404 Not Found
+
+
+
+ """.trimIndent()
+ }
+}
\ No newline at end of file
diff --git a/app/src/commonTest/java/org/tasks/makers/CaldavTaskMaker.kt b/app/src/commonTest/java/org/tasks/makers/CaldavTaskMaker.kt
index 07b0f7b48..050cfee62 100644
--- a/app/src/commonTest/java/org/tasks/makers/CaldavTaskMaker.kt
+++ b/app/src/commonTest/java/org/tasks/makers/CaldavTaskMaker.kt
@@ -14,6 +14,8 @@ object CaldavTaskMaker {
val REMOTE_PARENT: Property = newProperty()
val REMOTE_ORDER: Property = newProperty()
val VTODO: Property = newProperty()
+ val ETAG: Property = newProperty()
+ val OBJECT: Property = newProperty()
private val instantiator = Instantiator {
val task = CaldavTask(it.valueOf(TASK, 1L), it.valueOf(CALENDAR, "calendar"))
@@ -21,6 +23,8 @@ object CaldavTaskMaker {
task.remoteParent = it.valueOf(REMOTE_PARENT, null as String?)
task.vtodo = it.valueOf(VTODO, null as String?)
task.order = it.valueOf(REMOTE_ORDER, null as Long?)
+ task.etag = it.valueOf(ETAG, null as String?)
+ task.`object` = it.valueOf(OBJECT, task.remoteId?.let { id -> "$id.ics" })
task
}
diff --git a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt
index b35ba49c3..06a03a1b0 100644
--- a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt
+++ b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt
@@ -105,7 +105,7 @@ class CaldavSynchronizer @Inject constructor(
}
}
setError(account, message)
- } catch (e: DavException) {
+ } catch (e: Exception) {
setError(account, e.message)
firebase.reportException(e)
}