fix: align createCard due-date contract with api client ownership

This commit is contained in:
2026-03-16 13:44:06 -04:00
parent efe19c794f
commit 3d8b9e4491
4 changed files with 83 additions and 22 deletions

View File

@@ -4,6 +4,7 @@ import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.time.Instant
import java.time.LocalDate
import org.json.JSONArray
import org.json.JSONObject
import kotlinx.coroutines.Dispatchers
@@ -79,7 +80,7 @@ interface KanbnApiClient {
listPublicId: String,
title: String,
description: String?,
dueDate: String?,
dueDate: LocalDate?,
tagPublicIds: List<String>,
): BoardsApiResult<CreatedEntityRef> {
return BoardsApiResult.Failure("Card creation is not implemented.")
@@ -300,16 +301,18 @@ class HttpKanbnApiClient : KanbnApiClient {
appendIndex: Int,
): BoardsApiResult<CreatedEntityRef> {
return withContext(Dispatchers.IO) {
val payload = JSONObject()
.put("boardPublicId", boardPublicId)
.put("name", title)
.put("index", appendIndex)
val payload =
"{" +
"\"boardPublicId\":\"${jsonEscape(boardPublicId)}\"," +
"\"name\":\"${jsonEscape(title)}\"," +
"\"index\":$appendIndex" +
"}"
request(
baseUrl = baseUrl,
path = "/api/v1/lists",
method = "POST",
apiKey = apiKey,
body = payload.toString(),
body = payload,
) { code, body ->
if (code in 200..299) {
parseCreatedEntityRef(body)
@@ -328,23 +331,29 @@ class HttpKanbnApiClient : KanbnApiClient {
listPublicId: String,
title: String,
description: String?,
dueDate: String?,
dueDate: LocalDate?,
tagPublicIds: List<String>,
): BoardsApiResult<CreatedEntityRef> {
return withContext(Dispatchers.IO) {
val payload = JSONObject()
.put("listPublicId", listPublicId)
.put("title", title)
.put("description", description ?: "")
.put("dueDate", dueDate)
.put("index", 0)
.put("labelPublicIds", JSONArray(tagPublicIds))
val payloadFields = mutableListOf(
"\"listPublicId\":\"${jsonEscape(listPublicId)}\"",
"\"title\":\"${jsonEscape(title)}\"",
"\"index\":0",
"\"labelPublicIds\":${jsonStringArray(tagPublicIds)}",
)
if (description != null) {
payloadFields += "\"description\":\"${jsonEscape(description)}\""
}
if (dueDate != null) {
payloadFields += "\"dueDate\":\"${dueDate}T00:00:00Z\""
}
val payload = "{${payloadFields.joinToString(",")}}"
request(
baseUrl = baseUrl,
path = "/api/v1/cards",
method = "POST",
apiKey = apiKey,
body = payload.toString(),
body = payload,
) { code, body ->
if (code in 200..299) {
parseCreatedEntityRef(body)
@@ -890,6 +899,10 @@ class HttpKanbnApiClient : KanbnApiClient {
return builder.toString()
}
private fun jsonStringArray(values: List<String>): String {
return values.joinToString(prefix = "[", postfix = "]", separator = ",") { "\"${jsonEscape(it)}\"" }
}
private class MiniJsonParser(private val input: String) {
private var index = 0

View File

@@ -3,6 +3,7 @@ package space.hackenslacker.kanbn4droid.app.boarddetail
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.time.LocalDate
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient
import space.hackenslacker.kanbn4droid.app.auth.LabelDetail
@@ -73,7 +74,7 @@ class BoardDetailRepository(
listId: String,
title: String,
description: String?,
dueDate: String?,
dueDate: LocalDate?,
tagIds: Collection<String>,
): BoardsApiResult<CreatedEntityRef> {
val normalizedListId = listId.trim()
@@ -87,7 +88,6 @@ class BoardDetailRepository(
}
val normalizedDescription = description?.trim()?.takeIf { it.isNotBlank() }
val normalizedDueDate = dueDate?.trim()?.takeIf { it.isNotBlank() }
val normalizedTagIds = tagIds
.map { it.trim() }
.filter { it.isNotBlank() }
@@ -104,7 +104,7 @@ class BoardDetailRepository(
listPublicId = normalizedListId,
title = normalizedTitle,
description = normalizedDescription,
dueDate = normalizedDueDate,
dueDate = dueDate,
tagPublicIds = normalizedTagIds,
)
}

View File

@@ -6,6 +6,7 @@ import java.net.InetSocketAddress
import java.net.ServerSocket
import java.net.Socket
import java.time.Instant
import java.time.LocalDate
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
@@ -21,6 +22,52 @@ import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
class HttpKanbnApiClientBoardDetailParsingTest {
@Test
fun createCardFormatsLocalDateAsUtcMidnightInPayload() = runTest {
TestServer().use { server ->
server.register(path = "/api/v1/cards", method = "POST", status = 200, responseBody = """{"publicId":"card-new"}""")
val result = HttpKanbnApiClient().createCard(
baseUrl = server.baseUrl,
apiKey = "api",
listPublicId = "list-1",
title = "Card",
description = "Description",
dueDate = LocalDate.of(2026, 3, 16),
tagPublicIds = listOf("tag-1"),
)
assertTrue(result is BoardsApiResult.Success<*>)
val request = server.findRequest("POST", "/api/v1/cards")
assertNotNull(request)
assertTrue(request?.body?.contains("\"dueDate\":\"2026-03-16T00:00:00Z\"") == true)
assertTrue(request?.body?.contains("\"description\":\"Description\"") == true)
}
}
@Test
fun createCardOmitsNullDueDateAndDescriptionInPayload() = runTest {
TestServer().use { server ->
server.register(path = "/api/v1/cards", method = "POST", status = 200, responseBody = """{"publicId":"card-new"}""")
val result = HttpKanbnApiClient().createCard(
baseUrl = server.baseUrl,
apiKey = "api",
listPublicId = "list-1",
title = "Card",
description = null,
dueDate = null,
tagPublicIds = emptyList(),
)
assertTrue(result is BoardsApiResult.Success<*>)
val request = server.findRequest("POST", "/api/v1/cards")
assertNotNull(request)
assertTrue(request?.body?.contains("\"dueDate\"") == false)
assertTrue(request?.body?.contains("\"description\"") == false)
}
}
@Test
fun getBoardDetailParsesWrappedPayloadWithDueDateVariants() = runTest {
TestServer().use { server ->

View File

@@ -1,6 +1,7 @@
package space.hackenslacker.kanbn4droid.app.boarddetail
import kotlinx.coroutines.test.runTest
import java.time.LocalDate
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -216,7 +217,7 @@ class BoardDetailRepositoryTest {
listId = " list-1 ",
title = " Card title ",
description = " Description ",
dueDate = "2026-03-16T10:15:30Z",
dueDate = LocalDate.of(2026, 3, 16),
tagIds = listOf(" tag-1 ", "", "tag-2", "tag-1", " ", " tag-2"),
)
@@ -224,7 +225,7 @@ class BoardDetailRepositoryTest {
assertEquals("list-1", apiClient.lastCreateCardListPublicId)
assertEquals("Card title", apiClient.lastCreateCardTitle)
assertEquals("Description", apiClient.lastCreateCardDescription)
assertEquals("2026-03-16T10:15:30Z", apiClient.lastCreateCardDueDate)
assertEquals(LocalDate.of(2026, 3, 16), apiClient.lastCreateCardDueDate)
assertEquals(listOf("tag-1", "tag-2"), apiClient.lastCreateCardTagPublicIds)
}
@@ -509,7 +510,7 @@ class BoardDetailRepositoryTest {
var lastCreateCardListPublicId: String? = null
var lastCreateCardTitle: String? = null
var lastCreateCardDescription: String? = null
var lastCreateCardDueDate: String? = null
var lastCreateCardDueDate: LocalDate? = null
var lastCreateCardTagPublicIds: List<String> = emptyList()
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
@@ -558,7 +559,7 @@ class BoardDetailRepositoryTest {
listPublicId: String,
title: String,
description: String?,
dueDate: String?,
dueDate: LocalDate?,
tagPublicIds: List<String>,
): BoardsApiResult<CreatedEntityRef> {
lastCreateCardListPublicId = listPublicId