fix: fail on malformed board detail responses

This commit is contained in:
2026-03-16 00:27:45 -04:00
parent 9602b7959f
commit e7ad14902d
2 changed files with 61 additions and 12 deletions

View File

@@ -227,7 +227,9 @@ class HttpKanbnApiClient : KanbnApiClient {
apiKey = apiKey,
) { code, body ->
if (code in 200..299) {
BoardsApiResult.Success(parseBoardDetail(body, boardId))
parseBoardDetail(body, boardId)
?.let { BoardsApiResult.Success(it) }
?: BoardsApiResult.Failure("Malformed board detail response.")
} else {
BoardsApiResult.Failure(serverMessage(body, code))
}
@@ -482,9 +484,13 @@ class HttpKanbnApiClient : KanbnApiClient {
return workspaces
}
private fun parseBoardDetail(body: String, fallbackId: String): BoardDetail {
private fun parseBoardDetail(body: String, fallbackId: String): BoardDetail? {
if (body.isBlank()) {
return BoardDetail(id = fallbackId, title = "Board", lists = emptyList())
}
val root = parseJsonObject(body)
?: return BoardDetail(id = fallbackId, title = "Board", lists = emptyList())
?: return null
val data = root["data"] as? Map<*, *>
val board = (data?.get("board") as? Map<*, *>)
?: data
@@ -592,15 +598,21 @@ class HttpKanbnApiClient : KanbnApiClient {
private fun jsonEscape(value: String): String {
val builder = StringBuilder()
value.forEach { ch ->
when (ch) {
'\\' -> builder.append("\\\\")
'"' -> builder.append("\\\"")
'\b' -> builder.append("\\b")
'\u000C' -> builder.append("\\f")
'\n' -> builder.append("\\n")
'\r' -> builder.append("\\r")
'\t' -> builder.append("\\t")
else -> builder.append(ch)
if (ch.code in 0x00..0x1F) {
when (ch) {
'\b' -> builder.append("\\b")
'\u000C' -> builder.append("\\f")
'\n' -> builder.append("\\n")
'\r' -> builder.append("\\r")
'\t' -> builder.append("\\t")
else -> builder.append("\\u%04x".format(ch.code))
}
} else {
when (ch) {
'\\' -> builder.append("\\\\")
'"' -> builder.append("\\\"")
else -> builder.append(ch)
}
}
}
return builder.toString()

View File

@@ -186,6 +186,25 @@ class HttpKanbnApiClientBoardDetailParsingTest {
}
}
@Test
fun getBoardDetailFailsOnMalformedJsonPayload() = runTest {
TestServer().use { server ->
server.register(
path = "/api/v1/boards/malformed",
status = 200,
responseBody = "{\"data\": {\"id\": \"broken\"",
)
val result = HttpKanbnApiClient().getBoardDetail(server.baseUrl, "key", "malformed")
assertTrue(result is BoardsApiResult.Failure)
assertEquals(
"Malformed board detail response.",
(result as BoardsApiResult.Failure).message,
)
}
}
@Test
fun requestMappingMatchesContractForBoardDetailAndMutations() = runTest {
TestServer().use { server ->
@@ -229,6 +248,24 @@ class HttpKanbnApiClientBoardDetailParsingTest {
}
}
@Test
fun renameListEscapesControlCharactersInRequestBody() = runTest {
TestServer().use { server ->
server.register(path = "/api/v1/lists/l-esc", status = 200, responseBody = "{}")
val raw = "name\u0000line\u001f\n\tend"
val result = HttpKanbnApiClient().renameList(server.baseUrl, "api", "l-esc", raw)
assertTrue(result is BoardsApiResult.Success<*>)
val request = server.findRequest("PATCH", "/api/v1/lists/l-esc")
assertNotNull(request)
assertEquals(
"{\"name\":\"name\\u0000line\\u001f\\n\\tend\"}",
request?.body,
)
}
}
@Test
fun serverMessageIsPropagatedWithFallbackWhenMissing() = runTest {
TestServer().use { server ->