diff --git a/android-client-sdk/src/main/java/com/devcycle/sdk/android/openfeature/DevCycleProvider.kt b/android-client-sdk/src/main/java/com/devcycle/sdk/android/openfeature/DevCycleProvider.kt index 4d0366c1..b3690dc7 100644 --- a/android-client-sdk/src/main/java/com/devcycle/sdk/android/openfeature/DevCycleProvider.kt +++ b/android-client-sdk/src/main/java/com/devcycle/sdk/android/openfeature/DevCycleProvider.kt @@ -5,7 +5,6 @@ import com.devcycle.sdk.android.api.DevCycleCallback import com.devcycle.sdk.android.api.DevCycleClient import com.devcycle.sdk.android.api.DevCycleOptions import com.devcycle.sdk.android.model.BaseConfigVariable -import com.devcycle.sdk.android.model.DevCycleEvent import com.devcycle.sdk.android.model.DevCycleUser import com.devcycle.sdk.android.model.Variable import com.devcycle.sdk.android.util.DevCycleLogger @@ -14,7 +13,6 @@ import dev.openfeature.sdk.exceptions.OpenFeatureError import kotlinx.coroutines.suspendCancellableCoroutine import org.json.JSONArray import org.json.JSONObject -import java.math.BigDecimal import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -29,7 +27,15 @@ class DevCycleProvider( /** * The DevCycle client instance - created during initialization */ - private var devCycleClient: DevCycleClient? = null + private var _devcycleClient: DevCycleClient? = null + + val devcycleClient: DevCycleClient + get() = _devcycleClient + ?: error( + """ + DevCycleClient is not initialized. Call OpenFeatureAPI.setProvider() / OpenFeatureAPI.setProviderAndWait() with this provider instance to initialize the DevCycleClient. + """.trimIndent() + ) /** * Helper function to create a ProviderEvaluation from a DevCycle variable @@ -37,7 +43,7 @@ class DevCycleProvider( private fun createProviderEvaluation(variable: Variable<*>, value: T): ProviderEvaluation { val metadataBuilder = EvaluationMetadata.builder() var hasMetadata = false - + // Add evaluation details and target ID if available variable.eval?.let { evalReason -> evalReason.details?.let { details -> @@ -49,7 +55,7 @@ class DevCycleProvider( hasMetadata = true } } - + return ProviderEvaluation( value = value, variant = variable.key, @@ -93,14 +99,14 @@ class DevCycleProvider( .withContext(context) .withSDKKey(sdkKey) .withUser(user) - + options?.let { clientBuilder.withOptions(it) } - - devCycleClient = clientBuilder.build() + + _devcycleClient = clientBuilder.build() // Wait for DevCycle client to fully initialize suspendCancellableCoroutine { continuation -> - devCycleClient!!.onInitialized(object : DevCycleCallback { + _devcycleClient!!.onInitialized(object : DevCycleCallback { override fun onSuccess(result: String) { DevCycleLogger.d("DevCycle OpenFeature provider initialized successfully") continuation.resume(Unit) @@ -124,8 +130,8 @@ class DevCycleProvider( } override suspend fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) { - try { - val client = devCycleClient + try { + val client = _devcycleClient if (client == null) { DevCycleLogger.w( "Context set before DevCycleProvider was fully initialized. " + @@ -157,7 +163,7 @@ class DevCycleProvider( } override fun shutdown() { - devCycleClient?.close() + _devcycleClient?.close() } override fun getBooleanEvaluation( @@ -165,7 +171,7 @@ class DevCycleProvider( defaultValue: Boolean, context: EvaluationContext? ): ProviderEvaluation { - val client = devCycleClient ?: return createDefaultProviderEvaluation(defaultValue) + val client = _devcycleClient ?: return createDefaultProviderEvaluation(defaultValue) val variable = client.variable(key, defaultValue) return createProviderEvaluation(variable, variable.value) } @@ -175,7 +181,7 @@ class DevCycleProvider( defaultValue: Double, context: EvaluationContext? ): ProviderEvaluation { - val client = devCycleClient ?: return createDefaultProviderEvaluation(defaultValue) + val client = _devcycleClient ?: return createDefaultProviderEvaluation(defaultValue) val variable = client.variable(key, defaultValue) return createProviderEvaluation(variable, variable.value.toDouble()) } @@ -185,7 +191,7 @@ class DevCycleProvider( defaultValue: Int, context: EvaluationContext? ): ProviderEvaluation { - val client = devCycleClient ?: return createDefaultProviderEvaluation(defaultValue) + val client = _devcycleClient ?: return createDefaultProviderEvaluation(defaultValue) val variable = client.variable(key, defaultValue) return createProviderEvaluation(variable, variable.value.toInt()) } @@ -195,7 +201,7 @@ class DevCycleProvider( defaultValue: Value, context: EvaluationContext? ): ProviderEvaluation { - val client = devCycleClient ?: return createDefaultProviderEvaluation(defaultValue) + val client = _devcycleClient ?: return createDefaultProviderEvaluation(defaultValue) val (result, variable) = when { defaultValue is Value.Structure -> { @@ -235,7 +241,7 @@ class DevCycleProvider( defaultValue: String, context: EvaluationContext? ): ProviderEvaluation { - val client = devCycleClient ?: return createDefaultProviderEvaluation(defaultValue) + val client = _devcycleClient ?: return createDefaultProviderEvaluation(defaultValue) val variable = client.variable(key, defaultValue) return createProviderEvaluation(variable, variable.value) } @@ -245,7 +251,7 @@ class DevCycleProvider( context: EvaluationContext?, details: TrackingEventDetails? ) { - val client = devCycleClient + val client = _devcycleClient if (client == null) { DevCycleLogger.w("Cannot track event '$trackingEventName': DevCycle client not initialized") return diff --git a/android-client-sdk/src/test/java/com/devcycle/sdk/android/openfeature/DevCycleProviderTest.kt b/android-client-sdk/src/test/java/com/devcycle/sdk/android/openfeature/DevCycleProviderTest.kt index 1f7e5a0b..25e0bf9f 100644 --- a/android-client-sdk/src/test/java/com/devcycle/sdk/android/openfeature/DevCycleProviderTest.kt +++ b/android-client-sdk/src/test/java/com/devcycle/sdk/android/openfeature/DevCycleProviderTest.kt @@ -3,22 +3,27 @@ package com.devcycle.sdk.android.openfeature import android.content.Context import com.devcycle.sdk.android.api.DevCycleCallback import com.devcycle.sdk.android.api.DevCycleClient -import com.devcycle.sdk.android.api.DevCycleOptions import com.devcycle.sdk.android.model.BaseConfigVariable -import com.devcycle.sdk.android.model.DevCycleUser import com.devcycle.sdk.android.model.EvalReason import com.devcycle.sdk.android.model.Variable -import dev.openfeature.sdk.* -import dev.openfeature.sdk.exceptions.OpenFeatureError -import io.mockk.* +import dev.openfeature.sdk.EvaluationMetadata +import dev.openfeature.sdk.ImmutableContext +import dev.openfeature.sdk.TrackingEventDetails +import dev.openfeature.sdk.Value +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkAll import kotlinx.coroutines.runBlocking -import org.json.JSONArray -import org.json.JSONObject +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.AfterEach -import dev.openfeature.sdk.EvaluationMetadata class DevCycleProviderTest { @@ -30,7 +35,7 @@ class DevCycleProviderTest { fun setup() { mockContext = mockk(relaxed = true) mockDevCycleClient = mockk(relaxed = true) - + // Mock the DevCycleClient.builder() static method chain mockkObject(DevCycleClient.Companion) val mockBuilder = mockk(relaxed = true) @@ -40,19 +45,19 @@ class DevCycleProviderTest { every { mockBuilder.withUser(any()) } returns mockBuilder every { mockBuilder.withOptions(any()) } returns mockBuilder every { mockBuilder.build() } returns mockDevCycleClient - + // Mock the onInitialized method to immediately call the success callback every { mockDevCycleClient.onInitialized(any()) } answers { val callback = firstArg>() callback.onSuccess("initialized") } - + // Mock the identifyUser method to immediately call the success callback every { mockDevCycleClient.identifyUser(any(), any()) } answers { val callback = secondArg>>() callback.onSuccess(emptyMap()) } - + // Create the provider - now it won't try to create a real DevCycleClient provider = DevCycleProvider("test-sdk-key", mockContext) } @@ -67,6 +72,15 @@ class DevCycleProviderTest { assertEquals("DevCycle", provider.metadata.name) } + @Test + fun `accessing devcycleClient throws exception when client not initialized`() { + val exception = assertThrows(IllegalStateException::class.java) { provider.devcycleClient } + assertTrue( + exception.message?.contains("DevCycleClient is not initialized") == true, + "Exception message should contain 'DevCycleClient is not initialized'" + ) + } + @Test fun `getBooleanEvaluation returns default when client not initialized`() { val result = provider.getBooleanEvaluation("test-flag", false, null) @@ -121,7 +135,7 @@ class DevCycleProviderTest { @Test fun `onContextSet handles uninitialized client gracefully`() { val newContext = ImmutableContext(targetingKey = "new-user") - + // Should not throw an exception assertDoesNotThrow { runBlocking { @@ -142,7 +156,7 @@ class DevCycleProviderTest { fun `initialize throws exception with invalid setup`() { // This test would only work if we had a way to make DevCycleClient construction fail // For now, we'll just verify that initialize can be called without crashing - + // Should not throw when initialize is called with valid params assertDoesNotThrow { runBlocking { @@ -164,11 +178,11 @@ class DevCycleProviderTest { @Test fun `createProviderEvaluation includes metadata when eval details are available`() { setupInitializedProvider() - + // Create a mock variable with eval information val mockVariable = mockk>(relaxed = true) val mockEvalReason = mockk(relaxed = true) - + every { mockVariable.key } returns "test-variable" every { mockVariable.value } returns "test-value" every { mockVariable.isDefaulted } returns false @@ -176,15 +190,15 @@ class DevCycleProviderTest { every { mockEvalReason.reason } returns "TARGETING_MATCH" every { mockEvalReason.details } returns "Test evaluation details" every { mockEvalReason.targetId } returns "test-target-123" - + every { mockDevCycleClient.variable("test-variable", "default") } returns mockVariable - + val result = provider.getStringEvaluation("test-variable", "default", null) - + assertEquals("test-value", result.value) assertEquals("test-variable", result.variant) assertEquals("TARGETING_MATCH", result.reason) - + // Check that metadata contains the eval details and target ID assertNotNull(result.metadata) assertEquals("Test evaluation details", result.metadata.getString("evalDetails")) @@ -194,11 +208,11 @@ class DevCycleProviderTest { @Test fun `createProviderEvaluation includes partial metadata when only some eval details are available`() { setupInitializedProvider() - + // Create a mock variable with eval information but only details (no targetId) val mockVariable = mockk>(relaxed = true) val mockEvalReason = mockk(relaxed = true) - + every { mockVariable.key } returns "test-variable" every { mockVariable.value } returns "test-value" every { mockVariable.isDefaulted } returns false @@ -206,15 +220,15 @@ class DevCycleProviderTest { every { mockEvalReason.reason } returns "TARGETING_MATCH" every { mockEvalReason.details } returns "Test evaluation details" every { mockEvalReason.targetId } returns null // No target ID - + every { mockDevCycleClient.variable("test-variable", "default") } returns mockVariable - + val result = provider.getStringEvaluation("test-variable", "default", null) - + assertEquals("test-value", result.value) assertEquals("test-variable", result.variant) assertEquals("TARGETING_MATCH", result.reason) - + // Check that metadata contains the eval details but not target ID assertNotNull(result.metadata) assertEquals("Test evaluation details", result.metadata.getString("evalDetails")) @@ -224,32 +238,38 @@ class DevCycleProviderTest { @Test fun `createProviderEvaluation uses empty metadata when no eval details are available`() { setupInitializedProvider() - + // Create a mock variable with no eval information val mockVariable = mockk>(relaxed = true) - + every { mockVariable.key } returns "test-variable" every { mockVariable.value } returns "test-value" every { mockVariable.isDefaulted } returns true every { mockVariable.eval } returns null // No eval data - + every { mockDevCycleClient.variable("test-variable", "default") } returns mockVariable - + val result = provider.getStringEvaluation("test-variable", "default", null) - + assertEquals("test-value", result.value) assertEquals("test-variable", result.variant) assertEquals("DEFAULT", result.reason) - + // Check that metadata is EMPTY when no eval data is available assertEquals(EvaluationMetadata.EMPTY, result.metadata) assertNull(result.metadata.getString("evalDetails")) assertNull(result.metadata.getString("evalTargetId")) } + @Test + fun `accessing devcycleClient returns devcycleClient when client is initialized`() { + setupInitializedProvider() + assertEquals(mockDevCycleClient, provider.devcycleClient) + } + private fun setupInitializedProvider() { // Make the devCycleClient available (simulate successful initialization) - val providerField = DevCycleProvider::class.java.getDeclaredField("devCycleClient") + val providerField = DevCycleProvider::class.java.getDeclaredField("_devcycleClient") providerField.isAccessible = true providerField.set(provider, mockDevCycleClient) }