From f85586ddc768bc9ba342dcb7c6318f9c387c8dd8 Mon Sep 17 00:00:00 2001 From: Wally Hackenslacker Date: Mon, 16 Mar 2026 20:41:07 -0400 Subject: [PATCH] fix: make card comment refresh failures non-fatal --- .../app/carddetail/CardDetailRepository.kt | 24 ++++++-- .../carddetail/CardDetailRepositoryTest.kt | 55 +++++++++++++++++++ 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/space/hackenslacker/kanbn4droid/app/carddetail/CardDetailRepository.kt b/app/src/main/java/space/hackenslacker/kanbn4droid/app/carddetail/CardDetailRepository.kt index bf33fba..cf427d4 100644 --- a/app/src/main/java/space/hackenslacker/kanbn4droid/app/carddetail/CardDetailRepository.kt +++ b/app/src/main/java/space/hackenslacker/kanbn4droid/app/carddetail/CardDetailRepository.kt @@ -19,6 +19,7 @@ class CardDetailRepository( companion object { const val MISSING_SESSION_MESSAGE = "Missing session. Please sign in again." private const val SESSION_EXPIRED_MESSAGE = "Session expired. Please sign in again." + private val AUTH_STATUS_CODE_REGEX = Regex("\\b(401|403)\\b") } sealed interface Result { @@ -206,7 +207,11 @@ class CardDetailRepository( is BoardsApiResult.Failure -> return mapFailure(addCommentResult.message) } - return listActivities(normalizedCardId) + return when (val refreshResult = listActivities(normalizedCardId)) { + is Result.Success -> refreshResult + is Result.Failure.SessionExpired -> refreshResult + is Result.Failure.Generic -> Result.Success(emptyList()) + } } private suspend fun session(): Result { @@ -231,11 +236,18 @@ class CardDetailRepository( private fun isAuthFailure(message: String): Boolean { val lower = message.lowercase() - return lower.contains("authentication failed") || - lower.contains("server error: 401") || - lower.contains("server error: 403") || - lower.contains(" 401") || - lower.contains(" 403") + val hasAuthToken = + lower.contains("authentication failed") || + lower.contains("unauthorized") || + lower.contains("forbidden") + val hasAuthStatusCode = AUTH_STATUS_CODE_REGEX.containsMatchIn(lower) && + ( + lower.contains("server error") || + lower.contains("http") || + lower.contains("status") || + lower.contains("code") + ) + return hasAuthToken || hasAuthStatusCode } private data class SessionSnapshot( diff --git a/app/src/test/java/space/hackenslacker/kanbn4droid/app/carddetail/CardDetailRepositoryTest.kt b/app/src/test/java/space/hackenslacker/kanbn4droid/app/carddetail/CardDetailRepositoryTest.kt index 86fe78f..ccd8637 100644 --- a/app/src/test/java/space/hackenslacker/kanbn4droid/app/carddetail/CardDetailRepositoryTest.kt +++ b/app/src/test/java/space/hackenslacker/kanbn4droid/app/carddetail/CardDetailRepositoryTest.kt @@ -120,6 +120,36 @@ class CardDetailRepositoryTest { assertEquals("hello", apiClient.lastComment) } + @Test + fun addComment_refreshGenericFailure_returnsSuccessWithEmptyActivities() = runTest { + val apiClient = FakeCardDetailApiClient().apply { + addCommentResult = BoardsApiResult.Success(Unit) + listActivitiesResult = BoardsApiResult.Failure("Server temporarily unavailable") + } + val repository = createRepository(apiClient = apiClient) + + val result = repository.addComment(cardId = "card-1", comment = "hello") + + assertTrue(result is CardDetailRepository.Result.Success) + val activities = (result as CardDetailRepository.Result.Success>).value + assertTrue(activities.isEmpty()) + assertEquals(1, apiClient.addCommentCalls) + assertEquals(1, apiClient.listActivitiesCalls) + } + + @Test + fun addComment_refreshAuthFailure_mapsToSessionExpired() = runTest { + val apiClient = FakeCardDetailApiClient().apply { + addCommentResult = BoardsApiResult.Success(Unit) + listActivitiesResult = BoardsApiResult.Failure("Server error: 403") + } + val repository = createRepository(apiClient = apiClient) + + val result = repository.addComment(cardId = "card-1", comment = "hello") + + assertTrue(result is CardDetailRepository.Result.Failure.SessionExpired) + } + @Test fun addComment_authFailureMapsToSessionExpired() = runTest { val apiClient = FakeCardDetailApiClient().apply { @@ -132,6 +162,31 @@ class CardDetailRepositoryTest { assertTrue(result is CardDetailRepository.Result.Failure.SessionExpired) } + @Test + fun listActivities_nonAuthNumericMessage_remainsGenericFailure() = runTest { + val apiClient = FakeCardDetailApiClient().apply { + listActivitiesResult = BoardsApiResult.Failure("Server error: 1401") + } + val repository = createRepository(apiClient = apiClient) + + val result = repository.listActivities("card-1") + + assertTrue(result is CardDetailRepository.Result.Failure.Generic) + assertEquals("Server error: 1401", (result as CardDetailRepository.Result.Failure.Generic).message) + } + + @Test + fun listActivities_forbiddenMessage_mapsToSessionExpired() = runTest { + val apiClient = FakeCardDetailApiClient().apply { + listActivitiesResult = BoardsApiResult.Failure("Forbidden") + } + val repository = createRepository(apiClient = apiClient) + + val result = repository.listActivities("card-1") + + assertTrue(result is CardDetailRepository.Result.Failure.SessionExpired) + } + @Test fun updateDueDate_dateOnlyInputNormalizesToUtcMidnightPayloadContract() = runTest { val apiClient = FakeCardDetailApiClient()