fix board-detail card move compatibility across API variants
This commit is contained in:
18
AGENTS.md
18
AGENTS.md
@@ -97,7 +97,23 @@ Kanbn4Droid is an unofficial app to connect to and manipulate data stored in sel
|
|||||||
- Pressing "Delete" in the modal dialog MUST show a second confirmation modal asking if the user is sure, with buttons for "Cancel" and "I'm sure".
|
- Pressing "Delete" in the modal dialog MUST show a second confirmation modal asking if the user is sure, with buttons for "Cancel" and "I'm sure".
|
||||||
- Only on pressing "I'm sure" in the second confirmation modal dialog should a board delete request be sent to the API.
|
- Only on pressing "I'm sure" in the second confirmation modal dialog should a board delete request be sent to the API.
|
||||||
- Long-pressing any of the buttons must show a tooltip with the button name.
|
- Long-pressing any of the buttons must show a tooltip with the button name.
|
||||||
- Current status: implemented in `BoardDetailActivity` with `ViewPager2` (one list per page), inline list-title edit, card rendering (title/tags/due date locale formatting and expiry color), cross-page card selection, page-scoped select-all, move dialog with list selector, two-step delete confirmation, mutation guards while in progress, and API-backed reload/reconciliation behavior through `BoardDetailViewModel` and `BoardDetailRepository`. Label chip border colors are hydrated from the Kan.bn `Get a label by public ID` endpoint (`colourCode`) and cached in-memory by `BoardDetailRepository` so each label color is fetched only once per app process. Selection action icons use local vector drawables (`ic_select_all_grid_24`, `ic_move_cards_horizontal_24`, `ic_delete_24`) with day/night resource variants so dark mode uses light icon fills automatically. Startup blocking dialogs are shown for missing board id and missing session.
|
- The view has a floating + button that shows a modal dialog that allows creating a new card using the Kan.bn API.
|
||||||
|
- The new card is added to the top of the currently shown list.
|
||||||
|
- The modal dialog has a field for the card's name. This field is mandatory
|
||||||
|
- Below the card name field there is a markdown-enabled text area for an optional card description.
|
||||||
|
- Below the card description field there is an optional date field to set the card's due date.
|
||||||
|
- Below the card's due date field there is an optional multi-value selector that allows choosing the card's tags from the tags available for the current board.
|
||||||
|
- The title bar of the view has two icon-only buttons for "Filter by tag" (icon is three bars of decreasing width, widest on top) and "Search" (icon is a leaning looking glass)
|
||||||
|
- The filter by tag button opens a modal dialog that shows a multi-value selector that allows choosing from the tags available on the current board. The modal has a title that says "Filter by tag". The modal has buttons for "Cancel" and "Filter".
|
||||||
|
- The search button a modal dialog that shows a text field that has the placeholder value "Search". The modal has a title that seas "Search by title". The modal has buttons for "Cancel" and "Search".
|
||||||
|
- Applying a filter or search makes the active board show only the cards that match the given criteria (selected tags or matching title).
|
||||||
|
- The filters are applied locally without contacting the server.
|
||||||
|
- The search by title filter matches any part of the title. Example: searching for "Duke" matches "Duke Nukem" as well as "Nukem Duke"
|
||||||
|
- When a filter by tag or search is applied the corresponding button in the title bar gets highlighted.
|
||||||
|
- Tapping on the filter by tag or search buttonswhen either of them is applied disables the active filter.
|
||||||
|
- When a card(s) is selected by a long press, the filter by tag and search buttons get hidden by the select all, move card and delete card buttons until all cards are deselected.
|
||||||
|
- When a card(s) is selected by a long press, the back arrow in the title bar and the back system button remove all selections.
|
||||||
|
- Current status: implemented in `BoardDetailActivity` with `ViewPager2` (one list per page), inline list-title edit, card rendering (title/tags/due date locale formatting and expiry color), cross-page card selection, page-scoped select-all, move dialog with list selector, two-step delete confirmation, mutation guards while in progress, and API-backed reload/reconciliation behavior through `BoardDetailViewModel` and `BoardDetailRepository`. Card move requests try these variants in order for compatibility across Kan.bn API versions: `PUT /api/v1/cards/{cardPublicId}` with `listPublicId`, then a GET+full-body `PUT /api/v1/cards/{cardPublicId}` payload (`title`, `description`, `index`, `listPublicId`, `dueDate`), then `PUT /api/v1/cards/{cardPublicId}` with `listId`, then `PATCH /api/v1/cards/{cardPublicId}` with `listId`. Board detail parsing now prefers public ids (`publicId`/`public_id`) over internal `id` values so follow-up card/list mutations target the correct API identifiers. Label chip border colors are hydrated from the Kan.bn `Get a label by public ID` endpoint (`colourCode`) and cached in-memory by `BoardDetailRepository` so each label color is fetched only once per app process. Selection action icons use local vector drawables (`ic_select_all_grid_24`, `ic_move_cards_horizontal_24`, `ic_delete_24`) with day/night resource variants so dark mode uses light icon fills automatically. Startup blocking dialogs are shown for missing board id and missing session.
|
||||||
|
|
||||||
**Card detail view**
|
**Card detail view**
|
||||||
- The view shows the card's title in bold letters. Tapping on the card's title allows editing it.
|
- The view shows the card's title in bold letters. Tapping on the card's title allows editing it.
|
||||||
|
|||||||
@@ -276,12 +276,13 @@ class HttpKanbnApiClient : KanbnApiClient {
|
|||||||
targetListId: String,
|
targetListId: String,
|
||||||
): BoardsApiResult<Unit> {
|
): BoardsApiResult<Unit> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
request(
|
val putListPublicIdBody = "{\"listPublicId\":\"${jsonEscape(targetListId)}\"}"
|
||||||
|
val putResult = request(
|
||||||
baseUrl = baseUrl,
|
baseUrl = baseUrl,
|
||||||
path = "/api/v1/cards/$cardId",
|
path = "/api/v1/cards/$cardId",
|
||||||
method = "PATCH",
|
method = "PUT",
|
||||||
apiKey = apiKey,
|
apiKey = apiKey,
|
||||||
body = "{\"listId\":\"${jsonEscape(targetListId)}\"}",
|
body = putListPublicIdBody,
|
||||||
) { code, body ->
|
) { code, body ->
|
||||||
if (code in 200..299) {
|
if (code in 200..299) {
|
||||||
BoardsApiResult.Success(Unit)
|
BoardsApiResult.Success(Unit)
|
||||||
@@ -289,9 +290,122 @@ class HttpKanbnApiClient : KanbnApiClient {
|
|||||||
BoardsApiResult.Failure(serverMessage(body, code))
|
BoardsApiResult.Failure(serverMessage(body, code))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (putResult is BoardsApiResult.Success) {
|
||||||
|
return@withContext putResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val cardResponse = request(
|
||||||
|
baseUrl = baseUrl,
|
||||||
|
path = "/api/v1/cards/$cardId",
|
||||||
|
method = "GET",
|
||||||
|
apiKey = apiKey,
|
||||||
|
) { code, body ->
|
||||||
|
if (code in 200..299) {
|
||||||
|
BoardsApiResult.Success(body)
|
||||||
|
} else {
|
||||||
|
BoardsApiResult.Failure(serverMessage(body, code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cardResponse is BoardsApiResult.Success) {
|
||||||
|
val fullUpdateBody = buildFullCardMovePayload(cardResponse.value, targetListId)
|
||||||
|
if (fullUpdateBody != null) {
|
||||||
|
val putFullResult = request(
|
||||||
|
baseUrl = baseUrl,
|
||||||
|
path = "/api/v1/cards/$cardId",
|
||||||
|
method = "PUT",
|
||||||
|
apiKey = apiKey,
|
||||||
|
body = fullUpdateBody,
|
||||||
|
) { code, body ->
|
||||||
|
if (code in 200..299) {
|
||||||
|
BoardsApiResult.Success(Unit)
|
||||||
|
} else {
|
||||||
|
BoardsApiResult.Failure(serverMessage(body, code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (putFullResult is BoardsApiResult.Success) {
|
||||||
|
return@withContext putFullResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val putListIdBody = "{\"listId\":\"${jsonEscape(targetListId)}\"}"
|
||||||
|
val putWithListIdResult = request(
|
||||||
|
baseUrl = baseUrl,
|
||||||
|
path = "/api/v1/cards/$cardId",
|
||||||
|
method = "PUT",
|
||||||
|
apiKey = apiKey,
|
||||||
|
body = putListIdBody,
|
||||||
|
) { code, body ->
|
||||||
|
if (code in 200..299) {
|
||||||
|
BoardsApiResult.Success(Unit)
|
||||||
|
} else {
|
||||||
|
BoardsApiResult.Failure(serverMessage(body, code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (putWithListIdResult is BoardsApiResult.Success) {
|
||||||
|
return@withContext putWithListIdResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val patchResult = request(
|
||||||
|
baseUrl = baseUrl,
|
||||||
|
path = "/api/v1/cards/$cardId",
|
||||||
|
method = "PATCH",
|
||||||
|
apiKey = apiKey,
|
||||||
|
body = putListIdBody,
|
||||||
|
) { code, body ->
|
||||||
|
if (code in 200..299) {
|
||||||
|
BoardsApiResult.Success(Unit)
|
||||||
|
} else {
|
||||||
|
BoardsApiResult.Failure(serverMessage(body, code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patchResult is BoardsApiResult.Success) {
|
||||||
|
return@withContext patchResult
|
||||||
|
}
|
||||||
|
|
||||||
|
putWithListIdResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildFullCardMovePayload(cardBody: String, targetListId: String): String? {
|
||||||
|
if (cardBody.isBlank()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val root = parseJsonObject(cardBody) ?: return null
|
||||||
|
val card = (root["card"] as? Map<*, *>) ?: root
|
||||||
|
val title = extractString(card, "title", "name")
|
||||||
|
if (title.isBlank()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val description = firstPresent(card, "description", "body")?.toString().orEmpty()
|
||||||
|
val indexValue = firstPresent(card, "index", "position")
|
||||||
|
val indexNumber = when (indexValue) {
|
||||||
|
is Number -> indexValue
|
||||||
|
is String -> indexValue.toDoubleOrNull()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
val dueValue = firstPresent(card, "dueDate", "dueAt", "due_at", "due")?.toString()?.trim().orEmpty()
|
||||||
|
val dueField = if (dueValue.isBlank() || dueValue.equals("null", ignoreCase = true)) {
|
||||||
|
"null"
|
||||||
|
} else {
|
||||||
|
"\"${jsonEscape(dueValue)}\""
|
||||||
|
}
|
||||||
|
val indexField = indexNumber?.toString() ?: "0"
|
||||||
|
|
||||||
|
return "{" +
|
||||||
|
"\"title\":\"${jsonEscape(title)}\"," +
|
||||||
|
"\"description\":\"${jsonEscape(description)}\"," +
|
||||||
|
"\"index\":$indexField," +
|
||||||
|
"\"listPublicId\":\"${jsonEscape(targetListId)}\"," +
|
||||||
|
"\"dueDate\":$dueField" +
|
||||||
|
"}"
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun deleteCard(
|
override suspend fun deleteCard(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
apiKey: String,
|
apiKey: String,
|
||||||
@@ -615,11 +729,11 @@ class HttpKanbnApiClient : KanbnApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun extractId(source: Map<*, *>): String {
|
private fun extractId(source: Map<*, *>): String {
|
||||||
val directId = source["id"]?.toString().orEmpty()
|
val publicId = extractString(source, "publicId", "public_id")
|
||||||
if (directId.isNotBlank()) {
|
if (publicId.isNotBlank()) {
|
||||||
return directId
|
return publicId
|
||||||
}
|
}
|
||||||
return extractString(source, "publicId", "public_id")
|
return source["id"]?.toString().orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractTitle(source: Map<*, *>, fallback: String): String {
|
private fun extractTitle(source: Map<*, *>, fallback: String): String {
|
||||||
|
|||||||
@@ -141,6 +141,50 @@ class HttpKanbnApiClientBoardDetailParsingTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getBoardDetailPrefersPublicIdsWhenBothIdFormsExist() = runTest {
|
||||||
|
TestServer().use { server ->
|
||||||
|
server.register(
|
||||||
|
path = "/api/v1/boards/board-public",
|
||||||
|
status = 200,
|
||||||
|
responseBody =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"id": "board-internal",
|
||||||
|
"publicId": "board-public",
|
||||||
|
"title": "Board",
|
||||||
|
"lists": [
|
||||||
|
{
|
||||||
|
"id": "list-internal",
|
||||||
|
"public_id": "list-public",
|
||||||
|
"title": "List",
|
||||||
|
"cards": [
|
||||||
|
{
|
||||||
|
"id": "card-internal",
|
||||||
|
"publicId": "card-public",
|
||||||
|
"title": "Card",
|
||||||
|
"labels": [
|
||||||
|
{"id": "tag-internal", "publicId": "tag-public", "name": "Tag", "color": "#111111"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = HttpKanbnApiClient().getBoardDetail(server.baseUrl, "key", "board-public")
|
||||||
|
|
||||||
|
assertTrue(result is BoardsApiResult.Success<*>)
|
||||||
|
val detail = (result as BoardsApiResult.Success<*>).value as BoardDetail
|
||||||
|
assertEquals("board-public", detail.id)
|
||||||
|
assertEquals("list-public", detail.lists[0].id)
|
||||||
|
assertEquals("card-public", detail.lists[0].cards[0].id)
|
||||||
|
assertEquals("tag-public", detail.lists[0].cards[0].tags[0].id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getBoardDetailParsesPlainDataWrapperWithoutBoardKey() = runTest {
|
fun getBoardDetailParsesPlainDataWrapperWithoutBoardKey() = runTest {
|
||||||
TestServer().use { server ->
|
TestServer().use { server ->
|
||||||
@@ -237,9 +281,9 @@ class HttpKanbnApiClientBoardDetailParsingTest {
|
|||||||
assertEquals("{\"name\":\"New title\"}", renameRequest?.body)
|
assertEquals("{\"name\":\"New title\"}", renameRequest?.body)
|
||||||
assertEquals("api-123", renameRequest?.apiKey)
|
assertEquals("api-123", renameRequest?.apiKey)
|
||||||
|
|
||||||
val moveRequest = server.findRequest("PATCH", "/api/v1/cards/c-1")
|
val moveRequest = server.findRequest("PUT", "/api/v1/cards/c-1")
|
||||||
assertNotNull(moveRequest)
|
assertNotNull(moveRequest)
|
||||||
assertEquals("{\"listId\":\"l-9\"}", moveRequest?.body)
|
assertEquals("{\"listPublicId\":\"l-9\"}", moveRequest?.body)
|
||||||
assertEquals("api-123", moveRequest?.apiKey)
|
assertEquals("api-123", moveRequest?.apiKey)
|
||||||
|
|
||||||
val deleteRequest = server.findRequest("DELETE", "/api/v1/cards/c-2")
|
val deleteRequest = server.findRequest("DELETE", "/api/v1/cards/c-2")
|
||||||
@@ -316,6 +360,84 @@ class HttpKanbnApiClientBoardDetailParsingTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun moveCardFallsBackToPutListIdThenPatchWhenPutListPublicIdFails() = runTest {
|
||||||
|
TestServer().use { server ->
|
||||||
|
server.register(
|
||||||
|
path = "/api/v1/cards/c-fallback-move",
|
||||||
|
method = "PUT",
|
||||||
|
status = 400,
|
||||||
|
responseBody = """{"message":"Failed to update card"}""",
|
||||||
|
)
|
||||||
|
server.register(
|
||||||
|
path = "/api/v1/cards/c-fallback-move",
|
||||||
|
method = "PATCH",
|
||||||
|
status = 200,
|
||||||
|
responseBody = "{}",
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = HttpKanbnApiClient().moveCard(
|
||||||
|
baseUrl = server.baseUrl,
|
||||||
|
apiKey = "api",
|
||||||
|
cardId = "c-fallback-move",
|
||||||
|
targetListId = "l-target",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(result is BoardsApiResult.Success<*>)
|
||||||
|
val putRequests = server.findRequests("PUT", "/api/v1/cards/c-fallback-move")
|
||||||
|
val patchRequest = server.findRequest("PATCH", "/api/v1/cards/c-fallback-move")
|
||||||
|
assertEquals(2, putRequests.size)
|
||||||
|
assertNotNull(patchRequest)
|
||||||
|
assertEquals("{\"listPublicId\":\"l-target\"}", putRequests[0].body)
|
||||||
|
assertEquals("{\"listId\":\"l-target\"}", putRequests[1].body)
|
||||||
|
assertEquals("{\"listId\":\"l-target\"}", patchRequest?.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun moveCardUsesFullPutPayloadWhenMinimalPayloadFails() = runTest {
|
||||||
|
TestServer().use { server ->
|
||||||
|
server.register(
|
||||||
|
path = "/api/v1/cards/c-full",
|
||||||
|
method = "PUT",
|
||||||
|
status = 500,
|
||||||
|
responseBody = """{"message":"Failed to update card"}""",
|
||||||
|
)
|
||||||
|
server.register(
|
||||||
|
path = "/api/v1/cards/c-full",
|
||||||
|
method = "GET",
|
||||||
|
status = 200,
|
||||||
|
responseBody = """{"publicId":"c-full","title":"Card title","description":"Desc","dueDate":null}""",
|
||||||
|
)
|
||||||
|
server.registerSequence(
|
||||||
|
path = "/api/v1/cards/c-full",
|
||||||
|
method = "PUT",
|
||||||
|
responses = listOf(
|
||||||
|
500 to """{"message":"Failed to update card"}""",
|
||||||
|
200 to "{}",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = HttpKanbnApiClient().moveCard(
|
||||||
|
baseUrl = server.baseUrl,
|
||||||
|
apiKey = "api",
|
||||||
|
cardId = "c-full",
|
||||||
|
targetListId = "l-target",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(result is BoardsApiResult.Success<*>)
|
||||||
|
val putRequests = server.findRequests("PUT", "/api/v1/cards/c-full")
|
||||||
|
assertTrue(putRequests.size >= 2)
|
||||||
|
assertEquals("{\"listPublicId\":\"l-target\"}", putRequests[0].body)
|
||||||
|
assertEquals(
|
||||||
|
"{\"title\":\"Card title\",\"description\":\"Desc\",\"index\":0,\"listPublicId\":\"l-target\",\"dueDate\":null}",
|
||||||
|
putRequests[1].body,
|
||||||
|
)
|
||||||
|
val getRequest = server.findRequest("GET", "/api/v1/cards/c-full")
|
||||||
|
assertNotNull(getRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun deleteCardFailureUsesServerMessageAndFallback() = runTest {
|
fun deleteCardFailureUsesServerMessageAndFallback() = runTest {
|
||||||
TestServer().use { server ->
|
TestServer().use { server ->
|
||||||
@@ -429,6 +551,7 @@ class HttpKanbnApiClientBoardDetailParsingTest {
|
|||||||
private class TestServer : AutoCloseable {
|
private class TestServer : AutoCloseable {
|
||||||
private val requests = CopyOnWriteArrayList<CapturedRequest>()
|
private val requests = CopyOnWriteArrayList<CapturedRequest>()
|
||||||
private val responses = mutableMapOf<String, Pair<Int, String>>()
|
private val responses = mutableMapOf<String, Pair<Int, String>>()
|
||||||
|
private val responseSequences = mutableMapOf<String, ArrayDeque<Pair<Int, String>>>()
|
||||||
private val running = AtomicBoolean(true)
|
private val running = AtomicBoolean(true)
|
||||||
private val serverSocket = ServerSocket().apply {
|
private val serverSocket = ServerSocket().apply {
|
||||||
bind(InetSocketAddress("127.0.0.1", 0))
|
bind(InetSocketAddress("127.0.0.1", 0))
|
||||||
@@ -454,16 +577,29 @@ class HttpKanbnApiClientBoardDetailParsingTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun register(path: String, status: Int, responseBody: String) {
|
fun register(path: String, status: Int, responseBody: String) {
|
||||||
responses["GET $path"] = status to responseBody
|
register(path = path, method = "GET", status = status, responseBody = responseBody)
|
||||||
responses["PATCH $path"] = status to responseBody
|
register(path = path, method = "PATCH", status = status, responseBody = responseBody)
|
||||||
responses["DELETE $path"] = status to responseBody
|
register(path = path, method = "PUT", status = status, responseBody = responseBody)
|
||||||
responses["POST $path"] = status to responseBody
|
register(path = path, method = "DELETE", status = status, responseBody = responseBody)
|
||||||
|
register(path = path, method = "POST", status = status, responseBody = responseBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun register(path: String, method: String, status: Int, responseBody: String) {
|
||||||
|
responses["${method.uppercase()} $path"] = status to responseBody
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerSequence(path: String, method: String, responses: List<Pair<Int, String>>) {
|
||||||
|
responseSequences["${method.uppercase()} $path"] = ArrayDeque(responses)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findRequest(method: String, path: String): CapturedRequest? {
|
fun findRequest(method: String, path: String): CapturedRequest? {
|
||||||
return requests.firstOrNull { it.method == method && it.path == path }
|
return requests.firstOrNull { it.method == method && it.path == path }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun findRequests(method: String, path: String): List<CapturedRequest> {
|
||||||
|
return requests.filter { it.method == method && it.path == path }
|
||||||
|
}
|
||||||
|
|
||||||
private fun handle(socket: Socket) {
|
private fun handle(socket: Socket) {
|
||||||
socket.use { s ->
|
socket.use { s ->
|
||||||
s.soTimeout = 3_000
|
s.soTimeout = 3_000
|
||||||
@@ -516,7 +652,10 @@ class HttpKanbnApiClientBoardDetailParsingTest {
|
|||||||
val effectiveMethod = methodOverride ?: method
|
val effectiveMethod = methodOverride ?: method
|
||||||
requests += CapturedRequest(method = effectiveMethod, path = path, body = body, apiKey = apiKey)
|
requests += CapturedRequest(method = effectiveMethod, path = path, body = body, apiKey = apiKey)
|
||||||
|
|
||||||
val response = responses["$effectiveMethod $path"] ?: responses["$method $path"] ?: (404 to "")
|
val sequenceKey = "$effectiveMethod $path"
|
||||||
|
val sequence = responseSequences[sequenceKey]
|
||||||
|
val sequencedResponse = if (sequence != null && sequence.isNotEmpty()) sequence.removeFirst() else null
|
||||||
|
val response = sequencedResponse ?: responses[sequenceKey] ?: responses["$method $path"] ?: (404 to "")
|
||||||
writeResponse(output, response.first, response.second)
|
writeResponse(output, response.first, response.second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user