diff --git a/app/src/main/java/space/hackenslacker/kanbn4droid/app/boards/BoardsRepository.kt b/app/src/main/java/space/hackenslacker/kanbn4droid/app/boards/BoardsRepository.kt index a1b6d14..661df92 100644 --- a/app/src/main/java/space/hackenslacker/kanbn4droid/app/boards/BoardsRepository.kt +++ b/app/src/main/java/space/hackenslacker/kanbn4droid/app/boards/BoardsRepository.kt @@ -64,6 +64,7 @@ class BoardsRepository( is BoardsApiResult.Failure -> return sessionResult } + val previousWorkspaceId = sessionStore.getWorkspaceId()?.takeIf { it.isNotBlank() } sessionStore.saveWorkspaceId(normalizedWorkspaceId) val listBoardsResult = apiClient.listBoards( baseUrl = session.baseUrl, @@ -72,7 +73,14 @@ class BoardsRepository( ) return when (listBoardsResult) { is BoardsApiResult.Success -> BoardsApiResult.Success(Unit) - is BoardsApiResult.Failure -> listBoardsResult + is BoardsApiResult.Failure -> { + if (previousWorkspaceId != null) { + sessionStore.saveWorkspaceId(previousWorkspaceId) + } else { + sessionStore.clearWorkspaceId() + } + listBoardsResult + } } } diff --git a/app/src/main/java/space/hackenslacker/kanbn4droid/app/boards/BoardsViewModel.kt b/app/src/main/java/space/hackenslacker/kanbn4droid/app/boards/BoardsViewModel.kt index a0eb5b5..7b85697 100644 --- a/app/src/main/java/space/hackenslacker/kanbn4droid/app/boards/BoardsViewModel.kt +++ b/app/src/main/java/space/hackenslacker/kanbn4droid/app/boards/BoardsViewModel.kt @@ -101,7 +101,11 @@ class BoardsViewModel( ), ) } - _events.emit(BoardsUiEvent.ShowServerError(result.message)) + if (result.message.isUnauthorizedFailureMessage()) { + _events.emit(BoardsUiEvent.ForceSignOut) + } else { + _events.emit(BoardsUiEvent.ShowServerError(result.message)) + } } } } @@ -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 +} diff --git a/app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsViewModelTest.kt b/app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsViewModelTest.kt index 92e0055..2905340 100644 --- a/app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsViewModelTest.kt +++ b/app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsViewModelTest.kt @@ -318,6 +318,54 @@ class BoardsViewModelTest { 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( apiClient: FakeBoardsApiClient, sessionStore: InMemorySessionStore = InMemorySessionStore("https://kan.bn/", workspaceId = "ws-1"),