From 5cea6956667a37697587d089c1efc1b4a56fb938 Mon Sep 17 00:00:00 2001 From: Polat Olu Date: Mon, 13 Oct 2025 10:55:07 +0100 Subject: [PATCH 1/7] implementation and testing --- .../src/main/java/com/flagsmith/Flagsmith.kt | 2 +- .../internal/FlagsmithEventService.kt | 3 + .../internal/FlagsmithRetrofitService.kt | 23 +++++ .../test/java/com/flagsmith/UserAgentTests.kt | 83 +++++++++++++++++++ 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt diff --git a/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt b/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt index 2842383..bf24dc6 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt @@ -51,7 +51,7 @@ class Flagsmith constructor( private val eventService: FlagsmithEventService? = if (!enableRealtimeUpdates) null - else FlagsmithEventService(eventSourceBaseUrl = eventSourceBaseUrl, environmentKey = environmentKey) { event -> + else FlagsmithEventService(eventSourceBaseUrl = eventSourceBaseUrl, environmentKey = environmentKey, context = context) { event -> if (event.isSuccess) { lastEventUpdate = event.getOrNull()?.updatedAt ?: lastEventUpdate diff --git a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt index e10888d..8d24fda 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt @@ -1,5 +1,6 @@ package com.flagsmith.internal +import android.content.Context import android.util.Log import com.flagsmith.entities.FlagEvent import com.google.gson.Gson @@ -15,10 +16,12 @@ import java.util.concurrent.TimeUnit internal class FlagsmithEventService constructor( private val eventSourceBaseUrl: String?, private val environmentKey: String, + private val context: Context?, private val updates: (Result) -> Unit ) { private val sseClient = OkHttpClient.Builder() .addInterceptor(FlagsmithRetrofitService.envKeyInterceptor(environmentKey)) + .addInterceptor(FlagsmithRetrofitService.userAgentInterceptor(context)) .connectTimeout(6, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.MINUTES) .writeTimeout(10, TimeUnit.MINUTES) diff --git a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt index 60e9eab..1a06cde 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt @@ -39,6 +39,28 @@ interface FlagsmithRetrofitService { private const val UPDATED_AT_HEADER = "x-flagsmith-document-updated-at" private const val ACCEPT_HEADER_VALUE = "application/json" private const val CONTENT_TYPE_HEADER_VALUE = "application/json; charset=utf-8" + private const val USER_AGENT_HEADER = "User-Agent" + private const val USER_AGENT_PREFIX = "flagsmith-kotlin-android-sdk" + + private fun getUserAgent(context: Context?): String { + return try { + val packageInfo = context?.packageManager?.getPackageInfo(context.packageName, 0) + val version = packageInfo?.versionName ?: "unknown" + "$USER_AGENT_PREFIX/$version" + } catch (e: Exception) { + "$USER_AGENT_PREFIX/unknown" + } + } + + fun userAgentInterceptor(context: Context?): Interceptor { + return Interceptor { chain -> + val userAgent = getUserAgent(context) + val request = chain.request().newBuilder() + .addHeader(USER_AGENT_HEADER, userAgent) + .build() + chain.proceed(request) + } + } fun create( baseUrl: String, @@ -92,6 +114,7 @@ interface FlagsmithRetrofitService { val client = OkHttpClient.Builder() .addInterceptor(envKeyInterceptor(environmentKey)) + .addInterceptor(userAgentInterceptor(context)) .addInterceptor(updatedAtInterceptor(timeTracker)) .addInterceptor(jsonContentTypeInterceptor()) .let { if (cacheConfig.enableCache) it.addNetworkInterceptor(cacheControlInterceptor()) else it } diff --git a/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt b/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt new file mode 100644 index 0000000..c4a5f38 --- /dev/null +++ b/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt @@ -0,0 +1,83 @@ +package com.flagsmith + +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import com.flagsmith.internal.FlagsmithRetrofitService +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.junit.MockitoJUnitRunner +import kotlin.test.assertEquals + +@RunWith(MockitoJUnitRunner::class) +class UserAgentTests { + + @Mock + private lateinit var mockContext: Context + + @Mock + private lateinit var mockPackageManager: PackageManager + + @Mock + private lateinit var mockPackageInfo: PackageInfo + + @Test + fun `test User-Agent header format with valid version`() { + // Given + val expectedVersion = "1.2.3" + mockPackageInfo.versionName = expectedVersion + Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager) + Mockito.`when`(mockPackageManager.getPackageInfo(mockContext.packageName, 0)).thenReturn(mockPackageInfo) + + // When + val interceptor = FlagsmithRetrofitService.userAgentInterceptor(mockContext) + + // Then + // We can't easily test the interceptor without making actual HTTP calls, + // but we can verify the User-Agent string format by testing the logic + val expectedUserAgent = "flagsmith-kotlin-android-sdk/$expectedVersion" + assertEquals(expectedUserAgent, "flagsmith-kotlin-android-sdk/$expectedVersion") + } + + @Test + fun `test User-Agent header format with null context`() { + // When + val interceptor = FlagsmithRetrofitService.userAgentInterceptor(null) + + // Then + val expectedUserAgent = "flagsmith-kotlin-android-sdk/unknown" + assertEquals(expectedUserAgent, "flagsmith-kotlin-android-sdk/unknown") + } + + @Test + fun `test User-Agent header format with exception during version retrieval`() { + // Given + Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager) + Mockito.`when`(mockPackageManager.getPackageInfo(mockContext.packageName, 0)) + .thenThrow(PackageManager.NameNotFoundException("Package not found")) + + // When + val interceptor = FlagsmithRetrofitService.userAgentInterceptor(mockContext) + + // Then + val expectedUserAgent = "flagsmith-kotlin-android-sdk/unknown" + assertEquals(expectedUserAgent, "flagsmith-kotlin-android-sdk/unknown") + } + + @Test + fun `test User-Agent header format with null version name`() { + // Given + mockPackageInfo.versionName = null + Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager) + Mockito.`when`(mockPackageManager.getPackageInfo(mockContext.packageName, 0)).thenReturn(mockPackageInfo) + + // When + val interceptor = FlagsmithRetrofitService.userAgentInterceptor(mockContext) + + // Then + val expectedUserAgent = "flagsmith-kotlin-android-sdk/unknown" + assertEquals(expectedUserAgent, "flagsmith-kotlin-android-sdk/unknown") + } +} \ No newline at end of file From b184964126207d6c2d8c22ca535a953925ad202b Mon Sep 17 00:00:00 2001 From: Polat Olu Date: Thu, 16 Oct 2025 16:44:06 +0100 Subject: [PATCH 2/7] Test improvements --- .../test/java/com/flagsmith/UserAgentTests.kt | 220 +++++++++++++++--- 1 file changed, 191 insertions(+), 29 deletions(-) diff --git a/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt b/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt index c4a5f38..4286921 100644 --- a/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt +++ b/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt @@ -3,17 +3,28 @@ package com.flagsmith import android.content.Context import android.content.pm.PackageInfo import android.content.pm.PackageManager -import com.flagsmith.internal.FlagsmithRetrofitService +import com.flagsmith.entities.Trait +import com.flagsmith.mockResponses.MockEndpoint +import com.flagsmith.mockResponses.mockResponseFor +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test -import org.junit.runner.RunWith +import org.mockserver.integration.ClientAndServer +import org.mockserver.model.HttpRequest.request import org.mockito.Mock import org.mockito.Mockito import org.mockito.junit.MockitoJUnitRunner -import kotlin.test.assertEquals +import org.junit.runner.RunWith @RunWith(MockitoJUnitRunner::class) class UserAgentTests { + private lateinit var mockServer: ClientAndServer + private lateinit var flagsmith: Flagsmith + @Mock private lateinit var mockContext: Context @@ -23,61 +34,212 @@ class UserAgentTests { @Mock private lateinit var mockPackageInfo: PackageInfo + @Before + fun setup() { + mockServer = ClientAndServer.startClientAndServer() + } + + @After + fun tearDown() { + mockServer.stop() + } + @Test - fun `test User-Agent header format with valid version`() { - // Given - val expectedVersion = "1.2.3" - mockPackageInfo.versionName = expectedVersion + fun testUserAgentHeaderSentWithValidVersion() { + // Given - Use a realistic app version (not SDK version) + // The User-Agent shows the APP's version, not the SDK's version + // This helps Flagsmith support team identify which app version is making requests + val expectedAppVersion = "2.4.1" + mockPackageInfo.versionName = expectedAppVersion Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager) + Mockito.`when`(mockContext.packageName).thenReturn("com.test.app") Mockito.`when`(mockPackageManager.getPackageInfo(mockContext.packageName, 0)).thenReturn(mockPackageInfo) + flagsmith = Flagsmith( + environmentKey = "test-key", + baseUrl = "http://localhost:${mockServer.localPort}", + context = mockContext, + enableAnalytics = false, + cacheConfig = FlagsmithCacheConfig(enableCache = false) + ) + + mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) + // When - val interceptor = FlagsmithRetrofitService.userAgentInterceptor(mockContext) - - // Then - // We can't easily test the interceptor without making actual HTTP calls, - // but we can verify the User-Agent string format by testing the logic - val expectedUserAgent = "flagsmith-kotlin-android-sdk/$expectedVersion" - assertEquals(expectedUserAgent, "flagsmith-kotlin-android-sdk/$expectedVersion") + runBlocking { + val result = flagsmith.getFeatureFlagsSync() + assertTrue(result.isSuccess) + } + + // Then - Verify User-Agent contains the APP's version, not SDK version + mockServer.verify( + request() + .withPath("/flags/") + .withMethod("GET") + .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/$expectedAppVersion") + ) } @Test - fun `test User-Agent header format with null context`() { + fun testUserAgentHeaderSentWithNullContext() { + // Given + flagsmith = Flagsmith( + environmentKey = "test-key", + baseUrl = "http://localhost:${mockServer.localPort}", + context = null, + enableAnalytics = false, + cacheConfig = FlagsmithCacheConfig(enableCache = false) + ) + + mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) + // When - val interceptor = FlagsmithRetrofitService.userAgentInterceptor(null) - + runBlocking { + val result = flagsmith.getFeatureFlagsSync() + assertTrue(result.isSuccess) + } + // Then - val expectedUserAgent = "flagsmith-kotlin-android-sdk/unknown" - assertEquals(expectedUserAgent, "flagsmith-kotlin-android-sdk/unknown") + mockServer.verify( + request() + .withPath("/flags/") + .withMethod("GET") + .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") + ) } @Test - fun `test User-Agent header format with exception during version retrieval`() { + fun testUserAgentHeaderSentWithExceptionDuringVersionRetrieval() { // Given Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager) + Mockito.`when`(mockContext.packageName).thenReturn("com.test.app") Mockito.`when`(mockPackageManager.getPackageInfo(mockContext.packageName, 0)) .thenThrow(PackageManager.NameNotFoundException("Package not found")) + flagsmith = Flagsmith( + environmentKey = "test-key", + baseUrl = "http://localhost:${mockServer.localPort}", + context = mockContext, + enableAnalytics = false, + cacheConfig = FlagsmithCacheConfig(enableCache = false) + ) + + mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) + // When - val interceptor = FlagsmithRetrofitService.userAgentInterceptor(mockContext) - + runBlocking { + val result = flagsmith.getFeatureFlagsSync() + assertTrue(result.isSuccess) + } + // Then - val expectedUserAgent = "flagsmith-kotlin-android-sdk/unknown" - assertEquals(expectedUserAgent, "flagsmith-kotlin-android-sdk/unknown") + mockServer.verify( + request() + .withPath("/flags/") + .withMethod("GET") + .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") + ) } @Test - fun `test User-Agent header format with null version name`() { + fun testUserAgentHeaderSentWithNullVersionName() { // Given mockPackageInfo.versionName = null Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager) + Mockito.`when`(mockContext.packageName).thenReturn("com.test.app") Mockito.`when`(mockPackageManager.getPackageInfo(mockContext.packageName, 0)).thenReturn(mockPackageInfo) + flagsmith = Flagsmith( + environmentKey = "test-key", + baseUrl = "http://localhost:${mockServer.localPort}", + context = mockContext, + enableAnalytics = false, + cacheConfig = FlagsmithCacheConfig(enableCache = false) + ) + + mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) + // When - val interceptor = FlagsmithRetrofitService.userAgentInterceptor(mockContext) - + runBlocking { + val result = flagsmith.getFeatureFlagsSync() + assertTrue(result.isSuccess) + } + // Then - val expectedUserAgent = "flagsmith-kotlin-android-sdk/unknown" - assertEquals(expectedUserAgent, "flagsmith-kotlin-android-sdk/unknown") + mockServer.verify( + request() + .withPath("/flags/") + .withMethod("GET") + .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") + ) + } + + @Test + fun testUserAgentHeaderSentWithIdentityRequest() { + // Given + val expectedVersion = "2.1.0" + mockPackageInfo.versionName = expectedVersion + Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager) + Mockito.`when`(mockContext.packageName).thenReturn("com.test.app") + Mockito.`when`(mockPackageManager.getPackageInfo(mockContext.packageName, 0)).thenReturn(mockPackageInfo) + + flagsmith = Flagsmith( + environmentKey = "test-key", + baseUrl = "http://localhost:${mockServer.localPort}", + context = mockContext, + enableAnalytics = false, + cacheConfig = FlagsmithCacheConfig(enableCache = false) + ) + + mockServer.mockResponseFor(MockEndpoint.GET_IDENTITIES) + + // When + runBlocking { + val result = flagsmith.getIdentitySync("test-user") + assertTrue(result.isSuccess) + } + + // Then + mockServer.verify( + request() + .withPath("/identities/") + .withMethod("GET") + .withQueryStringParameter("identifier", "test-user") + .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/$expectedVersion") + ) + } + + @Test + fun testUserAgentHeaderSentWithTraitRequest() { + // Given + val expectedAppVersion = "3.0.1" + mockPackageInfo.versionName = expectedAppVersion + Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager) + Mockito.`when`(mockContext.packageName).thenReturn("com.test.app") + Mockito.`when`(mockPackageManager.getPackageInfo(mockContext.packageName, 0)).thenReturn(mockPackageInfo) + + flagsmith = Flagsmith( + environmentKey = "test-key", + baseUrl = "http://localhost:${mockServer.localPort}", + context = mockContext, + enableAnalytics = false, + cacheConfig = FlagsmithCacheConfig(enableCache = false) + ) + + mockServer.mockResponseFor(MockEndpoint.SET_TRAIT) + + // When + runBlocking { + val result = flagsmith.setTraitSync(Trait(key = "test-key", traitValue = "test-value"), "test-user") + assertTrue(result.isSuccess) + } + + // Then - Verify the traits request has correct User-Agent + mockServer.verify( + request() + .withPath("/identities/") + .withMethod("POST") + .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/$expectedAppVersion") + ) } } \ No newline at end of file From e7b671fbf757f0337fd015c960405588e6b5afeb Mon Sep 17 00:00:00 2001 From: Polat Olu Date: Fri, 17 Oct 2025 13:17:05 +0100 Subject: [PATCH 3/7] Review updates --- .../internal/FlagsmithRetrofitService.kt | 15 +++- .../test/java/com/flagsmith/UserAgentTests.kt | 82 ++++--------------- 2 files changed, 29 insertions(+), 68 deletions(-) diff --git a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt index 1a06cde..8ca4a81 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt @@ -43,12 +43,19 @@ interface FlagsmithRetrofitService { private const val USER_AGENT_PREFIX = "flagsmith-kotlin-android-sdk" private fun getUserAgent(context: Context?): String { + val sdkVersion = getSdkVersion() + return "$USER_AGENT_PREFIX/$sdkVersion" + } + + private fun getSdkVersion(): String { return try { - val packageInfo = context?.packageManager?.getPackageInfo(context.packageName, 0) - val version = packageInfo?.versionName ?: "unknown" - "$USER_AGENT_PREFIX/$version" + // Try to get version from BuildConfig + val buildConfigClass = Class.forName("com.flagsmith.kotlin.BuildConfig") + val versionField = buildConfigClass.getField("VERSION_NAME") + versionField.get(null) as String } catch (e: Exception) { - "$USER_AGENT_PREFIX/unknown" + // Fallback to hardcoded version if BuildConfig is not available + "unknown" } } diff --git a/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt b/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt index 4286921..8759275 100644 --- a/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt +++ b/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt @@ -1,39 +1,21 @@ package com.flagsmith -import android.content.Context -import android.content.pm.PackageInfo -import android.content.pm.PackageManager import com.flagsmith.entities.Trait import com.flagsmith.mockResponses.MockEndpoint import com.flagsmith.mockResponses.mockResponseFor import kotlinx.coroutines.runBlocking import org.junit.After -import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.mockserver.integration.ClientAndServer import org.mockserver.model.HttpRequest.request -import org.mockito.Mock -import org.mockito.Mockito -import org.mockito.junit.MockitoJUnitRunner -import org.junit.runner.RunWith -@RunWith(MockitoJUnitRunner::class) class UserAgentTests { private lateinit var mockServer: ClientAndServer private lateinit var flagsmith: Flagsmith - @Mock - private lateinit var mockContext: Context - - @Mock - private lateinit var mockPackageManager: PackageManager - - @Mock - private lateinit var mockPackageInfo: PackageInfo - @Before fun setup() { mockServer = ClientAndServer.startClientAndServer() @@ -46,19 +28,13 @@ class UserAgentTests { @Test fun testUserAgentHeaderSentWithValidVersion() { - // Given - Use a realistic app version (not SDK version) - // The User-Agent shows the APP's version, not the SDK's version - // This helps Flagsmith support team identify which app version is making requests - val expectedAppVersion = "2.4.1" - mockPackageInfo.versionName = expectedAppVersion - Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager) - Mockito.`when`(mockContext.packageName).thenReturn("com.test.app") - Mockito.`when`(mockPackageManager.getPackageInfo(mockContext.packageName, 0)).thenReturn(mockPackageInfo) - + // Given - The User-Agent now shows SDK version or "unknown" (not app version) + // This is because getUserAgent() method was updated to return SDK version + // In tests, BuildConfig is not available, so it returns "unknown" flagsmith = Flagsmith( environmentKey = "test-key", baseUrl = "http://localhost:${mockServer.localPort}", - context = mockContext, + context = null, enableAnalytics = false, cacheConfig = FlagsmithCacheConfig(enableCache = false) ) @@ -71,12 +47,12 @@ class UserAgentTests { assertTrue(result.isSuccess) } - // Then - Verify User-Agent contains the APP's version, not SDK version + // Then - Verify User-Agent contains "unknown" since BuildConfig is not available in tests mockServer.verify( request() .withPath("/flags/") .withMethod("GET") - .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/$expectedAppVersion") + .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") ) } @@ -110,16 +86,11 @@ class UserAgentTests { @Test fun testUserAgentHeaderSentWithExceptionDuringVersionRetrieval() { - // Given - Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager) - Mockito.`when`(mockContext.packageName).thenReturn("com.test.app") - Mockito.`when`(mockPackageManager.getPackageInfo(mockContext.packageName, 0)) - .thenThrow(PackageManager.NameNotFoundException("Package not found")) - + // Given - Even with context, getUserAgent() now returns SDK version or "unknown" flagsmith = Flagsmith( environmentKey = "test-key", baseUrl = "http://localhost:${mockServer.localPort}", - context = mockContext, + context = null, enableAnalytics = false, cacheConfig = FlagsmithCacheConfig(enableCache = false) ) @@ -143,16 +114,11 @@ class UserAgentTests { @Test fun testUserAgentHeaderSentWithNullVersionName() { - // Given - mockPackageInfo.versionName = null - Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager) - Mockito.`when`(mockContext.packageName).thenReturn("com.test.app") - Mockito.`when`(mockPackageManager.getPackageInfo(mockContext.packageName, 0)).thenReturn(mockPackageInfo) - + // Given - getUserAgent() now returns SDK version or "unknown" regardless of context flagsmith = Flagsmith( environmentKey = "test-key", baseUrl = "http://localhost:${mockServer.localPort}", - context = mockContext, + context = null, enableAnalytics = false, cacheConfig = FlagsmithCacheConfig(enableCache = false) ) @@ -176,17 +142,11 @@ class UserAgentTests { @Test fun testUserAgentHeaderSentWithIdentityRequest() { - // Given - val expectedVersion = "2.1.0" - mockPackageInfo.versionName = expectedVersion - Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager) - Mockito.`when`(mockContext.packageName).thenReturn("com.test.app") - Mockito.`when`(mockPackageManager.getPackageInfo(mockContext.packageName, 0)).thenReturn(mockPackageInfo) - + // Given - getUserAgent() now returns SDK version or "unknown" flagsmith = Flagsmith( environmentKey = "test-key", baseUrl = "http://localhost:${mockServer.localPort}", - context = mockContext, + context = null, enableAnalytics = false, cacheConfig = FlagsmithCacheConfig(enableCache = false) ) @@ -199,29 +159,23 @@ class UserAgentTests { assertTrue(result.isSuccess) } - // Then + // Then - Verify User-Agent contains "unknown" since BuildConfig is not available in tests mockServer.verify( request() .withPath("/identities/") .withMethod("GET") .withQueryStringParameter("identifier", "test-user") - .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/$expectedVersion") + .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") ) } @Test fun testUserAgentHeaderSentWithTraitRequest() { - // Given - val expectedAppVersion = "3.0.1" - mockPackageInfo.versionName = expectedAppVersion - Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager) - Mockito.`when`(mockContext.packageName).thenReturn("com.test.app") - Mockito.`when`(mockPackageManager.getPackageInfo(mockContext.packageName, 0)).thenReturn(mockPackageInfo) - + // Given - getUserAgent() now returns SDK version or "unknown" flagsmith = Flagsmith( environmentKey = "test-key", baseUrl = "http://localhost:${mockServer.localPort}", - context = mockContext, + context = null, enableAnalytics = false, cacheConfig = FlagsmithCacheConfig(enableCache = false) ) @@ -234,12 +188,12 @@ class UserAgentTests { assertTrue(result.isSuccess) } - // Then - Verify the traits request has correct User-Agent + // Then - Verify the traits request has correct User-Agent with "unknown" since BuildConfig is not available in tests mockServer.verify( request() .withPath("/identities/") .withMethod("POST") - .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/$expectedAppVersion") + .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") ) } } \ No newline at end of file From 97643e4aa8d1dfd50609402b68c959453912d352 Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Fri, 14 Nov 2025 15:24:03 +0000 Subject: [PATCH 4/7] Add hardcoded release please version like the iOS client --- .../internal/FlagsmithRetrofitService.kt | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt index 8ca4a81..4908597 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt @@ -52,13 +52,26 @@ interface FlagsmithRetrofitService { // Try to get version from BuildConfig val buildConfigClass = Class.forName("com.flagsmith.kotlin.BuildConfig") val versionField = buildConfigClass.getField("VERSION_NAME") - versionField.get(null) as String - } catch (e: Exception) { - // Fallback to hardcoded version if BuildConfig is not available - "unknown" + val version = versionField.get(null) as? String + + // If version is null or empty, fall back to hardcoded version + if (version.isNullOrBlank()) { + getHardcodedVersion() + } else { + version + } + } catch (_: Exception) { + // Fallback to hardcoded version if BuildConfig is not available or any error occurs + getHardcodedVersion() } } + private fun getHardcodedVersion(): String { + // x-release-please-start-version + return "1.8.0" + // x-release-please-end + } + fun userAgentInterceptor(context: Context?): Interceptor { return Interceptor { chain -> val userAgent = getUserAgent(context) From 6416cb799f889b430a3c117c2e8a90d03c43fc04 Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Fri, 14 Nov 2025 15:50:32 +0000 Subject: [PATCH 5/7] Updated the tests to add more explicit unit testing and testing around release-please effectiveness --- FlagsmithClient/build.gradle.kts | 3 + .../test/java/com/flagsmith/UserAgentTests.kt | 113 ++++++------ .../internal/SdkVersionRetrievalTest.kt | 172 ++++++++++++++++++ release-please-config.json | 7 +- 4 files changed, 233 insertions(+), 62 deletions(-) create mode 100644 FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt diff --git a/FlagsmithClient/build.gradle.kts b/FlagsmithClient/build.gradle.kts index 265d3d9..788f534 100644 --- a/FlagsmithClient/build.gradle.kts +++ b/FlagsmithClient/build.gradle.kts @@ -82,6 +82,9 @@ dependencies { implementation("com.squareup.okhttp3:okhttp-sse:4.11.0") testImplementation("com.squareup.okhttp3:okhttp-sse:4.11.0") + // MockWebServer for testing HTTP interactions + testImplementation("com.squareup.okhttp3:mockwebserver:4.11.0") + testImplementation("junit:junit:4.13.2") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") testImplementation("org.mock-server:mockserver-netty-no-dependencies:5.14.0") diff --git a/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt b/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt index 8759275..25d5df5 100644 --- a/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt +++ b/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt @@ -5,6 +5,7 @@ import com.flagsmith.mockResponses.MockEndpoint import com.flagsmith.mockResponses.mockResponseFor import kotlinx.coroutines.runBlocking import org.junit.After +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -16,6 +17,15 @@ class UserAgentTests { private lateinit var mockServer: ClientAndServer private lateinit var flagsmith: Flagsmith + companion object { + // Expected version when BuildConfig is not available (in tests) + // This matches the hardcoded version in FlagsmithRetrofitService.getHardcodedVersion() + // x-release-please-start-version + private const val EXPECTED_FALLBACK_VERSION = "1.8.0" + // x-release-please-end + private const val EXPECTED_USER_AGENT = "flagsmith-kotlin-android-sdk/$EXPECTED_FALLBACK_VERSION" + } + @Before fun setup() { mockServer = ClientAndServer.startClientAndServer() @@ -27,10 +37,8 @@ class UserAgentTests { } @Test - fun testUserAgentHeaderSentWithValidVersion() { - // Given - The User-Agent now shows SDK version or "unknown" (not app version) - // This is because getUserAgent() method was updated to return SDK version - // In tests, BuildConfig is not available, so it returns "unknown" + fun testUserAgentHeaderSentWithGetFlags() { + // Given - BuildConfig is not available in tests, so falls back to hardcoded version (1.8.0) flagsmith = Flagsmith( environmentKey = "test-key", baseUrl = "http://localhost:${mockServer.localPort}", @@ -47,18 +55,19 @@ class UserAgentTests { assertTrue(result.isSuccess) } - // Then - Verify User-Agent contains "unknown" since BuildConfig is not available in tests + // Then - Verify User-Agent contains hardcoded version since BuildConfig is not available in tests mockServer.verify( request() .withPath("/flags/") .withMethod("GET") - .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") + .withHeader("User-Agent", EXPECTED_USER_AGENT) ) } @Test fun testUserAgentHeaderSentWithNullContext() { - // Given + // Given - Context being null doesn't affect SDK version retrieval + // BuildConfig lookup is independent of Android context flagsmith = Flagsmith( environmentKey = "test-key", baseUrl = "http://localhost:${mockServer.localPort}", @@ -75,18 +84,18 @@ class UserAgentTests { assertTrue(result.isSuccess) } - // Then + // Then - Should still get hardcoded version mockServer.verify( request() .withPath("/flags/") .withMethod("GET") - .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") + .withHeader("User-Agent", EXPECTED_USER_AGENT) ) } @Test - fun testUserAgentHeaderSentWithExceptionDuringVersionRetrieval() { - // Given - Even with context, getUserAgent() now returns SDK version or "unknown" + fun testUserAgentHeaderSentWithIdentityRequest() { + // Given - Testing that User-Agent header is sent consistently across all API endpoints flagsmith = Flagsmith( environmentKey = "test-key", baseUrl = "http://localhost:${mockServer.localPort}", @@ -95,26 +104,27 @@ class UserAgentTests { cacheConfig = FlagsmithCacheConfig(enableCache = false) ) - mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) + mockServer.mockResponseFor(MockEndpoint.GET_IDENTITIES) // When runBlocking { - val result = flagsmith.getFeatureFlagsSync() + val result = flagsmith.getIdentitySync("test-user") assertTrue(result.isSuccess) } - // Then + // Then - Verify User-Agent header is sent with GET /identities/ mockServer.verify( request() - .withPath("/flags/") + .withPath("/identities/") .withMethod("GET") - .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") + .withQueryStringParameter("identifier", "test-user") + .withHeader("User-Agent", EXPECTED_USER_AGENT) ) } @Test - fun testUserAgentHeaderSentWithNullVersionName() { - // Given - getUserAgent() now returns SDK version or "unknown" regardless of context + fun testUserAgentHeaderSentWithTraitRequest() { + // Given - Testing that User-Agent header is sent with POST requests flagsmith = Flagsmith( environmentKey = "test-key", baseUrl = "http://localhost:${mockServer.localPort}", @@ -123,26 +133,26 @@ class UserAgentTests { cacheConfig = FlagsmithCacheConfig(enableCache = false) ) - mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) + mockServer.mockResponseFor(MockEndpoint.SET_TRAIT) // When runBlocking { - val result = flagsmith.getFeatureFlagsSync() + val result = flagsmith.setTraitSync(Trait(key = "test-key", traitValue = "test-value"), "test-user") assertTrue(result.isSuccess) } - // Then + // Then - Verify User-Agent header is sent with POST /identities/ mockServer.verify( request() - .withPath("/flags/") - .withMethod("GET") - .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") + .withPath("/identities/") + .withMethod("POST") + .withHeader("User-Agent", EXPECTED_USER_AGENT) ) } @Test - fun testUserAgentHeaderSentWithIdentityRequest() { - // Given - getUserAgent() now returns SDK version or "unknown" + fun testUserAgentFormat() { + // Given flagsmith = Flagsmith( environmentKey = "test-key", baseUrl = "http://localhost:${mockServer.localPort}", @@ -151,49 +161,30 @@ class UserAgentTests { cacheConfig = FlagsmithCacheConfig(enableCache = false) ) - mockServer.mockResponseFor(MockEndpoint.GET_IDENTITIES) + mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) // When runBlocking { - val result = flagsmith.getIdentitySync("test-user") - assertTrue(result.isSuccess) + flagsmith.getFeatureFlagsSync() } - // Then - Verify User-Agent contains "unknown" since BuildConfig is not available in tests - mockServer.verify( - request() - .withPath("/identities/") - .withMethod("GET") - .withQueryStringParameter("identifier", "test-user") - .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") + // Then - Verify User-Agent follows the format: flagsmith-kotlin-android-sdk/{version} + val requests = mockServer.retrieveRecordedRequests( + request().withPath("/flags/") ) - } - @Test - fun testUserAgentHeaderSentWithTraitRequest() { - // Given - getUserAgent() now returns SDK version or "unknown" - flagsmith = Flagsmith( - environmentKey = "test-key", - baseUrl = "http://localhost:${mockServer.localPort}", - context = null, - enableAnalytics = false, - cacheConfig = FlagsmithCacheConfig(enableCache = false) - ) + assertEquals(1, requests.size) + val userAgentHeader = requests[0].getFirstHeader("User-Agent") - mockServer.mockResponseFor(MockEndpoint.SET_TRAIT) + // Verify format + assertTrue("User-Agent should start with 'flagsmith-kotlin-android-sdk/'", + userAgentHeader.startsWith("flagsmith-kotlin-android-sdk/")) - // When - runBlocking { - val result = flagsmith.setTraitSync(Trait(key = "test-key", traitValue = "test-value"), "test-user") - assertTrue(result.isSuccess) - } + // Verify version part exists and is not empty + val version = userAgentHeader.substringAfter("flagsmith-kotlin-android-sdk/") + assertTrue("Version should not be empty", version.isNotEmpty()) - // Then - Verify the traits request has correct User-Agent with "unknown" since BuildConfig is not available in tests - mockServer.verify( - request() - .withPath("/identities/") - .withMethod("POST") - .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") - ) + // In test environment, should be the hardcoded fallback version + assertEquals(EXPECTED_FALLBACK_VERSION, version) } -} \ No newline at end of file +} diff --git a/FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt b/FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt new file mode 100644 index 0000000..3c1a6aa --- /dev/null +++ b/FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt @@ -0,0 +1,172 @@ +package com.flagsmith.internal + +import com.flagsmith.FlagsmithCacheConfig +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +/** + * Unit tests for SDK version retrieval functionality in FlagsmithRetrofitService. + * + * These tests verify the robustness of getSdkVersion() and its fallback behavior + * when BuildConfig is not available or returns invalid values. + */ +class SdkVersionRetrievalTest { + + private lateinit var mockServer: MockWebServer + + companion object { + // This should match the version in getHardcodedVersion() + // and in .release-please-manifest.json + // x-release-please-start-version + private const val EXPECTED_FALLBACK_VERSION = "1.8.0" + // x-release-please-end + private const val USER_AGENT_PREFIX = "flagsmith-kotlin-android-sdk" + } + + @Before + fun setup() { + mockServer = MockWebServer() + mockServer.start() + } + + @After + fun tearDown() { + mockServer.shutdown() + } + + @Test + fun testUserAgentInterceptorReturnsValidFormat() { + // Given - Create a client with the user agent interceptor + val interceptor = FlagsmithRetrofitService.userAgentInterceptor(null) + val client = OkHttpClient.Builder() + .addInterceptor(interceptor) + .build() + + mockServer.enqueue(MockResponse().setResponseCode(200).setBody("{}")) + + // When - Make a request + val request = Request.Builder() + .url(mockServer.url("/")) + .build() + + client.newCall(request).execute().use { response -> + // Then - Verify the request was made with the correct User-Agent header + val recordedRequest = mockServer.takeRequest() + val userAgent = recordedRequest.getHeader("User-Agent") + + assertNotNull("User-Agent header should be present", userAgent) + assertTrue( + "User-Agent should start with correct prefix: $userAgent", + userAgent!!.startsWith("$USER_AGENT_PREFIX/") + ) + } + } + + @Test + fun testFallbackVersionIsUsedWhenBuildConfigNotAvailable() { + // Given - In test environment, BuildConfig is not available + // Create a client with the user agent interceptor + val interceptor = FlagsmithRetrofitService.userAgentInterceptor(null) + val client = OkHttpClient.Builder() + .addInterceptor(interceptor) + .build() + + mockServer.enqueue(MockResponse().setResponseCode(200).setBody("{}")) + + // When - Make a request + val request = Request.Builder() + .url(mockServer.url("/")) + .build() + + client.newCall(request).execute().use { response -> + // Then - Should use the hardcoded fallback version + val recordedRequest = mockServer.takeRequest() + val userAgent = recordedRequest.getHeader("User-Agent") + + assertEquals( + "User-Agent should contain fallback version when BuildConfig is not available", + "$USER_AGENT_PREFIX/$EXPECTED_FALLBACK_VERSION", + userAgent + ) + } + } + + @Test + fun testVersionFormatIsValid() { + // Given - Create a client with the user agent interceptor + val interceptor = FlagsmithRetrofitService.userAgentInterceptor(null) + val client = OkHttpClient.Builder() + .addInterceptor(interceptor) + .build() + + mockServer.enqueue(MockResponse().setResponseCode(200).setBody("{}")) + + // When - Make a request + val request = Request.Builder() + .url(mockServer.url("/")) + .build() + + client.newCall(request).execute().use { response -> + // Then - Verify version format is semantic versioning compatible + val recordedRequest = mockServer.takeRequest() + val userAgent = recordedRequest.getHeader("User-Agent")!! + val version = userAgent.substringAfter("$USER_AGENT_PREFIX/") + + assertTrue("Version should not be empty", version.isNotEmpty()) + assertTrue("Version should not contain whitespace", version.trim() == version) + + // Version should match semantic versioning pattern (X.Y.Z) or be a valid identifier + val semverPattern = Regex("^\\d+\\.\\d+\\.\\d+.*$") + assertTrue( + "Version should follow semantic versioning or be a valid identifier: $version", + semverPattern.matches(version) || version.matches(Regex("^[a-zA-Z0-9._-]+$")) + ) + } + } + + @Test + fun testUserAgentHeaderIsPersistentAcrossRequests() { + // Given - Create a client with the user agent interceptor + val interceptor = FlagsmithRetrofitService.userAgentInterceptor(null) + val client = OkHttpClient.Builder() + .addInterceptor(interceptor) + .build() + + mockServer.enqueue(MockResponse().setResponseCode(200).setBody("{}")) + mockServer.enqueue(MockResponse().setResponseCode(200).setBody("{}")) + + // When - Make multiple requests + val request1 = Request.Builder().url(mockServer.url("/first")).build() + val request2 = Request.Builder().url(mockServer.url("/second")).build() + + client.newCall(request1).execute().close() + client.newCall(request2).execute().close() + + // Then - Both requests should have the same User-Agent + val recordedRequest1 = mockServer.takeRequest() + val recordedRequest2 = mockServer.takeRequest() + + val userAgent1 = recordedRequest1.getHeader("User-Agent") + val userAgent2 = recordedRequest2.getHeader("User-Agent") + + assertEquals( + "User-Agent should be consistent across requests", + userAgent1, + userAgent2 + ) + + assertEquals( + "User-Agent should be the expected value", + "$USER_AGENT_PREFIX/$EXPECTED_FALLBACK_VERSION", + userAgent1 + ) + } +} diff --git a/release-please-config.json b/release-please-config.json index 81d97a3..04ccdde 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -8,7 +8,12 @@ "bump-patch-for-minor-pre-major": false, "draft": false, "prerelease": false, - "include-component-in-tag": false + "include-component-in-tag": false, + "extra-files": [ + "FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt", + "FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt", + "FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt" + ] } }, "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", From 2d6527f1308945e080bfbe3718bb66e28a1db799 Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Tue, 18 Nov 2025 15:10:04 +0000 Subject: [PATCH 6/7] Now just use the release-please version and tidy up the tests --- .../internal/FlagsmithRetrofitService.kt | 19 --------- .../test/java/com/flagsmith/UserAgentTests.kt | 18 ++++----- .../internal/SdkVersionRetrievalTest.kt | 39 ++----------------- 3 files changed, 12 insertions(+), 64 deletions(-) diff --git a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt index 4908597..9907117 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt @@ -48,25 +48,6 @@ interface FlagsmithRetrofitService { } private fun getSdkVersion(): String { - return try { - // Try to get version from BuildConfig - val buildConfigClass = Class.forName("com.flagsmith.kotlin.BuildConfig") - val versionField = buildConfigClass.getField("VERSION_NAME") - val version = versionField.get(null) as? String - - // If version is null or empty, fall back to hardcoded version - if (version.isNullOrBlank()) { - getHardcodedVersion() - } else { - version - } - } catch (_: Exception) { - // Fallback to hardcoded version if BuildConfig is not available or any error occurs - getHardcodedVersion() - } - } - - private fun getHardcodedVersion(): String { // x-release-please-start-version return "1.8.0" // x-release-please-end diff --git a/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt b/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt index 25d5df5..2ca3ef9 100644 --- a/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt +++ b/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt @@ -18,12 +18,11 @@ class UserAgentTests { private lateinit var flagsmith: Flagsmith companion object { - // Expected version when BuildConfig is not available (in tests) - // This matches the hardcoded version in FlagsmithRetrofitService.getHardcodedVersion() + // Expected version set by release-please in FlagsmithRetrofitService.getSdkVersion() // x-release-please-start-version - private const val EXPECTED_FALLBACK_VERSION = "1.8.0" + private const val EXPECTED_SDK_VERSION = "1.8.0" // x-release-please-end - private const val EXPECTED_USER_AGENT = "flagsmith-kotlin-android-sdk/$EXPECTED_FALLBACK_VERSION" + private const val EXPECTED_USER_AGENT = "flagsmith-kotlin-android-sdk/$EXPECTED_SDK_VERSION" } @Before @@ -38,7 +37,7 @@ class UserAgentTests { @Test fun testUserAgentHeaderSentWithGetFlags() { - // Given - BuildConfig is not available in tests, so falls back to hardcoded version (1.8.0) + // Given - SDK version is set by release-please flagsmith = Flagsmith( environmentKey = "test-key", baseUrl = "http://localhost:${mockServer.localPort}", @@ -55,7 +54,7 @@ class UserAgentTests { assertTrue(result.isSuccess) } - // Then - Verify User-Agent contains hardcoded version since BuildConfig is not available in tests + // Then - Verify User-Agent contains the SDK version from release-please mockServer.verify( request() .withPath("/flags/") @@ -67,7 +66,6 @@ class UserAgentTests { @Test fun testUserAgentHeaderSentWithNullContext() { // Given - Context being null doesn't affect SDK version retrieval - // BuildConfig lookup is independent of Android context flagsmith = Flagsmith( environmentKey = "test-key", baseUrl = "http://localhost:${mockServer.localPort}", @@ -84,7 +82,7 @@ class UserAgentTests { assertTrue(result.isSuccess) } - // Then - Should still get hardcoded version + // Then - Should get the SDK version from release-please mockServer.verify( request() .withPath("/flags/") @@ -184,7 +182,7 @@ class UserAgentTests { val version = userAgentHeader.substringAfter("flagsmith-kotlin-android-sdk/") assertTrue("Version should not be empty", version.isNotEmpty()) - // In test environment, should be the hardcoded fallback version - assertEquals(EXPECTED_FALLBACK_VERSION, version) + // Should be the version set by release-please + assertEquals(EXPECTED_SDK_VERSION, version) } } diff --git a/FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt b/FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt index 3c1a6aa..241b55a 100644 --- a/FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt +++ b/FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt @@ -15,18 +15,16 @@ import org.junit.Test /** * Unit tests for SDK version retrieval functionality in FlagsmithRetrofitService. * - * These tests verify the robustness of getSdkVersion() and its fallback behavior - * when BuildConfig is not available or returns invalid values. + * These tests verify that getSdkVersion() correctly returns the version set by release-please. */ class SdkVersionRetrievalTest { private lateinit var mockServer: MockWebServer companion object { - // This should match the version in getHardcodedVersion() - // and in .release-please-manifest.json + // This should match the version in getSdkVersion() and in .release-please-manifest.json // x-release-please-start-version - private const val EXPECTED_FALLBACK_VERSION = "1.8.0" + private const val EXPECTED_SDK_VERSION = "1.8.0" // x-release-please-end private const val USER_AGENT_PREFIX = "flagsmith-kotlin-android-sdk" } @@ -70,35 +68,6 @@ class SdkVersionRetrievalTest { } } - @Test - fun testFallbackVersionIsUsedWhenBuildConfigNotAvailable() { - // Given - In test environment, BuildConfig is not available - // Create a client with the user agent interceptor - val interceptor = FlagsmithRetrofitService.userAgentInterceptor(null) - val client = OkHttpClient.Builder() - .addInterceptor(interceptor) - .build() - - mockServer.enqueue(MockResponse().setResponseCode(200).setBody("{}")) - - // When - Make a request - val request = Request.Builder() - .url(mockServer.url("/")) - .build() - - client.newCall(request).execute().use { response -> - // Then - Should use the hardcoded fallback version - val recordedRequest = mockServer.takeRequest() - val userAgent = recordedRequest.getHeader("User-Agent") - - assertEquals( - "User-Agent should contain fallback version when BuildConfig is not available", - "$USER_AGENT_PREFIX/$EXPECTED_FALLBACK_VERSION", - userAgent - ) - } - } - @Test fun testVersionFormatIsValid() { // Given - Create a client with the user agent interceptor @@ -165,7 +134,7 @@ class SdkVersionRetrievalTest { assertEquals( "User-Agent should be the expected value", - "$USER_AGENT_PREFIX/$EXPECTED_FALLBACK_VERSION", + "$USER_AGENT_PREFIX/$EXPECTED_SDK_VERSION", userAgent1 ) } From c1c4edc714d4acbd6178ad70df4d4a8c73064abc Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Tue, 18 Nov 2025 15:43:21 +0000 Subject: [PATCH 7/7] No longer need to use the Context --- FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt | 2 +- .../java/com/flagsmith/internal/FlagsmithEventService.kt | 4 +--- .../com/flagsmith/internal/FlagsmithRetrofitService.kt | 8 ++++---- .../com/flagsmith/internal/SdkVersionRetrievalTest.kt | 6 +++--- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt b/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt index bf24dc6..2842383 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt @@ -51,7 +51,7 @@ class Flagsmith constructor( private val eventService: FlagsmithEventService? = if (!enableRealtimeUpdates) null - else FlagsmithEventService(eventSourceBaseUrl = eventSourceBaseUrl, environmentKey = environmentKey, context = context) { event -> + else FlagsmithEventService(eventSourceBaseUrl = eventSourceBaseUrl, environmentKey = environmentKey) { event -> if (event.isSuccess) { lastEventUpdate = event.getOrNull()?.updatedAt ?: lastEventUpdate diff --git a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt index 8d24fda..b37254c 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt @@ -1,6 +1,5 @@ package com.flagsmith.internal -import android.content.Context import android.util.Log import com.flagsmith.entities.FlagEvent import com.google.gson.Gson @@ -16,12 +15,11 @@ import java.util.concurrent.TimeUnit internal class FlagsmithEventService constructor( private val eventSourceBaseUrl: String?, private val environmentKey: String, - private val context: Context?, private val updates: (Result) -> Unit ) { private val sseClient = OkHttpClient.Builder() .addInterceptor(FlagsmithRetrofitService.envKeyInterceptor(environmentKey)) - .addInterceptor(FlagsmithRetrofitService.userAgentInterceptor(context)) + .addInterceptor(FlagsmithRetrofitService.userAgentInterceptor()) .connectTimeout(6, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.MINUTES) .writeTimeout(10, TimeUnit.MINUTES) diff --git a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt index 9907117..7078eb2 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt @@ -42,7 +42,7 @@ interface FlagsmithRetrofitService { private const val USER_AGENT_HEADER = "User-Agent" private const val USER_AGENT_PREFIX = "flagsmith-kotlin-android-sdk" - private fun getUserAgent(context: Context?): String { + private fun getUserAgent(): String { val sdkVersion = getSdkVersion() return "$USER_AGENT_PREFIX/$sdkVersion" } @@ -53,9 +53,9 @@ interface FlagsmithRetrofitService { // x-release-please-end } - fun userAgentInterceptor(context: Context?): Interceptor { + fun userAgentInterceptor(): Interceptor { return Interceptor { chain -> - val userAgent = getUserAgent(context) + val userAgent = getUserAgent() val request = chain.request().newBuilder() .addHeader(USER_AGENT_HEADER, userAgent) .build() @@ -115,7 +115,7 @@ interface FlagsmithRetrofitService { val client = OkHttpClient.Builder() .addInterceptor(envKeyInterceptor(environmentKey)) - .addInterceptor(userAgentInterceptor(context)) + .addInterceptor(userAgentInterceptor()) .addInterceptor(updatedAtInterceptor(timeTracker)) .addInterceptor(jsonContentTypeInterceptor()) .let { if (cacheConfig.enableCache) it.addNetworkInterceptor(cacheControlInterceptor()) else it } diff --git a/FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt b/FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt index 241b55a..9848266 100644 --- a/FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt +++ b/FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt @@ -43,7 +43,7 @@ class SdkVersionRetrievalTest { @Test fun testUserAgentInterceptorReturnsValidFormat() { // Given - Create a client with the user agent interceptor - val interceptor = FlagsmithRetrofitService.userAgentInterceptor(null) + val interceptor = FlagsmithRetrofitService.userAgentInterceptor() val client = OkHttpClient.Builder() .addInterceptor(interceptor) .build() @@ -71,7 +71,7 @@ class SdkVersionRetrievalTest { @Test fun testVersionFormatIsValid() { // Given - Create a client with the user agent interceptor - val interceptor = FlagsmithRetrofitService.userAgentInterceptor(null) + val interceptor = FlagsmithRetrofitService.userAgentInterceptor() val client = OkHttpClient.Builder() .addInterceptor(interceptor) .build() @@ -104,7 +104,7 @@ class SdkVersionRetrievalTest { @Test fun testUserAgentHeaderIsPersistentAcrossRequests() { // Given - Create a client with the user agent interceptor - val interceptor = FlagsmithRetrofitService.userAgentInterceptor(null) + val interceptor = FlagsmithRetrofitService.userAgentInterceptor() val client = OkHttpClient.Builder() .addInterceptor(interceptor) .build()