Skip to content

Commit

Permalink
Merge pull request #39 from gravatar/hamorillo/13-corutines
Browse files Browse the repository at this point in the history
Coroutines and necessary Ktlint configuration
  • Loading branch information
hamorillo authored Jan 25, 2024
2 parents f030678 + 1384a98 commit f81e731
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 33 deletions.
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@
root = true

[*.{kt,kts}]
ktlint_code_style = ktlint_official
ktlint_function_naming_ignore_when_annotated_with = Composable
max_line_length=120
ktlint_function_signature_body_expression_wrapping = default
ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 6
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_string-template-indent = disabled
5 changes: 1 addition & 4 deletions app/src/main/java/com/gravatar/demoapp/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@ class MainActivity : ComponentActivity() {
}

@Composable
fun Greeting(
name: String,
modifier: Modifier = Modifier,
) {
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier,
Expand Down
2 changes: 2 additions & 0 deletions gravatar/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ android {
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

testImplementation("junit:junit:4.13.2")
testImplementation("org.robolectric:robolectric:4.11.1")
testImplementation("io.mockk:mockk-android:1.13.9")
testImplementation("io.mockk:mockk-agent:1.13.9")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")

androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
Expand Down
28 changes: 12 additions & 16 deletions gravatar/src/main/java/com/gravatar/GravatarApi.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.gravatar

import android.os.Handler
import android.os.Looper
import android.util.Log
import com.gravatar.di.container.GravatarSdkContainer
import com.gravatar.logger.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.asRequestBody
Expand All @@ -14,6 +13,7 @@ import retrofit2.Response
import java.io.File
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import com.gravatar.di.container.GravatarSdkContainer.Companion.instance as GravatarSdkDI

class GravatarApi(private val okHttpClient: OkHttpClient? = null) {
private companion object {
Expand All @@ -27,29 +27,28 @@ class GravatarApi(private val okHttpClient: OkHttpClient? = null) {
UNKNOWN,
}

val coroutineScope = CoroutineScope(GravatarSdkDI.dispatcherDefault)

fun uploadGravatar(
file: File,
email: String,
accessToken: String,
gravatarUploadListener: GravatarUploadListener,
) {
val service = GravatarSdkContainer.instance.getGravatarApiService(okHttpClient)
val service = GravatarSdkDI.getGravatarApiService(okHttpClient)
val identity = MultipartBody.Part.createFormData("account", email)
val filePart =
MultipartBody.Part.createFormData("filedata", file.name, file.asRequestBody())

service.uploadImage("Bearer $accessToken", identity, filePart).enqueue(
object : Callback<ResponseBody> {
override fun onResponse(
call: Call<ResponseBody>,
response: Response<ResponseBody>,
) {
Handler(Looper.getMainLooper()).post {
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
coroutineScope.launch {
if (response.isSuccessful) {
gravatarUploadListener.onSuccess()
} else {
// Log the response body for debugging purposes if the response is not successful
Log.w(
Logger.w(
LOG_TAG,
"Network call unsuccessful trying to upload Gravatar: $response.body",
)
Expand All @@ -64,17 +63,14 @@ class GravatarApi(private val okHttpClient: OkHttpClient? = null) {
}
}

override fun onFailure(
call: Call<ResponseBody>,
t: Throwable,
) {
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
val error: ErrorType =
when (t) {
is SocketTimeoutException -> ErrorType.TIMEOUT
is UnknownHostException -> ErrorType.NETWORK
else -> ErrorType.UNKNOWN
}
Handler(Looper.getMainLooper()).post {
coroutineScope.launch {
gravatarUploadListener.onError(error)
}
}
Expand Down
6 changes: 1 addition & 5 deletions gravatar/src/main/java/com/gravatar/GravatarUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@ fun emailAddressToGravatarUrl(
return emailAddressToGravatarUri(email, defaultAvatarImage, size).toString()
}

fun emailAddressToGravatarUri(
email: String,
defaultAvatarImage: DefaultAvatarImage? = null,
size: Int? = null,
): Uri {
fun emailAddressToGravatarUri(email: String, defaultAvatarImage: DefaultAvatarImage? = null, size: Int? = null): Uri {
return Uri.Builder()
.scheme("https")
.authority(GRAVATAR_IMAGE_HOST)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.gravatar.di.container

import com.gravatar.GravatarApiService
import com.gravatar.GravatarConstants.GRAVATAR_API_BASE_URL
import kotlinx.coroutines.Dispatchers
import okhttp3.OkHttpClient
import retrofit2.Retrofit

Expand All @@ -14,6 +15,9 @@ class GravatarSdkContainer private constructor() {

private fun getRetrofitBuilder() = Retrofit.Builder().baseUrl(GRAVATAR_API_BASE_URL)

val dispatcherDefault = Dispatchers.Default
val dispatcherIO = Dispatchers.IO

fun getGravatarApiService(okHttpClient: OkHttpClient? = null): GravatarApiService {
return getRetrofitBuilder().apply {
okHttpClient?.let { client(it) }
Expand Down
11 changes: 11 additions & 0 deletions gravatar/src/main/java/com/gravatar/logger/Logger.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.gravatar.logger

import android.util.Log

object Logger {
fun i(tag: String, message: String) = Log.i(tag, message)

fun w(tag: String, message: String) = Log.w(tag, message)

fun e(tag: String, message: String) = Log.e(tag, message)
}
126 changes: 122 additions & 4 deletions gravatar/src/test/java/com/gravatar/GravatarApiTest.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
package com.gravatar

import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.test.runTest
import okhttp3.ResponseBody
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import retrofit2.Call
import retrofit2.Callback
import java.io.File
import java.net.SocketTimeoutException
import java.net.UnknownHostException

@RunWith(RobolectricTestRunner::class)
class GravatarApiTest {
@get:Rule
var gravatarSdkTest = GravatarSdkContainerRule()
var containerRule = GravatarSdkContainerRule()

private lateinit var gravatarApi: GravatarApi

Expand All @@ -20,10 +31,24 @@ class GravatarApiTest {
}

@Test
fun `given an file, email and accessToken when uploading avatar then Gravatar service is invoked`() {
gravatarApi.uploadGravatar(File("avatarFile"), "email", "accessToken", mockk())
fun `given a file, email and accessToken when uploading avatar then Gravatar service is invoked`() = runTest {
val uploadGravatarListener = spyk<GravatarApi.GravatarUploadListener>()
val callResponse = mockk<Call<ResponseBody>>()
every { containerRule.gravatarApiServiceMock.uploadImage(any(), any(), any()) } returns callResponse
every { callResponse.enqueue(any()) } answers { call ->
@Suppress("UNCHECKED_CAST")
(call.invocation.args[0] as? Callback<ResponseBody>)?.onResponse(
callResponse,
mockk(relaxed = true) {
every { isSuccessful } returns true
},
)
}

gravatarApi.uploadGravatar(File("avatarFile"), "email", "accessToken", uploadGravatarListener)

verify(exactly = 1) {
gravatarSdkTest.gravatarApiServiceMock.uploadImage(
containerRule.gravatarApiServiceMock.uploadImage(
"Bearer accessToken",
withArg {
assertTrue(
Expand All @@ -39,5 +64,98 @@ class GravatarApiTest {
},
)
}
verify(exactly = 1) {
uploadGravatarListener.onSuccess()
}
}

@Test
fun `given gravatar update when a unknown error occurs then Gravatar returns UNKNOWN error`() =
`given a gravatar update when a error occurs then Gravatar returns the expected error`(
100,
GravatarApi.ErrorType.UNKNOWN,
)

@Test
fun `given gravatar update when a server error occurs then Gravatar returns SERVER error`() =
`given a gravatar update when a error occurs then Gravatar returns the expected error`(
HttpResponseCode.SERVER_ERRORS.random(),
GravatarApi.ErrorType.SERVER,
)

@Test
fun `given gravatar update when a timeout occurs then Gravatar returns TIMEOUT error`() =
`given a gravatar update when a error occurs then Gravatar returns the expected error`(
HttpResponseCode.HTTP_CLIENT_TIMEOUT,
GravatarApi.ErrorType.TIMEOUT,
)

@Test
fun `given gravatar update when a SocketTimeoutException occurs then Gravatar returns TIMEOUT error`() =
`given a gravatar update when a exception occurs then Gravatar returns the expected error`(
SocketTimeoutException(),
GravatarApi.ErrorType.TIMEOUT,
)

@Test
fun `given gravatar update when a UnknownHostException occurs then Gravatar returns NETWORK error`() =
`given a gravatar update when a exception occurs then Gravatar returns the expected error`(
UnknownHostException(),
GravatarApi.ErrorType.NETWORK,
)

@Test
fun `given gravatar update when a Exception occurs then Gravatar returns UNKNOWN error`() =
`given a gravatar update when a exception occurs then Gravatar returns the expected error`(
Exception(),
GravatarApi.ErrorType.UNKNOWN,
)

@Suppress("Cast")
private fun `given a gravatar update when a error occurs then Gravatar returns the expected error`(
httpResponseCode: Int,
expectedErrorType: GravatarApi.ErrorType,
) = runTest {
val uploadGravatarListener = spyk<GravatarApi.GravatarUploadListener>()
val callResponse = mockk<Call<ResponseBody>>()
every { containerRule.gravatarApiServiceMock.uploadImage(any(), any(), any()) } returns callResponse
every { callResponse.enqueue(any()) } answers { call ->
@Suppress("UNCHECKED_CAST")
(call.invocation.args[0] as? Callback<ResponseBody>)?.onResponse(
callResponse,
mockk(relaxed = true) {
every { isSuccessful } returns false
every { code() } returns httpResponseCode
},
)
}

gravatarApi.uploadGravatar(File("avatarFile"), "email", "accessToken", uploadGravatarListener)

verify(exactly = 1) {
uploadGravatarListener.onError(expectedErrorType)
}
}

private fun `given a gravatar update when a exception occurs then Gravatar returns the expected error`(
exception: Throwable,
expectedErrorType: GravatarApi.ErrorType,
) = runTest {
val uploadGravatarListener = spyk<GravatarApi.GravatarUploadListener>()
val callResponse = mockk<Call<ResponseBody>>()
every { containerRule.gravatarApiServiceMock.uploadImage(any(), any(), any()) } returns callResponse
every { callResponse.enqueue(any()) } answers { call ->
@Suppress("UNCHECKED_CAST")
(call.invocation.args[0] as? Callback<ResponseBody>)?.onFailure(
callResponse,
exception,
)
}

gravatarApi.uploadGravatar(File("avatarFile"), "email", "accessToken", uploadGravatarListener)

verify(exactly = 1) {
uploadGravatarListener.onError(expectedErrorType)
}
}
}
11 changes: 7 additions & 4 deletions gravatar/src/test/java/com/gravatar/GravatarSdkContainerRule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,27 @@ import com.gravatar.di.container.GravatarSdkContainer
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement

@OptIn(ExperimentalCoroutinesApi::class)
class GravatarSdkContainerRule : TestRule {
val testDispatcher = UnconfinedTestDispatcher()

var gravatarSdkContainerMock = mockk<GravatarSdkContainer>()
var gravatarApiServiceMock = mockk<GravatarApiService>()

override fun apply(
base: Statement,
description: Description,
): Statement {
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
gravatarSdkContainerMock = mockk<GravatarSdkContainer>()
gravatarApiServiceMock = mockk<GravatarApiService>(relaxed = true)
mockkObject(GravatarSdkContainer)
every { GravatarSdkContainer.instance } returns gravatarSdkContainerMock
every { gravatarSdkContainerMock.dispatcherDefault } returns testDispatcher
every { gravatarSdkContainerMock.getGravatarApiService(any()) } returns gravatarApiServiceMock

base.evaluate()
Expand Down
28 changes: 28 additions & 0 deletions gravatar/src/test/java/com/gravatar/GravatarSdkTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.gravatar

import com.gravatar.di.container.GravatarSdkContainer
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement

class GravatarSdkTest : TestRule {
var gravatarSdkContainerMock = mockk<GravatarSdkContainer>()
var gravatarApiServiceMock = mockk<GravatarApiService>()

override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
gravatarSdkContainerMock = mockk<GravatarSdkContainer>()
gravatarApiServiceMock = mockk<GravatarApiService>(relaxed = true)
mockkObject(GravatarSdkContainer)
every { GravatarSdkContainer.instance } returns gravatarSdkContainerMock
every { gravatarSdkContainerMock.getGravatarApiService(any()) } returns gravatarApiServiceMock

base.evaluate()
}
}
}
}

0 comments on commit f81e731

Please sign in to comment.