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(":core")
kotlinTest(it)
testImplementation 'screen-state:state-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.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
@ -16,7 +17,8 @@ class LoginModule(
fun loginState(): LoginState {
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
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 app.dapk.state.async
import app.dapk.state.change
import app.dapk.state.createReducer
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
fun loginReducer(
chatEngine: ChatEngine,
pushTokenRegistrar: PushTokenRegistrar,
errorTracker: ErrorTracker,
loginUseCase: LoginUseCase,
eventEmitter: suspend (LoginEvent) -> Unit,
) = createReducer(
initialState = LoginScreenState(showServerUrl = false, content = LoginScreenState.Content.Idle),
@ -25,45 +18,29 @@ fun loginReducer(
LoginScreenState(state.showServerUrl, content = LoginScreenState.Content.Idle)
},
change(LoginAction.UpdateContent::class) { action, state ->
state.copy(content = action.content)
},
change(LoginAction.UpdateContent::class) { action, state -> state.copy(content = action.content) },
change(LoginAction.UpdateState::class) { action, _ ->
action.state
},
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 = chatEngine.login(request)) {
is LoginResult.Success -> {
runCatching {
listOf(
async { pushTokenRegistrar.registerCurrentToken() },
async { chatEngine.preloadMe() },
).awaitAll()
}
eventEmitter.invoke(LoginEvent.LoginComplete)
}
is LoginResult.Error -> {
errorTracker.track(result.cause)
dispatch(LoginAction.UpdateContent(LoginScreenState.Content.Error(result.cause)))
}
when (val result = loginUseCase.run(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 suspend fun ChatEngine.preloadMe() = this.me(forceRefresh = false)
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()
}