fix: align createCard due-date contract with api client ownership
This commit is contained in:
@@ -4,6 +4,7 @@ import java.io.IOException
|
|||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -79,7 +80,7 @@ interface KanbnApiClient {
|
|||||||
listPublicId: String,
|
listPublicId: String,
|
||||||
title: String,
|
title: String,
|
||||||
description: String?,
|
description: String?,
|
||||||
dueDate: String?,
|
dueDate: LocalDate?,
|
||||||
tagPublicIds: List<String>,
|
tagPublicIds: List<String>,
|
||||||
): BoardsApiResult<CreatedEntityRef> {
|
): BoardsApiResult<CreatedEntityRef> {
|
||||||
return BoardsApiResult.Failure("Card creation is not implemented.")
|
return BoardsApiResult.Failure("Card creation is not implemented.")
|
||||||
@@ -300,16 +301,18 @@ class HttpKanbnApiClient : KanbnApiClient {
|
|||||||
appendIndex: Int,
|
appendIndex: Int,
|
||||||
): BoardsApiResult<CreatedEntityRef> {
|
): BoardsApiResult<CreatedEntityRef> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val payload = JSONObject()
|
val payload =
|
||||||
.put("boardPublicId", boardPublicId)
|
"{" +
|
||||||
.put("name", title)
|
"\"boardPublicId\":\"${jsonEscape(boardPublicId)}\"," +
|
||||||
.put("index", appendIndex)
|
"\"name\":\"${jsonEscape(title)}\"," +
|
||||||
|
"\"index\":$appendIndex" +
|
||||||
|
"}"
|
||||||
request(
|
request(
|
||||||
baseUrl = baseUrl,
|
baseUrl = baseUrl,
|
||||||
path = "/api/v1/lists",
|
path = "/api/v1/lists",
|
||||||
method = "POST",
|
method = "POST",
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
body = payload.toString(),
|
body = payload,
|
||||||
) { code, body ->
|
) { code, body ->
|
||||||
if (code in 200..299) {
|
if (code in 200..299) {
|
||||||
parseCreatedEntityRef(body)
|
parseCreatedEntityRef(body)
|
||||||
@@ -328,23 +331,29 @@ class HttpKanbnApiClient : KanbnApiClient {
|
|||||||
listPublicId: String,
|
listPublicId: String,
|
||||||
title: String,
|
title: String,
|
||||||
description: String?,
|
description: String?,
|
||||||
dueDate: String?,
|
dueDate: LocalDate?,
|
||||||
tagPublicIds: List<String>,
|
tagPublicIds: List<String>,
|
||||||
): BoardsApiResult<CreatedEntityRef> {
|
): BoardsApiResult<CreatedEntityRef> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val payload = JSONObject()
|
val payloadFields = mutableListOf(
|
||||||
.put("listPublicId", listPublicId)
|
"\"listPublicId\":\"${jsonEscape(listPublicId)}\"",
|
||||||
.put("title", title)
|
"\"title\":\"${jsonEscape(title)}\"",
|
||||||
.put("description", description ?: "")
|
"\"index\":0",
|
||||||
.put("dueDate", dueDate)
|
"\"labelPublicIds\":${jsonStringArray(tagPublicIds)}",
|
||||||
.put("index", 0)
|
)
|
||||||
.put("labelPublicIds", JSONArray(tagPublicIds))
|
if (description != null) {
|
||||||
|
payloadFields += "\"description\":\"${jsonEscape(description)}\""
|
||||||
|
}
|
||||||
|
if (dueDate != null) {
|
||||||
|
payloadFields += "\"dueDate\":\"${dueDate}T00:00:00Z\""
|
||||||
|
}
|
||||||
|
val payload = "{${payloadFields.joinToString(",")}}"
|
||||||
request(
|
request(
|
||||||
baseUrl = baseUrl,
|
baseUrl = baseUrl,
|
||||||
path = "/api/v1/cards",
|
path = "/api/v1/cards",
|
||||||
method = "POST",
|
method = "POST",
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
body = payload.toString(),
|
body = payload,
|
||||||
) { code, body ->
|
) { code, body ->
|
||||||
if (code in 200..299) {
|
if (code in 200..299) {
|
||||||
parseCreatedEntityRef(body)
|
parseCreatedEntityRef(body)
|
||||||
@@ -890,6 +899,10 @@ class HttpKanbnApiClient : KanbnApiClient {
|
|||||||
return builder.toString()
|
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 class MiniJsonParser(private val input: String) {
|
||||||
private var index = 0
|
private var index = 0
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package space.hackenslacker.kanbn4droid.app.boarddetail
|
|||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.time.LocalDate
|
||||||
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
|
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
|
||||||
import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient
|
import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient
|
||||||
import space.hackenslacker.kanbn4droid.app.auth.LabelDetail
|
import space.hackenslacker.kanbn4droid.app.auth.LabelDetail
|
||||||
@@ -73,7 +74,7 @@ class BoardDetailRepository(
|
|||||||
listId: String,
|
listId: String,
|
||||||
title: String,
|
title: String,
|
||||||
description: String?,
|
description: String?,
|
||||||
dueDate: String?,
|
dueDate: LocalDate?,
|
||||||
tagIds: Collection<String>,
|
tagIds: Collection<String>,
|
||||||
): BoardsApiResult<CreatedEntityRef> {
|
): BoardsApiResult<CreatedEntityRef> {
|
||||||
val normalizedListId = listId.trim()
|
val normalizedListId = listId.trim()
|
||||||
@@ -87,7 +88,6 @@ class BoardDetailRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val normalizedDescription = description?.trim()?.takeIf { it.isNotBlank() }
|
val normalizedDescription = description?.trim()?.takeIf { it.isNotBlank() }
|
||||||
val normalizedDueDate = dueDate?.trim()?.takeIf { it.isNotBlank() }
|
|
||||||
val normalizedTagIds = tagIds
|
val normalizedTagIds = tagIds
|
||||||
.map { it.trim() }
|
.map { it.trim() }
|
||||||
.filter { it.isNotBlank() }
|
.filter { it.isNotBlank() }
|
||||||
@@ -104,7 +104,7 @@ class BoardDetailRepository(
|
|||||||
listPublicId = normalizedListId,
|
listPublicId = normalizedListId,
|
||||||
title = normalizedTitle,
|
title = normalizedTitle,
|
||||||
description = normalizedDescription,
|
description = normalizedDescription,
|
||||||
dueDate = normalizedDueDate,
|
dueDate = dueDate,
|
||||||
tagPublicIds = normalizedTagIds,
|
tagPublicIds = normalizedTagIds,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.net.InetSocketAddress
|
|||||||
import java.net.ServerSocket
|
import java.net.ServerSocket
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -21,6 +22,52 @@ import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
|
|||||||
|
|
||||||
class HttpKanbnApiClientBoardDetailParsingTest {
|
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
|
@Test
|
||||||
fun getBoardDetailParsesWrappedPayloadWithDueDateVariants() = runTest {
|
fun getBoardDetailParsesWrappedPayloadWithDueDateVariants() = runTest {
|
||||||
TestServer().use { server ->
|
TestServer().use { server ->
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package space.hackenslacker.kanbn4droid.app.boarddetail
|
package space.hackenslacker.kanbn4droid.app.boarddetail
|
||||||
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import java.time.LocalDate
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -216,7 +217,7 @@ class BoardDetailRepositoryTest {
|
|||||||
listId = " list-1 ",
|
listId = " list-1 ",
|
||||||
title = " Card title ",
|
title = " Card title ",
|
||||||
description = " Description ",
|
description = " Description ",
|
||||||
dueDate = "2026-03-16T10:15:30Z",
|
dueDate = LocalDate.of(2026, 3, 16),
|
||||||
tagIds = listOf(" tag-1 ", "", "tag-2", "tag-1", " ", " tag-2"),
|
tagIds = listOf(" tag-1 ", "", "tag-2", "tag-1", " ", " tag-2"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -224,7 +225,7 @@ class BoardDetailRepositoryTest {
|
|||||||
assertEquals("list-1", apiClient.lastCreateCardListPublicId)
|
assertEquals("list-1", apiClient.lastCreateCardListPublicId)
|
||||||
assertEquals("Card title", apiClient.lastCreateCardTitle)
|
assertEquals("Card title", apiClient.lastCreateCardTitle)
|
||||||
assertEquals("Description", apiClient.lastCreateCardDescription)
|
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)
|
assertEquals(listOf("tag-1", "tag-2"), apiClient.lastCreateCardTagPublicIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,7 +510,7 @@ class BoardDetailRepositoryTest {
|
|||||||
var lastCreateCardListPublicId: String? = null
|
var lastCreateCardListPublicId: String? = null
|
||||||
var lastCreateCardTitle: String? = null
|
var lastCreateCardTitle: String? = null
|
||||||
var lastCreateCardDescription: String? = null
|
var lastCreateCardDescription: String? = null
|
||||||
var lastCreateCardDueDate: String? = null
|
var lastCreateCardDueDate: LocalDate? = null
|
||||||
var lastCreateCardTagPublicIds: List<String> = emptyList()
|
var lastCreateCardTagPublicIds: List<String> = emptyList()
|
||||||
|
|
||||||
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
|
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
|
||||||
@@ -558,7 +559,7 @@ class BoardDetailRepositoryTest {
|
|||||||
listPublicId: String,
|
listPublicId: String,
|
||||||
title: String,
|
title: String,
|
||||||
description: String?,
|
description: String?,
|
||||||
dueDate: String?,
|
dueDate: LocalDate?,
|
||||||
tagPublicIds: List<String>,
|
tagPublicIds: List<String>,
|
||||||
): BoardsApiResult<CreatedEntityRef> {
|
): BoardsApiResult<CreatedEntityRef> {
|
||||||
lastCreateCardListPublicId = listPublicId
|
lastCreateCardListPublicId = listPublicId
|
||||||
|
|||||||
Reference in New Issue
Block a user