From 7ca0224d7a0b0c6c5a40a4b8c07c77899f55be21 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 1 Oct 2022 10:40:32 +0100 Subject: [PATCH] extracting me fetching logic to its own usecase and testing --- .idea/inspectionProfiles/Project_Default.xml | 4 + .../kotlin/fake/FakeHttpResponse.kt | 16 ++++ .../kotlin/fake/FakeMatrixHttpClient.kt | 7 +- .../testFixtures/kotlin/fixture/HttpError.kt | 14 ++++ matrix/services/profile/build.gradle | 9 +++ .../app/dapk/st/matrix/room/ProfileService.kt | 6 +- .../room/internal/DefaultProfileService.kt | 47 +---------- .../st/matrix/room/internal/FetchMeUseCase.kt | 53 +++++++++++++ .../room/internal/FetchMeUseCaseTest.kt | 78 +++++++++++++++++++ 9 files changed, 186 insertions(+), 48 deletions(-) create mode 100644 matrix/matrix-http/src/testFixtures/kotlin/fake/FakeHttpResponse.kt create mode 100644 matrix/matrix-http/src/testFixtures/kotlin/fixture/HttpError.kt create mode 100644 matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCase.kt create mode 100644 matrix/services/profile/src/test/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCaseTest.kt diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index bd2a505..2762fb3 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -4,6 +4,9 @@ + + @@ -25,5 +28,6 @@ + \ No newline at end of file diff --git a/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeHttpResponse.kt b/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeHttpResponse.kt new file mode 100644 index 0000000..fd08667 --- /dev/null +++ b/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeHttpResponse.kt @@ -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(relaxed = true) + + fun givenStatus(code: Int) { + every { instance.status } returns HttpStatusCode(code, "") + } + +} \ No newline at end of file diff --git a/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeMatrixHttpClient.kt b/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeMatrixHttpClient.kt index ee97b55..1742bfd 100644 --- a/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeMatrixHttpClient.kt +++ b/matrix/matrix-http/src/testFixtures/kotlin/fake/FakeMatrixHttpClient.kt @@ -1,6 +1,7 @@ package fake import app.dapk.st.matrix.http.MatrixHttpClient +import io.ktor.client.plugins.* import io.mockk.coEvery import io.mockk.mockk @@ -8,4 +9,8 @@ class FakeMatrixHttpClient : MatrixHttpClient by mockk() { fun given(request: MatrixHttpClient.HttpRequest, response: T) { coEvery { execute(request) } returns response } -} \ No newline at end of file + + fun errors(request: MatrixHttpClient.HttpRequest, cause: Throwable) { + coEvery { execute(request) } throws cause + } +} diff --git a/matrix/matrix-http/src/testFixtures/kotlin/fixture/HttpError.kt b/matrix/matrix-http/src/testFixtures/kotlin/fixture/HttpError.kt new file mode 100644 index 0000000..7722914 --- /dev/null +++ b/matrix/matrix-http/src/testFixtures/kotlin/fixture/HttpError.kt @@ -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 = "" +) diff --git a/matrix/services/profile/build.gradle b/matrix/services/profile/build.gradle index eaa3259..6440905 100644 --- a/matrix/services/profile/build.gradle +++ b/matrix/services/profile/build.gradle @@ -1,5 +1,14 @@ +plugins { id 'java-test-fixtures' } applyMatrixServiceModule(project) dependencies { 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"))) } \ No newline at end of file diff --git a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileService.kt b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileService.kt index 73e4768..f5d6039 100644 --- a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileService.kt +++ b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/ProfileService.kt @@ -10,6 +10,7 @@ import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.HomeServerUrl import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.room.internal.DefaultProfileService +import app.dapk.st.matrix.room.internal.FetchMeUseCase private val SERVICE_KEY = ProfileService::class @@ -31,8 +32,9 @@ fun MatrixServiceInstaller.installProfileService( singletonFlows: SingletonFlows, credentialsStore: CredentialsStore, ): InstallExtender { - return this.install { (httpClient, _, _, logger) -> - SERVICE_KEY to DefaultProfileService(httpClient, logger, profileStore, singletonFlows, credentialsStore) + return this.install { (httpClient, _, _, _) -> + val fetchMeUseCase = FetchMeUseCase(httpClient, credentialsStore) + SERVICE_KEY to DefaultProfileService(profileStore, singletonFlows, fetchMeUseCase) } } diff --git a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt index 7d4067e..168808d 100644 --- a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt +++ b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultProfileService.kt @@ -1,22 +1,14 @@ package app.dapk.st.matrix.room.internal 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.ProfileStore -import io.ktor.client.call.* -import io.ktor.client.plugins.* import kotlinx.coroutines.flow.first -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable internal class DefaultProfileService( - private val httpClient: MatrixHttpClient, - private val logger: MatrixLogger, private val profileStore: ProfileStore, private val singletonFlows: SingletonFlows, - private val credentialsStore: CredentialsStore, + private val fetchMeUseCase: FetchMeUseCase, ) : ProfileService { override suspend fun me(forceRefresh: Boolean): ProfileService.Me { @@ -29,42 +21,7 @@ internal class DefaultProfileService( } private 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 - } - } - ) + return fetchMeUseCase.fetchMe() } } -internal fun profileRequest(userId: UserId) = MatrixHttpClient.HttpRequest.httpRequest( - 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, -) \ No newline at end of file diff --git a/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCase.kt b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCase.kt new file mode 100644 index 0000000..10401f3 --- /dev/null +++ b/matrix/services/profile/src/main/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCase.kt @@ -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( + 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, +) diff --git a/matrix/services/profile/src/test/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCaseTest.kt b/matrix/services/profile/src/test/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCaseTest.kt new file mode 100644 index 0000000..5ecf4a5 --- /dev/null +++ b/matrix/services/profile/src/test/kotlin/app/dapk/st/matrix/room/internal/FetchMeUseCaseTest.kt @@ -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 + ) + } + +} \ No newline at end of file