feat: implement settings apply coordinator with rollback
This commit is contained in:
@@ -7,12 +7,28 @@ class SessionPreferences(context: Context) : SessionStore {
|
|||||||
private val preferences: SharedPreferences =
|
private val preferences: SharedPreferences =
|
||||||
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
override fun getThemeMode(): String = preferences.getString(KEY_THEME_MODE, THEME_MODE_SYSTEM) ?: THEME_MODE_SYSTEM
|
||||||
|
|
||||||
|
override fun saveThemeMode(themeMode: String) {
|
||||||
|
preferences.edit().putString(KEY_THEME_MODE, themeMode).apply()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getBaseUrl(): String? = preferences.getString(KEY_BASE_URL, null)
|
override fun getBaseUrl(): String? = preferences.getString(KEY_BASE_URL, null)
|
||||||
|
|
||||||
override fun saveBaseUrl(url: String) {
|
override fun saveBaseUrl(url: String) {
|
||||||
preferences.edit().putString(KEY_BASE_URL, url).apply()
|
preferences.edit().putString(KEY_BASE_URL, url).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getApiKey(): String? = preferences.getString(KEY_API_KEY, null)
|
||||||
|
|
||||||
|
override fun saveApiKey(apiKey: String) {
|
||||||
|
preferences.edit().putString(KEY_API_KEY, apiKey).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearApiKey() {
|
||||||
|
preferences.edit().remove(KEY_API_KEY).apply()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getWorkspaceId(): String? = preferences.getString(KEY_WORKSPACE_ID, null)
|
override fun getWorkspaceId(): String? = preferences.getString(KEY_WORKSPACE_ID, null)
|
||||||
|
|
||||||
override fun saveWorkspaceId(workspaceId: String) {
|
override fun saveWorkspaceId(workspaceId: String) {
|
||||||
@@ -27,9 +43,55 @@ class SessionPreferences(context: Context) : SessionStore {
|
|||||||
preferences.edit().remove(KEY_WORKSPACE_ID).apply()
|
preferences.edit().remove(KEY_WORKSPACE_ID).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun initializeDraftsFromCommitted() {
|
||||||
|
val editor = preferences.edit()
|
||||||
|
editor.putString(KEY_DRAFT_THEME_MODE, getThemeMode())
|
||||||
|
editor.putString(KEY_DRAFT_BASE_URL, getBaseUrl())
|
||||||
|
editor.putString(KEY_DRAFT_API_KEY, getApiKey())
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDraftThemeMode(): String {
|
||||||
|
return preferences.getString(KEY_DRAFT_THEME_MODE, getThemeMode()) ?: getThemeMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveDraftThemeMode(themeMode: String) {
|
||||||
|
preferences.edit().putString(KEY_DRAFT_THEME_MODE, themeMode).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDraftBaseUrl(): String? = preferences.getString(KEY_DRAFT_BASE_URL, getBaseUrl())
|
||||||
|
|
||||||
|
override fun saveDraftBaseUrl(url: String) {
|
||||||
|
preferences.edit().putString(KEY_DRAFT_BASE_URL, url).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDraftApiKey(): String? = preferences.getString(KEY_DRAFT_API_KEY, getApiKey())
|
||||||
|
|
||||||
|
override fun saveDraftApiKey(apiKey: String) {
|
||||||
|
preferences.edit().putString(KEY_DRAFT_API_KEY, apiKey).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resetDraftsFromCommitted() {
|
||||||
|
initializeDraftsFromCommitted()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun syncDraftsToCommitted() {
|
||||||
|
val editor = preferences.edit()
|
||||||
|
editor.putString(KEY_THEME_MODE, getDraftThemeMode())
|
||||||
|
editor.putString(KEY_BASE_URL, getDraftBaseUrl())
|
||||||
|
editor.putString(KEY_API_KEY, getDraftApiKey())
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
private const val PREFS_NAME = "kanbn_session"
|
private const val PREFS_NAME = "kanbn_session"
|
||||||
|
private const val THEME_MODE_SYSTEM = "system"
|
||||||
|
private const val KEY_THEME_MODE = "theme_mode"
|
||||||
private const val KEY_BASE_URL = "base_url"
|
private const val KEY_BASE_URL = "base_url"
|
||||||
|
private const val KEY_API_KEY = "api_key"
|
||||||
private const val KEY_WORKSPACE_ID = "workspace_id"
|
private const val KEY_WORKSPACE_ID = "workspace_id"
|
||||||
|
private const val KEY_DRAFT_THEME_MODE = "draft_theme_mode"
|
||||||
|
private const val KEY_DRAFT_BASE_URL = "draft_base_url"
|
||||||
|
private const val KEY_DRAFT_API_KEY = "draft_api_key"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,41 @@
|
|||||||
package space.hackenslacker.kanbn4droid.app.auth
|
package space.hackenslacker.kanbn4droid.app.auth
|
||||||
|
|
||||||
interface SessionStore {
|
interface SessionStore {
|
||||||
|
fun getThemeMode(): String = "system"
|
||||||
|
fun saveThemeMode(themeMode: String) {}
|
||||||
fun getBaseUrl(): String?
|
fun getBaseUrl(): String?
|
||||||
fun saveBaseUrl(url: String)
|
fun saveBaseUrl(url: String)
|
||||||
|
fun getApiKey(): String? = null
|
||||||
|
fun saveApiKey(apiKey: String) {}
|
||||||
|
fun clearApiKey() {}
|
||||||
fun getWorkspaceId(): String?
|
fun getWorkspaceId(): String?
|
||||||
fun saveWorkspaceId(workspaceId: String)
|
fun saveWorkspaceId(workspaceId: String)
|
||||||
fun clearBaseUrl()
|
fun clearBaseUrl()
|
||||||
fun clearWorkspaceId()
|
fun clearWorkspaceId()
|
||||||
|
|
||||||
|
fun initializeDraftsFromCommitted() {}
|
||||||
|
fun getDraftThemeMode(): String = getThemeMode()
|
||||||
|
fun saveDraftThemeMode(themeMode: String) {
|
||||||
|
saveThemeMode(themeMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDraftBaseUrl(): String? = getBaseUrl()
|
||||||
|
fun saveDraftBaseUrl(url: String) {
|
||||||
|
saveBaseUrl(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDraftApiKey(): String? = getApiKey()
|
||||||
|
fun saveDraftApiKey(apiKey: String) {
|
||||||
|
saveApiKey(apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetDraftsFromCommitted() {
|
||||||
|
initializeDraftsFromCommitted()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncDraftsToCommitted() {
|
||||||
|
saveThemeMode(getDraftThemeMode())
|
||||||
|
getDraftBaseUrl()?.let(::saveBaseUrl) ?: clearBaseUrl()
|
||||||
|
getDraftApiKey()?.let(::saveApiKey) ?: clearApiKey()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package space.hackenslacker.kanbn4droid.app.auth
|
||||||
|
|
||||||
|
sealed interface SettingsApplyResult {
|
||||||
|
data object NoChanges : SettingsApplyResult
|
||||||
|
data class SuccessNoCredentialChange(val themeChanged: Boolean) : SettingsApplyResult
|
||||||
|
data object SuccessCredentialChange : SettingsApplyResult
|
||||||
|
data class ValidationError(val field: String, val message: String) : SettingsApplyResult
|
||||||
|
data class AuthError(val message: String) : SettingsApplyResult
|
||||||
|
data class NetworkError(val message: String) : SettingsApplyResult
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsApplyCoordinator(
|
||||||
|
private val sessionStore: SessionStore,
|
||||||
|
private val apiClient: KanbnApiClient,
|
||||||
|
) {
|
||||||
|
suspend fun apply(): SettingsApplyResult {
|
||||||
|
val snapshot = LastKnownGoodSnapshot.capture(sessionStore)
|
||||||
|
|
||||||
|
val draftTheme = sessionStore.getDraftThemeMode().trim().ifBlank { "system" }
|
||||||
|
val draftBaseUrlRaw = sessionStore.getDraftBaseUrl().orEmpty()
|
||||||
|
val draftApiKey = sessionStore.getDraftApiKey().orEmpty().trim()
|
||||||
|
|
||||||
|
val normalizedDraftBaseUrl = when (val normalized = UrlNormalizer.normalize(draftBaseUrlRaw)) {
|
||||||
|
is UrlValidationResult.Valid -> normalized.normalizedUrl
|
||||||
|
is UrlValidationResult.Invalid -> {
|
||||||
|
sessionStore.resetDraftsFromCommitted()
|
||||||
|
return SettingsApplyResult.ValidationError(
|
||||||
|
field = FIELD_BASE_URL,
|
||||||
|
message = normalized.message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draftApiKey.isBlank()) {
|
||||||
|
sessionStore.resetDraftsFromCommitted()
|
||||||
|
return SettingsApplyResult.ValidationError(
|
||||||
|
field = FIELD_API_KEY,
|
||||||
|
message = "API key is required",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val themeChanged = draftTheme != snapshot.committedThemeMode
|
||||||
|
val credentialChanged = normalizedDraftBaseUrl != snapshot.committedBaseUrl ||
|
||||||
|
draftApiKey != snapshot.committedApiKey
|
||||||
|
if (!themeChanged && !credentialChanged) {
|
||||||
|
return SettingsApplyResult.NoChanges
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentialChanged) {
|
||||||
|
when (val auth = apiClient.healthCheck(normalizedDraftBaseUrl, draftApiKey)) {
|
||||||
|
is AuthResult.Success -> Unit
|
||||||
|
is AuthResult.Failure -> {
|
||||||
|
snapshot.restore(sessionStore)
|
||||||
|
return when (auth.reason) {
|
||||||
|
AuthFailureReason.Authentication -> SettingsApplyResult.AuthError(auth.message)
|
||||||
|
AuthFailureReason.Connectivity,
|
||||||
|
AuthFailureReason.Server,
|
||||||
|
AuthFailureReason.Unexpected,
|
||||||
|
-> SettingsApplyResult.NetworkError(auth.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
sessionStore.saveDraftThemeMode(draftTheme)
|
||||||
|
sessionStore.saveDraftBaseUrl(normalizedDraftBaseUrl)
|
||||||
|
sessionStore.saveDraftApiKey(draftApiKey)
|
||||||
|
sessionStore.syncDraftsToCommitted()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
snapshot.restore(sessionStore)
|
||||||
|
return SettingsApplyResult.NetworkError("Failed to apply settings changes.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (credentialChanged) {
|
||||||
|
SettingsApplyResult.SuccessCredentialChange
|
||||||
|
} else {
|
||||||
|
SettingsApplyResult.SuccessNoCredentialChange(themeChanged = themeChanged)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class LastKnownGoodSnapshot(
|
||||||
|
val committedThemeMode: String,
|
||||||
|
val committedBaseUrl: String,
|
||||||
|
val committedApiKey: String,
|
||||||
|
) {
|
||||||
|
fun restore(sessionStore: SessionStore) {
|
||||||
|
sessionStore.saveThemeMode(committedThemeMode)
|
||||||
|
sessionStore.saveBaseUrl(committedBaseUrl)
|
||||||
|
sessionStore.saveApiKey(committedApiKey)
|
||||||
|
sessionStore.saveDraftThemeMode(committedThemeMode)
|
||||||
|
sessionStore.saveDraftBaseUrl(committedBaseUrl)
|
||||||
|
sessionStore.saveDraftApiKey(committedApiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun capture(sessionStore: SessionStore): LastKnownGoodSnapshot {
|
||||||
|
return LastKnownGoodSnapshot(
|
||||||
|
committedThemeMode = sessionStore.getThemeMode(),
|
||||||
|
committedBaseUrl = sessionStore.getBaseUrl().orEmpty(),
|
||||||
|
committedApiKey = sessionStore.getApiKey().orEmpty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private const val FIELD_BASE_URL = "baseUrl"
|
||||||
|
private const val FIELD_API_KEY = "apiKey"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
package space.hackenslacker.kanbn4droid.app.auth
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SettingsApplyCoordinatorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun applyNoChangesReturnsNoChangesWithoutAuthCall() = runTest {
|
||||||
|
val sessionStore = FakeSessionStore()
|
||||||
|
val apiClient = FakeKanbnApiClient()
|
||||||
|
val coordinator = SettingsApplyCoordinator(sessionStore, apiClient)
|
||||||
|
|
||||||
|
val result = coordinator.apply()
|
||||||
|
|
||||||
|
assertEquals(SettingsApplyResult.NoChanges, result)
|
||||||
|
assertEquals(0, apiClient.healthCheckCalls.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun applyCredentialChangeSuccessPersistsAndReturnsSuccessCredentialChange() = runTest {
|
||||||
|
val sessionStore = FakeSessionStore().apply {
|
||||||
|
saveDraftBaseUrl("https://next.kan.bn")
|
||||||
|
saveDraftApiKey("next-key")
|
||||||
|
}
|
||||||
|
val apiClient = FakeKanbnApiClient(
|
||||||
|
healthCheckResult = AuthResult.Success,
|
||||||
|
)
|
||||||
|
val coordinator = SettingsApplyCoordinator(sessionStore, apiClient)
|
||||||
|
|
||||||
|
val result = coordinator.apply()
|
||||||
|
|
||||||
|
assertEquals(SettingsApplyResult.SuccessCredentialChange, result)
|
||||||
|
assertEquals(1, apiClient.healthCheckCalls.size)
|
||||||
|
assertEquals("https://next.kan.bn/", apiClient.healthCheckCalls.single().baseUrl)
|
||||||
|
assertEquals("next-key", apiClient.healthCheckCalls.single().apiKey)
|
||||||
|
assertEquals("https://next.kan.bn/", sessionStore.getBaseUrl())
|
||||||
|
assertEquals("next-key", sessionStore.getApiKey())
|
||||||
|
assertEquals(sessionStore.getBaseUrl(), sessionStore.getDraftBaseUrl())
|
||||||
|
assertEquals(sessionStore.getApiKey(), sessionStore.getDraftApiKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun applyAuthFailureRollsBackCommittedAndDraftValues() = runTest {
|
||||||
|
val sessionStore = FakeSessionStore().apply {
|
||||||
|
saveDraftBaseUrl("https://broken.kan.bn")
|
||||||
|
saveDraftApiKey("bad-key")
|
||||||
|
}
|
||||||
|
val apiClient = FakeKanbnApiClient(
|
||||||
|
healthCheckResult = AuthResult.Failure(
|
||||||
|
message = "Authentication failed. Check your API key.",
|
||||||
|
reason = AuthFailureReason.Authentication,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val coordinator = SettingsApplyCoordinator(sessionStore, apiClient)
|
||||||
|
|
||||||
|
val result = coordinator.apply()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
SettingsApplyResult.AuthError("Authentication failed. Check your API key."),
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assertEquals("https://kan.bn/", sessionStore.getBaseUrl())
|
||||||
|
assertEquals("existing-key", sessionStore.getApiKey())
|
||||||
|
assertEquals("https://kan.bn/", sessionStore.getDraftBaseUrl())
|
||||||
|
assertEquals("existing-key", sessionStore.getDraftApiKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun applyPersistFailureAfterAuthRollsBack() = runTest {
|
||||||
|
val sessionStore = FakeSessionStore(
|
||||||
|
failSyncToCommitted = true,
|
||||||
|
).apply {
|
||||||
|
saveDraftBaseUrl("https://next.kan.bn")
|
||||||
|
saveDraftApiKey("next-key")
|
||||||
|
}
|
||||||
|
val apiClient = FakeKanbnApiClient(
|
||||||
|
healthCheckResult = AuthResult.Success,
|
||||||
|
)
|
||||||
|
val coordinator = SettingsApplyCoordinator(sessionStore, apiClient)
|
||||||
|
|
||||||
|
val result = coordinator.apply()
|
||||||
|
|
||||||
|
assertTrue(result is SettingsApplyResult.NetworkError)
|
||||||
|
result as SettingsApplyResult.NetworkError
|
||||||
|
assertTrue(result.message.contains("apply", ignoreCase = true))
|
||||||
|
assertEquals("https://kan.bn/", sessionStore.getBaseUrl())
|
||||||
|
assertEquals("existing-key", sessionStore.getApiKey())
|
||||||
|
assertEquals("https://kan.bn/", sessionStore.getDraftBaseUrl())
|
||||||
|
assertEquals("existing-key", sessionStore.getDraftApiKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun applyInvalidUrlReturnsValidationError() = runTest {
|
||||||
|
val sessionStore = FakeSessionStore().apply {
|
||||||
|
saveDraftBaseUrl("ftp://kan.bn")
|
||||||
|
}
|
||||||
|
val apiClient = FakeKanbnApiClient()
|
||||||
|
val coordinator = SettingsApplyCoordinator(sessionStore, apiClient)
|
||||||
|
|
||||||
|
val result = coordinator.apply()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
SettingsApplyResult.ValidationError(
|
||||||
|
field = "baseUrl",
|
||||||
|
message = "Base URL must start with http:// or https://",
|
||||||
|
),
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assertEquals(0, apiClient.healthCheckCalls.size)
|
||||||
|
assertEquals("https://kan.bn/", sessionStore.getDraftBaseUrl())
|
||||||
|
assertEquals("existing-key", sessionStore.getDraftApiKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun applyBlankApiKeyReturnsValidationErrorForApiKey() = runTest {
|
||||||
|
val sessionStore = FakeSessionStore().apply {
|
||||||
|
saveDraftApiKey(" ")
|
||||||
|
}
|
||||||
|
val apiClient = FakeKanbnApiClient()
|
||||||
|
val coordinator = SettingsApplyCoordinator(sessionStore, apiClient)
|
||||||
|
|
||||||
|
val result = coordinator.apply()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
SettingsApplyResult.ValidationError(
|
||||||
|
field = "apiKey",
|
||||||
|
message = "API key is required",
|
||||||
|
),
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assertEquals(0, apiClient.healthCheckCalls.size)
|
||||||
|
assertEquals("existing-key", sessionStore.getDraftApiKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun applyThemeAndCredentialsFailureRollsBackThemeDraft() = runTest {
|
||||||
|
val sessionStore = FakeSessionStore().apply {
|
||||||
|
saveDraftThemeMode("dark")
|
||||||
|
saveDraftBaseUrl("https://next.kan.bn")
|
||||||
|
saveDraftApiKey("next-key")
|
||||||
|
}
|
||||||
|
val apiClient = FakeKanbnApiClient(
|
||||||
|
healthCheckResult = AuthResult.Failure(
|
||||||
|
message = "Cannot reach server. Check your connection and URL.",
|
||||||
|
reason = AuthFailureReason.Connectivity,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val coordinator = SettingsApplyCoordinator(sessionStore, apiClient)
|
||||||
|
|
||||||
|
val result = coordinator.apply()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
SettingsApplyResult.NetworkError("Cannot reach server. Check your connection and URL."),
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
assertEquals("system", sessionStore.getThemeMode())
|
||||||
|
assertEquals("system", sessionStore.getDraftThemeMode())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun applyThemeChangeSuccessSignalsThemeMode() = runTest {
|
||||||
|
val sessionStore = FakeSessionStore().apply {
|
||||||
|
saveDraftThemeMode("dark")
|
||||||
|
}
|
||||||
|
val apiClient = FakeKanbnApiClient()
|
||||||
|
val coordinator = SettingsApplyCoordinator(sessionStore, apiClient)
|
||||||
|
|
||||||
|
val result = coordinator.apply()
|
||||||
|
|
||||||
|
assertEquals(SettingsApplyResult.SuccessNoCredentialChange(themeChanged = true), result)
|
||||||
|
assertEquals(0, apiClient.healthCheckCalls.size)
|
||||||
|
assertEquals("dark", sessionStore.getThemeMode())
|
||||||
|
assertEquals("dark", sessionStore.getDraftThemeMode())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeSessionStore(
|
||||||
|
private val failSyncToCommitted: Boolean = false,
|
||||||
|
) : SessionStore {
|
||||||
|
private var committedThemeMode: String = "system"
|
||||||
|
private var committedBaseUrl: String? = "https://kan.bn/"
|
||||||
|
private var committedApiKey: String? = "existing-key"
|
||||||
|
|
||||||
|
private var draftThemeMode: String = committedThemeMode
|
||||||
|
private var draftBaseUrl: String? = committedBaseUrl
|
||||||
|
private var draftApiKey: String? = committedApiKey
|
||||||
|
|
||||||
|
override fun getThemeMode(): String = committedThemeMode
|
||||||
|
|
||||||
|
override fun saveThemeMode(themeMode: String) {
|
||||||
|
committedThemeMode = themeMode
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBaseUrl(): String? = committedBaseUrl
|
||||||
|
|
||||||
|
override fun saveBaseUrl(url: String) {
|
||||||
|
committedBaseUrl = url
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getApiKey(): String? = committedApiKey
|
||||||
|
|
||||||
|
override fun saveApiKey(apiKey: String) {
|
||||||
|
committedApiKey = apiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearApiKey() {
|
||||||
|
committedApiKey = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getWorkspaceId(): String? = null
|
||||||
|
|
||||||
|
override fun saveWorkspaceId(workspaceId: String) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearBaseUrl() {
|
||||||
|
committedBaseUrl = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearWorkspaceId() {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initializeDraftsFromCommitted() {
|
||||||
|
draftThemeMode = committedThemeMode
|
||||||
|
draftBaseUrl = committedBaseUrl
|
||||||
|
draftApiKey = committedApiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDraftThemeMode(): String = draftThemeMode
|
||||||
|
|
||||||
|
override fun saveDraftThemeMode(themeMode: String) {
|
||||||
|
draftThemeMode = themeMode
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDraftBaseUrl(): String? = draftBaseUrl
|
||||||
|
|
||||||
|
override fun saveDraftBaseUrl(url: String) {
|
||||||
|
draftBaseUrl = url
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDraftApiKey(): String? = draftApiKey
|
||||||
|
|
||||||
|
override fun saveDraftApiKey(apiKey: String) {
|
||||||
|
draftApiKey = apiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resetDraftsFromCommitted() {
|
||||||
|
initializeDraftsFromCommitted()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun syncDraftsToCommitted() {
|
||||||
|
committedThemeMode = draftThemeMode
|
||||||
|
committedBaseUrl = draftBaseUrl
|
||||||
|
committedApiKey = draftApiKey
|
||||||
|
if (failSyncToCommitted) {
|
||||||
|
throw IllegalStateException("sync failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeKanbnApiClient(
|
||||||
|
private val healthCheckResult: AuthResult = AuthResult.Success,
|
||||||
|
) : KanbnApiClient {
|
||||||
|
val healthCheckCalls = mutableListOf<HealthCheckCall>()
|
||||||
|
|
||||||
|
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult {
|
||||||
|
healthCheckCalls += HealthCheckCall(baseUrl = baseUrl, apiKey = apiKey)
|
||||||
|
return healthCheckResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class HealthCheckCall(
|
||||||
|
val baseUrl: String,
|
||||||
|
val apiKey: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user