commit 82353b2e2157ce69e994357fd675816605a495e2
parent 27b5ed16f83c2dd8a02d4b2ed768435de004479f
Author: triesap <tyson@radroots.org>
Date: Sat, 1 Aug 2026 18:27:40 +0000
accounts: implement explicit account status actions
- select accounts only through validated identifiers
- target login and logout actions by explicit account ID
- preserve unrelated accounts and current selection
- return typed failures for every missing target
Diffstat:
2 files changed, 103 insertions(+), 0 deletions(-)
diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/accounts/state/AccountsReducer.kt b/app/desktop/src/main/kotlin/org/radroots/studio/accounts/state/AccountsReducer.kt
@@ -1,6 +1,7 @@
package org.radroots.studio.accounts.state
import org.radroots.studio.accounts.model.Account
+import org.radroots.studio.accounts.model.AccountId
import org.radroots.studio.accounts.model.AccountsAction
import org.radroots.studio.accounts.model.AccountsProblem
import org.radroots.studio.accounts.model.AccountsState
@@ -26,6 +27,17 @@ class AccountsReducer(
addDraft = state.addDraft.copy(serverUrl = action.value),
)
AccountsAction.SubmitAddAccount -> submitAddAccount(state)
+ is AccountsAction.SelectAccount -> selectAccount(state, action)
+ is AccountsAction.LogIn -> setLoginStatus(
+ state = state,
+ accountId = action.accountId,
+ loginStatus = LoginStatus.LoggedIn,
+ )
+ is AccountsAction.LogOut -> setLoginStatus(
+ state = state,
+ accountId = action.accountId,
+ loginStatus = LoginStatus.LoggedOut,
+ )
AccountsAction.DismissProblem -> state.copy(problem = null)
else -> state
}
@@ -59,4 +71,36 @@ class AccountsReducer(
problem = null,
)
}
+
+ private fun selectAccount(
+ state: AccountsState,
+ action: AccountsAction.SelectAccount,
+ ): AccountsState = if (state.accounts.any { it.id == action.accountId }) {
+ state.copy(
+ selectedAccountId = action.accountId,
+ problem = null,
+ )
+ } else {
+ state.copy(problem = AccountsProblem.AccountNotFound(action.accountId))
+ }
+
+ private fun setLoginStatus(
+ state: AccountsState,
+ accountId: AccountId,
+ loginStatus: LoginStatus,
+ ): AccountsState {
+ if (state.accounts.none { it.id == accountId }) {
+ return state.copy(problem = AccountsProblem.AccountNotFound(accountId))
+ }
+ return state.copy(
+ accounts = state.accounts.map { account ->
+ if (account.id == accountId) {
+ account.copy(loginStatus = loginStatus)
+ } else {
+ account
+ }
+ },
+ problem = null,
+ )
+ }
}
diff --git a/app/desktop/src/test/kotlin/org/radroots/studio/accounts/state/AccountsReducerTest.kt b/app/desktop/src/test/kotlin/org/radroots/studio/accounts/state/AccountsReducerTest.kt
@@ -113,6 +113,65 @@ class AccountsReducerTest {
assertEquals(state.copy(problem = null), result)
}
+ @Test
+ fun selectionTargetsAnExplicitExistingAccount() {
+ val first = testAccount()
+ val second = testAccount(id = "account-2", displayName = "Second Account")
+ val base = testAccountsState(first, second)
+ val reducer = reducerWithIds("unused")
+
+ val selected = reducer.reduce(
+ base.copy(problem = AccountsProblem.InvalidServerUrl),
+ AccountsAction.SelectAccount(second.id),
+ )
+ val missingId = AccountId("missing")
+ val missing = reducer.reduce(
+ selected,
+ AccountsAction.SelectAccount(missingId),
+ )
+
+ assertEquals(second.id, selected.selectedAccountId)
+ assertEquals(null, selected.problem)
+ assertEquals(second.id, missing.selectedAccountId)
+ assertEquals(AccountsProblem.AccountNotFound(missingId), missing.problem)
+ }
+
+ @Test
+ fun loginAndLogoutUpdateOnlyTheExplicitAccount() {
+ val first = testAccount()
+ val second = testAccount(id = "account-2", displayName = "Second Account")
+ val base = testAccountsState(first, second)
+ val reducer = reducerWithIds("unused")
+
+ val loggedIn = reducer.reduce(base, AccountsAction.LogIn(second.id))
+ val idempotentLogin = reducer.reduce(loggedIn, AccountsAction.LogIn(second.id))
+ val loggedOut = reducer.reduce(idempotentLogin, AccountsAction.LogOut(second.id))
+ val idempotentLogout = reducer.reduce(loggedOut, AccountsAction.LogOut(second.id))
+
+ assertEquals(LoginStatus.LoggedOut, loggedIn.accounts[0].loginStatus)
+ assertEquals(LoginStatus.LoggedIn, loggedIn.accounts[1].loginStatus)
+ assertEquals(loggedIn, idempotentLogin)
+ assertEquals(LoginStatus.LoggedOut, loggedOut.accounts[1].loginStatus)
+ assertEquals(loggedOut, idempotentLogout)
+ assertEquals(first.id, loggedOut.selectedAccountId)
+ }
+
+ @Test
+ fun loginAndLogoutRejectMissingTargets() {
+ val base = testAccountsState(testAccount())
+ val missingId = AccountId("missing")
+ val reducer = reducerWithIds("unused")
+
+ listOf(
+ reducer.reduce(base, AccountsAction.LogIn(missingId)),
+ reducer.reduce(base, AccountsAction.LogOut(missingId)),
+ ).forEach { result ->
+ assertEquals(base.accounts, result.accounts)
+ assertEquals(base.selectedAccountId, result.selectedAccountId)
+ assertEquals(AccountsProblem.AccountNotFound(missingId), result.problem)
+ }
+ }
+
private fun draftState(
displayName: String,
serverUrl: String,