From f8dbd31c117d3daf5978114eb175f6d5769dc531 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Apr 2023 21:15:46 +0200 Subject: [PATCH] Test for Oidc --- .../features/login/impl/oidc/OidcPresenter.kt | 28 ++-- .../features/login/impl/oidc/OidcUrlParser.kt | 2 +- .../login/impl/root/LoginRootPresenter.kt | 6 +- .../changeserver/ChangeServerPresenterTest.kt | 4 +- .../login/impl/oidc/OidcPresenterTest.kt | 145 ++++++++++++++++++ .../login/impl/oidc/OidcUrlParserTest.kt | 59 +++++++ .../login/impl/root/LoginRootPresenterTest.kt | 116 +++++++++++++- .../android/libraries/matrix/test/TestData.kt | 3 +- .../test/auth/FakeAuthenticationService.kt | 26 ++++ 9 files changed, 365 insertions(+), 24 deletions(-) create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt index 76faa090d1..1c7debfd6f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt @@ -51,25 +51,27 @@ class OidcPresenter @AssistedInject constructor( fun handleCancel() { requestState = Async.Loading() localCoroutineScope.launch { - requestState = try { - authenticationService.cancelOidcLogin() - // Then go back - Async.Success(Unit) - } catch (throwable: Throwable) { - Async.Failure(throwable) - } + authenticationService.cancelOidcLogin() + .fold( + onSuccess = { + // Then go back + requestState = Async.Success(Unit) + }, + onFailure = { + requestState = Async.Failure(it) + } + ) } } fun handleSuccess(url: String) { requestState = Async.Loading() localCoroutineScope.launch { - try { - authenticationService.loginWithOidc(url) - // Then the node tree will be updated, there is nothing to do - } catch (throwable: Throwable) { - requestState = Async.Failure(throwable) - } + authenticationService.loginWithOidc(url) + .onFailure { + requestState = Async.Failure(it) + } + // On success, the node tree will be updated, there is nothing to do } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt index eb55368f47..a28e63ede8 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt @@ -32,7 +32,7 @@ class OidcUrlParser { * Return a OidcAction, or null if the url is not a OidcUrl */ fun parse(url: String): OidcAction? { - if (!url.startsWith(OidcConfig.redirectUri)) return null + if (url.startsWith(OidcConfig.redirectUri).not()) return null if (url.contains("error=access_denied")) return OidcAction.GoBack if (url.contains("code=")) return OidcAction.Success(url) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt index 0d1b8de7fd..cf00075c4e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt @@ -97,10 +97,12 @@ class LoginRootPresenter @Inject constructor( homeserver: String, state: MutableState>, ) = launch { - state.value = Async.Loading() suspend { authenticationService.setHomeserver(homeserver) - authenticationService.getHomeserverDetails().value!! + .map { + authenticationService.getHomeserverDetails().value!! + } + .getOrThrow() }.execute(state) } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt index cc216cc9dd..a30bd9449c 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt @@ -20,9 +20,9 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.login.impl.util.LoginConstants import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.test.A_HOMESERVER -import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2 import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService @@ -39,7 +39,7 @@ class ChangeServerPresenterTest { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.homeserver).isEqualTo(A_HOMESERVER_URL) + assertThat(initialState.homeserver).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL) assertThat(initialState.submitEnabled).isTrue() } } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt new file mode 100644 index 0000000000..0fba87f2e3 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.login.impl.oidc + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.test.A_THROWABLE +import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA +import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class OidcPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = OidcPresenter( + A_OIDC_DATA, + FakeAuthenticationService(), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.oidcDetails).isEqualTo(A_OIDC_DATA) + assertThat(initialState.requestState).isEqualTo(Async.Uninitialized) + } + } + + @Test + fun `present - go back`() = runTest { + val presenter = OidcPresenter( + A_OIDC_DATA, + FakeAuthenticationService(), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(OidcEvents.Cancel) + val loadingState = awaitItem() + assertThat(loadingState.requestState).isEqualTo(Async.Loading()) + val finalState = awaitItem() + assertThat(finalState.requestState).isEqualTo(Async.Success(Unit)) + } + } + + @Test + fun `present - go back with failure`() = runTest { + val authenticationService = FakeAuthenticationService() + val presenter = OidcPresenter( + A_OIDC_DATA, + authenticationService, + ) + authenticationService.givenOidcCancelError(A_THROWABLE) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(OidcEvents.Cancel) + val loadingState = awaitItem() + assertThat(loadingState.requestState).isEqualTo(Async.Loading()) + val finalState = awaitItem() + assertThat(finalState.requestState).isEqualTo(Async.Failure(A_THROWABLE)) + // Note: in real life I do not think this can happen, and the app should not block the user. + } + } + + @Test + fun `present - user cancels from webview`() = runTest { + val presenter = OidcPresenter( + A_OIDC_DATA, + FakeAuthenticationService(), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.GoBack)) + val loadingState = awaitItem() + assertThat(loadingState.requestState).isEqualTo(Async.Loading()) + val finalState = awaitItem() + assertThat(finalState.requestState).isEqualTo(Async.Success(Unit)) + } + } + + @Test + fun `present - login success`() = runTest { + val presenter = OidcPresenter( + A_OIDC_DATA, + FakeAuthenticationService(), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL"))) + val loadingState = awaitItem() + assertThat(loadingState.requestState).isEqualTo(Async.Loading()) + // In this case, no success, the session is created and the node get destroyed. + } + } + + @Test + fun `present - login error`() = runTest { + val authenticationService = FakeAuthenticationService() + val presenter = OidcPresenter( + A_OIDC_DATA, + authenticationService, + ) + authenticationService.givenLoginError(A_THROWABLE) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL"))) + val loadingState = awaitItem() + assertThat(loadingState.requestState).isEqualTo(Async.Loading()) + val errorState = awaitItem() + assertThat(errorState.requestState).isEqualTo(Async.Failure(A_THROWABLE)) + errorState.eventSink.invoke(OidcEvents.ClearError) + val finalState = awaitItem() + assertThat(finalState.requestState).isEqualTo(Async.Uninitialized) + } + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt new file mode 100644 index 0000000000..d13f24c885 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.login.impl.oidc + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.auth.OidcConfig +import org.junit.Assert +import org.junit.Test + +class OidcUrlParserTest { + @Test + fun `test empty url`() { + val sut = OidcUrlParser() + assertThat(sut.parse("")).isNull() + } + + @Test + fun `test regular url`() { + val sut = OidcUrlParser() + assertThat(sut.parse("https://matrix.org")).isNull() + } + + @Test + fun `test cancel url`() { + val sut = OidcUrlParser() + val aCancelUrl = OidcConfig.redirectUri + "?error=access_denied&state=IFF1UETGye2ZA8pO" + assertThat(sut.parse(aCancelUrl)).isEqualTo(OidcAction.GoBack) + } + + @Test + fun `test success url`() { + val sut = OidcUrlParser() + val aSuccessUrl = OidcConfig.redirectUri + "?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" + assertThat(sut.parse(aSuccessUrl)).isEqualTo(OidcAction.Success(aSuccessUrl)) + } + + @Test + fun `test unknown url`() { + val sut = OidcUrlParser() + val anUnknownUrl = OidcConfig.redirectUri + "?state=IFF1UETGye2ZA8pO&goat=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" + Assert.assertThrows(IllegalStateException::class.java) { + assertThat(sut.parse(anUnknownUrl)) + } + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt index be940feacf..c072656e36 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt @@ -20,11 +20,16 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.login.impl.util.LoginConstants +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.test.A_HOMESERVER +import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC import io.element.android.libraries.matrix.test.A_PASSWORD import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import kotlinx.coroutines.test.runTest import org.junit.Test @@ -39,18 +44,79 @@ class LoginRootPresenterTest { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.homeserverDetails).isEqualTo(A_HOMESERVER) + assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL) + assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized) assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn) assertThat(initialState.formState).isEqualTo(LoginFormState.Default) assertThat(initialState.submitEnabled).isFalse() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - initial state server load`() = runTest { + val authenticationService = FakeAuthenticationService() + val presenter = LoginRootPresenter( + authenticationService, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL) + assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized) + assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn) + assertThat(initialState.formState).isEqualTo(LoginFormState.Default) + assertThat(initialState.submitEnabled).isFalse() + val loadingState = awaitItem() + assertThat(loadingState.homeserverDetails).isEqualTo(Async.Loading()) + authenticationService.givenHomeserver(A_HOMESERVER) + skipItems(1) + val loadedState = awaitItem() + assertThat(loadedState.homeserverDetails).isEqualTo(Async.Success(A_HOMESERVER)) + } + } + + @Test + fun `present - initial state server load error and retry`() = runTest { + val authenticationService = FakeAuthenticationService() + val presenter = LoginRootPresenter( + authenticationService, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL) + assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized) + assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn) + assertThat(initialState.formState).isEqualTo(LoginFormState.Default) + assertThat(initialState.submitEnabled).isFalse() + val loadingState = awaitItem() + assertThat(loadingState.homeserverDetails).isEqualTo(Async.Loading()) + val aThrowable = Throwable("Error") + authenticationService.givenChangeServerError(aThrowable) + val errorState = awaitItem() + assertThat(errorState.homeserverDetails).isEqualTo(Async.Failure(aThrowable)) + // Retry + errorState.eventSink.invoke(LoginRootEvents.RetryFetchServerInfo) + val loadingState2 = awaitItem() + assertThat(loadingState2.homeserverDetails).isEqualTo(Async.Loading()) + authenticationService.givenChangeServerError(null) + authenticationService.givenHomeserver(A_HOMESERVER) + skipItems(1) + val loadedState = awaitItem() + assertThat(loadedState.homeserverDetails).isEqualTo(Async.Success(A_HOMESERVER)) } } @Test fun `present - enter login and password`() = runTest { + val authenticationService = FakeAuthenticationService() val presenter = LoginRootPresenter( - FakeAuthenticationService(), + authenticationService, ) + authenticationService.givenHomeserver(A_HOMESERVER) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -67,10 +133,49 @@ class LoginRootPresenterTest { } @Test - fun `present - submit`() = runTest { + fun `present - oidc login`() = runTest { + val authenticationService = FakeAuthenticationService() val presenter = LoginRootPresenter( - FakeAuthenticationService(), + authenticationService, ) + authenticationService.givenHomeserver(A_HOMESERVER_OIDC) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.submitEnabled).isTrue() + initialState.eventSink.invoke(LoginRootEvents.Submit) + val oidcState = awaitItem() + assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.OidcStarted(A_OIDC_DATA)) + } + } + + @Test + fun `present - oidc login error`() = runTest { + val authenticationService = FakeAuthenticationService() + val presenter = LoginRootPresenter( + authenticationService, + ) + authenticationService.givenHomeserver(A_HOMESERVER_OIDC) + authenticationService.givenOidcError(A_THROWABLE) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.submitEnabled).isTrue() + initialState.eventSink.invoke(LoginRootEvents.Submit) + val oidcState = awaitItem() + assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE)) + } + } + + @Test + fun `present - submit`() = runTest { + val authenticationService = FakeAuthenticationService() + val presenter = LoginRootPresenter( + authenticationService, + ) + authenticationService.givenHomeserver(A_HOMESERVER) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -93,6 +198,7 @@ class LoginRootPresenterTest { val presenter = LoginRootPresenter( authenticationService, ) + authenticationService.givenHomeserver(A_HOMESERVER) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -116,11 +222,11 @@ class LoginRootPresenterTest { val presenter = LoginRootPresenter( authenticationService, ) + authenticationService.givenHomeserver(A_HOMESERVER) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - // Submit will return an error authenticationService.givenLoginError(A_THROWABLE) initialState.eventSink(LoginRootEvents.Submit) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index d9322ed2ae..be9f41dd6c 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -47,7 +47,8 @@ const val ANOTHER_MESSAGE = "Hello universe!" const val A_HOMESERVER_URL = "matrix.org" const val A_HOMESERVER_URL_2 = "matrix-client.org" -val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, true, null) +val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidc = false) +val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidc = true) const val AN_AVATAR_URL = "mxc://data" diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt index 886eac95ef..5b6b8b1e28 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.test.auth import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails +import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.test.A_USER_ID import kotlinx.coroutines.delay @@ -27,8 +28,12 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOf +val A_OIDC_DATA = OidcDetails(url = "a-url") + class FakeAuthenticationService : MatrixAuthenticationService { private var homeserver = MutableStateFlow(null) + private var oidcError: Throwable? = null + private var oidcCancelError: Throwable? = null private var loginError: Throwable? = null private var changeServerError: Throwable? = null @@ -62,6 +67,27 @@ class FakeAuthenticationService : MatrixAuthenticationService { return loginError?.let { Result.failure(it) } ?: Result.success(A_USER_ID) } + override suspend fun getOidcUrl(): Result { + return oidcError?.let { Result.failure(it) } ?: Result.success(A_OIDC_DATA) + } + + override suspend fun cancelOidcLogin(): Result { + return oidcCancelError?.let { Result.failure(it) } ?: Result.success(Unit) + } + + override suspend fun loginWithOidc(callbackUrl: String): Result { + delay(100) + return loginError?.let { Result.failure(it) } ?: Result.success(A_USER_ID) + } + + fun givenOidcError(throwable: Throwable?) { + oidcError = throwable + } + + fun givenOidcCancelError(throwable: Throwable?) { + oidcCancelError = throwable + } + fun givenLoginError(throwable: Throwable?) { loginError = throwable }