Skip to content

Commit

Permalink
feat[mock]: mock primary setup
Browse files Browse the repository at this point in the history
  • Loading branch information
SaifulSaif007 committed Aug 20, 2024
1 parent 8261d46 commit b252da8
Show file tree
Hide file tree
Showing 16 changed files with 2,999 additions and 7 deletions.
3 changes: 3 additions & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ dependencies {
implementation(libs.bundles.retrofit)
implementation(libs.timber)

debugImplementation(libs.mock.webserver)
implementation(libs.logging.interceptor)

testImplementation(libs.junit)
androidTestImplementation(libs.junit.ext)
androidTestImplementation(libs.espresso)
Expand Down
10 changes: 5 additions & 5 deletions core/src/main/java/com/saiful/core/data/ResponseInterceptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ class ResponseInterceptor(
return chain.proceed(chain.request()).apply {
if (!this.isSuccessful) {
try {
when (this.code()) {
when (this.code) {
HttpURLConnection.HTTP_UNAUTHORIZED -> {
this.body()?.source()?.let { source ->
this.body?.source()?.let { source ->
moshi.adapter(ApiErrorResponse::class.java).fromJson(source)
?.let { apiErrorResponse ->
throw DomainException(
error = if (apiErrorResponse.errors.isNotEmpty()) apiErrorResponse.errors[0] else genericErrorMsg,
message = "unauthorized",
code = this.code()
code = this.code
)
}
}
Expand All @@ -33,15 +33,15 @@ class ResponseInterceptor(
throw DomainException(
error = "unknown",
message = genericErrorMsg,
code = this.code()
code = this.code
)
}
}
} catch (ex: Exception) {
throw DomainException(
error = "unknown",
message = genericErrorMsg,
code = this.code()
code = this.code
)
}
}
Expand Down
21 changes: 20 additions & 1 deletion core/src/main/java/com/saiful/core/di/CoreNetworkModule.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package com.saiful.core.di

import com.saiful.core.BuildConfig
import com.saiful.core.data.RequestInterceptor
import com.saiful.core.data.ResponseInterceptor
import com.saiful.core.di.qualifiers.BaseUrl
import com.saiful.core.di.qualifiers.GenericErrorMessage
import com.saiful.core.mock.*
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit
Expand All @@ -19,6 +22,14 @@ import java.util.concurrent.TimeUnit
@InstallIn(SingletonComponent::class)
object CoreNetworkModule {

private val loggingInterceptor: HttpLoggingInterceptor = if (BuildConfig.DEBUG) {
val logger = HttpLoggingInterceptor()
logger.setLevel(HttpLoggingInterceptor.Level.BODY)
} else {
val logger = HttpLoggingInterceptor()
logger.setLevel(HttpLoggingInterceptor.Level.NONE)
}

@Provides
fun provideMoshi(): Moshi {
return Moshi.Builder()
Expand All @@ -45,14 +56,22 @@ object CoreNetworkModule {
@Provides
fun provideOKHttpClient(
requestInterceptor: RequestInterceptor,
responseInterceptor: ResponseInterceptor
responseInterceptor: ResponseInterceptor,
@BaseUrl baseUrl: String,
mockMaker: MockMaker,
mockServerManager: MockServerManager
): OkHttpClient {

mockServerManager.startServer()

return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.addInterceptor(requestInterceptor)
.addInterceptor(responseInterceptor)
.addInterceptor(MockInterceptor(baseUrl, mockMaker))
.addNetworkInterceptor(loggingInterceptor)
.build()
}

Expand Down
48 changes: 48 additions & 0 deletions core/src/main/java/com/saiful/core/di/MockApiModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.saiful.core.di

import com.saiful.core.mock.*
import com.saiful.core.utils.DispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.*
import timber.log.Timber
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object MockApiModule {

@Singleton
@Provides
fun provideCoroutineScope(dispatcherProvider: DispatcherProvider): CoroutineScope {
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Timber.e("CoreModule -> CoroutineExceptionHandler -> ${throwable.localizedMessage}")
}
return CoroutineScope(SupervisorJob() + dispatcherProvider.io() + exceptionHandler)
}

@Singleton
@Provides
fun provideMockMaker(): MockMaker {
return MockMaker().apply {
putMap(PhotoMockApi.getMap())
}
}

@Singleton
@Provides
fun provideMockDispatcher(mockMaker: MockMaker): MockDispatcher {
return MockDispatcher(mockMaker)
}

@Singleton
@Provides
fun provideMockManager(
coroutineScope: CoroutineScope,
mockDispatcher: MockDispatcher
): MockServerManager {
return MockServerManager(coroutineScope, mockDispatcher)
}
}
11 changes: 11 additions & 0 deletions core/src/main/java/com/saiful/core/mock/MockApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.saiful.core.mock

import okhttp3.mockwebserver.RecordedRequest

data class MockApi(
val path: String,
val shouldMock: Boolean = true,
val responseCode: Int = 200,
val delay: Long = 500L,
val findPath: (RecordedRequest) -> String = { path }
)
26 changes: 26 additions & 0 deletions core/src/main/java/com/saiful/core/mock/MockDispatcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.saiful.core.mock

import okhttp3.mockwebserver.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject

class MockDispatcher @Inject constructor(
private val mockMaker: MockMaker
) : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
return try {
val apiEndPoint = request.path?.removePrefix("/") ?: ""
mockMaker.findMockApi(apiEndPoint)?.let { mockApi ->
MockResponse().setResponseCode(mockApi.responseCode).setBody(
ResourceUtils.getJsonString("${mockApi.findPath(request)}/${mockApi.responseCode}.json")
).apply {
if (mockApi.delay > 0) this.setBodyDelay(mockApi.delay, TimeUnit.MILLISECONDS)
}
} ?: throw NullPointerException("API mocker not found!")


} catch (ex: Exception) {
MockResponse().setResponseCode(404)
}
}
}
25 changes: 25 additions & 0 deletions core/src/main/java/com/saiful/core/mock/MockInterceptor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.saiful.core.mock

import com.saiful.core.components.logger.logDebug
import okhttp3.*

class MockInterceptor(
private val baseUrl: String,
private val mockMaker: MockMaker,
) : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
var request: Request = chain.request()
val apiEndPoint = request.url.toString().replace(baseUrl, "")
if (mockMaker.findMockApi(apiEndPoint)?.shouldMock == true) {
val newBaseUrl = "http://localhost:8080/"
val newEndPointUrl = newBaseUrl + apiEndPoint
request = request.newBuilder()
.url(newEndPointUrl)
.build()
}
logDebug("MOCK", "Mocker: request -> $request")
return chain.proceed(request)
}

}
23 changes: 23 additions & 0 deletions core/src/main/java/com/saiful/core/mock/MockMaker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.saiful.core.mock

class MockMaker {

private val mockApis = mutableMapOf<Regex, MockApi>()

private fun getMapper() = buildMap {
putAll(mockApis)
}

fun putMap(map: Map<Regex, MockApi>) {
mockApis.putAll(map)
}

fun findMockApi(api: String): MockApi? {
for ((regex, mockApi) in getMapper()) {
if (regex.containsMatchIn(api)) {
return mockApi
}
}
return null
}
}
25 changes: 25 additions & 0 deletions core/src/main/java/com/saiful/core/mock/MockServerManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.saiful.core.mock

import com.saiful.core.components.logger.logDebug
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import okhttp3.mockwebserver.MockWebServer

class MockServerManager(
private val coroutineScope: CoroutineScope,
private val mockDispatcher: MockDispatcher
) {
private var mockServer: MockWebServer? = null

fun startServer() {
coroutineScope.launch {
mockServer = MockWebServer()
mockServer?.apply {
dispatcher = mockDispatcher
}?.start(8080)
}.invokeOnCompletion {
logDebug(msg = "Mock server started")
}
}

}
20 changes: 20 additions & 0 deletions core/src/main/java/com/saiful/core/mock/PhotoMockApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.saiful.core.mock

object PhotoMockApi {

private val PHOTOS = Regex("^photos\\?page=\\d+&per_page=\\d+\$")

fun getMap(): Map<Regex, MockApi> = mapOf(
PHOTOS to MockApi(
path = "photos",
findPath = { recordedRequest ->
val regex = """page=(\d+)""".toRegex()
when (regex.find(recordedRequest.path.toString())?.groupValues?.get(1) ?: "") {
"1" -> "photos/one"
"2" -> "photos/two"
else -> "photos"
}
}
)
)
}
24 changes: 24 additions & 0 deletions core/src/main/java/com/saiful/core/mock/ResourceUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.saiful.core.mock

import okio.buffer
import okio.source
import java.io.IOException
import java.nio.charset.StandardCharsets

internal object ResourceUtils {
@Throws(exceptionClasses = [IOException::class])
fun getJsonString(path : String) : String {
// Load the JSON response
return try {
this.javaClass
.classLoader
?.getResourceAsStream(path)
?.source()
?.buffer()
?.readString(StandardCharsets.UTF_8)
.orEmpty()
} catch (exception: IOException) {
throw exception
}
}
}
Loading

0 comments on commit b252da8

Please sign in to comment.