adding tests around login reducer

This commit is contained in:
Adam Brown 2022-12-13 20:35:12 +00:00
parent 821c317916
commit 27e29ebd34
6 changed files with 175 additions and 31 deletions

View File

@ -16,6 +16,9 @@ dependencies {
implementation project(":design-library") implementation project(":design-library")
implementation project(":core") implementation project(":core")
kotlinTest(it)
testImplementation 'screen-state:state-test' testImplementation 'screen-state:state-test'
testImplementation 'chat-engine:chat-engine-test' testImplementation 'chat-engine:chat-engine-test'
androidImportFixturesWorkaround(project, project(":core"))
} }

View File

@ -4,6 +4,7 @@ import app.dapk.st.core.ProvidableModule
import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.engine.ChatEngine import app.dapk.st.engine.ChatEngine
import app.dapk.st.login.state.LoginState 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.login.state.loginReducer
import app.dapk.st.push.PushModule import app.dapk.st.push.PushModule
import app.dapk.st.state.createStateViewModel import app.dapk.st.state.createStateViewModel
@ -16,7 +17,8 @@ class LoginModule(
fun loginState(): LoginState { fun loginState(): LoginState {
return createStateViewModel { return createStateViewModel {
loginReducer(chatEngine, pushModule.pushTokenRegistrar(), errorTracker, it) val loginUseCase = LoginUseCase(chatEngine, pushModule.pushTokenRegistrars(), errorTracker)
loginReducer(loginUseCase, it)
} }
} }
} }

View File

@ -1,22 +1,15 @@
package app.dapk.st.login.state package app.dapk.st.login.state
import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.logP import app.dapk.st.core.logP
import app.dapk.st.engine.ChatEngine
import app.dapk.st.engine.LoginRequest import app.dapk.st.engine.LoginRequest
import app.dapk.st.engine.LoginResult import app.dapk.st.engine.LoginResult
import app.dapk.st.push.PushTokenRegistrar
import app.dapk.state.async import app.dapk.state.async
import app.dapk.state.change import app.dapk.state.change
import app.dapk.state.createReducer import app.dapk.state.createReducer
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
fun loginReducer( fun loginReducer(
chatEngine: ChatEngine, loginUseCase: LoginUseCase,
pushTokenRegistrar: PushTokenRegistrar,
errorTracker: ErrorTracker,
eventEmitter: suspend (LoginEvent) -> Unit, eventEmitter: suspend (LoginEvent) -> Unit,
) = createReducer( ) = createReducer(
initialState = LoginScreenState(showServerUrl = false, content = LoginScreenState.Content.Idle), initialState = LoginScreenState(showServerUrl = false, content = LoginScreenState.Content.Idle),
@ -25,45 +18,29 @@ fun loginReducer(
LoginScreenState(state.showServerUrl, content = LoginScreenState.Content.Idle) LoginScreenState(state.showServerUrl, content = LoginScreenState.Content.Idle)
}, },
change(LoginAction.UpdateContent::class) { action, state -> change(LoginAction.UpdateContent::class) { action, state -> state.copy(content = action.content) },
state.copy(content = action.content)
},
change(LoginAction.UpdateState::class) { action, _ -> change(LoginAction.UpdateState::class) { action, _ -> action.state },
action.state
},
async(LoginAction.Login::class) { action -> async(LoginAction.Login::class) { action ->
coroutineScope.launch { coroutineScope.launch {
logP("login") { logP("login") {
dispatch(LoginAction.UpdateContent(LoginScreenState.Content.Loading)) dispatch(LoginAction.UpdateContent(LoginScreenState.Content.Loading))
val request = LoginRequest(action.userName, action.password, action.serverUrl.takeIfNotEmpty()) val request = LoginRequest(action.userName, action.password, action.serverUrl.takeIfNotEmpty())
when (val result = chatEngine.login(request)) {
is LoginResult.Success -> {
runCatching {
listOf(
async { pushTokenRegistrar.registerCurrentToken() },
async { chatEngine.preloadMe() },
).awaitAll()
}
eventEmitter.invoke(LoginEvent.LoginComplete)
}
is LoginResult.Error -> { when (val result = loginUseCase.run(request)) {
errorTracker.track(result.cause) is LoginResult.Error -> dispatch(LoginAction.UpdateContent(LoginScreenState.Content.Error(result.cause)))
dispatch(LoginAction.UpdateContent(LoginScreenState.Content.Error(result.cause)))
}
LoginResult.MissingWellKnown -> { LoginResult.MissingWellKnown -> {
eventEmitter.invoke(LoginEvent.WellKnownMissing) eventEmitter.invoke(LoginEvent.WellKnownMissing)
dispatch(LoginAction.UpdateState(LoginScreenState(showServerUrl = true, content = LoginScreenState.Content.Idle))) dispatch(LoginAction.UpdateState(LoginScreenState(showServerUrl = true, content = LoginScreenState.Content.Idle)))
} }
is LoginResult.Success -> eventEmitter.invoke(LoginEvent.LoginComplete)
} }
} }
} }
}, },
) )
private suspend fun ChatEngine.preloadMe() = this.me(forceRefresh = false)
private fun String?.takeIfNotEmpty() = this?.takeIf { it.isNotEmpty() } private fun String?.takeIfNotEmpty() = this?.takeIf { it.isNotEmpty() }

View File

@ -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 run(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)
}

View File

@ -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"))

View File

@ -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.run(loginRequest) }.delegateReturn()
}