fix: handle workspace-switch unauthorized and rollback edge case

This commit is contained in:
2026-03-18 08:50:07 -04:00
parent f27aa6969d
commit d9d751c461
3 changed files with 67 additions and 2 deletions

View File

@@ -64,6 +64,7 @@ class BoardsRepository(
is BoardsApiResult.Failure -> return sessionResult is BoardsApiResult.Failure -> return sessionResult
} }
val previousWorkspaceId = sessionStore.getWorkspaceId()?.takeIf { it.isNotBlank() }
sessionStore.saveWorkspaceId(normalizedWorkspaceId) sessionStore.saveWorkspaceId(normalizedWorkspaceId)
val listBoardsResult = apiClient.listBoards( val listBoardsResult = apiClient.listBoards(
baseUrl = session.baseUrl, baseUrl = session.baseUrl,
@@ -72,7 +73,14 @@ class BoardsRepository(
) )
return when (listBoardsResult) { return when (listBoardsResult) {
is BoardsApiResult.Success -> BoardsApiResult.Success(Unit) is BoardsApiResult.Success -> BoardsApiResult.Success(Unit)
is BoardsApiResult.Failure -> listBoardsResult is BoardsApiResult.Failure -> {
if (previousWorkspaceId != null) {
sessionStore.saveWorkspaceId(previousWorkspaceId)
} else {
sessionStore.clearWorkspaceId()
}
listBoardsResult
}
} }
} }

View File

@@ -101,11 +101,15 @@ class BoardsViewModel(
), ),
) )
} }
if (result.message.isUnauthorizedFailureMessage()) {
_events.emit(BoardsUiEvent.ForceSignOut)
} else {
_events.emit(BoardsUiEvent.ShowServerError(result.message)) _events.emit(BoardsUiEvent.ShowServerError(result.message))
} }
} }
} }
} }
}
fun refreshBoards() { fun refreshBoards() {
fetchBoards(initial = false, refresh = true) fetchBoards(initial = false, refresh = true)
@@ -288,3 +292,8 @@ class BoardsViewModel(
} }
} }
} }
private fun String.isUnauthorizedFailureMessage(): Boolean {
val normalized = lowercase()
return "401" in normalized || "403" in normalized || "authentication" in normalized || "unauthorized" in normalized
}

View File

@@ -318,6 +318,54 @@ class BoardsViewModelTest {
assertEquals("ws-1", sessionStore.getWorkspaceId()) assertEquals("ws-1", sessionStore.getWorkspaceId())
} }
@Test
fun workspaceSwitchUnauthorizedEmitsForceSignOut() = runTest {
val api = FakeBoardsApiClient().apply {
usersMeResult = BoardsApiResult.Success(DrawerProfile(displayName = "Alice", email = null))
workspacesResult = BoardsApiResult.Success(
listOf(
WorkspaceSummary("ws-1", "Main"),
WorkspaceSummary("ws-2", "Platform"),
),
)
listBoardsResults.addLast(BoardsApiResult.Failure("Server error: 401"))
}
val sessionStore = InMemorySessionStore("https://kan.bn/", workspaceId = "ws-1")
val viewModel = newViewModel(api, sessionStore = sessionStore)
viewModel.loadDrawerData()
advanceUntilIdle()
val eventDeferred = async { viewModel.events.first() }
viewModel.onWorkspaceSelected("ws-2")
advanceUntilIdle()
val event = eventDeferred.await()
assertTrue(event is BoardsUiEvent.ForceSignOut)
assertEquals("ws-1", sessionStore.getWorkspaceId())
}
@Test
fun workspaceSwitchFailureWithNullPreviousRollsBackPersistedWorkspaceId() = runTest {
val api = FakeBoardsApiClient().apply {
usersMeResult = BoardsApiResult.Success(DrawerProfile(displayName = "Alice", email = null))
workspacesResult = BoardsApiResult.Failure("Server error: 500")
listBoardsResults.addLast(BoardsApiResult.Failure("Server error: 500"))
}
val sessionStore = InMemorySessionStore("https://kan.bn/", workspaceId = null)
val viewModel = newViewModel(api, sessionStore = sessionStore)
viewModel.loadDrawerData()
advanceUntilIdle()
assertEquals(null, viewModel.uiState.value.drawer.activeWorkspaceId)
assertEquals(null, sessionStore.getWorkspaceId())
viewModel.onWorkspaceSelected("ws-2")
advanceUntilIdle()
assertEquals(null, sessionStore.getWorkspaceId())
assertEquals(null, viewModel.uiState.value.drawer.activeWorkspaceId)
}
private fun newViewModel( private fun newViewModel(
apiClient: FakeBoardsApiClient, apiClient: FakeBoardsApiClient,
sessionStore: InMemorySessionStore = InMemorySessionStore("https://kan.bn/", workspaceId = "ws-1"), sessionStore: InMemorySessionStore = InMemorySessionStore("https://kan.bn/", workspaceId = "ws-1"),