Compare commits
4 Commits
1db5c53375
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| cdb8ba58da | |||
| de59a1d0e7 | |||
| d162841708 | |||
| 29e859bc01 |
@@ -387,25 +387,47 @@ class HttpKanbnApiClient : KanbnApiClient {
|
|||||||
tagPublicIds: List<String>,
|
tagPublicIds: List<String>,
|
||||||
): BoardsApiResult<CreatedEntityRef> {
|
): BoardsApiResult<CreatedEntityRef> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val payloadFields = mutableListOf(
|
val canonicalPayload = buildCreateCardPayload(
|
||||||
"\"listPublicId\":\"${jsonEscape(listPublicId)}\"",
|
listPublicId = listPublicId,
|
||||||
"\"title\":\"${jsonEscape(title)}\"",
|
title = title,
|
||||||
"\"index\":0",
|
description = description,
|
||||||
"\"labelPublicIds\":${jsonStringArray(tagPublicIds)}",
|
dueDate = dueDate,
|
||||||
|
tagPublicIds = tagPublicIds,
|
||||||
|
aliasKeys = false,
|
||||||
)
|
)
|
||||||
if (description != null) {
|
val canonicalResult = request(
|
||||||
payloadFields += "\"description\":\"${jsonEscape(description)}\""
|
baseUrl = baseUrl,
|
||||||
|
path = "/api/v1/cards",
|
||||||
|
method = "POST",
|
||||||
|
apiKey = apiKey,
|
||||||
|
body = canonicalPayload,
|
||||||
|
) { code, body ->
|
||||||
|
if (code in 200..299) {
|
||||||
|
parseCreatedEntityRef(body)
|
||||||
|
?.let { BoardsApiResult.Success(it) }
|
||||||
|
?: BoardsApiResult.Failure("Malformed create card response.")
|
||||||
|
} else {
|
||||||
|
BoardsApiResult.Failure(serverMessage(body, code))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (dueDate != null) {
|
if (canonicalResult is BoardsApiResult.Success) {
|
||||||
payloadFields += "\"dueDate\":\"${dueDate}T00:00:00Z\""
|
return@withContext canonicalResult
|
||||||
}
|
}
|
||||||
val payload = "{${payloadFields.joinToString(",")}}"
|
|
||||||
|
val aliasPayload = buildCreateCardPayload(
|
||||||
|
listPublicId = listPublicId,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
dueDate = dueDate,
|
||||||
|
tagPublicIds = tagPublicIds,
|
||||||
|
aliasKeys = true,
|
||||||
|
)
|
||||||
request(
|
request(
|
||||||
baseUrl = baseUrl,
|
baseUrl = baseUrl,
|
||||||
path = "/api/v1/cards",
|
path = "/api/v1/cards",
|
||||||
method = "POST",
|
method = "POST",
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
body = payload,
|
body = aliasPayload,
|
||||||
) { code, body ->
|
) { code, body ->
|
||||||
if (code in 200..299) {
|
if (code in 200..299) {
|
||||||
parseCreatedEntityRef(body)
|
parseCreatedEntityRef(body)
|
||||||
@@ -418,6 +440,44 @@ class HttpKanbnApiClient : KanbnApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildCreateCardPayload(
|
||||||
|
listPublicId: String,
|
||||||
|
title: String,
|
||||||
|
description: String?,
|
||||||
|
dueDate: LocalDate?,
|
||||||
|
tagPublicIds: List<String>,
|
||||||
|
aliasKeys: Boolean,
|
||||||
|
): String {
|
||||||
|
val payloadFields = mutableListOf(
|
||||||
|
"\"listPublicId\":\"${jsonEscape(listPublicId)}\"",
|
||||||
|
if (aliasKeys) {
|
||||||
|
"\"name\":\"${jsonEscape(title)}\""
|
||||||
|
} else {
|
||||||
|
"\"title\":\"${jsonEscape(title)}\""
|
||||||
|
},
|
||||||
|
"\"description\":\"${jsonEscape(description.orEmpty())}\"",
|
||||||
|
"\"labelPublicIds\":${jsonStringArray(tagPublicIds)}",
|
||||||
|
"\"memberPublicIds\":[]",
|
||||||
|
"\"position\":\"start\"",
|
||||||
|
)
|
||||||
|
if (aliasKeys) {
|
||||||
|
payloadFields += if (aliasKeys) {
|
||||||
|
"\"body\":\"${jsonEscape(description.orEmpty())}\""
|
||||||
|
} else {
|
||||||
|
"\"description\":\"${jsonEscape(description.orEmpty())}\""
|
||||||
|
}
|
||||||
|
payloadFields += "\"index\":0"
|
||||||
|
}
|
||||||
|
if (dueDate != null) {
|
||||||
|
payloadFields += if (aliasKeys) {
|
||||||
|
"\"dueAt\":\"${dueDate}T00:00:00Z\""
|
||||||
|
} else {
|
||||||
|
"\"dueDate\":\"${dueDate}T00:00:00Z\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "{${payloadFields.joinToString(",")}}"
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun moveCard(
|
override suspend fun moveCard(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
apiKey: String,
|
apiKey: String,
|
||||||
@@ -936,15 +996,11 @@ class HttpKanbnApiClient : KanbnApiClient {
|
|||||||
|
|
||||||
private fun isTemplateBoard(item: JSONObject): Boolean {
|
private fun isTemplateBoard(item: JSONObject): Boolean {
|
||||||
val typeKeys = listOf("type", "boardType", "kind")
|
val typeKeys = listOf("type", "boardType", "kind")
|
||||||
val hasTemplateType = typeKeys.any { key ->
|
return typeKeys.any { key ->
|
||||||
item.optString(key)
|
item.optString(key)
|
||||||
.trim()
|
.trim()
|
||||||
.equals("template", ignoreCase = true)
|
.equals("template", ignoreCase = true)
|
||||||
}
|
}
|
||||||
if (hasTemplateType) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return item.optBoolean("isTemplate", false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseSingleBoard(body: String, fallbackName: String): BoardSummary {
|
private fun parseSingleBoard(body: String, fallbackName: String): BoardSummary {
|
||||||
|
|||||||
+34
-3
@@ -99,7 +99,7 @@ class HttpKanbnApiClientBoardDetailParsingTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun createCard_sendsTopIndex_andOmittedDueDateWhenNull() = runTest {
|
fun createCard_sendsRequiredValidationFields_whenOptionalsAreNull() = runTest {
|
||||||
TestServer().use { server ->
|
TestServer().use { server ->
|
||||||
server.register(path = "/api/v1/cards", method = "POST", status = 200, responseBody = """{"publicId":"card-new"}""")
|
server.register(path = "/api/v1/cards", method = "POST", status = 200, responseBody = """{"publicId":"card-new"}""")
|
||||||
|
|
||||||
@@ -116,9 +116,10 @@ class HttpKanbnApiClientBoardDetailParsingTest {
|
|||||||
assertTrue(result is BoardsApiResult.Success<*>)
|
assertTrue(result is BoardsApiResult.Success<*>)
|
||||||
val request = server.findRequest("POST", "/api/v1/cards")
|
val request = server.findRequest("POST", "/api/v1/cards")
|
||||||
assertNotNull(request)
|
assertNotNull(request)
|
||||||
assertTrue(request?.body?.contains("\"index\":0") == true)
|
assertTrue(request?.body?.contains("\"position\":\"start\"") == true)
|
||||||
|
assertTrue(request?.body?.contains("\"memberPublicIds\":[]") == true)
|
||||||
|
assertTrue(request?.body?.contains("\"description\":\"\"") == true)
|
||||||
assertTrue(request?.body?.contains("\"dueDate\"") == false)
|
assertTrue(request?.body?.contains("\"dueDate\"") == false)
|
||||||
assertTrue(request?.body?.contains("\"description\"") == false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +173,36 @@ class HttpKanbnApiClientBoardDetailParsingTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createCard_retriesWithAliasPayloadWhenServerRejectsCanonicalFields() = runTest {
|
||||||
|
TestServer().use { server ->
|
||||||
|
server.registerSequence(
|
||||||
|
path = "/api/v1/cards",
|
||||||
|
method = "POST",
|
||||||
|
responses = listOf(
|
||||||
|
400 to "{}",
|
||||||
|
200 to """{"card":{"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 requests = server.findRequests("POST", "/api/v1/cards")
|
||||||
|
assertEquals(2, requests.size)
|
||||||
|
assertTrue(requests[0].body.contains("\"title\":\"Card\""))
|
||||||
|
assertTrue(requests[1].body.contains("\"name\":\"Card\""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getBoardDetailParsesWrappedPayloadWithDueDateVariants() = runTest {
|
fun getBoardDetailParsesWrappedPayloadWithDueDateVariants() = runTest {
|
||||||
TestServer().use { server ->
|
TestServer().use { server ->
|
||||||
|
|||||||
+1
-2
@@ -32,7 +32,6 @@ class HttpKanbnApiClientBoardsParsingTest {
|
|||||||
{"id":"board-1","name":"Roadmap"},
|
{"id":"board-1","name":"Roadmap"},
|
||||||
{"id":"tmpl-1","name":"Template A","type":"template"},
|
{"id":"tmpl-1","name":"Template A","type":"template"},
|
||||||
{"id":"tmpl-2","name":"Template B","kind":"template"},
|
{"id":"tmpl-2","name":"Template B","kind":"template"},
|
||||||
{"id":"tmpl-3","name":"Template C","isTemplate":true},
|
|
||||||
{"id":"tmpl-4","name":"Template D","boardType":" Template "},
|
{"id":"tmpl-4","name":"Template D","boardType":" Template "},
|
||||||
{"publicId":"board-2","title":"Backlog"}
|
{"publicId":"board-2","title":"Backlog"}
|
||||||
]
|
]
|
||||||
@@ -59,7 +58,7 @@ class HttpKanbnApiClientBoardsParsingTest {
|
|||||||
"boards": [
|
"boards": [
|
||||||
{"id":"tmpl-1","name":"Template A","type":"template"},
|
{"id":"tmpl-1","name":"Template A","type":"template"},
|
||||||
{"id":"tmpl-2","name":"Template B","kind":"template"},
|
{"id":"tmpl-2","name":"Template B","kind":"template"},
|
||||||
{"id":"tmpl-3","name":"Template C","isTemplate":true}
|
{"id":"tmpl-3","name":"Template C","boardType":"TEMPLATE"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
|
|||||||
+1
-62
@@ -109,65 +109,6 @@ class BoardsRepositoryTest {
|
|||||||
assertEquals("No workspaces available for this account.", (result as BoardsApiResult.Failure).message)
|
assertEquals("No workspaces available for this account.", (result as BoardsApiResult.Failure).message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun listBoardsExcludesBoardsWhoseIdsMatchTemplates() = runTest {
|
|
||||||
val fakeApi = FakeBoardsApiClient().apply {
|
|
||||||
workspacesResult = BoardsApiResult.Success(listOf(WorkspaceSummary("ws-1", "Main")))
|
|
||||||
listBoardsResult = BoardsApiResult.Success(
|
|
||||||
listOf(
|
|
||||||
BoardSummary("board-1", "Juegos"),
|
|
||||||
BoardSummary("tpl-1", "GTD Simplificado"),
|
|
||||||
BoardSummary("tpl-2", "Kanban"),
|
|
||||||
BoardSummary("board-2", "Task Tension"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
listTemplatesResult = BoardsApiResult.Success(
|
|
||||||
listOf(
|
|
||||||
BoardTemplate("tpl-1", "GTD Simplificado"),
|
|
||||||
BoardTemplate("tpl-2", "Kanban"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val repository = BoardsRepository(
|
|
||||||
sessionStore = InMemorySessionStore("https://kan.bn/"),
|
|
||||||
apiKeyStore = InMemoryApiKeyStore("api"),
|
|
||||||
apiClient = fakeApi,
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = repository.listBoards()
|
|
||||||
|
|
||||||
assertTrue(result is BoardsApiResult.Success)
|
|
||||||
val boards = (result as BoardsApiResult.Success).value
|
|
||||||
assertEquals(listOf("board-1", "board-2"), boards.map { it.id })
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun listBoardsReturnsApiBoardsWhenTemplateListingFails() = runTest {
|
|
||||||
val fakeApi = FakeBoardsApiClient().apply {
|
|
||||||
workspacesResult = BoardsApiResult.Success(listOf(WorkspaceSummary("ws-1", "Main")))
|
|
||||||
listBoardsResult = BoardsApiResult.Success(
|
|
||||||
listOf(
|
|
||||||
BoardSummary("board-1", "Juegos"),
|
|
||||||
BoardSummary("tpl-1", "GTD Simplificado"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
listTemplatesResult = BoardsApiResult.Failure("Template endpoint unavailable")
|
|
||||||
}
|
|
||||||
|
|
||||||
val repository = BoardsRepository(
|
|
||||||
sessionStore = InMemorySessionStore("https://kan.bn/"),
|
|
||||||
apiKeyStore = InMemoryApiKeyStore("api"),
|
|
||||||
apiClient = fakeApi,
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = repository.listBoards()
|
|
||||||
|
|
||||||
assertTrue(result is BoardsApiResult.Success)
|
|
||||||
val boards = (result as BoardsApiResult.Success).value
|
|
||||||
assertEquals(listOf("board-1", "tpl-1"), boards.map { it.id })
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun createBoardTrimsNameAndPassesTemplateId() = runTest {
|
fun createBoardTrimsNameAndPassesTemplateId() = runTest {
|
||||||
val fakeApi = FakeBoardsApiClient().apply {
|
val fakeApi = FakeBoardsApiClient().apply {
|
||||||
@@ -248,7 +189,6 @@ class BoardsRepositoryTest {
|
|||||||
|
|
||||||
private class FakeBoardsApiClient : KanbnApiClient {
|
private class FakeBoardsApiClient : KanbnApiClient {
|
||||||
var listBoardsResult: BoardsApiResult<List<BoardSummary>> = BoardsApiResult.Success(emptyList())
|
var listBoardsResult: BoardsApiResult<List<BoardSummary>> = BoardsApiResult.Success(emptyList())
|
||||||
var listTemplatesResult: BoardsApiResult<List<BoardTemplate>> = BoardsApiResult.Success(emptyList())
|
|
||||||
var workspacesResult: BoardsApiResult<List<WorkspaceSummary>> =
|
var workspacesResult: BoardsApiResult<List<WorkspaceSummary>> =
|
||||||
BoardsApiResult.Success(listOf(WorkspaceSummary("ws-1", "Main")))
|
BoardsApiResult.Success(listOf(WorkspaceSummary("ws-1", "Main")))
|
||||||
var createBoardResult: BoardsApiResult<BoardSummary> = BoardsApiResult.Success(BoardSummary("new", "New"))
|
var createBoardResult: BoardsApiResult<BoardSummary> = BoardsApiResult.Success(BoardSummary("new", "New"))
|
||||||
@@ -285,8 +225,7 @@ class BoardsRepositoryTest {
|
|||||||
apiKey: String,
|
apiKey: String,
|
||||||
workspaceId: String,
|
workspaceId: String,
|
||||||
): BoardsApiResult<List<BoardTemplate>> {
|
): BoardsApiResult<List<BoardTemplate>> {
|
||||||
lastWorkspaceId = workspaceId
|
return BoardsApiResult.Success(emptyList())
|
||||||
return listTemplatesResult
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createBoard(
|
override suspend fun createBoard(
|
||||||
|
|||||||
Reference in New Issue
Block a user