fix(02-07): use kotlinx.coroutines.test.runTest in commonTest for wasmJs
- Add kotlinx-coroutines-test to commonTest dependencies in composeApp - Refactor AuthSessionTest and LoginViewModelTest from runBlocking to runTest so the wasmJs test target compiles (runBlocking is JVM/Native-only) - App.kt picks up spotless-imposed brace blocks under the auth-gate when - Log pre-existing SecureAuthStateStoreContractTest and ios SecureAuthStateStore ktlint failures to deferred-items.md (out of scope per gsd scope-boundary rule) Rule 3 (blocking): adding the multiplatform test runtime is needed so ./gradlew check on commonTest sources can compile across all KMP targets.
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
# Deferred Items — Phase 02 (auth foundation)
|
||||||
|
|
||||||
|
## Pre-existing failures discovered during 02-07 `./gradlew check`
|
||||||
|
|
||||||
|
### `SecureAuthStateStoreContractTest` (Android JVM unit test) — pre-existing
|
||||||
|
|
||||||
|
- **Tests:** `clearRemovesStoredValue`, `writeOverwritesPreviousValueAndReadReturnsLatest`
|
||||||
|
- **File:** `composeApp/src/androidUnitTest/.../SecureAuthStateStoreContractTest.kt`
|
||||||
|
- **Failure:** `java.lang.IllegalStateException` at construction (Android Keystore not available in
|
||||||
|
plain JVM unit tests under Robolectric-less harness).
|
||||||
|
- **Provenance:** Reproduced on `master` HEAD before any 02-07 change (verified via `git stash`
|
||||||
|
+ run of `./gradlew :composeApp:testDebugUnitTest`).
|
||||||
|
- **Not caused by 02-07.** Source plan was 02-04 (Android secure-store actuals). Likely
|
||||||
|
needs Robolectric or an instrumented (`androidTest`) target. Out of scope for 02-07's
|
||||||
|
UI gate plan.
|
||||||
|
- **Action:** Track for a follow-up Android-test infra task; do not block Phase 02 on it.
|
||||||
|
|
||||||
|
### Spotless `property-naming` lint in `SecureAuthStateStore.ios.kt:L31` — pre-existing
|
||||||
|
|
||||||
|
- Reproduced on `master` HEAD before any 02-07 change.
|
||||||
|
- Source plan: 02-05 (iOS auth actuals).
|
||||||
|
- ktlint expects SCREAMING_SNAKE_CASE for an immutable property; the iOS implementation
|
||||||
|
uses camelCase. Fix is a one-line rename or `suppressLintsFor` annotation.
|
||||||
|
- Out of scope for 02-07; track for follow-up.
|
||||||
@@ -23,12 +23,21 @@ version = "1.0.0"
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "dev.ulfrx.recipe"
|
namespace = "dev.ulfrx.recipe"
|
||||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
compileSdk =
|
||||||
|
libs.versions.android.compileSdk
|
||||||
|
.get()
|
||||||
|
.toInt()
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "dev.ulfrx.recipe"
|
applicationId = "dev.ulfrx.recipe"
|
||||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
minSdk =
|
||||||
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
libs.versions.android.minSdk
|
||||||
|
.get()
|
||||||
|
.toInt()
|
||||||
|
targetSdk =
|
||||||
|
libs.versions.android.targetSdk
|
||||||
|
.get()
|
||||||
|
.toInt()
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
@@ -71,7 +80,9 @@ kotlin {
|
|||||||
isStatic = true
|
isStatic = true
|
||||||
}
|
}
|
||||||
pod("AppAuth") {
|
pod("AppAuth") {
|
||||||
version = libs.versions.appauth.ios.get()
|
version =
|
||||||
|
libs.versions.appauth.ios
|
||||||
|
.get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +115,12 @@ kotlin {
|
|||||||
implementation(libs.multiplatform.settings)
|
implementation(libs.multiplatform.settings)
|
||||||
implementation(libs.multiplatform.settings.coroutines)
|
implementation(libs.multiplatform.settings.coroutines)
|
||||||
}
|
}
|
||||||
|
commonTest.dependencies {
|
||||||
|
// 02-07: kotlinx.coroutines.test.runTest is the multiplatform-safe
|
||||||
|
// alternative to runBlocking (which is JVM/Native-only and breaks the
|
||||||
|
// wasmJs test target). All commonTest coroutine tests use it.
|
||||||
|
implementation(libs.kotlinx.coroutinesTest)
|
||||||
|
}
|
||||||
androidMain.dependencies {
|
androidMain.dependencies {
|
||||||
implementation(libs.compose.uiToolingPreview)
|
implementation(libs.compose.uiToolingPreview)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package dev.ulfrx.recipe
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.ulfrx.recipe.auth.AuthSession
|
import dev.ulfrx.recipe.auth.AuthSession
|
||||||
import dev.ulfrx.recipe.auth.AuthState
|
import dev.ulfrx.recipe.auth.AuthState
|
||||||
import dev.ulfrx.recipe.ui.screens.auth.LoginScreen
|
import dev.ulfrx.recipe.ui.screens.auth.LoginScreen
|
||||||
@@ -36,13 +36,20 @@ fun App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
when (val current = authState) {
|
when (val current = authState) {
|
||||||
AuthState.Loading -> SplashScreen()
|
AuthState.Loading -> {
|
||||||
AuthState.Unauthenticated -> LoginScreen(viewModel = koinViewModel<LoginViewModel>())
|
SplashScreen()
|
||||||
is AuthState.Authenticated ->
|
}
|
||||||
|
|
||||||
|
AuthState.Unauthenticated -> {
|
||||||
|
LoginScreen(viewModel = koinViewModel<LoginViewModel>())
|
||||||
|
}
|
||||||
|
|
||||||
|
is AuthState.Authenticated -> {
|
||||||
PostLoginPlaceholderScreen(
|
PostLoginPlaceholderScreen(
|
||||||
user = current.user,
|
user = current.user,
|
||||||
viewModel = koinViewModel<PostLoginViewModel>(),
|
viewModel = koinViewModel<PostLoginViewModel>(),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dev.ulfrx.recipe.auth
|
package dev.ulfrx.recipe.auth
|
||||||
|
|
||||||
import dev.ulfrx.recipe.shared.dto.User
|
import dev.ulfrx.recipe.shared.dto.User
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertIs
|
import kotlin.test.assertIs
|
||||||
@@ -10,7 +10,7 @@ import kotlin.test.assertNull
|
|||||||
class AuthSessionTest {
|
class AuthSessionTest {
|
||||||
@Test
|
@Test
|
||||||
fun emptyStoreInitializesLoadingToUnauthenticated() {
|
fun emptyStoreInitializesLoadingToUnauthenticated() {
|
||||||
runBlocking {
|
runTest {
|
||||||
val session = newSession(store = FakeAuthStateStore())
|
val session = newSession(store = FakeAuthStateStore())
|
||||||
|
|
||||||
assertIs<AuthState.Loading>(session.state.value)
|
assertIs<AuthState.Loading>(session.state.value)
|
||||||
@@ -23,7 +23,7 @@ class AuthSessionTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun successfulLoginWritesAuthStateJsonFetchesMeAndEmitsAuthenticatedWithNullHouseholdId() {
|
fun successfulLoginWritesAuthStateJsonFetchesMeAndEmitsAuthenticatedWithNullHouseholdId() {
|
||||||
runBlocking {
|
runTest {
|
||||||
val store = FakeAuthStateStore()
|
val store = FakeAuthStateStore()
|
||||||
val oidcClient =
|
val oidcClient =
|
||||||
FakeOidcClient(
|
FakeOidcClient(
|
||||||
@@ -51,7 +51,7 @@ class AuthSessionTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun existingStoreRefreshesBeforeMeAndEmitsAuthenticatedWithoutLogin() {
|
fun existingStoreRefreshesBeforeMeAndEmitsAuthenticatedWithoutLogin() {
|
||||||
runBlocking {
|
runTest {
|
||||||
val store = FakeAuthStateStore(value = "stored-auth-state-json")
|
val store = FakeAuthStateStore(value = "stored-auth-state-json")
|
||||||
val oidcClient =
|
val oidcClient =
|
||||||
FakeOidcClient(
|
FakeOidcClient(
|
||||||
@@ -80,7 +80,7 @@ class AuthSessionTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun refreshInvalidGrantClearsStoreAndEmitsUnauthenticatedWithoutUiError() {
|
fun refreshInvalidGrantClearsStoreAndEmitsUnauthenticatedWithoutUiError() {
|
||||||
runBlocking {
|
runTest {
|
||||||
val store = FakeAuthStateStore(value = "stored-auth-state-json")
|
val store = FakeAuthStateStore(value = "stored-auth-state-json")
|
||||||
val oidcClient =
|
val oidcClient =
|
||||||
FakeOidcClient(
|
FakeOidcClient(
|
||||||
@@ -97,7 +97,7 @@ class AuthSessionTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun refreshAuthErrorClearsStoreAndEmitsUnauthenticatedWithoutUiError() {
|
fun refreshAuthErrorClearsStoreAndEmitsUnauthenticatedWithoutUiError() {
|
||||||
runBlocking {
|
runTest {
|
||||||
val store = FakeAuthStateStore(value = "stored-auth-state-json")
|
val store = FakeAuthStateStore(value = "stored-auth-state-json")
|
||||||
val oidcClient =
|
val oidcClient =
|
||||||
FakeOidcClient(
|
FakeOidcClient(
|
||||||
@@ -114,7 +114,7 @@ class AuthSessionTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun logoutCallsEndSessionThenClearsStoreAndEmitsUnauthenticatedWhenLogoutSucceeds() {
|
fun logoutCallsEndSessionThenClearsStoreAndEmitsUnauthenticatedWhenLogoutSucceeds() {
|
||||||
runBlocking {
|
runTest {
|
||||||
val store = FakeAuthStateStore(value = AUTH_STATE_JSON)
|
val store = FakeAuthStateStore(value = AUTH_STATE_JSON)
|
||||||
val oidcClient = FakeOidcClient()
|
val oidcClient = FakeOidcClient()
|
||||||
val session = newSession(store = store, oidcClient = oidcClient)
|
val session = newSession(store = store, oidcClient = oidcClient)
|
||||||
@@ -129,7 +129,7 @@ class AuthSessionTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun logoutClearsStoreAndEmitsUnauthenticatedEvenWhenEndSessionThrows() {
|
fun logoutClearsStoreAndEmitsUnauthenticatedEvenWhenEndSessionThrows() {
|
||||||
runBlocking {
|
runTest {
|
||||||
val store = FakeAuthStateStore(value = AUTH_STATE_JSON)
|
val store = FakeAuthStateStore(value = AUTH_STATE_JSON)
|
||||||
val oidcClient = FakeOidcClient(logoutThrows = true)
|
val oidcClient = FakeOidcClient(logoutThrows = true)
|
||||||
val session = newSession(store = store, oidcClient = oidcClient)
|
val session = newSession(store = store, oidcClient = oidcClient)
|
||||||
@@ -144,7 +144,7 @@ class AuthSessionTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun loginCancelledMapsToUiRenderableCancelledResult() {
|
fun loginCancelledMapsToUiRenderableCancelledResult() {
|
||||||
runBlocking {
|
runTest {
|
||||||
val store = FakeAuthStateStore()
|
val store = FakeAuthStateStore()
|
||||||
val session =
|
val session =
|
||||||
newSession(
|
newSession(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package dev.ulfrx.recipe.ui.screens.auth
|
package dev.ulfrx.recipe.ui.screens.auth
|
||||||
|
|
||||||
import dev.ulfrx.recipe.auth.AuthLoginResult
|
|
||||||
import dev.ulfrx.recipe.auth.AuthSession
|
import dev.ulfrx.recipe.auth.AuthSession
|
||||||
import dev.ulfrx.recipe.auth.AuthStateStore
|
import dev.ulfrx.recipe.auth.AuthStateStore
|
||||||
import dev.ulfrx.recipe.auth.MeGateway
|
import dev.ulfrx.recipe.auth.MeGateway
|
||||||
@@ -8,10 +7,7 @@ import dev.ulfrx.recipe.auth.OidcClientGateway
|
|||||||
import dev.ulfrx.recipe.auth.OidcResult
|
import dev.ulfrx.recipe.auth.OidcResult
|
||||||
import dev.ulfrx.recipe.shared.dto.User
|
import dev.ulfrx.recipe.shared.dto.User
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.yield
|
|
||||||
import recipe.composeapp.generated.resources.Res
|
import recipe.composeapp.generated.resources.Res
|
||||||
import recipe.composeapp.generated.resources.auth_error_cancelled
|
import recipe.composeapp.generated.resources.auth_error_cancelled
|
||||||
import recipe.composeapp.generated.resources.auth_error_network
|
import recipe.composeapp.generated.resources.auth_error_network
|
||||||
@@ -23,8 +19,8 @@ import kotlin.test.assertTrue
|
|||||||
|
|
||||||
class LoginViewModelTest {
|
class LoginViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun cancelledAuthFailureMapsToCancelledStringResource() {
|
fun cancelledAuthFailureMapsToCancelledStringResource() =
|
||||||
runBlocking {
|
runTest {
|
||||||
val session = newSession(loginResult = OidcResult.Cancelled)
|
val session = newSession(loginResult = OidcResult.Cancelled)
|
||||||
val viewModel = LoginViewModel(session)
|
val viewModel = LoginViewModel(session)
|
||||||
|
|
||||||
@@ -33,11 +29,10 @@ class LoginViewModelTest {
|
|||||||
assertEquals(Res.string.auth_error_cancelled, viewModel.state.value.errorKey)
|
assertEquals(Res.string.auth_error_cancelled, viewModel.state.value.errorKey)
|
||||||
assertEquals(false, viewModel.state.value.isLoading)
|
assertEquals(false, viewModel.state.value.isLoading)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun networkAuthFailureMapsToNetworkStringResource() {
|
fun networkAuthFailureMapsToNetworkStringResource() =
|
||||||
runBlocking {
|
runTest {
|
||||||
val session = newSession(loginResult = OidcResult.NetworkError)
|
val session = newSession(loginResult = OidcResult.NetworkError)
|
||||||
val viewModel = LoginViewModel(session)
|
val viewModel = LoginViewModel(session)
|
||||||
|
|
||||||
@@ -46,11 +41,10 @@ class LoginViewModelTest {
|
|||||||
assertEquals(Res.string.auth_error_network, viewModel.state.value.errorKey)
|
assertEquals(Res.string.auth_error_network, viewModel.state.value.errorKey)
|
||||||
assertEquals(false, viewModel.state.value.isLoading)
|
assertEquals(false, viewModel.state.value.isLoading)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun unknownAuthFailureMapsToUnknownStringResource() {
|
fun unknownAuthFailureMapsToUnknownStringResource() =
|
||||||
runBlocking {
|
runTest {
|
||||||
val session = newSession(loginResult = OidcResult.AuthError("token endpoint failed"))
|
val session = newSession(loginResult = OidcResult.AuthError("token endpoint failed"))
|
||||||
val viewModel = LoginViewModel(session)
|
val viewModel = LoginViewModel(session)
|
||||||
|
|
||||||
@@ -59,11 +53,10 @@ class LoginViewModelTest {
|
|||||||
assertEquals(Res.string.auth_error_unknown, viewModel.state.value.errorKey)
|
assertEquals(Res.string.auth_error_unknown, viewModel.state.value.errorKey)
|
||||||
assertEquals(false, viewModel.state.value.isLoading)
|
assertEquals(false, viewModel.state.value.isLoading)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun successClearsErrorAndStopsLoading() {
|
fun successClearsErrorAndStopsLoading() =
|
||||||
runBlocking {
|
runTest {
|
||||||
val session =
|
val session =
|
||||||
newSession(
|
newSession(
|
||||||
loginResult =
|
loginResult =
|
||||||
@@ -81,77 +74,44 @@ class LoginViewModelTest {
|
|||||||
assertNull(viewModel.state.value.errorKey)
|
assertNull(viewModel.state.value.errorKey)
|
||||||
assertEquals(false, viewModel.state.value.isLoading)
|
assertEquals(false, viewModel.state.value.isLoading)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun startingNewSignInClearsPreviousErrorAndSetsLoading() {
|
fun startingNewSignInClearsPreviousErrorAndSetsLoading() =
|
||||||
runBlocking {
|
runTest {
|
||||||
// First login fails with cancelled to seed an error.
|
// Queue: first login resolves Cancelled to seed an inline error.
|
||||||
val firstOidc = FakeOidcClient(loginResult = OidcResult.Cancelled)
|
// Second login awaits a gate so we can synchronously observe the
|
||||||
val session = AuthSession(firstOidc, FakeAuthStateStore(), FakeMeClient(USER))
|
// "loading=true, error=null" intermediate state contract from UI-SPEC.
|
||||||
|
val gate = CompletableDeferred<OidcResult>()
|
||||||
|
val queue = mutableListOf<OidcResult>(OidcResult.Cancelled)
|
||||||
|
val oidc =
|
||||||
|
object : OidcClientGateway {
|
||||||
|
override suspend fun login(): OidcResult = if (queue.isNotEmpty()) queue.removeAt(0) else gate.await()
|
||||||
|
|
||||||
|
override suspend fun refresh(authStateJson: String): OidcResult = OidcResult.AuthError("not used")
|
||||||
|
|
||||||
|
override suspend fun logout(authStateJson: String) {}
|
||||||
|
}
|
||||||
|
val session = AuthSession(oidc, FakeAuthStateStore(), FakeMeClient(USER))
|
||||||
val viewModel = LoginViewModel(session)
|
val viewModel = LoginViewModel(session)
|
||||||
|
|
||||||
|
// First attempt: error seeded.
|
||||||
viewModel.onSignInClick().join()
|
viewModel.onSignInClick().join()
|
||||||
assertEquals(Res.string.auth_error_cancelled, viewModel.state.value.errorKey)
|
assertEquals(Res.string.auth_error_cancelled, viewModel.state.value.errorKey)
|
||||||
|
|
||||||
// Second login: gate the result so we can observe loading=true and error cleared.
|
// Second attempt: launching the job sets loading=true + clears error
|
||||||
val gate = CompletableDeferred<OidcResult>()
|
// BEFORE suspending. onSignInClick() does that synchronously before
|
||||||
val gatedOidc =
|
// returning the launched Job, so we can assert immediately.
|
||||||
object : OidcClientGateway {
|
val job = viewModel.onSignInClick()
|
||||||
override suspend fun login(): OidcResult = gate.await()
|
assertTrue(viewModel.state.value.isLoading)
|
||||||
|
assertNull(viewModel.state.value.errorKey)
|
||||||
override suspend fun refresh(authStateJson: String): OidcResult =
|
|
||||||
OidcResult.AuthError("not used")
|
|
||||||
|
|
||||||
override suspend fun logout(authStateJson: String) {}
|
|
||||||
}
|
|
||||||
val gatedSession = AuthSession(gatedOidc, FakeAuthStateStore(), FakeMeClient(USER))
|
|
||||||
val gatedViewModel = LoginViewModel(gatedSession)
|
|
||||||
|
|
||||||
// Seed an error on the gated VM by directly priming via the cancelled session.
|
|
||||||
// Simpler: assert behavior with a single VM that has both attempts.
|
|
||||||
val singleOidcQueue =
|
|
||||||
mutableListOf<OidcResult>(
|
|
||||||
OidcResult.Cancelled,
|
|
||||||
)
|
|
||||||
val singleOidc =
|
|
||||||
object : OidcClientGateway {
|
|
||||||
override suspend fun login(): OidcResult {
|
|
||||||
// First call returns immediately; subsequent calls await the gate.
|
|
||||||
return if (singleOidcQueue.isNotEmpty()) {
|
|
||||||
singleOidcQueue.removeAt(0)
|
|
||||||
} else {
|
|
||||||
gate.await()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun refresh(authStateJson: String): OidcResult =
|
|
||||||
OidcResult.AuthError("not used")
|
|
||||||
|
|
||||||
override suspend fun logout(authStateJson: String) {}
|
|
||||||
}
|
|
||||||
val singleSession = AuthSession(singleOidc, FakeAuthStateStore(), FakeMeClient(USER))
|
|
||||||
val singleVm = LoginViewModel(singleSession)
|
|
||||||
singleVm.onSignInClick().join()
|
|
||||||
assertEquals(Res.string.auth_error_cancelled, singleVm.state.value.errorKey)
|
|
||||||
|
|
||||||
// Now start the second login and observe immediate loading + cleared error.
|
|
||||||
val job =
|
|
||||||
async(Dispatchers.Unconfined) {
|
|
||||||
singleVm.onSignInClick().join()
|
|
||||||
}
|
|
||||||
// Yield to let the VM enter the loading state before await suspends.
|
|
||||||
yield()
|
|
||||||
assertTrue(singleVm.state.value.isLoading)
|
|
||||||
assertNull(singleVm.state.value.errorKey)
|
|
||||||
|
|
||||||
|
// Release the gate; the second login also returns Cancelled.
|
||||||
gate.complete(OidcResult.Cancelled)
|
gate.complete(OidcResult.Cancelled)
|
||||||
job.await()
|
job.join()
|
||||||
|
|
||||||
// After completion, loading is false and the new (still cancelled) error is set.
|
assertEquals(false, viewModel.state.value.isLoading)
|
||||||
assertEquals(false, singleVm.state.value.isLoading)
|
assertEquals(Res.string.auth_error_cancelled, viewModel.state.value.errorKey)
|
||||||
assertEquals(Res.string.auth_error_cancelled, singleVm.state.value.errorKey)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun newSession(
|
private fun newSession(
|
||||||
loginResult: OidcResult,
|
loginResult: OidcResult,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMul
|
|||||||
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
|
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
|
||||||
compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" }
|
compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" }
|
||||||
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||||
|
kotlinx-coroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
||||||
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
||||||
ktor-serverCore = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" }
|
ktor-serverCore = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" }
|
||||||
ktor-serverNetty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }
|
ktor-serverNetty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }
|
||||||
|
|||||||
Reference in New Issue
Block a user