Skip to content
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: 2 additions & 2 deletions gradle/libraries.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
# -------------------------------------------------------------------
jdk = "17"
minSdk = "24"
targetSdk = "34"
compileSdk = "36"
targetSdk = "35"
compileSdk = "35"
sourceCompatibility = "17"
targetCompatibility = "17"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ internal fun <S : Any, E : Any> Int.toAnswer(
bodyError: E? = null,
message: String,
): Answer<S, NetworkError<E>> = when (this) {
in (200..399) -> Answer.Success(body!!)
in (200..399) -> body?.let { Answer.Success(it) }
?: Answer.Failure(
NetworkError.Unknown(
code = this,
body = bodyError,
message = "Success response with null body"
)
)
in (400..499) -> Answer.Failure(
NetworkError.Client(
code = this,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.raxdenstudios.commons.network.interceptor

import okhttp3.Interceptor
import okhttp3.Response

class AuthTokenInterceptor(
private val tokenProvider: () -> String?
) : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val token = tokenProvider()

val request = if (token != null) {
originalRequest.newBuilder()
.header("Authorization", "Bearer $token")
.build()
} else {
originalRequest
}

return chain.proceed(request)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ class CacheLoggerInterceptor(

override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.networkResponse != null)
printMessage("Response from network")
if (response.cacheResponse != null)
printMessage("(HIT) Response from cache")
response.networkResponse?.let { printMessage("Response from network") }
response.cacheResponse?.let { printMessage("(HIT) Response from cache") }
return response
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@ class CacheOfflineInterceptor(

@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()

if (!isNetworkAvailable()) {
request = chain.request().newBuilder()
val request = if (!isNetworkAvailable()) {
chain.request().newBuilder()
.removeHeader(HEADER_PRAGMA)
.removeHeader(HEADER_CACHE_CONTROL)
.cacheControl(cacheControl)
.build()
} else {
chain.request()
}
return chain.proceed(request)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.raxdenstudios.commons.network.interceptor

import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException

class ErrorResponseInterceptor : Interceptor {

@Suppress("MagicNumber")
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())

if (!response.isSuccessful) {
val errorBody = response.body?.string()
throw when (response.code) {
401 -> UnauthorizedException(errorBody)
403 -> ForbiddenException(errorBody)
404 -> NotFoundException(errorBody)
500 -> ServerException(errorBody)
else -> HttpException(response.code, errorBody)
}
}

return response
}
}

open class HttpException(
val code: Int,
val errorBody: String?
) : IOException("HTTP $code: ${errorBody ?: "Unknown error"}")

class UnauthorizedException(errorBody: String?) : HttpException(401, errorBody)
class ForbiddenException(errorBody: String?) : HttpException(403, errorBody)
class NotFoundException(errorBody: String?) : HttpException(404, errorBody)
class ServerException(errorBody: String?) : HttpException(500, errorBody)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.raxdenstudios.commons.network.interceptor

import okhttp3.Interceptor
import okhttp3.Response

class HeadersInterceptor(
private val headers: Map<String, String>
) : Interceptor {

companion object {
fun withUserAgent(userAgent: String) = HeadersInterceptor(
mapOf("User-Agent" to userAgent)
)

fun withApiVersion(version: String) = HeadersInterceptor(
mapOf("X-API-Version" to version)
)

fun withHeaders(vararg pairs: Pair<String, String>) = HeadersInterceptor(
mapOf(*pairs)
)
}

override fun intercept(chain: Interceptor.Chain): Response {
val requestBuilder = chain.request().newBuilder()
headers.forEach { (key, value) ->
requestBuilder.header(key, value)
}
return chain.proceed(requestBuilder.build())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.raxdenstudios.commons.network.interceptor

import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException

class NetworkMonitorInterceptor(
private val isNetworkAvailable: () -> Boolean
) : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
if (!isNetworkAvailable()) {
throw NoNetworkException("No network connection available")
}
return chain.proceed(chain.request())
}
}

class NoNetworkException(message: String) : IOException(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.raxdenstudios.commons.network.interceptor

import okhttp3.Interceptor
import okhttp3.Response

class RequestTimingInterceptor(
private val onRequestCompleted: (url: String, durationMs: Long) -> Unit
) : Interceptor {

@Suppress("TooGenericExceptionCaught")
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val startTime = System.currentTimeMillis()

val response = try {
chain.proceed(request)
} finally {
val duration = System.currentTimeMillis() - startTime
onRequestCompleted(request.url.toString(), duration)
}

return response
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.raxdenstudios.commons.network.interceptor

import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException

class RetryInterceptor(
private val maxRetries: Int = 3,
private val initialDelayMs: Long = 1000,
private val shouldRetry: (Response) -> Boolean = { !it.isSuccessful }
) : Interceptor {

companion object {
val default = RetryInterceptor()

fun withMaxRetries(retries: Int) = RetryInterceptor(maxRetries = retries)

fun withDelay(delayMs: Long) = RetryInterceptor(initialDelayMs = delayMs)
}

@Suppress("TooGenericExceptionCaught")
override fun intercept(chain: Interceptor.Chain): Response {
var attempt = 0
var response: Response? = null
var exception: IOException? = null

while (attempt < maxRetries) {
try {
response = chain.proceed(chain.request())

if (!shouldRetry(response)) {
return response
}

response.close()
} catch (e: IOException) {
exception = e
}

attempt++
if (attempt < maxRetries) {
Thread.sleep(initialDelayMs * (1 shl (attempt - 1)))
}
}

throw exception ?: IOException("Max retries exceeded")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.raxdenstudios.commons.network.interceptor

import com.google.common.truth.Truth.assertThat
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import org.junit.Test

internal class AuthTokenInterceptorTest {

@Test
fun `intercept should add Authorization header when token is available`() {
val token = "test-token-123"
val tokenProvider = { token }
val interceptor = AuthTokenInterceptor(tokenProvider)

val chain = mockk<Interceptor.Chain>()
val request = mockk<Request>()
val requestBuilder = mockk<Request.Builder>()
val authorizedRequest = mockk<Request>()
val response = mockk<Response>()

every { chain.request() } returns request
every { request.newBuilder() } returns requestBuilder
every { requestBuilder.header("Authorization", "Bearer $token") } returns requestBuilder
every { requestBuilder.build() } returns authorizedRequest
every { chain.proceed(authorizedRequest) } returns response

val result = interceptor.intercept(chain)

assertThat(result).isEqualTo(response)
verify { requestBuilder.header("Authorization", "Bearer $token") }
verify { chain.proceed(authorizedRequest) }
}

@Test
fun `intercept should not add Authorization header when token is null`() {
val tokenProvider = { null }
val interceptor = AuthTokenInterceptor(tokenProvider)

val chain = mockk<Interceptor.Chain>()
val request = mockk<Request>()
val response = mockk<Response>()

every { chain.request() } returns request
every { chain.proceed(request) } returns response

val result = interceptor.intercept(chain)

assertThat(result).isEqualTo(response)
verify { chain.proceed(request) }
}

@Test
fun `intercept should use latest token from provider`() {
var token: String? = "initial-token"
val tokenProvider = { token }
val interceptor = AuthTokenInterceptor(tokenProvider)

val chain = mockk<Interceptor.Chain>()
val request = mockk<Request>()
val requestBuilder = mockk<Request.Builder>()
val authorizedRequest = mockk<Request>()
val response = mockk<Response>()

every { chain.request() } returns request
every { request.newBuilder() } returns requestBuilder
every { requestBuilder.header("Authorization", any()) } returns requestBuilder
every { requestBuilder.build() } returns authorizedRequest
every { chain.proceed(authorizedRequest) } returns response

interceptor.intercept(chain)
verify { requestBuilder.header("Authorization", "Bearer initial-token") }

token = "updated-token"
interceptor.intercept(chain)
verify { requestBuilder.header("Authorization", "Bearer updated-token") }
}
}
Loading
Loading