diff --git a/app/src/main/java/org/tasks/sync/microsoft/MicrosoftConverter.kt b/app/src/main/java/org/tasks/sync/microsoft/MicrosoftConverter.kt new file mode 100644 index 000000000..6be350d67 --- /dev/null +++ b/app/src/main/java/org/tasks/sync/microsoft/MicrosoftConverter.kt @@ -0,0 +1,78 @@ +package org.tasks.sync.microsoft + +import com.todoroo.astrid.data.Task +import org.tasks.data.CaldavTask +import org.tasks.data.TagData +import org.tasks.time.DateTime +import java.text.SimpleDateFormat +import java.util.* + +object MicrosoftConverter { + + private const val TYPE_TEXT = "text" + + fun Task.applyRemote( + remote: Tasks.Task, + defaultPriority: Int, + ) { + title = remote.title + notes = remote.body?.content?.takeIf { it.isNotBlank() } + priority = when { + remote.importance == Tasks.Task.Importance.high -> Task.Priority.HIGH + priority != Task.Priority.HIGH -> priority + defaultPriority != Task.Priority.HIGH -> defaultPriority + else -> Task.Priority.NONE + } + completionDate = remote.completedDateTime + ?.let { + val tz = TimeZone.getTimeZone(it.timeZone) + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.ssssss") + .apply { timeZone = tz } + .parse(it.dateTime) + ?.time + ?.let { ts -> DateTime(ts, tz).toLocal().millis } + ?: System.currentTimeMillis() + } + ?: 0L + // checklist to subtasks + // due date + // repeat + // modification date + // creation date + // sync reminders + // sync files + } + + fun Task.toRemote(caldavTask: CaldavTask, tags: List): Tasks.Task { + return Tasks.Task( + id = caldavTask.remoteId, + title = title, + body = notes?.let { + Tasks.Task.Body( + content = it, + contentType = TYPE_TEXT, + ) + }, + importance = when (priority) { + Task.Priority.HIGH -> Tasks.Task.Importance.high + Task.Priority.MEDIUM -> Tasks.Task.Importance.normal + else -> Tasks.Task.Importance.low + }, + status = if (isCompleted) { + Tasks.Task.Status.completed + } else { + Tasks.Task.Status.notStarted + }, + categories = tags.map { it.name!! }.takeIf { it.isNotEmpty() }, + completedDateTime = if (isCompleted) { + Tasks.Task.CompletedDateTime( + dateTime = DateTime(completionDate).toUTC() + .toString("yyyy-MM-dd'T'HH:mm:ss.SSSSSSS"), + timeZone = "UTC", + ) + } else { + null + } + ) + } +} \ No newline at end of file diff --git a/app/src/test/java/org/tasks/TestUtilities.kt b/app/src/test/java/org/tasks/TestUtilities.kt index 8652d63ed..2cefee5c2 100644 --- a/app/src/test/java/org/tasks/TestUtilities.kt +++ b/app/src/test/java/org/tasks/TestUtilities.kt @@ -2,6 +2,7 @@ package org.tasks import android.content.Context import at.bitfire.ical4android.Task.Companion.tasksFromReader +import com.squareup.moshi.Moshi import com.todoroo.astrid.data.Task import kotlinx.coroutines.runBlocking import org.tasks.caldav.applyRemote @@ -9,6 +10,8 @@ import org.tasks.caldav.iCalendar.Companion.reminders import org.tasks.data.Alarm import org.tasks.data.CaldavTask import org.tasks.preferences.Preferences +import org.tasks.sync.microsoft.MicrosoftConverter.applyRemote +import org.tasks.sync.microsoft.Tasks import org.tasks.time.DateTime import java.io.StringReader import java.nio.file.Files @@ -16,7 +19,8 @@ import java.nio.file.Paths import java.util.* object TestUtilities { - fun withTZ(id: String, runnable: suspend () -> Unit) = withTZ(TimeZone.getTimeZone(id), runnable) + fun withTZ(id: String, runnable: suspend () -> Unit) = + withTZ(TimeZone.getTimeZone(id), runnable) fun withTZ(tz: TimeZone, runnable: suspend () -> Unit) { val def = TimeZone.getDefault() @@ -31,10 +35,10 @@ object TestUtilities { } fun assertEquals(expected: Long, actual: DateTime) = - org.junit.Assert.assertEquals(expected, actual.millis) + org.junit.Assert.assertEquals(expected, actual.millis) fun assertEquals(expected: DateTime, actual: Long?) = - org.junit.Assert.assertEquals(expected.millis, actual) + org.junit.Assert.assertEquals(expected.millis, actual) fun newPreferences(context: Context): Preferences { return Preferences(context, "test_preferences") @@ -42,34 +46,43 @@ object TestUtilities { fun vtodo(path: String): Task { val task = Task() - task.applyRemote(fromResource(path), null) + task.applyRemote(icalendarFromFile(path), null) return task } val String.alarms: List - get() = fromResource(this).reminders + get() = icalendarFromFile(this).reminders fun setup(path: String): Triple { val task = Task() - val vtodo = readFile(path) - val remote = fromString(vtodo) + val remote = icalendarFromFile(path) task.applyRemote(remote, null) return Triple(task, CaldavTask(), remote) } - private fun fromResource(path: String): at.bitfire.ical4android.Task = - fromString(readFile(path)) + fun icalendarFromFile(path: String): at.bitfire.ical4android.Task = + tasksFromReader(StringReader(readFile(path))) + .takeIf { it.size == 1 } + ?.first() + ?: throw IllegalStateException() + + fun mstodo( + path: String, + task: Task = Task(), + defaultPriority: Int = Task.Priority.NONE, + ): Pair { + val remote = mstodoFromFile(path) + task.applyRemote(remote, defaultPriority) + return Pair(task, remote) + } + + private fun mstodoFromFile(path: String): Tasks.Task = + Moshi.Builder().build().adapter(Tasks::class.java).fromJson(readFile(path))!!.value.first() fun readFile(path: String): String { val uri = javaClass.classLoader?.getResource(path)?.toURI() - ?: throw IllegalArgumentException() + ?: throw IllegalArgumentException() val paths = Paths.get(uri) return String(Files.readAllBytes(paths), Charsets.UTF_8) } - - fun fromString(task: String): at.bitfire.ical4android.Task = - tasksFromReader(StringReader(task)) - .takeIf { it.size == 1 } - ?.first() - ?: throw IllegalStateException() } \ No newline at end of file diff --git a/app/src/test/java/org/tasks/sync/microsoft/ConvertFromMicrosoftTests.kt b/app/src/test/java/org/tasks/sync/microsoft/ConvertFromMicrosoftTests.kt new file mode 100644 index 000000000..f71a113d8 --- /dev/null +++ b/app/src/test/java/org/tasks/sync/microsoft/ConvertFromMicrosoftTests.kt @@ -0,0 +1,65 @@ +package org.tasks.sync.microsoft + +import com.natpryce.makeiteasy.MakeItEasy +import com.todoroo.astrid.data.Task +import org.junit.Assert +import org.junit.Test +import org.tasks.TestUtilities +import org.tasks.makers.TaskMaker +import org.tasks.time.DateTime + +class ConvertFromMicrosoftTests { + @Test + fun titleFromRemote() { + val (local, _) = TestUtilities.mstodo("microsoft/basic_task.txt") + Assert.assertEquals("Basic task", local.title) + } + + @Test + fun useNullForBlankBody() { + val (local, _) = TestUtilities.mstodo("microsoft/basic_task.txt") + Assert.assertNull(local.notes) + } + + @Test + fun keepPriority() { + val (local, _) = TestUtilities.mstodo( + "microsoft/basic_task.txt", + task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.PRIORITY, Task.Priority.MEDIUM)), + defaultPriority = Task.Priority.LOW + ) + Assert.assertEquals(Task.Priority.MEDIUM, local.priority) + } + + @Test + fun useDefaultPriority() { + val (local, _) = TestUtilities.mstodo( + "microsoft/basic_task.txt", + task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.PRIORITY, Task.Priority.HIGH)), + defaultPriority = Task.Priority.LOW + ) + Assert.assertEquals(Task.Priority.LOW, local.priority) + } + + @Test + fun noPriorityWhenDefaultIsHigh() { + val (local, _) = TestUtilities.mstodo( + "microsoft/basic_task.txt", + task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.PRIORITY, Task.Priority.HIGH)), + defaultPriority = Task.Priority.HIGH + ) + Assert.assertEquals(Task.Priority.NONE, local.priority) + } + + @Test + fun noCompletionDate() { + val (local, _) = TestUtilities.mstodo("microsoft/basic_task.txt") + Assert.assertEquals(0, local.completionDate) + } + + @Test + fun parseCompletionDate() { + val (local, _) = TestUtilities.mstodo("microsoft/completed_task.txt") + Assert.assertEquals(DateTime(2022, 9, 18, 0, 0).millis, local.completionDate) + } +} \ No newline at end of file diff --git a/app/src/test/java/org/tasks/sync/microsoft/ConvertToMicrosoftTests.kt b/app/src/test/java/org/tasks/sync/microsoft/ConvertToMicrosoftTests.kt new file mode 100644 index 000000000..2ff8c3e56 --- /dev/null +++ b/app/src/test/java/org/tasks/sync/microsoft/ConvertToMicrosoftTests.kt @@ -0,0 +1,115 @@ +package org.tasks.sync.microsoft + +import com.natpryce.makeiteasy.MakeItEasy.with +import com.todoroo.astrid.data.Task +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.tasks.makers.CaldavTaskMaker.REMOTE_ID +import org.tasks.makers.CaldavTaskMaker.newCaldavTask +import org.tasks.makers.TagDataMaker.NAME +import org.tasks.makers.TagDataMaker.newTagData +import org.tasks.makers.TaskMaker.COMPLETION_TIME +import org.tasks.makers.TaskMaker.DESCRIPTION +import org.tasks.makers.TaskMaker.PRIORITY +import org.tasks.makers.TaskMaker.TITLE +import org.tasks.makers.TaskMaker.newTask +import org.tasks.sync.microsoft.MicrosoftConverter.toRemote +import org.tasks.sync.microsoft.Tasks.Task.Importance +import org.tasks.time.DateTime + +class ConvertToMicrosoftTests { + @Test + fun noIdForNewTask() { + val remote = + newTask().toRemote(newCaldavTask(with(REMOTE_ID, null as String?)), emptyList()) + assertNull(remote.id) + } + + @Test + fun setTitle() { + val remote = + newTask(with(TITLE, "title")).toRemote(newCaldavTask(), emptyList()) + assertEquals("title", remote.title) + } + + @Test + fun noBody() { + val remote = + newTask(with(DESCRIPTION, null as String?)) + .toRemote(newCaldavTask(), emptyList()) + assertNull(remote.body) + } + + @Test + fun setBody() { + val remote = + newTask(with(DESCRIPTION, "Description")) + .toRemote(newCaldavTask(), emptyList()) + assertEquals("Description", remote.body?.content) + assertEquals("text", remote.body?.contentType) + } + + @Test + fun setHighPriority() { + val remote = + newTask(with(PRIORITY, Task.Priority.HIGH)) + .toRemote(newCaldavTask(), emptyList()) + assertEquals(Importance.high, remote.importance) + } + + @Test + fun setNormalPriority() { + val remote = + newTask(with(PRIORITY, Task.Priority.MEDIUM)) + .toRemote(newCaldavTask(), emptyList()) + assertEquals(Importance.normal, remote.importance) + } + + @Test + fun setLowPriority() { + val remote = + newTask(with(PRIORITY, Task.Priority.LOW)) + .toRemote(newCaldavTask(), emptyList()) + assertEquals(Importance.low, remote.importance) + } + + @Test + fun setNoPriorityToLow() { + val remote = + newTask(with(PRIORITY, Task.Priority.NONE)) + .toRemote(newCaldavTask(), emptyList()) + assertEquals(Importance.low, remote.importance) + } + + @Test + fun statusForUncompletedTask() { + val remote = newTask().toRemote(newCaldavTask(), emptyList()) + assertEquals(Tasks.Task.Status.notStarted, remote.status) + } + + @Test + fun statusForCompletedTask() { + val remote = + newTask(with(COMPLETION_TIME, DateTime())).toRemote(newCaldavTask(), emptyList()) + assertEquals(Tasks.Task.Status.completed, remote.status) + } + + @Test + fun noCategories() { + val remote = newTask().toRemote(newCaldavTask(), emptyList()) + assertNull(remote.categories) + } + + @Test + fun setCategories() { + val remote = newTask().toRemote( + newCaldavTask(), + listOf( + newTagData(with(NAME, "tag1")), + newTagData(with(NAME, "tag2")), + ) + ) + assertEquals(listOf("tag1", "tag2"), remote.categories) + } +} \ No newline at end of file diff --git a/app/src/test/resources/microsoft/basic_task.txt b/app/src/test/resources/microsoft/basic_task.txt new file mode 100644 index 000000000..16385c29c --- /dev/null +++ b/app/src/test/resources/microsoft/basic_task.txt @@ -0,0 +1,23 @@ +{ + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(todoTask)", + "@odata.deltaLink": "https://graph.microsoft.com/v1.0/me/todo/lists/AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoALgAAA8dKmrSa60tBjeiKoPukmoQBAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAA==/tasks/delta?$deltatoken=l7WI41swwioT5csv4k99nj_e6d30OPWG40P_OyQlkkny185Z38qUU3jLaP2oRC5t77tVxtd6SMRj0dEaTBh3MyHws0GnzQINZQHOs_ww6Z8.lmA23NEL8QGEOxyZEu-Uk2FszHDtjqkPsyfdoZaFIeE", + "value": [ + { + "@odata.type": "#microsoft.graph.todoTask", + "@odata.etag": "W/\"SRPGnpbHYES1XW8UlYrtsgAFlRV2EQ==\"", + "importance": "normal", + "isReminderOn": false, + "status": "notStarted", + "title": "Basic task", + "createdDateTime": "2022-09-18T05:25:19.778574Z", + "lastModifiedDateTime": "2022-09-18T05:25:19.8723482Z", + "hasAttachments": false, + "categories": [], + "id": "AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoARgAAA8dKmrSa60tBjeiKoPukmoQHAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAEkTxp6Wx2BEtV1vFJWK7bIABZUEdtwAAAA=", + "body": { + "content": "", + "contentType": "text" + } + } + ] +} \ No newline at end of file diff --git a/app/src/test/resources/microsoft/completed_task.txt b/app/src/test/resources/microsoft/completed_task.txt new file mode 100644 index 000000000..9874199b6 --- /dev/null +++ b/app/src/test/resources/microsoft/completed_task.txt @@ -0,0 +1,27 @@ +{ + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(todoTask)", + "@odata.deltaLink": "https://graph.microsoft.com/v1.0/me/todo/lists/AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoALgAAA8dKmrSa60tBjeiKoPukmoQBAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAA==/tasks/delta?$deltatoken=l7WI41swwioT5csv4k99nj_e6d30OPWG40P_OyQlkkny185Z38qUU3jLaP2oRC5tqLCV4OXibNfHoYNFTO2TnftnvrLwkmDPtGMUcu3SygY.rtzLcPg02xeQVyGPsoU5bQ8c2Nt8tY8ZhjhN1OUfJdQ", + "value": [ + { + "@odata.type": "#microsoft.graph.todoTask", + "@odata.etag": "W/\"SRPGnpbHYES1XW8UlYrtsgAFlRV2eQ==\"", + "importance": "normal", + "isReminderOn": false, + "status": "completed", + "title": "Basic task", + "createdDateTime": "2022-09-18T05:25:19.778574Z", + "lastModifiedDateTime": "2022-09-18T06:25:27.6073845Z", + "hasAttachments": false, + "categories": [], + "id": "AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoARgAAA8dKmrSa60tBjeiKoPukmoQHAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAEkTxp6Wx2BEtV1vFJWK7bIABZUEdtwAAAA=", + "body": { + "content": "", + "contentType": "text" + }, + "completedDateTime": { + "dateTime": "2022-09-18T05:00:00.0000000", + "timeZone": "UTC" + } + } + ] +} \ No newline at end of file