extracting me fetching logic to its own usecase and testing

This commit is contained in:
Adam Brown 2022-10-01 10:40:32 +01:00 committed by Adam Brown
parent 1d1aff0ca9
commit 6532478fc8
9 changed files with 186 additions and 48 deletions

View File

@ -4,6 +4,9 @@
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
</inspection_tool> </inspection_tool>
@ -25,5 +28,6 @@
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PrivatePropertyName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
</profile> </profile>
</component> </component>

View File

@ -0,0 +1,16 @@
package fake
import io.ktor.client.statement.*
import io.ktor.http.*
import io.mockk.every
import io.mockk.mockk
class FakeHttpResponse {
val instance = mockk<HttpResponse>(relaxed = true)
fun givenStatus(code: Int) {
every { instance.status } returns HttpStatusCode(code, "")
}
}

View File

@ -1,6 +1,7 @@
package fake package fake
import app.dapk.st.matrix.http.MatrixHttpClient import app.dapk.st.matrix.http.MatrixHttpClient
import io.ktor.client.plugins.*
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.mockk import io.mockk.mockk
@ -8,4 +9,8 @@ class FakeMatrixHttpClient : MatrixHttpClient by mockk() {
fun <T : Any> given(request: MatrixHttpClient.HttpRequest<T>, response: T) { fun <T : Any> given(request: MatrixHttpClient.HttpRequest<T>, response: T) {
coEvery { execute(request) } returns response coEvery { execute(request) } returns response
} }
}
fun <T : Any> errors(request: MatrixHttpClient.HttpRequest<T>, cause: Throwable) {
coEvery { execute(request) } throws cause
}
}

View File

@ -0,0 +1,14 @@
package fixture
import fake.FakeHttpResponse
import io.ktor.client.plugins.*
fun a404HttpError() = ClientRequestException(
FakeHttpResponse().apply { givenStatus(404) }.instance,
cachedResponseText = ""
)
fun a403HttpError() = ClientRequestException(
FakeHttpResponse().apply { givenStatus(403) }.instance,
cachedResponseText = ""
)

View File

@ -1,5 +1,14 @@
plugins { id 'java-test-fixtures' }
applyMatrixServiceModule(project) applyMatrixServiceModule(project)
dependencies { dependencies {
implementation project(":core") implementation project(":core")
kotlinTest(it)
kotlinFixtures(it)
testImplementation(testFixtures(project(":matrix:common")))
testImplementation(testFixtures(project(":matrix:matrix-http")))
testImplementation(testFixtures(project(":core")))
testFixturesImplementation(testFixtures(project(":matrix:common")))
testFixturesImplementation(testFixtures(project(":core")))
} }

View File

@ -10,6 +10,7 @@ import app.dapk.st.matrix.common.CredentialsStore
import app.dapk.st.matrix.common.HomeServerUrl import app.dapk.st.matrix.common.HomeServerUrl
import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.common.UserId
import app.dapk.st.matrix.room.internal.DefaultProfileService import app.dapk.st.matrix.room.internal.DefaultProfileService
import app.dapk.st.matrix.room.internal.FetchMeUseCase
private val SERVICE_KEY = ProfileService::class private val SERVICE_KEY = ProfileService::class
@ -31,8 +32,9 @@ fun MatrixServiceInstaller.installProfileService(
singletonFlows: SingletonFlows, singletonFlows: SingletonFlows,
credentialsStore: CredentialsStore, credentialsStore: CredentialsStore,
): InstallExtender<ProfileService> { ): InstallExtender<ProfileService> {
return this.install { (httpClient, _, _, logger) -> return this.install { (httpClient, _, _, _) ->
SERVICE_KEY to DefaultProfileService(httpClient, logger, profileStore, singletonFlows, credentialsStore) val fetchMeUseCase = FetchMeUseCase(httpClient, credentialsStore)
SERVICE_KEY to DefaultProfileService(profileStore, singletonFlows, fetchMeUseCase)
} }
} }

View File

@ -1,22 +1,14 @@
package app.dapk.st.matrix.room.internal package app.dapk.st.matrix.room.internal
import app.dapk.st.core.SingletonFlows import app.dapk.st.core.SingletonFlows
import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.http.MatrixHttpClient
import app.dapk.st.matrix.room.ProfileService import app.dapk.st.matrix.room.ProfileService
import app.dapk.st.matrix.room.ProfileStore import app.dapk.st.matrix.room.ProfileStore
import io.ktor.client.call.*
import io.ktor.client.plugins.*
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
internal class DefaultProfileService( internal class DefaultProfileService(
private val httpClient: MatrixHttpClient,
private val logger: MatrixLogger,
private val profileStore: ProfileStore, private val profileStore: ProfileStore,
private val singletonFlows: SingletonFlows, private val singletonFlows: SingletonFlows,
private val credentialsStore: CredentialsStore, private val fetchMeUseCase: FetchMeUseCase,
) : ProfileService { ) : ProfileService {
override suspend fun me(forceRefresh: Boolean): ProfileService.Me { override suspend fun me(forceRefresh: Boolean): ProfileService.Me {
@ -29,42 +21,7 @@ internal class DefaultProfileService(
} }
private suspend fun fetchMe(): ProfileService.Me { private suspend fun fetchMe(): ProfileService.Me {
val credentials = credentialsStore.credentials()!! return fetchMeUseCase.fetchMe()
val userId = credentials.userId
return runCatching { httpClient.execute(profileRequest(userId)) }.fold(
onSuccess = {
ProfileService.Me(
userId,
it.displayName,
it.avatarUrl?.convertMxUrToUrl(credentials.homeServer)?.let { AvatarUrl(it) },
homeServerUrl = credentials.homeServer,
)
},
onFailure = {
when {
it is ClientRequestException && it.response.status.value == 404 -> {
ProfileService.Me(
userId,
displayName = null,
avatarUrl = null,
homeServerUrl = credentials.homeServer,
)
}
else -> throw it
}
}
)
} }
} }
internal fun profileRequest(userId: UserId) = MatrixHttpClient.HttpRequest.httpRequest<ApiMe>(
path = "_matrix/client/r0/profile/${userId.value}/",
method = MatrixHttpClient.Method.GET,
)
@Serializable
internal data class ApiMe(
@SerialName("displayname") val displayName: String? = null,
@SerialName("avatar_url") val avatarUrl: MxUrl? = null,
)

View File

@ -0,0 +1,53 @@
package app.dapk.st.matrix.room.internal
import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.http.MatrixHttpClient
import app.dapk.st.matrix.room.ProfileService
import io.ktor.client.plugins.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
internal class FetchMeUseCase(
private val httpClient: MatrixHttpClient,
private val credentialsStore: CredentialsStore,
) {
suspend fun fetchMe(): ProfileService.Me {
val credentials = credentialsStore.credentials()!!
val userId = credentials.userId
return runCatching { httpClient.execute(profileRequest(userId)) }.fold(
onSuccess = {
ProfileService.Me(
userId,
it.displayName,
it.avatarUrl?.convertMxUrToUrl(credentials.homeServer)?.let { AvatarUrl(it) },
homeServerUrl = credentials.homeServer,
)
},
onFailure = {
when {
it is ClientRequestException && it.response.status.value == 404 -> {
ProfileService.Me(
userId,
displayName = null,
avatarUrl = null,
homeServerUrl = credentials.homeServer,
)
}
else -> throw it
}
}
)
}
}
internal fun profileRequest(userId: UserId) = MatrixHttpClient.HttpRequest.httpRequest<ApiMe>(
path = "_matrix/client/r0/profile/${userId.value}/",
method = MatrixHttpClient.Method.GET,
)
@Serializable
internal data class ApiMe(
@SerialName("displayname") val displayName: String? = null,
@SerialName("avatar_url") val avatarUrl: MxUrl? = null,
)

View File

@ -0,0 +1,78 @@
package app.dapk.st.matrix.room.internal
import app.dapk.st.matrix.room.ProfileService
import fake.FakeCredentialsStore
import fake.FakeMatrixHttpClient
import fixture.a403HttpError
import fixture.a404HttpError
import fixture.aUserCredentials
import fixture.aUserId
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.coInvoking
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldThrow
import org.junit.Test
private val A_USER_CREDENTIALS = aUserCredentials()
private val A_USER_ID = aUserId()
private val AN_API_ME_RESPONSE = ApiMe(
displayName = "a display name",
avatarUrl = null,
)
private val AN_UNHANDLED_ERROR = RuntimeException()
class FetchMeUseCaseTest {
private val fakeHttpClient = FakeMatrixHttpClient()
private val fakeCredentialsStore = FakeCredentialsStore()
private val useCase = FetchMeUseCase(fakeHttpClient, fakeCredentialsStore)
@Test
fun `when fetching me, then returns Me instance`() = runTest {
fakeCredentialsStore.givenCredentials().returns(A_USER_CREDENTIALS)
fakeHttpClient.given(profileRequest(aUserId()), AN_API_ME_RESPONSE)
val result = useCase.fetchMe()
result shouldBeEqualTo ProfileService.Me(
userId = A_USER_ID,
displayName = AN_API_ME_RESPONSE.displayName,
avatarUrl = null,
homeServerUrl = fakeCredentialsStore.credentials()!!.homeServer
)
}
@Test
fun `given unhandled error, when fetching me, then throws`() = runTest {
fakeCredentialsStore.givenCredentials().returns(A_USER_CREDENTIALS)
fakeHttpClient.errors(profileRequest(aUserId()), AN_UNHANDLED_ERROR)
coInvoking { useCase.fetchMe() } shouldThrow AN_UNHANDLED_ERROR
}
@Test
fun `given 403, when fetching me, then throws`() = runTest {
val error = a403HttpError()
fakeCredentialsStore.givenCredentials().returns(A_USER_CREDENTIALS)
fakeHttpClient.errors(profileRequest(aUserId()), error)
coInvoking { useCase.fetchMe() } shouldThrow error
}
@Test
fun `given 404, when fetching me, then returns Me instance with empty profile fields`() = runTest {
fakeCredentialsStore.givenCredentials().returns(A_USER_CREDENTIALS)
fakeHttpClient.errors(profileRequest(aUserId()), a404HttpError())
val result = useCase.fetchMe()
result shouldBeEqualTo ProfileService.Me(
userId = A_USER_ID,
displayName = null,
avatarUrl = null,
homeServerUrl = fakeCredentialsStore.credentials()!!.homeServer
)
}
}