Add tests.

This commit is contained in:
Benoit Marty 2023-06-08 22:56:36 +02:00
parent e1e984cfb0
commit dbe7796704
7 changed files with 180 additions and 77 deletions

View file

@ -59,6 +59,7 @@ dependencies {
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.tests.testutils)
androidTestImplementation(libs.test.junitext) androidTestImplementation(libs.test.junitext)
} }

View file

@ -49,10 +49,11 @@ class DefaultHomeserverResolver @Inject constructor(
emit(Async.Uninitialized) emit(Async.Uninitialized)
// Debounce // Debounce
delay(300) delay(300)
val clean = userInput.trim() val trimmedUserInput = userInput.trim()
if (clean.length < 4) return@flow if (trimmedUserInput.length < 4) return@flow
emit(Async.Loading()) emit(Async.Loading())
val list = getUrlCandidate(clean.ensureProtocol().removeSuffix("/")) val candidateBase = trimmedUserInput.ensureProtocol().removeSuffix("/")
val list = getUrlCandidates(candidateBase)
val currentList = Collections.synchronizedList(mutableListOf<HomeserverData>()) val currentList = Collections.synchronizedList(mutableListOf<HomeserverData>())
// Run all the requests in parallel // Run all the requests in parallel
withContext(dispatchers.io) { withContext(dispatchers.io) {
@ -77,14 +78,14 @@ class DefaultHomeserverResolver @Inject constructor(
} }
}.awaitAll() }.awaitAll()
} }
// If list is empty, and the user as entered an URL, do not block the user. // If list is empty, and the user has entered an URL, do not block the user.
if (currentList.isEmpty()) { if (currentList.isEmpty()) {
if (userInput.isValidUrl()) { if (trimmedUserInput.isValidUrl()) {
emit( emit(
Async.Success( Async.Success(
listOf( listOf(
HomeserverData( HomeserverData(
homeserverUrl = userInput, homeserverUrl = trimmedUserInput,
isWellknownValid = false, isWellknownValid = false,
supportSlidingSync = false, supportSlidingSync = false,
) )
@ -97,7 +98,7 @@ class DefaultHomeserverResolver @Inject constructor(
} }
} }
private fun getUrlCandidate(data: String): List<String> { private fun getUrlCandidates(data: String): List<String> {
return buildList { return buildList {
if (data.contains(".")) { if (data.contains(".")) {
// TLD detected? // TLD detected?

View file

@ -0,0 +1,36 @@
/*
* 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.changeaccountprovider.form.network
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.network.RetrofitFactory
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultWellknownRequest @Inject constructor(
private val retrofitFactory: RetrofitFactory,
) : WellknownRequest {
/**
* Return the WellKnown data, if found.
* @param baseUrl for instance https://matrix.org
*/
override suspend fun execute(baseUrl: String): WellKnown {
val wellknownApi = retrofitFactory.create(baseUrl)
.create(WellknownAPI::class.java)
return wellknownApi.getWellKnown()
}
}

View file

@ -15,19 +15,10 @@
*/ */
package io.element.android.features.login.impl.changeaccountprovider.form.network package io.element.android.features.login.impl.changeaccountprovider.form.network
import io.element.android.libraries.network.RetrofitFactory interface WellknownRequest {
import javax.inject.Inject
class WellknownRequest @Inject constructor(
private val retrofitFactory: RetrofitFactory,
) {
/** /**
* Return the WellKnown data, if found. * Return the WellKnown data, or throw an error if not found.
* @param baseUrl for instance https://matrix.org * @param baseUrl for instance https://matrix.org
*/ */
suspend fun execute(baseUrl: String): WellKnown { suspend fun execute(baseUrl: String): WellKnown
val wellknownApi = retrofitFactory.create(baseUrl)
.create(WellknownAPI::class.java)
return wellknownApi.getWellKnown()
}
} }

View file

@ -21,22 +21,27 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnown
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnownBaseConfig
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnownSlidingSyncConfig
import io.element.android.features.login.impl.datasource.AccountProviderDataSource import io.element.android.features.login.impl.datasource.AccountProviderDataSource
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
class ChangeAccountProviderFormPresenterTest { class ChangeAccountProviderFormPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val homeServerResolver = FakeHomeServerResolver() val fakeWellknownRequest = FakeWellknownRequest()
val changeServerPresenter = ChangeServerPresenter( val changeServerPresenter = ChangeServerPresenter(
FakeAuthenticationService(), FakeAuthenticationService(),
AccountProviderDataSource() AccountProviderDataSource()
) )
val presenter = ChangeAccountProviderFormPresenter( val presenter = ChangeAccountProviderFormPresenter(
homeServerResolver, DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
changeServerPresenter changeServerPresenter
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
@ -50,13 +55,13 @@ class ChangeAccountProviderFormPresenterTest {
@Test @Test
fun `present - enter text no result`() = runTest { fun `present - enter text no result`() = runTest {
val homeServerResolver = FakeHomeServerResolver() val fakeWellknownRequest = FakeWellknownRequest()
val changeServerPresenter = ChangeServerPresenter( val changeServerPresenter = ChangeServerPresenter(
FakeAuthenticationService(), FakeAuthenticationService(),
AccountProviderDataSource() AccountProviderDataSource()
) )
val presenter = ChangeAccountProviderFormPresenter( val presenter = ChangeAccountProviderFormPresenter(
homeServerResolver, DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
changeServerPresenter changeServerPresenter
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
@ -73,12 +78,41 @@ class ChangeAccountProviderFormPresenterTest {
} }
@Test @Test
fun `present - enter text one then two results`() = runTest { fun `present - enter valid url no wellknown`() = runTest {
val homeServerResolver = FakeHomeServerResolver() val fakeWellknownRequest = FakeWellknownRequest()
homeServerResolver.givenResult( val changeServerPresenter = ChangeServerPresenter(
listOf( FakeAuthenticationService(),
listOf(aHomeserverData()), AccountProviderDataSource()
listOf(aHomeserverData(), aHomeserverData()), )
val presenter = ChangeAccountProviderFormPresenter(
DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
changeServerPresenter
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(ChangeAccountProviderFormEvents.UserInput("https://test.org"))
val withInputState = awaitItem()
assertThat(withInputState.userInput).isEqualTo("https://test.org")
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
assertThat(awaitItem().userInputResult).isEqualTo(
Async.Success(
listOf(
aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = false, supportSlidingSync = false)
)
)
)
}
}
@Test
fun `present - enter text one result no sliding sync`() = runTest {
val fakeWellknownRequest = FakeWellknownRequest()
fakeWellknownRequest.givenResultMap(
mapOf(
"https://test.org" to aWellKnown().copy(slidingSyncProxy = null),
) )
) )
val changeServerPresenter = ChangeServerPresenter( val changeServerPresenter = ChangeServerPresenter(
@ -86,7 +120,7 @@ class ChangeAccountProviderFormPresenterTest {
AccountProviderDataSource() AccountProviderDataSource()
) )
val presenter = ChangeAccountProviderFormPresenter( val presenter = ChangeAccountProviderFormPresenter(
homeServerResolver, DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
changeServerPresenter changeServerPresenter
) )
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
@ -98,8 +132,62 @@ class ChangeAccountProviderFormPresenterTest {
assertThat(withInputState.userInput).isEqualTo("test") assertThat(withInputState.userInput).isEqualTo("test")
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
assertThat(awaitItem().userInputResult).isEqualTo(Async.Success(listOf(aHomeserverData()))) assertThat(awaitItem().userInputResult).isEqualTo(
assertThat(awaitItem().userInputResult).isEqualTo(Async.Success(listOf(aHomeserverData(), aHomeserverData()))) Async.Success(
listOf(
aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = true, supportSlidingSync = false)
)
)
)
} }
} }
@Test
fun `present - enter text one result with sliding sync`() = runTest {
val fakeWellknownRequest = FakeWellknownRequest()
fakeWellknownRequest.givenResultMap(
mapOf(
"https://test.io" to aWellKnown(),
)
)
val changeServerPresenter = ChangeServerPresenter(
FakeAuthenticationService(),
AccountProviderDataSource()
)
val presenter = ChangeAccountProviderFormPresenter(
DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
changeServerPresenter
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(ChangeAccountProviderFormEvents.UserInput("test"))
val withInputState = awaitItem()
assertThat(withInputState.userInput).isEqualTo("test")
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
assertThat(awaitItem().userInputResult).isEqualTo(
Async.Success(
listOf(
aHomeserverData(homeserverUrl = "https://test.io")
)
)
)
}
}
private fun aWellKnown(): WellKnown {
return WellKnown(
homeServer = WellKnownBaseConfig(
baseURL = A_HOMESERVER_URL
),
identityServer = WellKnownBaseConfig(
baseURL = A_HOMESERVER_URL
),
slidingSyncProxy = WellKnownSlidingSyncConfig(
url = A_HOMESERVER_URL
)
)
}
} }

View file

@ -1,45 +0,0 @@
/*
* 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.changeaccountprovider.form
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class FakeHomeServerResolver : HomeserverResolver {
private var pendingResult: List<List<HomeserverData>> = emptyList()
fun givenResult(result: List<List<HomeserverData>>) {
pendingResult = result
}
override suspend fun resolve(userInput: String): Flow<Async<List<HomeserverData>>> = flow {
emit(Async.Uninitialized)
delay(FAKE_DELAY_IN_MS)
emit(Async.Loading())
// Sending the pending result
if (pendingResult.isEmpty()) {
emit(Async.Uninitialized)
} else {
pendingResult.forEach {
delay(FAKE_DELAY_IN_MS)
emit(Async.Success(it))
}
}
}
}

View file

@ -0,0 +1,31 @@
/*
* 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.changeaccountprovider.form
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnown
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellknownRequest
class FakeWellknownRequest : WellknownRequest {
private var resultMap: Map<String, WellKnown> = emptyMap()
fun givenResultMap(map: Map<String, WellKnown>) {
resultMap = map
}
override suspend fun execute(baseUrl: String): WellKnown {
return resultMap[baseUrl] ?: error("No result provided for $baseUrl")
}
}