Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing crash when logging in or opening profile when no profile has been set #177

Merged
merged 2 commits into from
Oct 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -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, "")
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package fake

import app.dapk.st.matrix.http.MatrixHttpClient
import io.ktor.client.plugins.*
import io.mockk.coEvery
import io.mockk.mockk

class FakeMatrixHttpClient : MatrixHttpClient by mockk() {
fun <T : Any> given(request: MatrixHttpClient.HttpRequest<T>, response: T) {
coEvery { execute(request) } returns response
}
}

fun <T : Any> errors(request: MatrixHttpClient.HttpRequest<T>, cause: Throwable) {
coEvery { execute(request) } throws cause
}
}
14 changes: 14 additions & 0 deletions matrix/matrix-http/src/testFixtures/kotlin/fixture/HttpError.kt
Original file line number Diff line number Diff line change
@@ -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 = ""
)
9 changes: 9 additions & 0 deletions matrix/services/profile/build.gradle
Original file line number Diff line number Diff line change
@@ -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")))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -31,8 +32,9 @@ fun MatrixServiceInstaller.installProfileService(
singletonFlows: SingletonFlows,
credentialsStore: CredentialsStore,
): InstallExtender<ProfileService> {
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)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +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 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 {
Expand All @@ -27,25 +21,7 @@ internal class DefaultProfileService(
}

private suspend fun fetchMe(): ProfileService.Me {
val credentials = credentialsStore.credentials()!!
val userId = credentials.userId
val result = httpClient.execute(profileRequest(userId))
return ProfileService.Me(
userId,
result.displayName,
result.avatarUrl?.convertMxUrToUrl(credentials.homeServer)?.let { AvatarUrl(it) },
homeServerUrl = credentials.homeServer,
)
return fetchMeUseCase.fetchMe()
}
}

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,
)
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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
)
}

}