Merge pull request #295 from ouchadam/tech/login-state
Tech/Converting login view model to state/reducer pattern
This commit is contained in:
commit
3d98a979fe
|
@ -1 +1 @@
|
|||
Subproject commit 8139eaaf57cee4ce9d0616d617e8aff7eb1480e3
|
||||
Subproject commit cdf3e1bffba4b69dd8f752c6cc7588b0e89a17af
|
|
@ -4,7 +4,7 @@ import app.dapk.st.core.ProvidableModule
|
|||
import app.dapk.st.directory.state.DirectoryState
|
||||
import app.dapk.st.domain.StoreModule
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.login.LoginViewModel
|
||||
import app.dapk.st.login.state.LoginState
|
||||
import app.dapk.st.profile.state.ProfileState
|
||||
|
||||
class HomeModule(
|
||||
|
@ -13,7 +13,7 @@ class HomeModule(
|
|||
val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
||||
) : ProvidableModule {
|
||||
|
||||
internal fun homeViewModel(directory: DirectoryState, login: LoginViewModel, profile: ProfileState): HomeViewModel {
|
||||
internal fun homeViewModel(directory: DirectoryState, login: LoginState, profile: ProfileState): HomeViewModel {
|
||||
return HomeViewModel(
|
||||
chatEngine,
|
||||
directory,
|
||||
|
|
|
@ -7,9 +7,7 @@ import app.dapk.st.directory.state.DirectoryState
|
|||
import app.dapk.st.domain.StoreCleaner
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.home.HomeScreenState.*
|
||||
import app.dapk.st.login.LoginViewModel
|
||||
import app.dapk.st.matrix.common.CredentialsStore
|
||||
import app.dapk.st.matrix.common.isSignedIn
|
||||
import app.dapk.st.login.state.LoginState
|
||||
import app.dapk.st.profile.state.ProfileAction
|
||||
import app.dapk.st.profile.state.ProfileState
|
||||
import app.dapk.st.viewmodel.DapkViewModel
|
||||
|
@ -22,7 +20,7 @@ import kotlinx.coroutines.launch
|
|||
internal class HomeViewModel(
|
||||
private val chatEngine: ChatEngine,
|
||||
private val directoryState: DirectoryState,
|
||||
private val loginViewModel: LoginViewModel,
|
||||
private val loginState: LoginState,
|
||||
private val profileState: ProfileState,
|
||||
private val cacheCleaner: StoreCleaner,
|
||||
private val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
||||
|
@ -33,7 +31,7 @@ internal class HomeViewModel(
|
|||
private var listenForInvitesJob: Job? = null
|
||||
|
||||
fun directory() = directoryState
|
||||
fun login() = loginViewModel
|
||||
fun login() = loginState
|
||||
fun profile() = profileState
|
||||
|
||||
fun start() {
|
||||
|
|
|
@ -23,10 +23,10 @@ import kotlinx.coroutines.flow.onEach
|
|||
|
||||
class MainActivity : DapkActivity() {
|
||||
|
||||
private val directoryViewModel by state { module<DirectoryModule>().directoryState() }
|
||||
private val loginViewModel by viewModel { module<LoginModule>().loginViewModel() }
|
||||
private val profileViewModel by state { module<ProfileModule>().profileState() }
|
||||
private val homeViewModel by viewModel { module<HomeModule>().homeViewModel(directoryViewModel, loginViewModel, profileViewModel) }
|
||||
private val directoryState by state { module<DirectoryModule>().directoryState() }
|
||||
private val loginState by state { module<LoginModule>().loginState() }
|
||||
private val profileState by state { module<ProfileModule>().profileState() }
|
||||
private val homeViewModel by viewModel { module<HomeModule>().homeViewModel(directoryState, loginState, profileState) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
|
@ -8,9 +8,17 @@ android {
|
|||
|
||||
dependencies {
|
||||
implementation "chat-engine:chat-engine"
|
||||
implementation 'screen-state:screen-android'
|
||||
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:push")
|
||||
implementation project(":domains:android:viewmodel")
|
||||
implementation project(":design-library")
|
||||
implementation project(":core")
|
||||
|
||||
kotlinTest(it)
|
||||
|
||||
testImplementation 'screen-state:state-test'
|
||||
testImplementation 'chat-engine:chat-engine-test'
|
||||
androidImportFixturesWorkaround(project, project(":core"))
|
||||
}
|
|
@ -3,7 +3,11 @@ package app.dapk.st.login
|
|||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.login.state.LoginState
|
||||
import app.dapk.st.login.state.LoginUseCase
|
||||
import app.dapk.st.login.state.loginReducer
|
||||
import app.dapk.st.push.PushModule
|
||||
import app.dapk.st.state.createStateViewModel
|
||||
|
||||
class LoginModule(
|
||||
private val chatEngine: ChatEngine,
|
||||
|
@ -11,7 +15,10 @@ class LoginModule(
|
|||
private val errorTracker: ErrorTracker,
|
||||
) : ProvidableModule {
|
||||
|
||||
fun loginViewModel(): LoginViewModel {
|
||||
return LoginViewModel(chatEngine, pushModule.pushTokenRegistrar(), errorTracker)
|
||||
fun loginState(): LoginState {
|
||||
return createStateViewModel {
|
||||
val loginUseCase = LoginUseCase(chatEngine, pushModule.pushTokenRegistrars(), errorTracker)
|
||||
loginReducer(loginUseCase, it)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,15 +33,18 @@ import androidx.compose.ui.unit.sp
|
|||
import app.dapk.st.core.StartObserving
|
||||
import app.dapk.st.core.components.CenteredLoading
|
||||
import app.dapk.st.design.components.GenericError
|
||||
import app.dapk.st.login.LoginEvent.LoginComplete
|
||||
import app.dapk.st.login.LoginScreenState.*
|
||||
import app.dapk.st.login.state.LoginAction
|
||||
import app.dapk.st.login.state.LoginEvent.LoginComplete
|
||||
import app.dapk.st.login.state.LoginEvent.WellKnownMissing
|
||||
import app.dapk.st.login.state.LoginScreenState
|
||||
import app.dapk.st.login.state.LoginState
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) {
|
||||
loginViewModel.ObserveEvents(onLoggedIn)
|
||||
fun LoginScreen(loginState: LoginState, onLoggedIn: () -> Unit) {
|
||||
loginState.ObserveEvents(onLoggedIn)
|
||||
LaunchedEffect(true) {
|
||||
loginViewModel.start()
|
||||
loginState.dispatch(LoginAction.ComponentLifecycle.Visible)
|
||||
}
|
||||
|
||||
var userName by rememberSaveable { mutableStateOf("") }
|
||||
|
@ -49,11 +52,12 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) {
|
|||
var serverUrl by rememberSaveable { mutableStateOf("") }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
when (val state = loginViewModel.state) {
|
||||
is Error -> GenericError(cause = state.cause, action = { loginViewModel.start() })
|
||||
Loading -> CenteredLoading()
|
||||
when (val content = loginState.current.content) {
|
||||
is LoginScreenState.Content.Error -> GenericError(cause = content.cause, action = { loginState.dispatch(LoginAction.ComponentLifecycle.Visible) })
|
||||
LoginScreenState.Content.Loading -> CenteredLoading()
|
||||
|
||||
is Content ->
|
||||
is LoginScreenState.Content.Idle -> {
|
||||
val showServerUrl = loginState.current.showServerUrl
|
||||
Row {
|
||||
Spacer(modifier = Modifier.weight(0.1f))
|
||||
Column(
|
||||
|
@ -88,7 +92,7 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) {
|
|||
keyboardOptions = KeyboardOptions(autoCorrect = false, keyboardType = KeyboardType.Email, imeAction = ImeAction.Next)
|
||||
)
|
||||
|
||||
val canDoLoginAttempt = if (state.showServerUrl) {
|
||||
val canDoLoginAttempt = if (showServerUrl) {
|
||||
userName.isNotEmpty() && password.isNotEmpty() && serverUrl.isNotEmpty()
|
||||
} else {
|
||||
userName.isNotEmpty() && password.isNotEmpty()
|
||||
|
@ -106,12 +110,12 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) {
|
|||
Icon(imageVector = Icons.Outlined.Lock, contentDescription = null)
|
||||
},
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { loginViewModel.login(userName, password, serverUrl) },
|
||||
onDone = { loginState.dispatch(LoginAction.Login(userName, password, serverUrl)) },
|
||||
onNext = { focusManager.moveFocus(FocusDirection.Down) },
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
imeAction = ImeAction.Done.takeIf { canDoLoginAttempt } ?: ImeAction.Next.takeIf { state.showServerUrl } ?: ImeAction.None,
|
||||
imeAction = ImeAction.Done.takeIf { canDoLoginAttempt } ?: ImeAction.Next.takeIf { showServerUrl } ?: ImeAction.None,
|
||||
keyboardType = KeyboardType.Password
|
||||
),
|
||||
visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
|
@ -123,7 +127,7 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) {
|
|||
}
|
||||
)
|
||||
|
||||
if (state.showServerUrl) {
|
||||
if (showServerUrl) {
|
||||
TextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = serverUrl,
|
||||
|
@ -133,7 +137,7 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) {
|
|||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Default.Web, contentDescription = null)
|
||||
},
|
||||
keyboardActions = KeyboardActions(onDone = { loginViewModel.login(userName, password, serverUrl) }),
|
||||
keyboardActions = KeyboardActions(onDone = { loginState.dispatch(LoginAction.Login(userName, password, serverUrl)) }),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
imeAction = ImeAction.Done.takeIf { canDoLoginAttempt } ?: ImeAction.None,
|
||||
|
@ -148,7 +152,7 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) {
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
keyboardController?.hide()
|
||||
loginViewModel.login(userName, password, serverUrl)
|
||||
loginState.dispatch(LoginAction.Login(userName, password, serverUrl))
|
||||
},
|
||||
enabled = canDoLoginAttempt
|
||||
) {
|
||||
|
@ -157,6 +161,7 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) {
|
|||
}
|
||||
Spacer(modifier = Modifier.weight(0.1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -183,13 +188,13 @@ private fun Modifier.autofill(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun LoginViewModel.ObserveEvents(onLoggedIn: () -> Unit) {
|
||||
private fun LoginState.ObserveEvents(onLoggedIn: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
StartObserving {
|
||||
this@ObserveEvents.events.launch {
|
||||
when (it) {
|
||||
LoginComplete -> onLoggedIn()
|
||||
LoginEvent.WellKnownMissing -> {
|
||||
WellKnownMissing -> {
|
||||
Toast.makeText(context, "Couldn't find the homeserver, please enter the server URL", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
package app.dapk.st.login
|
||||
|
||||
sealed interface LoginScreenState {
|
||||
|
||||
data class Content(val showServerUrl: Boolean) : LoginScreenState
|
||||
object Loading : LoginScreenState
|
||||
data class Error(val cause: Throwable) : LoginScreenState
|
||||
}
|
||||
|
||||
sealed interface LoginEvent {
|
||||
object LoginComplete : LoginEvent
|
||||
object WellKnownMissing : LoginEvent
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
package app.dapk.st.login
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
import app.dapk.st.core.logP
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.engine.LoginRequest
|
||||
import app.dapk.st.engine.LoginResult
|
||||
import app.dapk.st.login.LoginEvent.LoginComplete
|
||||
import app.dapk.st.login.LoginScreenState.*
|
||||
import app.dapk.st.push.PushTokenRegistrar
|
||||
import app.dapk.st.viewmodel.DapkViewModel
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class LoginViewModel(
|
||||
private val chatEngine: ChatEngine,
|
||||
private val pushTokenRegistrar: PushTokenRegistrar,
|
||||
private val errorTracker: ErrorTracker,
|
||||
) : DapkViewModel<LoginScreenState, LoginEvent>(
|
||||
initialState = Content(showServerUrl = false)
|
||||
) {
|
||||
|
||||
private var previousState: LoginScreenState? = null
|
||||
|
||||
fun login(userName: String, password: String, serverUrl: String?) {
|
||||
state = Loading
|
||||
viewModelScope.launch {
|
||||
logP("login") {
|
||||
when (val result = chatEngine.login(LoginRequest(userName, password, serverUrl.takeIfNotEmpty()))) {
|
||||
is LoginResult.Success -> {
|
||||
runCatching {
|
||||
listOf(
|
||||
async { pushTokenRegistrar.registerCurrentToken() },
|
||||
async { preloadMe() },
|
||||
).awaitAll()
|
||||
}
|
||||
_events.tryEmit(LoginComplete)
|
||||
}
|
||||
|
||||
is LoginResult.Error -> {
|
||||
errorTracker.track(result.cause)
|
||||
state = Error(result.cause)
|
||||
}
|
||||
|
||||
LoginResult.MissingWellKnown -> {
|
||||
_events.tryEmit(LoginEvent.WellKnownMissing)
|
||||
state = Content(showServerUrl = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun preloadMe() = chatEngine.me(forceRefresh = false)
|
||||
|
||||
fun start() {
|
||||
val showServerUrl = previousState?.let { it is Content && it.showServerUrl } ?: false
|
||||
state = Content(showServerUrl = showServerUrl)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun String?.takeIfNotEmpty() = this?.takeIf { it.isNotEmpty() }
|
|
@ -0,0 +1,15 @@
|
|||
package app.dapk.st.login.state
|
||||
|
||||
import app.dapk.state.Action
|
||||
|
||||
sealed interface LoginAction : Action {
|
||||
|
||||
sealed interface ComponentLifecycle : LoginAction {
|
||||
object Visible : ComponentLifecycle
|
||||
}
|
||||
|
||||
data class Login(val userName: String, val password: String, val serverUrl: String?) : LoginAction
|
||||
|
||||
data class UpdateContent(val content: LoginScreenState.Content) : LoginAction
|
||||
data class UpdateState(val state: LoginScreenState) : LoginAction
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package app.dapk.st.login.state
|
||||
|
||||
import app.dapk.st.core.logP
|
||||
import app.dapk.st.engine.LoginRequest
|
||||
import app.dapk.st.engine.LoginResult
|
||||
import app.dapk.state.async
|
||||
import app.dapk.state.change
|
||||
import app.dapk.state.createReducer
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
fun loginReducer(
|
||||
loginUseCase: LoginUseCase,
|
||||
eventEmitter: suspend (LoginEvent) -> Unit,
|
||||
) = createReducer(
|
||||
initialState = LoginScreenState(showServerUrl = false, content = LoginScreenState.Content.Idle),
|
||||
|
||||
change(LoginAction.ComponentLifecycle.Visible::class) { _, state ->
|
||||
LoginScreenState(state.showServerUrl, content = LoginScreenState.Content.Idle)
|
||||
},
|
||||
|
||||
change(LoginAction.UpdateContent::class) { action, state -> state.copy(content = action.content) },
|
||||
|
||||
change(LoginAction.UpdateState::class) { action, _ -> action.state },
|
||||
|
||||
async(LoginAction.Login::class) { action ->
|
||||
coroutineScope.launch {
|
||||
logP("login") {
|
||||
dispatch(LoginAction.UpdateContent(LoginScreenState.Content.Loading))
|
||||
val request = LoginRequest(action.userName, action.password, action.serverUrl.takeIfNotEmpty())
|
||||
|
||||
when (val result = loginUseCase.login(request)) {
|
||||
is LoginResult.Error -> dispatch(LoginAction.UpdateContent(LoginScreenState.Content.Error(result.cause)))
|
||||
|
||||
LoginResult.MissingWellKnown -> {
|
||||
eventEmitter.invoke(LoginEvent.WellKnownMissing)
|
||||
dispatch(LoginAction.UpdateState(LoginScreenState(showServerUrl = true, content = LoginScreenState.Content.Idle)))
|
||||
}
|
||||
|
||||
is LoginResult.Success -> eventEmitter.invoke(LoginEvent.LoginComplete)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private fun String?.takeIfNotEmpty() = this?.takeIf { it.isNotEmpty() }
|
|
@ -0,0 +1,22 @@
|
|||
package app.dapk.st.login.state
|
||||
|
||||
import app.dapk.st.state.State
|
||||
|
||||
typealias LoginState = State<LoginScreenState, LoginEvent>
|
||||
|
||||
data class LoginScreenState(
|
||||
val showServerUrl: Boolean,
|
||||
val content: Content,
|
||||
) {
|
||||
|
||||
sealed interface Content {
|
||||
object Idle : Content
|
||||
object Loading : Content
|
||||
data class Error(val cause: Throwable) : Content
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface LoginEvent {
|
||||
object LoginComplete : LoginEvent
|
||||
object WellKnownMissing : LoginEvent
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package app.dapk.st.login.state
|
||||
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
import app.dapk.st.core.logP
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.engine.LoginRequest
|
||||
import app.dapk.st.engine.LoginResult
|
||||
import app.dapk.st.push.PushTokenRegistrar
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
||||
class LoginUseCase(
|
||||
private val chatEngine: ChatEngine,
|
||||
private val pushTokenRegistrar: PushTokenRegistrar,
|
||||
private val errorTracker: ErrorTracker,
|
||||
) {
|
||||
suspend fun login(request: LoginRequest): LoginResult {
|
||||
return logP("login") {
|
||||
when (val result = chatEngine.login(request)) {
|
||||
is LoginResult.Success -> {
|
||||
coroutineScope {
|
||||
runCatching {
|
||||
listOf(
|
||||
async { pushTokenRegistrar.registerCurrentToken() },
|
||||
async { chatEngine.preloadMe() },
|
||||
).awaitAll()
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
is LoginResult.Error -> {
|
||||
errorTracker.track(result.cause)
|
||||
result
|
||||
}
|
||||
|
||||
LoginResult.MissingWellKnown -> result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ChatEngine.preloadMe() = this.me(forceRefresh = false)
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package app.dapk.st.login.state
|
||||
|
||||
import app.dapk.st.engine.LoginRequest
|
||||
import app.dapk.st.engine.LoginResult
|
||||
import app.dapk.st.login.state.fakes.FakeLoginUseCase
|
||||
import app.dapk.st.matrix.common.DeviceId
|
||||
import app.dapk.st.matrix.common.HomeServerUrl
|
||||
import app.dapk.st.matrix.common.UserCredentials
|
||||
import fixture.aUserId
|
||||
import org.junit.Test
|
||||
import test.assertDispatches
|
||||
import test.assertEvents
|
||||
import test.assertOnlyDispatches
|
||||
import test.testReducer
|
||||
|
||||
private val A_LOGIN_ACTION = LoginAction.Login(
|
||||
userName = "a-username",
|
||||
password = "a-password",
|
||||
serverUrl = "a-server-url",
|
||||
)
|
||||
|
||||
private val AN_ERROR_CAUSE = RuntimeException()
|
||||
|
||||
class LoginReducerTest {
|
||||
|
||||
private val fakeLoginUseCase = FakeLoginUseCase()
|
||||
|
||||
private val runReducerTest = testReducer { events: (LoginEvent) -> Unit ->
|
||||
loginReducer(fakeLoginUseCase.instance, events)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state is idle without server url`() = runReducerTest {
|
||||
assertInitialState(LoginScreenState(showServerUrl = false, content = LoginScreenState.Content.Idle))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given non initial state, when Visible, then updates state to Idle with previous showServerUrl`() = runReducerTest {
|
||||
setState(LoginScreenState(showServerUrl = true, LoginScreenState.Content.Loading))
|
||||
|
||||
reduce(LoginAction.ComponentLifecycle.Visible)
|
||||
|
||||
assertOnlyStateChange(LoginScreenState(showServerUrl = true, LoginScreenState.Content.Idle))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when UpdateContent, then only updates content state`() = runReducerTest {
|
||||
reduce(LoginAction.UpdateContent(LoginScreenState.Content.Loading))
|
||||
|
||||
assertOnlyStateChange {
|
||||
it.copy(content = LoginScreenState.Content.Loading)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when UpdateState, then only updates state`() = runReducerTest {
|
||||
reduce(LoginAction.UpdateState(LoginScreenState(showServerUrl = true, LoginScreenState.Content.Loading)))
|
||||
|
||||
assertOnlyStateChange(LoginScreenState(showServerUrl = true, LoginScreenState.Content.Loading))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given login errors, when Login, then updates content with loading and error`() = runReducerTest {
|
||||
fakeLoginUseCase.given(A_LOGIN_ACTION.toRequest()).returns(LoginResult.Error(AN_ERROR_CAUSE))
|
||||
|
||||
reduce(A_LOGIN_ACTION)
|
||||
|
||||
assertOnlyDispatches(
|
||||
LoginAction.UpdateContent(LoginScreenState.Content.Loading),
|
||||
LoginAction.UpdateContent(LoginScreenState.Content.Error(AN_ERROR_CAUSE)),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given login fails with WellKnownMissing, when Login, then emits WellKnownMissing event and updates content with loading and Idle showing server url`() =
|
||||
runReducerTest {
|
||||
fakeLoginUseCase.given(A_LOGIN_ACTION.toRequest()).returns(LoginResult.MissingWellKnown)
|
||||
|
||||
reduce(A_LOGIN_ACTION)
|
||||
|
||||
assertDispatches(
|
||||
LoginAction.UpdateContent(LoginScreenState.Content.Loading),
|
||||
LoginAction.UpdateState(LoginScreenState(showServerUrl = true, LoginScreenState.Content.Idle)),
|
||||
)
|
||||
assertEvents(LoginEvent.WellKnownMissing)
|
||||
assertNoStateChange()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given login success, when Login, then emits LoginComplete event`() = runReducerTest {
|
||||
fakeLoginUseCase.given(A_LOGIN_ACTION.toRequest()).returns(LoginResult.Success(aUserCredentials()))
|
||||
|
||||
reduce(A_LOGIN_ACTION)
|
||||
|
||||
assertDispatches(LoginAction.UpdateContent(LoginScreenState.Content.Loading))
|
||||
assertEvents(LoginEvent.LoginComplete)
|
||||
assertNoStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
private fun LoginAction.Login.toRequest() = LoginRequest(this.userName, this.password, this.serverUrl)
|
||||
|
||||
private fun aUserCredentials() = UserCredentials("ignored", HomeServerUrl("ignored"), aUserId(), DeviceId("ignored"))
|
|
@ -0,0 +1,72 @@
|
|||
package app.dapk.st.login.state
|
||||
|
||||
import app.dapk.st.engine.LoginRequest
|
||||
import app.dapk.st.engine.LoginResult
|
||||
import app.dapk.st.matrix.common.DeviceId
|
||||
import app.dapk.st.matrix.common.HomeServerUrl
|
||||
import app.dapk.st.matrix.common.UserCredentials
|
||||
import app.dapk.st.push.PushTokenRegistrar
|
||||
import fake.FakeChatEngine
|
||||
import fake.FakeErrorTracker
|
||||
import fixture.aUserId
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
import test.expect
|
||||
|
||||
private val A_LOGIN_ERROR = LoginResult.Error(RuntimeException())
|
||||
private val A_LOGIN_SUCCESS = LoginResult.Success(aUserCredentials())
|
||||
|
||||
private val A_LOGIN_REQUEST = LoginRequest(
|
||||
userName = "a-username",
|
||||
password = "a-password",
|
||||
serverUrl = "a-server-url",
|
||||
)
|
||||
|
||||
class LoginUseCaseTest {
|
||||
|
||||
private val fakeChatEngine = FakeChatEngine()
|
||||
private val fakePushTokenRegistrar = FakePushTokenRegistrar()
|
||||
private val fakeErrorTracker = FakeErrorTracker()
|
||||
|
||||
private val useCase = LoginUseCase(
|
||||
fakeChatEngine,
|
||||
fakePushTokenRegistrar,
|
||||
fakeErrorTracker,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `when logging in succeeds, then registers push token and preload me`() = runTest {
|
||||
fakeChatEngine.givenLogin(A_LOGIN_REQUEST).returns(A_LOGIN_SUCCESS)
|
||||
fakePushTokenRegistrar.expect { it.registerCurrentToken() }
|
||||
fakeChatEngine.expect { it.me(forceRefresh = false) }
|
||||
|
||||
val result = useCase.login(A_LOGIN_REQUEST)
|
||||
|
||||
result shouldBeEqualTo A_LOGIN_SUCCESS
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when logging in fails with MissingWellKnown, then does nothing`() = runTest {
|
||||
fakeChatEngine.givenLogin(A_LOGIN_REQUEST).returns(LoginResult.MissingWellKnown)
|
||||
|
||||
val result = useCase.login(A_LOGIN_REQUEST)
|
||||
|
||||
result shouldBeEqualTo LoginResult.MissingWellKnown
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when logging in errors, then tracks cause`() = runTest {
|
||||
fakeChatEngine.givenLogin(A_LOGIN_REQUEST).returns(A_LOGIN_ERROR)
|
||||
fakeErrorTracker.expect { it.track(A_LOGIN_ERROR.cause) }
|
||||
|
||||
val result = useCase.login(A_LOGIN_REQUEST)
|
||||
|
||||
result shouldBeEqualTo A_LOGIN_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
class FakePushTokenRegistrar : PushTokenRegistrar by mockk()
|
||||
|
||||
private fun aUserCredentials() = UserCredentials("ignored", HomeServerUrl("ignored"), aUserId(), DeviceId("ignored"))
|
|
@ -0,0 +1,15 @@
|
|||
package app.dapk.st.login.state.fakes
|
||||
|
||||
import app.dapk.st.engine.LoginRequest
|
||||
import app.dapk.st.login.state.LoginUseCase
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import test.delegateReturn
|
||||
|
||||
class FakeLoginUseCase {
|
||||
|
||||
val instance = mockk<LoginUseCase>()
|
||||
|
||||
fun given(loginRequest: LoginRequest) = coEvery { instance.login(loginRequest) }.delegateReturn()
|
||||
|
||||
}
|
Loading…
Reference in New Issue