diff --git a/google/tasks/build.gradle.kts b/google/tasks/build.gradle.kts index f60f72da..94c9f92f 100644 --- a/google/tasks/build.gradle.kts +++ b/google/tasks/build.gradle.kts @@ -36,6 +36,8 @@ kotlin { commonTest.dependencies { implementation(libs.kotlin.test) + implementation(libs.ktor.client.mock) + implementation(libs.kotlinx.coroutines.test) } } } diff --git a/google/tasks/src/commonMain/kotlin/net/opatry/google/tasks/model/ErrorResponse.kt b/google/tasks/src/commonMain/kotlin/net/opatry/google/tasks/model/ErrorResponse.kt new file mode 100644 index 00000000..577b7439 --- /dev/null +++ b/google/tasks/src/commonMain/kotlin/net/opatry/google/tasks/model/ErrorResponse.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.google.tasks.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ErrorResponse( + @SerialName("error") + val error: Error +) { + @Serializable + data class Error( + @SerialName("code") + val code: Int, + @SerialName("message") + val message: String, + @SerialName("errors") + val errors: List, + ) { + @Serializable + data class ErrorDetail( + @SerialName("message") + val message: String, + @SerialName("domain") + val domain: String, + @SerialName("reason") + val reason: String, + ) + } +} diff --git a/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/model/ErrorModelTest.kt b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/model/ErrorModelTest.kt new file mode 100644 index 00000000..772381d5 --- /dev/null +++ b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/model/ErrorModelTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.google.tasks.model + +import kotlinx.coroutines.test.runTest +import net.opatry.google.tasks.util.loadJsonAsObject +import kotlin.test.Test +import kotlin.test.assertEquals + + +class ErrorModelTest { + @Test + fun `parse error response from json`() = runTest { + val errorResponse = loadJsonAsObject("/error_400.json") + assertEquals( + ErrorResponse( + error = ErrorResponse.Error( + code = 400, + message = "Invalid task list ID", + errors = listOf( + ErrorResponse.Error.ErrorDetail( + message = "Invalid task list ID", + domain = "global", + reason = "invalid" + ) + ) + ) + ), + errorResponse + ) + } +} \ No newline at end of file diff --git a/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/model/TaskListModelTest.kt b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/model/TaskListModelTest.kt new file mode 100644 index 00000000..5f366e5a --- /dev/null +++ b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/model/TaskListModelTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.google.tasks.model + +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import net.opatry.google.tasks.util.loadJsonAsObject +import kotlin.test.Test +import kotlin.test.assertEquals + +class TaskListModelTest { + @Test + fun `parse empty task lists from json`() = runTest { + val taskLists = loadJsonAsObject>("/tasklists_empty.json") + assertEquals( + ResourceListResponse( + kind = ResourceType.TaskLists, + etag = "\"MjEwOTM2OTcxOQ\"", + items = emptyList() + ), + taskLists + ) + } + + @Test + fun `parse task lists from json`() = runTest { + val taskLists = loadJsonAsObject>("/tasklists.json") + assertEquals( + ResourceListResponse( + kind = ResourceType.TaskLists, + etag = "\"MjEwOTM2OTcxOQ\"", + items = listOf( + TaskList( + kind = ResourceType.TaskList, + id = "MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow", + etag = "\"LTkzOTI5MzMyNQ\"", + title = "My tasks", + updatedDate = Instant.parse("2024-10-26T08:48:46.790Z"), + selfLink = "https://www.googleapis.com/tasks/v1/users/@me/lists/MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow" + ), + TaskList( + kind = ResourceType.TaskList, + id = "OXl0d1JibXgyeW1zWWFIMw", + etag = "\"LTE4NjM1MzE4NDk\"", + title = "Other tasks", + updatedDate = Instant.parse("2024-10-15T16:04:48.522Z"), + selfLink = "https://www.googleapis.com/tasks/v1/users/@me/lists/OXl0d1JibXgyeW1zWWFIMw" + ), + ) + ), + taskLists + ) + } + + @Test + fun `parse task list from json`() = runTest { + val taskList = loadJsonAsObject("/tasklist.json") + assertEquals( + TaskList( + kind = ResourceType.TaskList, + id = "OXl0d1JibXgyeW1zWWFIMw", + etag = "\"LTE4NjM1MzE4NDk\"", + title = "Other tasks", + updatedDate = Instant.parse("2024-10-15T16:04:48.522Z"), + selfLink = "https://www.googleapis.com/tasks/v1/users/@me/lists/OXl0d1JibXgyeW1zWWFIMw" + ), + taskList + ) + } +} \ No newline at end of file diff --git a/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/model/TaskModelTest.kt b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/model/TaskModelTest.kt new file mode 100644 index 00000000..c5f26c34 --- /dev/null +++ b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/model/TaskModelTest.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2024 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + +package net.opatry.google.tasks.model + +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import net.opatry.google.tasks.util.loadJsonAsObject +import kotlin.test.Test +import kotlin.test.assertEquals + + +class TaskModelTest { + @Test + fun `parse empty tasks from json`() = runTest { + val tasks = loadJsonAsObject>("/tasks_empty.json") + assertEquals( + ResourceListResponse( + kind = ResourceType.Tasks, + etag = "\"LTkzOTI5MzMyNQ\"", + items = emptyList() + ), + tasks + ) + } + + @Test + fun `parse tasks from json`() = runTest { + val tasks = loadJsonAsObject>("/tasks_with_completed_and_hidden.json") + assertEquals( + ResourceListResponse( + kind = ResourceType.Tasks, + etag = "\"LTkzOTI5MzMyNQ\"", + items = listOf( + Task( + kind = ResourceType.Task, + id = "dnBVd2IwZUlMcjZWNU84YQ", + etag = "\"LTkzOTI4MTk4Nw\"", + title = "First task TODO", + updatedDate = Instant.parse("2024-10-26T08:48:57.000Z"), + selfLink = "https://www.googleapis.com/tasks/v1/lists/MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow/tasks/dnBVd2IwZUlMcjZWNU84YQ", + position = "00000000000000000000", + status = Task.Status.NeedsAction, + dueDate = Instant.parse("2024-10-28T00:00:00.000Z"), + links = emptyList(), + webViewLink = "https://tasks.google.com/task/vpUwb0eILr6V5O8a?sa=6" + ), + Task( + kind = ResourceType.Task, + id = "M3R6eUVFQzJJUzQzZC12Qg", + etag = "\"LTk0NDMxMTUxOQ\"", + title = "A completed task", + updatedDate = Instant.parse("2024-10-26T07:25:08.000Z"), + selfLink = "https://www.googleapis.com/tasks/v1/lists/MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow/tasks/M3R6eUVFQzJJUzQzZC12Qg", + position = "09999998270072491951", + status = Task.Status.Completed, + dueDate = Instant.parse("2024-10-26T00:00:00.000Z"), + completedDate = Instant.parse("2024-10-26T07:25:08.000Z"), + isHidden = true, + links = emptyList(), + webViewLink = "https://tasks.google.com/task/3tzyEEC2IS43d-vB?sa=6" + ), + Task( + kind = ResourceType.Task, + id = "OTJOZTNPYnJjbWQ0OF9mVQ", + etag = "\"LTE3MDM5ODAyMDc\"", + title = "🎵 with emoji", + updatedDate = Instant.parse("2024-10-17T12:23:59.000Z"), + selfLink = "https://www.googleapis.com/tasks/v1/lists/MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow/tasks/OTJOZTNPYnJjbWQ0OF9mVQ", + position = "00000000000000000002", + status = Task.Status.NeedsAction, + dueDate = Instant.parse("2024-10-28T00:00:00.000Z"), + links = emptyList(), + webViewLink = "https://tasks.google.com/task/92Ne3Obrcmd48_fU?sa=6" + ), + Task( + kind = ResourceType.Task, + id = "d254c01jY1NBNEpydUJJdw", + etag = "\"LTE4OTYzOTA2NjI\"", + title = "Deleted task", + updatedDate = Instant.parse("2024-10-15T06:57:09.000Z"), + selfLink = "https://www.googleapis.com/tasks/v1/lists/MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow/tasks/d254c01jY1NBNEpydUJJdw", + position = "00000000000000000000", + status = Task.Status.NeedsAction, + isDeleted = true, + links = emptyList(), + webViewLink = "https://tasks.google.com/task/wnxsMccSA4JruBIw?sa=6" + ), + Task( + kind = ResourceType.Task, + id = "T0dZdThPNDR1RUdydUdCbQ", + etag = "\"LTE5ODE3MDM5Mjg\"", + title = "Task with notes & due date", + updatedDate = Instant.parse("2024-10-14T07:15:16.000Z"), + selfLink = "https://www.googleapis.com/tasks/v1/lists/MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow/tasks/T0dZdThPNDR1RUdydUdCbQ", + position = "00000000000000000004", + notes = "Some notes", + status = Task.Status.NeedsAction, + dueDate = Instant.parse("2024-10-28T00:00:00.000Z"), + links = emptyList(), + webViewLink = "https://tasks.google.com/task/OGYu8O44uEGruGBm?sa=6" + ) + ) + ), + tasks + ) + } + + @Test + fun `parse task from json`() = runTest { + val task = loadJsonAsObject("/task.json") + assertEquals( + Task( + kind = ResourceType.Task, + id = "dnBVd2IwZUlMcjZWNU84YQ", + etag = "\"LTkzOTI4MTk4Nw\"", + title = "First task TODO", + updatedDate = Instant.parse("2024-10-26T08:48:57.000Z"), + selfLink = "https://www.googleapis.com/tasks/v1/lists/MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow/tasks/dnBVd2IwZUlMcjZWNU84YQ", + position = "00000000000000000000", + status = Task.Status.NeedsAction, + dueDate = Instant.parse("2024-10-28T00:00:00.000Z"), + links = emptyList(), + webViewLink = "https://tasks.google.com/task/vpUwb0eILr6V5O8a?sa=6" + ), + task + ) + } +} diff --git a/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/service/TaskListsApiTest.kt b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/service/TaskListsApiTest.kt new file mode 100644 index 00000000..695d823c --- /dev/null +++ b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/service/TaskListsApiTest.kt @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2024 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.google.tasks.service + +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.plugins.ClientRequestException +import io.ktor.client.request.HttpRequestData +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf +import io.ktor.utils.io.ByteReadChannel +import kotlinx.coroutines.runBlocking +import net.opatry.google.tasks.model.ResourceType +import net.opatry.google.tasks.model.TaskList +import net.opatry.google.tasks.util.loadJson +import org.junit.Assert.assertThrows +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class TaskListsApiTest { + + @Test + fun `TaskListsApi list`() { + var request: HttpRequestData? = null + val mockEngine = MockEngine { + request = it + respond( + content = ByteReadChannel(loadJson("/tasklists.json")), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTaskListsApi(mockEngine) { taskListsApi -> + val taskLists = taskListsApi.list() + + val queryParams = request?.url?.parameters + assertEquals("/tasks/v1/users/@me/lists", request?.url?.encodedPath) + assertEquals(1, queryParams?.names()?.size) + assertEquals("20", queryParams?.get("maxResults")) + assertEquals(HttpMethod.Get, request?.method) + assertEquals(ResourceType.TaskLists, taskLists.kind) + assertEquals(2, taskLists.items.size) + assertContentEquals(listOf("My tasks", "Other tasks"), taskLists.items.map(TaskList::title)) + } + } + + @Test + fun `TaskListsApi list failure`() { + val mockEngine = MockEngine { + respond( + content = ByteReadChannel(loadJson("/error_400.json")), + status = HttpStatusCode.BadRequest, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTaskListsApi(mockEngine) { taskListsApi -> + assertThrows(ClientRequestException::class.java) { + runBlocking { + taskListsApi.list() + } + } + } + } + + @Test + fun `TaskListsApi insert`() { + var request: HttpRequestData? = null + val mockEngine = MockEngine { + request = it + respond( + content = ByteReadChannel(loadJson("/tasklist.json")), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTaskListsApi(mockEngine) { taskListsApi -> + val taskList = taskListsApi.insert(TaskList(title = "Other tasks")) + + val queryParams = request?.url?.parameters + assertEquals("/tasks/v1/users/@me/lists", request?.url?.encodedPath) + assertEquals(0, queryParams?.names()?.size) + assertEquals(HttpMethod.Post, request?.method) + assertEquals(ResourceType.TaskList, taskList.kind) + assertEquals("Other tasks", taskList.title) + assertEquals("OXl0d1JibXgyeW1zWWFIMw", taskList.id) + } + } + + @Test + fun `TaskListsApi insert failure`() { + val mockEngine = MockEngine { + respond( + content = ByteReadChannel(loadJson("/error_400.json")), + status = HttpStatusCode.BadRequest, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTaskListsApi(mockEngine) { taskListsApi -> + assertThrows(ClientRequestException::class.java) { + runBlocking { + taskListsApi.insert(TaskList(title = "")) + } + } + } + } + + @Test + fun `TaskListsApi get`() { + var request: HttpRequestData? = null + val mockEngine = MockEngine { + request = it + respond( + content = ByteReadChannel(loadJson("/tasklist.json")), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTaskListsApi(mockEngine) { taskListsApi -> + val taskList = taskListsApi.get("OXl0d1JibXgyeW1zWWFIMw") + + val queryParams = request?.url?.parameters + assertEquals("/tasks/v1/users/@me/lists/OXl0d1JibXgyeW1zWWFIMw", request?.url?.encodedPath) + assertEquals(0, queryParams?.names()?.size) + assertEquals(HttpMethod.Get, request?.method) + assertEquals(ResourceType.TaskList, taskList.kind) + assertEquals("Other tasks", taskList.title) + assertEquals("OXl0d1JibXgyeW1zWWFIMw", taskList.id) + } + } + + @Test + fun `TaskListsApi get failure`() { + val mockEngine = MockEngine { + respond( + content = ByteReadChannel(loadJson("/error_400.json")), + status = HttpStatusCode.BadRequest, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTaskListsApi(mockEngine) { taskListsApi -> + assertThrows(ClientRequestException::class.java) { + runBlocking { + taskListsApi.get("") + } + } + } + } + + @Test + fun `TaskListsApi delete`() { + var request: HttpRequestData? = null + val mockEngine = MockEngine { + request = it + respond( + content = ByteReadChannel(""), + status = HttpStatusCode.NoContent, + ) + } + + usingTaskListsApi(mockEngine) { taskListsApi -> + taskListsApi.delete("OXl0d1JibXgyeW1zWWFIMw") + + val queryParams = request?.url?.parameters + assertEquals("/tasks/v1/users/@me/lists/OXl0d1JibXgyeW1zWWFIMw", request?.url?.encodedPath) + assertEquals(0, queryParams?.names()?.size) + assertEquals(HttpMethod.Delete, request?.method) + } + } + + @Test + fun `TaskListsApi delete failure`() { + val mockEngine = MockEngine { + respond( + content = ByteReadChannel(loadJson("/error_400.json")), + status = HttpStatusCode.BadRequest, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTaskListsApi(mockEngine) { taskListsApi -> + assertThrows(ClientRequestException::class.java) { + runBlocking { + taskListsApi.delete("") + } + } + } + } + + @Test + fun `TaskListsApi patch`() { + var request: HttpRequestData? = null + val mockEngine = MockEngine { + request = it + respond( + content = ByteReadChannel(loadJson("/tasklist.json")), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTaskListsApi(mockEngine) { taskListsApi -> + val taskList = taskListsApi.patch("OXl0d1JibXgyeW1zWWFIMw", TaskList(title = "Other tasks")) + + val queryParams = request?.url?.parameters + assertEquals("/tasks/v1/users/@me/lists/OXl0d1JibXgyeW1zWWFIMw", request?.url?.encodedPath) + assertEquals(0, queryParams?.names()?.size) + assertEquals(HttpMethod.Patch, request?.method) + assertEquals(ResourceType.TaskList, taskList.kind) + assertEquals("Other tasks", taskList.title) + assertEquals("OXl0d1JibXgyeW1zWWFIMw", taskList.id) + } + } + + @Test + fun `TaskListsApi patch failure`() { + val mockEngine = MockEngine { + respond( + content = ByteReadChannel(loadJson("/error_400.json")), + status = HttpStatusCode.BadRequest, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTaskListsApi(mockEngine) { taskListsApi -> + assertThrows(ClientRequestException::class.java) { + runBlocking { + taskListsApi.patch("", TaskList(title = "")) + } + } + } + } + + @Test + fun `TaskListsApi update`() { + var request: HttpRequestData? = null + val mockEngine = MockEngine { + request = it + respond( + content = ByteReadChannel(loadJson("/tasklist.json")), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTaskListsApi(mockEngine) { taskListsApi -> + val taskList = taskListsApi.update("OXl0d1JibXgyeW1zWWFIMw", TaskList(title = "Other tasks")) + + val queryParams = request?.url?.parameters + assertEquals("/tasks/v1/users/@me/lists/OXl0d1JibXgyeW1zWWFIMw", request?.url?.encodedPath) + assertEquals(0, queryParams?.names()?.size) + assertEquals(HttpMethod.Put, request?.method) + assertEquals(ResourceType.TaskList, taskList.kind) + assertEquals("Other tasks", taskList.title) + assertEquals("OXl0d1JibXgyeW1zWWFIMw", taskList.id) + } + } + + @Test + fun `TaskListsApi update failure`() { + val mockEngine = MockEngine { + respond( + content = ByteReadChannel(loadJson("/error_400.json")), + status = HttpStatusCode.BadRequest, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTaskListsApi(mockEngine) { taskListsApi -> + assertThrows(ClientRequestException::class.java) { + runBlocking { + taskListsApi.update("", TaskList(title = "")) + } + } + } + } +} \ No newline at end of file diff --git a/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/service/TasksApiTest.kt b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/service/TasksApiTest.kt new file mode 100644 index 00000000..ada0d8f9 --- /dev/null +++ b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/service/TasksApiTest.kt @@ -0,0 +1,395 @@ +/* + * Copyright (c) 2024 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.google.tasks.service + +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.plugins.ClientRequestException +import io.ktor.client.request.HttpRequestData +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf +import io.ktor.utils.io.ByteReadChannel +import kotlinx.coroutines.runBlocking +import net.opatry.google.tasks.model.ResourceType +import net.opatry.google.tasks.model.Task +import net.opatry.google.tasks.util.loadJson +import org.junit.Assert.assertThrows +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class TasksApiTest { + + @Test + fun `TasksApi list`() { + var request: HttpRequestData? = null + val mockEngine = MockEngine { + request = it + respond( + content = ByteReadChannel(loadJson("/tasks_with_completed_and_hidden.json")), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + val tasks = tasksApi.list("SOME_ID", showCompleted = true, showDeleted = true, showHidden = true, showAssigned = true) + + val queryParams = request?.url?.parameters + assertEquals("/tasks/v1/lists/SOME_ID/tasks", request?.url?.encodedPath) + assertEquals(5, queryParams?.names()?.size) + assertEquals("20", queryParams?.get("maxResults")) + assertEquals("true", queryParams?.get("showCompleted")) + assertEquals("true", queryParams?.get("showDeleted")) + assertEquals("true", queryParams?.get("showHidden")) + assertEquals("true", queryParams?.get("showAssigned")) + assertEquals(HttpMethod.Get, request?.method) + assertEquals(ResourceType.Tasks, tasks.kind) + assertEquals(5, tasks.items.size) + assertContentEquals(listOf("First task TODO", "A completed task", "🎵 with emoji", "Deleted task", "Task with notes & due date"), tasks.items.map(Task::title)) + } + } + + @Test + fun `TasksApi list failure`() { + val mockEngine = MockEngine { + respond( + content = ByteReadChannel(loadJson("/error_400.json")), + status = HttpStatusCode.BadRequest, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + assertThrows(ClientRequestException::class.java) { + runBlocking { + tasksApi.list("") + } + } + } + } + + @Test + fun `TasksApi insert`() { + var request: HttpRequestData? = null + val mockEngine = MockEngine { + request = it + respond( + content = ByteReadChannel(loadJson("/task.json")), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + val task = tasksApi.insert("SOME_ID", Task(title = "First task TODO")) + + val queryParams = request?.url?.parameters + assertEquals("/tasks/v1/lists/SOME_ID/tasks", request?.url?.encodedPath) + assertEquals(0, queryParams?.names()?.size) + assertEquals(HttpMethod.Post, request?.method) + assertEquals(ResourceType.Task, task.kind) + assertEquals("First task TODO", task.title) + assertEquals("dnBVd2IwZUlMcjZWNU84YQ", task.id) + } + } + + @Test + fun `TasksApi insert failure`() { + val mockEngine = MockEngine { + respond( + content = ByteReadChannel(loadJson("/error_400.json")), + status = HttpStatusCode.BadRequest, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + assertThrows(ClientRequestException::class.java) { + runBlocking { + tasksApi.insert("", Task(title = "")) + } + } + } + } + + @Test + fun `TasksApi get`() { + var request: HttpRequestData? = null + val mockEngine = MockEngine { + request = it + respond( + content = ByteReadChannel(loadJson("/task.json")), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + val task = tasksApi.get("SOME_ID", "dnBVd2IwZUlMcjZWNU84YQ") + + val queryParams = request?.url?.parameters + assertEquals("/tasks/v1/lists/SOME_ID/tasks/dnBVd2IwZUlMcjZWNU84YQ", request?.url?.encodedPath) + assertEquals(0, queryParams?.names()?.size) + assertEquals(HttpMethod.Get, request?.method) + assertEquals(ResourceType.Task, task.kind) + assertEquals("First task TODO", task.title) + assertEquals("dnBVd2IwZUlMcjZWNU84YQ", task.id) + } + } + + @Test + fun `TasksApi get failure`() { + val mockEngine = MockEngine { + respond( + content = ByteReadChannel(loadJson("/error_400.json")), + status = HttpStatusCode.BadRequest, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + assertThrows(ClientRequestException::class.java) { + runBlocking { + tasksApi.get("", "") + } + } + } + } + + @Test + fun `TasksApi delete`() { + var request: HttpRequestData? = null + val mockEngine = MockEngine { + request = it + respond( + content = ByteReadChannel(""), + status = HttpStatusCode.NoContent, + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + tasksApi.delete("SOME_ID", "dnBVd2IwZUlMcjZWNU84YQ") + + val queryParams = request?.url?.parameters + assertEquals("/tasks/v1/lists/SOME_ID/tasks/dnBVd2IwZUlMcjZWNU84YQ", request?.url?.encodedPath) + assertEquals(0, queryParams?.names()?.size) + assertEquals(HttpMethod.Delete, request?.method) + } + } + + @Test + fun `TasksApi delete failure`() { + val mockEngine = MockEngine { + respond( + content = ByteReadChannel(loadJson("/error_400.json")), + status = HttpStatusCode.BadRequest, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + assertThrows(ClientRequestException::class.java) { + runBlocking { + tasksApi.delete("", "") + } + } + } + } + + @Test + fun `TasksApi patch`() { + var request: HttpRequestData? = null + val mockEngine = MockEngine { + request = it + respond( + content = ByteReadChannel(loadJson("/task.json")), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + val task = tasksApi.patch("SOME_ID", "dnBVd2IwZUlMcjZWNU84YQ", Task(title = "First task TODO")) + + val queryParams = request?.url?.parameters + assertEquals("/tasks/v1/lists/SOME_ID/tasks/dnBVd2IwZUlMcjZWNU84YQ", request?.url?.encodedPath) + assertEquals(0, queryParams?.names()?.size) + assertEquals(HttpMethod.Patch, request?.method) + assertEquals(ResourceType.Task, task.kind) + assertEquals("First task TODO", task.title) + assertEquals("dnBVd2IwZUlMcjZWNU84YQ", task.id) + } + } + + @Test + fun `TasksApi patch failure`() { + val mockEngine = MockEngine { + respond( + content = ByteReadChannel(loadJson("/error_400.json")), + status = HttpStatusCode.BadRequest, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + assertThrows(ClientRequestException::class.java) { + runBlocking { + tasksApi.patch("", "", Task(title = "")) + } + } + } + } + + @Test + fun `TasksApi update`() { + var request: HttpRequestData? = null + val mockEngine = MockEngine { + request = it + respond( + content = ByteReadChannel(loadJson("/task.json")), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + val task = tasksApi.update("SOME_ID", "dnBVd2IwZUlMcjZWNU84YQ", Task(title = "First task TODO")) + + val queryParams = request?.url?.parameters + assertEquals("/tasks/v1/lists/SOME_ID/tasks/dnBVd2IwZUlMcjZWNU84YQ", request?.url?.encodedPath) + assertEquals(0, queryParams?.names()?.size) + assertEquals(HttpMethod.Put, request?.method) + assertEquals(ResourceType.Task, task.kind) + assertEquals("First task TODO", task.title) + assertEquals("dnBVd2IwZUlMcjZWNU84YQ", task.id) + } + } + + @Test + fun `TasksApi update failure`() { + val mockEngine = MockEngine { + respond( + content = ByteReadChannel(loadJson("/error_400.json")), + status = HttpStatusCode.BadRequest, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + assertThrows(ClientRequestException::class.java) { + runBlocking { + tasksApi.update("", "", Task(title = "")) + } + } + } + } + + @Test + fun `TasksApi move`() { + var request: HttpRequestData? = null + val mockEngine = MockEngine { + request = it + respond( + content = ByteReadChannel(loadJson("/task.json")), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + val task = tasksApi.move("SOME_ID", "dnBVd2IwZUlMcjZWNU84YQ", destinationTaskListId = "SOME_OTHER_ID") + + val queryParams = request?.url?.parameters + assertEquals("/tasks/v1/lists/SOME_ID/tasks/dnBVd2IwZUlMcjZWNU84YQ/move", request?.url?.encodedPath) + assertEquals(1, queryParams?.names()?.size) + assertEquals("SOME_OTHER_ID", queryParams?.get("destinationTasklist")) + assertEquals(HttpMethod.Post, request?.method) + assertEquals(ResourceType.Task, task.kind) + assertEquals("dnBVd2IwZUlMcjZWNU84YQ", task.id) + } + } + + @Test + fun `TasksApi move failure`() { + val mockEngine = MockEngine { + respond( + content = ByteReadChannel(loadJson("/error_400.json")), + status = HttpStatusCode.BadRequest, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + assertThrows(ClientRequestException::class.java) { + runBlocking { + tasksApi.move("", "") + } + } + } + } + + @Test + fun `TasksApi clear`() { + var request: HttpRequestData? = null + val mockEngine = MockEngine { + request = it + respond( + content = ByteReadChannel(""), + status = HttpStatusCode.NoContent, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + tasksApi.clear("SOME_ID") + + val queryParams = request?.url?.parameters + assertEquals("/tasks/v1/lists/SOME_ID/clear", request?.url?.encodedPath) + assertEquals(0, queryParams?.names()?.size) + assertEquals(HttpMethod.Post, request?.method) + } + } + + @Test + fun `TasksApi clear failure`() { + val mockEngine = MockEngine { + respond( + content = ByteReadChannel(loadJson("/error_400.json")), + status = HttpStatusCode.BadRequest, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + + usingTasksApi(mockEngine) { tasksApi -> + assertThrows(ClientRequestException::class.java) { + runBlocking { + tasksApi.clear("") + } + } + } + } +} \ No newline at end of file diff --git a/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/service/usingTaskListsApi.kt b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/service/usingTaskListsApi.kt new file mode 100644 index 00000000..30b0f150 --- /dev/null +++ b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/service/usingTaskListsApi.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.google.tasks.service + +import io.ktor.client.HttpClient +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import net.opatry.google.tasks.TaskListsApi + + +fun usingTaskListsApi( + httpClientEngine: HttpClientEngine, + test: suspend TestScope.(api: TaskListsApi) -> Unit +) { + val httpClient = HttpClient(httpClientEngine) { + install(ContentNegotiation) { + json() + } + } + + runTest { + test(TaskListsApi(httpClient)) + } +} diff --git a/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/service/usingTasksApi.kt b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/service/usingTasksApi.kt new file mode 100644 index 00000000..eedf6d65 --- /dev/null +++ b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/service/usingTasksApi.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.google.tasks.service + +import io.ktor.client.HttpClient +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import net.opatry.google.tasks.TasksApi + + +fun usingTasksApi( + httpClientEngine: HttpClientEngine, + test: suspend TestScope.(api: TasksApi) -> Unit +) { + val httpClient = HttpClient(httpClientEngine) { + install(ContentNegotiation) { + json() + } + } + + runTest { + test(TasksApi(httpClient)) + } +} diff --git a/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/util/JsonLoader.kt b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/util/JsonLoader.kt new file mode 100644 index 00000000..d697aa83 --- /dev/null +++ b/google/tasks/src/commonTest/kotlin/net/opatry/google/tasks/util/JsonLoader.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.google.tasks.util + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json + +private object JsonLoader + +suspend fun loadJson(fileName: String): String { + return withContext(Dispatchers.IO) { + JsonLoader::class.java.getResource(fileName)?.readText() + ?: error("Resource $fileName not found") + } +} + +suspend inline fun loadJsonAsObject(fileName: String): T { + return withContext(Dispatchers.Default) { + Json.decodeFromString(loadJson(fileName)) + } +} \ No newline at end of file diff --git a/google/tasks/src/commonTest/resources/error_400.json b/google/tasks/src/commonTest/resources/error_400.json new file mode 100644 index 00000000..6d0024ac --- /dev/null +++ b/google/tasks/src/commonTest/resources/error_400.json @@ -0,0 +1,13 @@ +{ + "error": { + "code": 400, + "message": "Invalid task list ID", + "errors": [ + { + "message": "Invalid task list ID", + "domain": "global", + "reason": "invalid" + } + ] + } +} \ No newline at end of file diff --git a/google/tasks/src/commonTest/resources/task.json b/google/tasks/src/commonTest/resources/task.json new file mode 100644 index 00000000..dcfb02ed --- /dev/null +++ b/google/tasks/src/commonTest/resources/task.json @@ -0,0 +1,13 @@ +{ + "kind": "tasks#task", + "id": "dnBVd2IwZUlMcjZWNU84YQ", + "etag": "\"LTkzOTI4MTk4Nw\"", + "title": "First task TODO", + "updated": "2024-10-26T08:48:57.000Z", + "selfLink": "https://www.googleapis.com/tasks/v1/lists/MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow/tasks/dnBVd2IwZUlMcjZWNU84YQ", + "position": "00000000000000000000", + "status": "needsAction", + "due": "2024-10-28T00:00:00.000Z", + "links": [], + "webViewLink": "https://tasks.google.com/task/vpUwb0eILr6V5O8a?sa=6" +} diff --git a/google/tasks/src/commonTest/resources/tasklist.json b/google/tasks/src/commonTest/resources/tasklist.json new file mode 100644 index 00000000..e587cec9 --- /dev/null +++ b/google/tasks/src/commonTest/resources/tasklist.json @@ -0,0 +1,8 @@ +{ + "kind": "tasks#taskList", + "id": "OXl0d1JibXgyeW1zWWFIMw", + "etag": "\"LTE4NjM1MzE4NDk\"", + "title": "Other tasks", + "updated": "2024-10-15T16:04:48.522Z", + "selfLink": "https://www.googleapis.com/tasks/v1/users/@me/lists/OXl0d1JibXgyeW1zWWFIMw" +} \ No newline at end of file diff --git a/google/tasks/src/commonTest/resources/tasklists.json b/google/tasks/src/commonTest/resources/tasklists.json new file mode 100644 index 00000000..39da61c4 --- /dev/null +++ b/google/tasks/src/commonTest/resources/tasklists.json @@ -0,0 +1,22 @@ +{ + "kind": "tasks#taskLists", + "etag": "\"MjEwOTM2OTcxOQ\"", + "items": [ + { + "kind": "tasks#taskList", + "id": "MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow", + "etag": "\"LTkzOTI5MzMyNQ\"", + "title": "My tasks", + "updated": "2024-10-26T08:48:46.790Z", + "selfLink": "https://www.googleapis.com/tasks/v1/users/@me/lists/MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow" + }, + { + "kind": "tasks#taskList", + "id": "OXl0d1JibXgyeW1zWWFIMw", + "etag": "\"LTE4NjM1MzE4NDk\"", + "title": "Other tasks", + "updated": "2024-10-15T16:04:48.522Z", + "selfLink": "https://www.googleapis.com/tasks/v1/users/@me/lists/OXl0d1JibXgyeW1zWWFIMw" + } + ] +} \ No newline at end of file diff --git a/google/tasks/src/commonTest/resources/tasklists_empty.json b/google/tasks/src/commonTest/resources/tasklists_empty.json new file mode 100644 index 00000000..f18b1600 --- /dev/null +++ b/google/tasks/src/commonTest/resources/tasklists_empty.json @@ -0,0 +1,5 @@ +{ + "kind": "tasks#taskLists", + "etag": "\"MjEwOTM2OTcxOQ\"", + "items": [] +} \ No newline at end of file diff --git a/google/tasks/src/commonTest/resources/tasks_empty.json b/google/tasks/src/commonTest/resources/tasks_empty.json new file mode 100644 index 00000000..1ab1184d --- /dev/null +++ b/google/tasks/src/commonTest/resources/tasks_empty.json @@ -0,0 +1,5 @@ +{ + "kind": "tasks#tasks", + "etag": "\"LTkzOTI5MzMyNQ\"", + "items": [] +} diff --git a/google/tasks/src/commonTest/resources/tasks_with_completed_and_hidden.json b/google/tasks/src/commonTest/resources/tasks_with_completed_and_hidden.json new file mode 100644 index 00000000..94db15cd --- /dev/null +++ b/google/tasks/src/commonTest/resources/tasks_with_completed_and_hidden.json @@ -0,0 +1,74 @@ +{ + "kind": "tasks#tasks", + "etag": "\"LTkzOTI5MzMyNQ\"", + "items": [ + { + "kind": "tasks#task", + "id": "dnBVd2IwZUlMcjZWNU84YQ", + "etag": "\"LTkzOTI4MTk4Nw\"", + "title": "First task TODO", + "updated": "2024-10-26T08:48:57.000Z", + "selfLink": "https://www.googleapis.com/tasks/v1/lists/MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow/tasks/dnBVd2IwZUlMcjZWNU84YQ", + "position": "00000000000000000000", + "status": "needsAction", + "due": "2024-10-28T00:00:00.000Z", + "links": [], + "webViewLink": "https://tasks.google.com/task/vpUwb0eILr6V5O8a?sa=6" + }, + { + "kind": "tasks#task", + "id": "M3R6eUVFQzJJUzQzZC12Qg", + "etag": "\"LTk0NDMxMTUxOQ\"", + "title": "A completed task", + "updated": "2024-10-26T07:25:08.000Z", + "selfLink": "https://www.googleapis.com/tasks/v1/lists/MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow/tasks/M3R6eUVFQzJJUzQzZC12Qg", + "position": "09999998270072491951", + "status": "completed", + "due": "2024-10-26T00:00:00.000Z", + "completed": "2024-10-26T07:25:08.000Z", + "hidden": true, + "links": [], + "webViewLink": "https://tasks.google.com/task/3tzyEEC2IS43d-vB?sa=6" + }, + { + "kind": "tasks#task", + "id": "OTJOZTNPYnJjbWQ0OF9mVQ", + "etag": "\"LTE3MDM5ODAyMDc\"", + "title": "🎵 with emoji", + "updated": "2024-10-17T12:23:59.000Z", + "selfLink": "https://www.googleapis.com/tasks/v1/lists/MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow/tasks/OTJOZTNPYnJjbWQ0OF9mVQ", + "position": "00000000000000000002", + "status": "needsAction", + "due": "2024-10-28T00:00:00.000Z", + "links": [], + "webViewLink": "https://tasks.google.com/task/92Ne3Obrcmd48_fU?sa=6" + }, + { + "kind": "tasks#task", + "id": "d254c01jY1NBNEpydUJJdw", + "etag": "\"LTE4OTYzOTA2NjI\"", + "title": "Deleted task", + "updated": "2024-10-15T06:57:09.000Z", + "selfLink": "https://www.googleapis.com/tasks/v1/lists/MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow/tasks/d254c01jY1NBNEpydUJJdw", + "position": "00000000000000000000", + "status": "needsAction", + "deleted": true, + "links": [], + "webViewLink": "https://tasks.google.com/task/wnxsMccSA4JruBIw?sa=6" + }, + { + "kind": "tasks#task", + "id": "T0dZdThPNDR1RUdydUdCbQ", + "etag": "\"LTE5ODE3MDM5Mjg\"", + "title": "Task with notes & due date", + "updated": "2024-10-14T07:15:16.000Z", + "selfLink": "https://www.googleapis.com/tasks/v1/lists/MTAwNDEyMDI1NDY0NDEwNzQ0NDI6MDow/tasks/T0dZdThPNDR1RUdydUdCbQ", + "position": "00000000000000000004", + "notes": "Some notes", + "status": "needsAction", + "due": "2024-10-28T00:00:00.000Z", + "links": [], + "webViewLink": "https://tasks.google.com/task/OGYu8O44uEGruGBm?sa=6" + } + ] +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bc8508bc..67b50a23 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ about-libraries = "11.2.3" kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.6.1" } kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.7.3" } @@ -33,6 +34,7 @@ ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }