fix: make card comment refresh failures non-fatal

This commit is contained in:
2026-03-16 20:41:07 -04:00
parent 82a3d59105
commit f85586ddc7
2 changed files with 73 additions and 6 deletions

View File

@@ -19,6 +19,7 @@ class CardDetailRepository(
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."
private const val SESSION_EXPIRED_MESSAGE = "Session expired. 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<out T> { sealed interface Result<out T> {
@@ -206,7 +207,11 @@ class CardDetailRepository(
is BoardsApiResult.Failure -> return mapFailure(addCommentResult.message) 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<SessionSnapshot> { private suspend fun session(): Result<SessionSnapshot> {
@@ -231,11 +236,18 @@ class CardDetailRepository(
private fun isAuthFailure(message: String): Boolean { private fun isAuthFailure(message: String): Boolean {
val lower = message.lowercase() val lower = message.lowercase()
return lower.contains("authentication failed") || val hasAuthToken =
lower.contains("server error: 401") || lower.contains("authentication failed") ||
lower.contains("server error: 403") || lower.contains("unauthorized") ||
lower.contains(" 401") || lower.contains("forbidden")
lower.contains(" 403") 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( private data class SessionSnapshot(

View File

@@ -120,6 +120,36 @@ class CardDetailRepositoryTest {
assertEquals("hello", apiClient.lastComment) 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<List<CardActivity>>).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 @Test
fun addComment_authFailureMapsToSessionExpired() = runTest { fun addComment_authFailureMapsToSessionExpired() = runTest {
val apiClient = FakeCardDetailApiClient().apply { val apiClient = FakeCardDetailApiClient().apply {
@@ -132,6 +162,31 @@ class CardDetailRepositoryTest {
assertTrue(result is CardDetailRepository.Result.Failure.SessionExpired) 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 @Test
fun updateDueDate_dateOnlyInputNormalizesToUtcMidnightPayloadContract() = runTest { fun updateDueDate_dateOnlyInputNormalizesToUtcMidnightPayloadContract() = runTest {
val apiClient = FakeCardDetailApiClient() val apiClient = FakeCardDetailApiClient()