From 9fcd2b1690beba68697ca69392bc5effe060574a Mon Sep 17 00:00:00 2001 From: David Perez Date: Thu, 30 Nov 2023 09:49:55 -0600 Subject: [PATCH] BIT-685: Add request headers to all network requests in the app (#300) --- .../network/di/PlatformNetworkModule.kt | 7 +++ .../network/interceptor/HeadersInterceptor.kt | 27 +++++++++++ .../network/retrofit/RetrofitsImpl.kt | 3 ++ .../datasource/network/util/HeaderUtils.kt | 45 +++++++++++++++++++ .../interceptor/HeadersInterceptorTest.kt | 35 +++++++++++++++ .../network/retrofit/RetrofitsTest.kt | 10 +++++ .../ui/platform/base/BaseComposeTest.kt | 16 +------ .../ui/platform/base/BaseRobolectricTest.kt | 21 +++++++++ 8 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/HeadersInterceptor.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/HeadersInterceptorTest.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/platform/base/BaseRobolectricTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt index cef1e287af..a0e3f469ff 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.datasource.network.di import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors +import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.HeadersInterceptor import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.RetrofitsImpl import com.x8bit.bitwarden.data.platform.datasource.network.serializer.LocalDateTimeSerializer @@ -37,6 +38,10 @@ object PlatformNetworkModule { @Singleton fun providesAuthTokenInterceptor(): AuthTokenInterceptor = AuthTokenInterceptor() + @Provides + @Singleton + fun providesHeadersInterceptor(): HeadersInterceptor = HeadersInterceptor() + @Provides @Singleton fun providesRefreshAuthenticator(): RefreshAuthenticator = RefreshAuthenticator() @@ -46,12 +51,14 @@ object PlatformNetworkModule { fun provideRetrofits( authTokenInterceptor: AuthTokenInterceptor, baseUrlInterceptors: BaseUrlInterceptors, + headersInterceptor: HeadersInterceptor, refreshAuthenticator: RefreshAuthenticator, json: Json, ): Retrofits = RetrofitsImpl( authTokenInterceptor = authTokenInterceptor, baseUrlInterceptors = baseUrlInterceptors, + headersInterceptor = headersInterceptor, refreshAuthenticator = refreshAuthenticator, json = json, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/HeadersInterceptor.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/HeadersInterceptor.kt new file mode 100644 index 0000000000..413cd79de2 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/HeadersInterceptor.kt @@ -0,0 +1,27 @@ +package com.x8bit.bitwarden.data.platform.datasource.network.interceptor + +import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_CLIENT_NAME +import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_CLIENT_VERSION +import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_DEVICE_TYPE +import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_USER_AGENT +import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_CLIENT_NAME +import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_CLIENT_VERSION +import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_DEVICE_TYPE +import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_USER_AGENT +import okhttp3.Interceptor +import okhttp3.Response + +/** + * Interceptor responsible for adding various headers to all API requests. + */ +class HeadersInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response = chain.proceed( + chain.request() + .newBuilder() + .header(HEADER_KEY_USER_AGENT, HEADER_VALUE_USER_AGENT) + .header(HEADER_KEY_CLIENT_NAME, HEADER_VALUE_CLIENT_NAME) + .header(HEADER_KEY_CLIENT_VERSION, HEADER_VALUE_CLIENT_VERSION) + .header(HEADER_KEY_DEVICE_TYPE, HEADER_VALUE_DEVICE_TYPE) + .build(), + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt index 93e8026e34..b8756521f2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt @@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapt import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptor import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors +import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.HeadersInterceptor import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient @@ -18,6 +19,7 @@ import retrofit2.Retrofit class RetrofitsImpl( authTokenInterceptor: AuthTokenInterceptor, baseUrlInterceptors: BaseUrlInterceptors, + headersInterceptor: HeadersInterceptor, refreshAuthenticator: RefreshAuthenticator, json: Json, ) : Retrofits { @@ -70,6 +72,7 @@ class RetrofitsImpl( private val baseOkHttpClient: OkHttpClient = OkHttpClient.Builder() + .addInterceptor(headersInterceptor) .build() private val authenticatedOkHttpClient: OkHttpClient by lazy { diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/HeaderUtils.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/HeaderUtils.kt index 92816d65f6..57b9d7cbc9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/HeaderUtils.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/HeaderUtils.kt @@ -1,5 +1,8 @@ package com.x8bit.bitwarden.data.platform.datasource.network.util +import android.os.Build +import com.x8bit.bitwarden.BuildConfig + /** * The bearer prefix used for the 'authorization' headers value. */ @@ -9,3 +12,45 @@ const val HEADER_VALUE_BEARER_PREFIX: String = "Bearer " * The key used for the 'authorization' headers. */ const val HEADER_KEY_AUTHORIZATION: String = "Authorization" + +/** + * The key used for the 'user-agent' headers. + */ +const val HEADER_KEY_USER_AGENT: String = "User-Agent" + +/** + * The value used for the 'user-agent' headers. + */ +@Suppress("MaxLineLength") +val HEADER_VALUE_USER_AGENT: String = + "Bitwarden_Mobile/${BuildConfig.VERSION_NAME} (Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT}; Model ${Build.MODEL})" + +/** + * The key used for the 'bitwarden-client-name' headers. + */ +const val HEADER_KEY_CLIENT_NAME: String = "Bitwarden-Client-Name" + +/** + * The value used for the 'bitwarden-client-name' headers. + */ +const val HEADER_VALUE_CLIENT_NAME: String = "mobile" + +/** + * The key used for the 'bitwarden-client-version' headers. + */ +const val HEADER_KEY_CLIENT_VERSION: String = "Bitwarden-Client-Version" + +/** + * The value used for the 'bitwarden-client-version' headers. + */ +const val HEADER_VALUE_CLIENT_VERSION: String = BuildConfig.VERSION_NAME + +/** + * The key used for the 'device-type' headers. + */ +const val HEADER_KEY_DEVICE_TYPE: String = "Device-Type" + +/** + * The value used for the 'device-type' headers. + */ +const val HEADER_VALUE_DEVICE_TYPE: String = "0" diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/HeadersInterceptorTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/HeadersInterceptorTest.kt new file mode 100644 index 0000000000..c664020245 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/HeadersInterceptorTest.kt @@ -0,0 +1,35 @@ +package com.x8bit.bitwarden.data.platform.datasource.network.interceptor + +import android.os.Build +import com.x8bit.bitwarden.BuildConfig +import com.x8bit.bitwarden.ui.platform.base.BaseRobolectricTest +import okhttp3.Request +import org.junit.Assert.assertEquals +import org.junit.Test + +class HeadersInterceptorTest : BaseRobolectricTest() { + + private val headersInterceptors = HeadersInterceptor() + + @Test + fun `intercept should modify original request to include custom headers`() { + // We reference the real BuildConfig here, since we don't want the test to break on every + // version bump. We are also doing the same thing for Build when the SDK gets incremented. + val versionName = BuildConfig.VERSION_NAME + val release = Build.VERSION.RELEASE + val sdk = Build.VERSION.SDK_INT + val originalRequest = Request.Builder().url("http://www.fake.com/").build() + val chain = FakeInterceptorChain(originalRequest) + + val response = headersInterceptors.intercept(chain) + + val request = response.request + assertEquals( + "Bitwarden_Mobile/$versionName (Android $release; SDK $sdk; Model robolectric)", + request.header("User-Agent"), + ) + assertEquals("mobile", request.header("Bitwarden-Client-Name")) + assertEquals(versionName, request.header("Bitwarden-Client-Version")) + assertEquals("0", request.header("Device-Type")) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsTest.kt index 520e8ec4af..182e8a8a81 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsTest.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.datasource.network.retrofit import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors +import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.HeadersInterceptor import io.mockk.every import io.mockk.mockk import io.mockk.slot @@ -37,6 +38,9 @@ class RetrofitsTest { mockIntercept { isEventsInterceptorCalled = true } } } + private val headersInterceptors = mockk { + mockIntercept { isheadersInterceptorCalled = true } + } private val refreshAuthenticator = mockk { mockAuthenticate { isRefreshAuthenticatorCalled = true } } @@ -46,12 +50,14 @@ class RetrofitsTest { private val retrofits = RetrofitsImpl( authTokenInterceptor = authTokenInterceptor, baseUrlInterceptors = baseUrlInterceptors, + headersInterceptor = headersInterceptors, refreshAuthenticator = refreshAuthenticator, json = json, ) private var isAuthInterceptorCalled = false private var isApiInterceptorCalled = false + private var isheadersInterceptorCalled = false private var isIdentityInterceptorCalled = false private var isEventsInterceptorCalled = false private var isRefreshAuthenticatorCalled = false @@ -122,6 +128,7 @@ class RetrofitsTest { assertTrue(isAuthInterceptorCalled) assertTrue(isApiInterceptorCalled) + assertTrue(isheadersInterceptorCalled) assertFalse(isIdentityInterceptorCalled) assertFalse(isEventsInterceptorCalled) } @@ -139,6 +146,7 @@ class RetrofitsTest { assertFalse(isAuthInterceptorCalled) assertTrue(isApiInterceptorCalled) + assertTrue(isheadersInterceptorCalled) assertFalse(isIdentityInterceptorCalled) assertFalse(isEventsInterceptorCalled) } @@ -156,6 +164,7 @@ class RetrofitsTest { assertFalse(isAuthInterceptorCalled) assertFalse(isApiInterceptorCalled) + assertTrue(isheadersInterceptorCalled) assertTrue(isIdentityInterceptorCalled) assertFalse(isEventsInterceptorCalled) } @@ -175,6 +184,7 @@ class RetrofitsTest { assertFalse(isAuthInterceptorCalled) assertFalse(isApiInterceptorCalled) + assertTrue(isheadersInterceptorCalled) assertFalse(isIdentityInterceptorCalled) assertFalse(isEventsInterceptorCalled) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/BaseComposeTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/BaseComposeTest.kt index 1fae5c1805..db99083f27 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/BaseComposeTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/BaseComposeTest.kt @@ -3,30 +3,16 @@ package com.x8bit.bitwarden.ui.platform.base import androidx.compose.runtime.Composable import androidx.compose.ui.test.junit4.createComposeRule import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme -import dagger.hilt.android.testing.HiltTestApplication import org.junit.Rule -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.shadows.ShadowLog /** * A base class that can be used for performing Compose-layer testing using Robolectric, Compose * Testing, and JUnit 4. */ -@Config( - application = HiltTestApplication::class, - sdk = [Config.NEWEST_SDK], -) -@RunWith(RobolectricTestRunner::class) -abstract class BaseComposeTest { +abstract class BaseComposeTest : BaseRobolectricTest() { @get:Rule val composeTestRule = createComposeRule() - init { - ShadowLog.stream = System.out - } - /** * Helper for testing a basic Composable function that only requires a Composable environment * with the [BitwardenTheme]. diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/BaseRobolectricTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/BaseRobolectricTest.kt new file mode 100644 index 0000000000..e5453562d3 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/BaseRobolectricTest.kt @@ -0,0 +1,21 @@ +package com.x8bit.bitwarden.ui.platform.base + +import dagger.hilt.android.testing.HiltTestApplication +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowLog + +/** + * A base class that can be used for performing tests that use Robolectric and JUnit 4. + */ +@Config( + application = HiltTestApplication::class, + sdk = [Config.NEWEST_SDK], +) +@RunWith(RobolectricTestRunner::class) +abstract class BaseRobolectricTest { + init { + ShadowLog.stream = System.out + } +}