feat: hydrate board label chip colors from API

This commit is contained in:
2026-03-16 03:42:27 -04:00
parent 02b9af0e51
commit 4246d01827
10 changed files with 384 additions and 3 deletions

View File

@@ -97,7 +97,7 @@ 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`. 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. - 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.
**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.
@@ -132,5 +132,5 @@ Kanbn4Droid is an unofficial app to connect to and manipulate data stored in sel
- It is preferable to use standard Android libraries and tools whenever possible. - It is preferable to use standard Android libraries and tools whenever possible.
- If a task would be better served by or can only be done with external libraries then it is allowed to use them but you MUST ask the user for permission to add the library to the project first. - If a task would be better served by or can only be done with external libraries then it is allowed to use them but you MUST ask the user for permission to add the library to the project first.
- The documentation for the Kan.bn API is available here https://docs.kan.bn/api-reference/introduction - The documentation for the Kan.bn API is available here https://docs.kan.bn/api-reference/introduction
- Never commit or push code unless explicitely prompted to do so. - Never push code unless explicitely prompted to do so.
- After every run, update this file as needed to reflect the changes made. - After every run, update this file as needed to reflect the changes made.

View File

@@ -25,6 +25,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.chip.Chip
import java.text.DateFormat import java.text.DateFormat
import java.util.ArrayDeque import java.util.ArrayDeque
import java.util.Date import java.util.Date
@@ -148,6 +149,22 @@ class BoardDetailFlowTest {
onView(withId(R.id.cardDueDateText)).check(matches(withCurrentTextColor(expectedColor ?: Color.BLACK))) onView(withId(R.id.cardDueDateText)).check(matches(withCurrentTextColor(expectedColor ?: Color.BLACK)))
} }
@Test
fun invalidTagColorFallsBackToOnSurfaceColor() {
defaultDataSource.currentDetail = detailWithInvalidTagColor()
val scenario = launchBoardDetail()
var expectedColor: Int? = null
scenario.onActivity { activity ->
expectedColor = MaterialColors.getColor(
activity.findViewById(android.R.id.content),
com.google.android.material.R.attr.colorOnSurface,
)
}
onView(withText("Backend")).check(matches(withChipStrokeColor(expectedColor ?: Color.DKGRAY)))
}
@Test @Test
fun dueDateUsesSystemLocaleFormatting() { fun dueDateUsesSystemLocaleFormatting() {
Locale.setDefault(Locale.FRANCE) Locale.setDefault(Locale.FRANCE)
@@ -571,6 +588,21 @@ class BoardDetailFlowTest {
} }
} }
private fun withChipStrokeColor(expectedColor: Int): Matcher<View> {
return object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("with chip stroke color: $expectedColor")
}
override fun matchesSafely(item: View): Boolean {
if (item !is Chip) {
return false
}
return item.chipStrokeColor?.defaultColor == expectedColor
}
}
}
private fun awaitCondition(timeoutMs: Long = 4_000L, condition: () -> Boolean) { private fun awaitCondition(timeoutMs: Long = 4_000L, condition: () -> Boolean) {
val instrumentation = androidx.test.platform.app.InstrumentationRegistry.getInstrumentation() val instrumentation = androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
@@ -757,6 +789,25 @@ class BoardDetailFlowTest {
) )
} }
fun detailWithInvalidTagColor(): BoardDetail {
return detailOneList().copy(
lists = listOf(
BoardListDetail(
id = "list-1",
title = "To Do",
cards = listOf(
BoardCardSummary(
id = "card-1",
title = "Card 1",
tags = listOf(BoardTagSummary("tag-1", "Backend", "bad-color")),
dueAtEpochMillis = null,
),
),
),
),
)
}
fun detailWithCardTitle(title: String): BoardDetail { fun detailWithCardTitle(title: String): BoardDetail {
return detailOneList().copy( return detailOneList().copy(
lists = listOf( lists = listOf(

View File

@@ -23,6 +23,7 @@ import org.junit.runner.RunWith
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
import space.hackenslacker.kanbn4droid.app.auth.AuthResult import space.hackenslacker.kanbn4droid.app.auth.AuthResult
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.SessionStore import space.hackenslacker.kanbn4droid.app.auth.SessionStore
import space.hackenslacker.kanbn4droid.app.boards.BoardSummary import space.hackenslacker.kanbn4droid.app.boards.BoardSummary
import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate
@@ -199,5 +200,13 @@ class BoardsFlowTest {
boards.removeAll { it.id == boardId } boards.removeAll { it.id == boardId }
return BoardsApiResult.Success(Unit) return BoardsApiResult.Success(Unit)
} }
override suspend fun getLabelByPublicId(
baseUrl: String,
apiKey: String,
labelId: String,
): BoardsApiResult<LabelDetail> {
return BoardsApiResult.Failure("Not needed in boards flow tests")
}
} }
} }

View File

@@ -21,6 +21,7 @@ import space.hackenslacker.kanbn4droid.app.auth.AuthFailureReason
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
import space.hackenslacker.kanbn4droid.app.auth.AuthResult import space.hackenslacker.kanbn4droid.app.auth.AuthResult
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.SessionStore import space.hackenslacker.kanbn4droid.app.auth.SessionStore
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@@ -200,5 +201,13 @@ class LoginFlowTest {
private val result: AuthResult, private val result: AuthResult,
) : KanbnApiClient { ) : KanbnApiClient {
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = result override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = result
override suspend fun getLabelByPublicId(
baseUrl: String,
apiKey: String,
labelId: String,
): space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult<LabelDetail> {
return space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult.Failure("Not needed in login tests")
}
} }
} }

View File

@@ -69,7 +69,16 @@ interface KanbnApiClient {
suspend fun deleteCard(baseUrl: String, apiKey: String, cardId: String): BoardsApiResult<Unit> { suspend fun deleteCard(baseUrl: String, apiKey: String, cardId: String): BoardsApiResult<Unit> {
return BoardsApiResult.Failure("Card deletion is not implemented.") return BoardsApiResult.Failure("Card deletion is not implemented.")
} }
suspend fun getLabelByPublicId(baseUrl: String, apiKey: String, labelId: String): BoardsApiResult<LabelDetail> {
return BoardsApiResult.Failure("Label detail is not implemented.")
} }
}
data class LabelDetail(
val id: String,
val colorHex: String,
)
class HttpKanbnApiClient : KanbnApiClient { class HttpKanbnApiClient : KanbnApiClient {
@@ -304,6 +313,29 @@ class HttpKanbnApiClient : KanbnApiClient {
} }
} }
override suspend fun getLabelByPublicId(
baseUrl: String,
apiKey: String,
labelId: String,
): BoardsApiResult<LabelDetail> {
return withContext(Dispatchers.IO) {
request(
baseUrl = baseUrl,
path = "/api/v1/labels/$labelId",
method = "GET",
apiKey = apiKey,
) { code, body ->
if (code in 200..299) {
parseLabelDetail(body, labelId)
?.let { BoardsApiResult.Success(it) }
?: BoardsApiResult.Failure("Malformed label detail response.")
} else {
BoardsApiResult.Failure(serverMessage(body, code))
}
}
}
}
private fun <T> request( private fun <T> request(
baseUrl: String, baseUrl: String,
path: String, path: String,
@@ -504,6 +536,23 @@ class HttpKanbnApiClient : KanbnApiClient {
return BoardDetail(id = boardId, title = boardTitle, lists = lists) return BoardDetail(id = boardId, title = boardTitle, lists = lists)
} }
private fun parseLabelDetail(body: String, fallbackId: String): LabelDetail? {
if (body.isBlank()) {
return null
}
val root = parseJsonObject(body) ?: return null
val data = root["data"] as? Map<*, *>
val label = (data?.get("label") as? Map<*, *>)
?: data
?: (root["label"] as? Map<*, *>)
?: root
val id = extractId(label).ifBlank { fallbackId }
val colorHex = extractString(label, "colourCode", "colorCode", "colorHex", "color", "hex")
return LabelDetail(id = id, colorHex = colorHex)
}
private fun parseLists(board: Map<*, *>): List<BoardListDetail> { private fun parseLists(board: Map<*, *>): List<BoardListDetail> {
return extractObjectArray(board, "lists", "items", "data").mapNotNull { rawList -> return extractObjectArray(board, "lists", "items", "data").mapNotNull { rawList ->
val id = extractId(rawList) val id = extractId(rawList)

View File

@@ -5,6 +5,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
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.SessionStore import space.hackenslacker.kanbn4droid.app.auth.SessionStore
import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
@@ -14,6 +15,8 @@ class BoardDetailRepository(
private val apiClient: KanbnApiClient, private val apiClient: KanbnApiClient,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) { ) {
private val labelColorCache = mutableMapOf<String, String>()
companion object { companion object {
const val MISSING_SESSION_MESSAGE = "Missing session. Please sign in again." const val MISSING_SESSION_MESSAGE = "Missing session. Please sign in again."
} }
@@ -33,8 +36,14 @@ class BoardDetailRepository(
baseUrl = session.baseUrl, baseUrl = session.baseUrl,
apiKey = session.apiKey, apiKey = session.apiKey,
boardId = normalizedBoardId, boardId = normalizedBoardId,
).mapSuccess { detail ->
hydrateLabelColors(
detail = detail,
baseUrl = session.baseUrl,
apiKey = session.apiKey,
) )
} }
}
suspend fun renameList(listId: String, newTitle: String): BoardsApiResult<Unit> { suspend fun renameList(listId: String, newTitle: String): BoardsApiResult<Unit> {
val normalizedListId = listId.trim() val normalizedListId = listId.trim()
@@ -192,6 +201,63 @@ class BoardDetailRepository(
} }
} }
private suspend fun hydrateLabelColors(
detail: BoardDetail,
baseUrl: String,
apiKey: String,
): BoardDetail {
val missingColorIds = detail.lists
.asSequence()
.flatMap { list -> list.cards.asSequence() }
.flatMap { card -> card.tags.asSequence() }
.map { it.id.trim() }
.filter { it.isNotBlank() }
.distinct()
.filter { !labelColorCache.containsKey(it) }
.toList()
missingColorIds.forEach { labelId ->
val colorHex = when (val labelResult = apiClient.getLabelByPublicId(baseUrl, apiKey, labelId)) {
is BoardsApiResult.Success -> normalizeColorHex(labelResult.value)
is BoardsApiResult.Failure -> ""
}
if (colorHex.isNotBlank()) {
labelColorCache[labelId] = colorHex
}
}
val hydratedLists = detail.lists.map { list ->
list.copy(
cards = list.cards.map { card ->
card.copy(
tags = card.tags.map { tag ->
val cached = labelColorCache[tag.id.trim()].orEmpty()
val merged = if (cached.isNotBlank()) cached else tag.colorHex
if (merged == tag.colorHex) {
tag
} else {
tag.copy(colorHex = merged)
}
},
)
},
)
}
return if (hydratedLists == detail.lists) detail else detail.copy(lists = hydratedLists)
}
private fun normalizeColorHex(label: LabelDetail): String {
return label.colorHex.trim()
}
private suspend fun <T, R> BoardsApiResult<T>.mapSuccess(transform: suspend (T) -> R): BoardsApiResult<R> {
return when (this) {
is BoardsApiResult.Success -> BoardsApiResult.Success(transform(this.value))
is BoardsApiResult.Failure -> BoardsApiResult.Failure(this.message)
}
}
private data class SessionSnapshot( private data class SessionSnapshot(
val baseUrl: String, val baseUrl: String,
val apiKey: String, val apiKey: String,

View File

@@ -341,6 +341,84 @@ class HttpKanbnApiClientBoardDetailParsingTest {
} }
} }
@Test
fun getLabelByPublicIdParsesColourCodeFromWrappedPayload() = runTest {
TestServer().use { server ->
server.register(
path = "/api/v1/labels/label-1",
status = 200,
responseBody =
"""
{
"data": {
"label": {
"public_id": "label-1",
"colourCode": "#A1B2C3"
}
}
}
""".trimIndent(),
)
val result = HttpKanbnApiClient().getLabelByPublicId(server.baseUrl, "api-key", "label-1")
assertTrue(result is BoardsApiResult.Success<*>)
val detail = (result as BoardsApiResult.Success<*>).value as LabelDetail
assertEquals("label-1", detail.id)
assertEquals("#A1B2C3", detail.colorHex)
}
}
@Test
fun getLabelByPublicIdUsesFallbackIdWhenPayloadHasNoId() = runTest {
TestServer().use { server ->
server.register(
path = "/api/v1/labels/label-fallback",
status = 200,
responseBody =
"""
{
"label": {
"colorCode": "#ABCDEF"
}
}
""".trimIndent(),
)
val result = HttpKanbnApiClient().getLabelByPublicId(server.baseUrl, "api-key", "label-fallback")
assertTrue(result is BoardsApiResult.Success<*>)
val detail = (result as BoardsApiResult.Success<*>).value as LabelDetail
assertEquals("label-fallback", detail.id)
assertEquals("#ABCDEF", detail.colorHex)
}
}
@Test
fun getLabelByPublicIdFailureUsesServerMessageAndFallback() = runTest {
TestServer().use { server ->
server.register(
path = "/api/v1/labels/label-msg",
status = 404,
responseBody = """{"message":"Label not found"}""",
)
server.register(
path = "/api/v1/labels/label-fallback",
status = 500,
responseBody = "{}",
)
val client = HttpKanbnApiClient()
val withMessage = client.getLabelByPublicId(server.baseUrl, "api", "label-msg")
val fallback = client.getLabelByPublicId(server.baseUrl, "api", "label-fallback")
assertTrue(withMessage is BoardsApiResult.Failure)
assertEquals("Label not found", (withMessage as BoardsApiResult.Failure).message)
assertTrue(fallback is BoardsApiResult.Failure)
assertEquals("Server error: 500", (fallback as BoardsApiResult.Failure).message)
}
}
private data class CapturedRequest( private data class CapturedRequest(
val method: String, val method: String,
val path: String, val path: String,

View File

@@ -7,6 +7,7 @@ import org.junit.Test
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
import space.hackenslacker.kanbn4droid.app.auth.AuthResult import space.hackenslacker.kanbn4droid.app.auth.AuthResult
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.SessionStore import space.hackenslacker.kanbn4droid.app.auth.SessionStore
import space.hackenslacker.kanbn4droid.app.boards.BoardSummary import space.hackenslacker.kanbn4droid.app.boards.BoardSummary
import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate
@@ -99,6 +100,95 @@ class BoardDetailRepositoryTest {
assertEquals("Server says no", (result as BoardsApiResult.Failure).message) assertEquals("Server says no", (result as BoardsApiResult.Failure).message)
} }
@Test
fun getBoardDetailFetchesLabelColorCodesAndCachesThem() = runTest {
val detailWithTags = BoardDetail(
id = "board-1",
title = "Board",
lists = listOf(
BoardListDetail(
id = "list-1",
title = "To Do",
cards = listOf(
BoardCardSummary(
id = "card-1",
title = "Card 1",
tags = listOf(
BoardTagSummary(id = "label-1", name = "Urgent", colorHex = ""),
BoardTagSummary(id = "label-2", name = "Backend", colorHex = "#000000"),
),
dueAtEpochMillis = null,
),
BoardCardSummary(
id = "card-2",
title = "Card 2",
tags = listOf(
BoardTagSummary(id = "label-1", name = "Urgent", colorHex = ""),
),
dueAtEpochMillis = null,
),
),
),
),
)
val apiClient = FakeBoardDetailApiClient().apply {
boardDetailResult = BoardsApiResult.Success(detailWithTags)
labelByIdResults = mapOf(
"label-1" to BoardsApiResult.Success(LabelDetail("label-1", "#112233")),
"label-2" to BoardsApiResult.Success(LabelDetail("label-2", "#445566")),
)
}
val repository = createRepository(apiClient = apiClient)
val first = repository.getBoardDetail("board-1")
val second = repository.getBoardDetail("board-1")
assertTrue(first is BoardsApiResult.Success<*>)
assertTrue(second is BoardsApiResult.Success<*>)
val firstDetail = (first as BoardsApiResult.Success<BoardDetail>).value
val secondDetail = (second as BoardsApiResult.Success<BoardDetail>).value
assertEquals("#112233", firstDetail.lists[0].cards[0].tags[0].colorHex)
assertEquals("#445566", firstDetail.lists[0].cards[0].tags[1].colorHex)
assertEquals("#112233", firstDetail.lists[0].cards[1].tags[0].colorHex)
assertEquals("#112233", secondDetail.lists[0].cards[0].tags[0].colorHex)
assertEquals("#445566", secondDetail.lists[0].cards[0].tags[1].colorHex)
assertEquals(listOf("label-1", "label-2"), apiClient.getLabelByPublicIdCalls)
}
@Test
fun getBoardDetailKeepsOriginalColorWhenLabelLookupFails() = runTest {
val detailWithTag = BoardDetail(
id = "board-1",
title = "Board",
lists = listOf(
BoardListDetail(
id = "list-1",
title = "To Do",
cards = listOf(
BoardCardSummary(
id = "card-1",
title = "Card 1",
tags = listOf(BoardTagSummary(id = "label-1", name = "Urgent", colorHex = "#ABCDEF")),
dueAtEpochMillis = null,
),
),
),
),
)
val apiClient = FakeBoardDetailApiClient().apply {
boardDetailResult = BoardsApiResult.Success(detailWithTag)
labelByIdResults = mapOf("label-1" to BoardsApiResult.Failure("Server unavailable"))
}
val repository = createRepository(apiClient = apiClient)
val result = repository.getBoardDetail("board-1")
assertTrue(result is BoardsApiResult.Success<*>)
val detail = (result as BoardsApiResult.Success<BoardDetail>).value
assertEquals("#ABCDEF", detail.lists[0].cards[0].tags[0].colorHex)
assertEquals(listOf("label-1"), apiClient.getLabelByPublicIdCalls)
}
@Test @Test
fun renameListPropagatesApiFailureMessageUnchanged() = runTest { fun renameListPropagatesApiFailureMessageUnchanged() = runTest {
val apiClient = FakeBoardDetailApiClient().apply { val apiClient = FakeBoardDetailApiClient().apply {
@@ -366,6 +456,7 @@ class BoardDetailRepositoryTest {
var renameListResult: BoardsApiResult<Unit> = BoardsApiResult.Success(Unit) var renameListResult: BoardsApiResult<Unit> = BoardsApiResult.Success(Unit)
var moveOutcomes: Map<String, BoardsApiResult<Unit>> = emptyMap() var moveOutcomes: Map<String, BoardsApiResult<Unit>> = emptyMap()
var deleteOutcomes: Map<String, BoardsApiResult<Unit>> = emptyMap() var deleteOutcomes: Map<String, BoardsApiResult<Unit>> = emptyMap()
var labelByIdResults: Map<String, BoardsApiResult<LabelDetail>> = emptyMap()
var listWorkspacesCalls: Int = 0 var listWorkspacesCalls: Int = 0
var lastBoardId: String? = null var lastBoardId: String? = null
@@ -374,6 +465,7 @@ class BoardDetailRepositoryTest {
var movedCardIds: MutableList<String> = mutableListOf() var movedCardIds: MutableList<String> = mutableListOf()
var deletedCardIds: MutableList<String> = mutableListOf() var deletedCardIds: MutableList<String> = mutableListOf()
var lastMoveTargetListId: String? = null var lastMoveTargetListId: String? = null
var getLabelByPublicIdCalls: MutableList<String> = mutableListOf()
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
@@ -425,6 +517,15 @@ class BoardDetailRepositoryTest {
return deleteOutcomes[cardId] ?: BoardsApiResult.Success(Unit) return deleteOutcomes[cardId] ?: BoardsApiResult.Success(Unit)
} }
override suspend fun getLabelByPublicId(
baseUrl: String,
apiKey: String,
labelId: String,
): BoardsApiResult<LabelDetail> {
getLabelByPublicIdCalls += labelId
return labelByIdResults[labelId] ?: BoardsApiResult.Failure("Missing fake label")
}
override suspend fun listBoards( override suspend fun listBoards(
baseUrl: String, baseUrl: String,
apiKey: String, apiKey: String,

View File

@@ -7,6 +7,7 @@ import org.junit.Test
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
import space.hackenslacker.kanbn4droid.app.auth.AuthResult import space.hackenslacker.kanbn4droid.app.auth.AuthResult
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.SessionStore import space.hackenslacker.kanbn4droid.app.auth.SessionStore
class BoardsRepositoryTest { class BoardsRepositoryTest {
@@ -250,5 +251,13 @@ class BoardsRepositoryTest {
lastDeletedId = boardId lastDeletedId = boardId
return deleteBoardResult return deleteBoardResult
} }
override suspend fun getLabelByPublicId(
baseUrl: String,
apiKey: String,
labelId: String,
): BoardsApiResult<LabelDetail> {
return BoardsApiResult.Failure("Not needed in boards tests")
}
} }
} }

View File

@@ -18,6 +18,7 @@ import org.junit.Test
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
import space.hackenslacker.kanbn4droid.app.auth.AuthResult import space.hackenslacker.kanbn4droid.app.auth.AuthResult
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.SessionStore import space.hackenslacker.kanbn4droid.app.auth.SessionStore
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@@ -194,5 +195,13 @@ class BoardsViewModelTest {
lastDeletedId = boardId lastDeletedId = boardId
return deleteBoardResult return deleteBoardResult
} }
override suspend fun getLabelByPublicId(
baseUrl: String,
apiKey: String,
labelId: String,
): BoardsApiResult<LabelDetail> {
return BoardsApiResult.Failure("Not needed in boards tests")
}
} }
} }