From 9602b7959f503e89bcb2bcca628f6a46439f4abc Mon Sep 17 00:00:00 2001 From: Wally Hackenslacker Date: Mon, 16 Mar 2026 00:25:24 -0400 Subject: [PATCH] fix: support plain data board wrapper parsing --- .../kanbn4droid/app/auth/KanbnApiClient.kt | 1 + ...ttpKanbnApiClientBoardDetailParsingTest.kt | 99 +++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/app/src/main/java/space/hackenslacker/kanbn4droid/app/auth/KanbnApiClient.kt b/app/src/main/java/space/hackenslacker/kanbn4droid/app/auth/KanbnApiClient.kt index ab8c586..e416074 100644 --- a/app/src/main/java/space/hackenslacker/kanbn4droid/app/auth/KanbnApiClient.kt +++ b/app/src/main/java/space/hackenslacker/kanbn4droid/app/auth/KanbnApiClient.kt @@ -487,6 +487,7 @@ class HttpKanbnApiClient : KanbnApiClient { ?: return BoardDetail(id = fallbackId, title = "Board", lists = emptyList()) val data = root["data"] as? Map<*, *> val board = (data?.get("board") as? Map<*, *>) + ?: data ?: (root["board"] as? Map<*, *>) ?: root diff --git a/app/src/test/java/space/hackenslacker/kanbn4droid/app/auth/HttpKanbnApiClientBoardDetailParsingTest.kt b/app/src/test/java/space/hackenslacker/kanbn4droid/app/auth/HttpKanbnApiClientBoardDetailParsingTest.kt index 5352c43..d5d9b4c 100644 --- a/app/src/test/java/space/hackenslacker/kanbn4droid/app/auth/HttpKanbnApiClientBoardDetailParsingTest.kt +++ b/app/src/test/java/space/hackenslacker/kanbn4droid/app/auth/HttpKanbnApiClientBoardDetailParsingTest.kt @@ -141,6 +141,51 @@ class HttpKanbnApiClientBoardDetailParsingTest { } } + @Test + fun getBoardDetailParsesPlainDataWrapperWithoutBoardKey() = runTest { + TestServer().use { server -> + server.register( + path = "/api/v1/boards/data-only", + status = 200, + responseBody = + """ + { + "data": { + "id": "data-only", + "name": "Wrapped board", + "lists": [ + { + "public_id": "list-9", + "name": "Queue", + "cards": [ + { + "publicId": "card-99", + "name": "Card in data wrapper", + "labels": [ + {"public_id": "tag-77", "title": "Infra", "color": "#ABCDEF"} + ] + } + ] + } + ] + } + } + """.trimIndent(), + ) + + val result = HttpKanbnApiClient().getBoardDetail(server.baseUrl, "key", "data-only") + + assertTrue(result is BoardsApiResult.Success<*>) + val detail = (result as BoardsApiResult.Success<*>).value as BoardDetail + assertEquals("data-only", detail.id) + assertEquals("Wrapped board", detail.title) + assertEquals(1, detail.lists.size) + assertEquals("list-9", detail.lists[0].id) + assertEquals("card-99", detail.lists[0].cards[0].id) + assertEquals("tag-77", detail.lists[0].cards[0].tags[0].id) + } + } + @Test fun requestMappingMatchesContractForBoardDetailAndMutations() = runTest { TestServer().use { server -> @@ -209,6 +254,56 @@ class HttpKanbnApiClientBoardDetailParsingTest { } } + @Test + fun moveCardFailureUsesServerMessageAndFallback() = runTest { + TestServer().use { server -> + server.register( + path = "/api/v1/cards/c-msg", + status = 409, + responseBody = """{"error":"Card cannot be moved"}""", + ) + server.register( + path = "/api/v1/cards/c-fallback", + status = 500, + responseBody = "{}", + ) + + val client = HttpKanbnApiClient() + val messageResult = client.moveCard(server.baseUrl, "api", "c-msg", "l-1") + val fallbackResult = client.moveCard(server.baseUrl, "api", "c-fallback", "l-1") + + assertTrue(messageResult is BoardsApiResult.Failure) + assertEquals("Card cannot be moved", (messageResult as BoardsApiResult.Failure).message) + assertTrue(fallbackResult is BoardsApiResult.Failure) + assertEquals("Server error: 500", (fallbackResult as BoardsApiResult.Failure).message) + } + } + + @Test + fun deleteCardFailureUsesServerMessageAndFallback() = runTest { + TestServer().use { server -> + server.register( + path = "/api/v1/cards/c-del-msg", + status = 403, + responseBody = """{"detail":"No permission to delete card"}""", + ) + server.register( + path = "/api/v1/cards/c-del-fallback", + status = 502, + responseBody = "[]", + ) + + val client = HttpKanbnApiClient() + val messageResult = client.deleteCard(server.baseUrl, "api", "c-del-msg") + val fallbackResult = client.deleteCard(server.baseUrl, "api", "c-del-fallback") + + assertTrue(messageResult is BoardsApiResult.Failure) + assertEquals("No permission to delete card", (messageResult as BoardsApiResult.Failure).message) + assertTrue(fallbackResult is BoardsApiResult.Failure) + assertEquals("Server error: 502", (fallbackResult as BoardsApiResult.Failure).message) + } + } + private data class CapturedRequest( val method: String, val path: String, @@ -316,7 +411,11 @@ class HttpKanbnApiClientBoardDetailParsingTest { val reason = when (status) { 200 -> "OK" 400 -> "Bad Request" + 403 -> "Forbidden" + 409 -> "Conflict" 404 -> "Not Found" + 500 -> "Internal Server Error" + 502 -> "Bad Gateway" 503 -> "Service Unavailable" else -> "Error" }