Skip to content
6 changes: 6 additions & 0 deletions kotlin-sdk/api/android/kotlin-sdk.api
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ public abstract interface class dev/openfeature/kotlin/sdk/Client : dev/openfeat
public abstract fun addHooks (Ljava/util/List;)V
public abstract fun getHooks ()Ljava/util/List;
public abstract fun getMetadata ()Ldev/openfeature/kotlin/sdk/ClientMetadata;
public fun getProviderStatus ()Ldev/openfeature/kotlin/sdk/OpenFeatureStatus;
public abstract fun getStatusFlow ()Lkotlinx/coroutines/flow/Flow;
}

public final class dev/openfeature/kotlin/sdk/Client$DefaultImpls {
public static fun getProviderStatus (Ldev/openfeature/kotlin/sdk/Client;)Ldev/openfeature/kotlin/sdk/OpenFeatureStatus;
}

public abstract interface class dev/openfeature/kotlin/sdk/ClientMetadata {
public abstract fun getName ()Ljava/lang/String;
}
Expand Down Expand Up @@ -313,6 +318,7 @@ public final class dev/openfeature/kotlin/sdk/OpenFeatureClient : dev/openfeatur
public fun getObjectDetails (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/FlagEvaluationOptions;)Ldev/openfeature/kotlin/sdk/FlagEvaluationDetails;
public fun getObjectValue (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;)Ldev/openfeature/kotlin/sdk/Value;
public fun getObjectValue (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/FlagEvaluationOptions;)Ldev/openfeature/kotlin/sdk/Value;
public fun getProviderStatus ()Ldev/openfeature/kotlin/sdk/OpenFeatureStatus;
public fun getStatusFlow ()Lkotlinx/coroutines/flow/Flow;
public fun getStringDetails (Ljava/lang/String;Ljava/lang/String;)Ldev/openfeature/kotlin/sdk/FlagEvaluationDetails;
public fun getStringDetails (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/FlagEvaluationOptions;)Ldev/openfeature/kotlin/sdk/FlagEvaluationDetails;
Expand Down
6 changes: 6 additions & 0 deletions kotlin-sdk/api/jvm/kotlin-sdk.api
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ public abstract interface class dev/openfeature/kotlin/sdk/Client : dev/openfeat
public abstract fun addHooks (Ljava/util/List;)V
public abstract fun getHooks ()Ljava/util/List;
public abstract fun getMetadata ()Ldev/openfeature/kotlin/sdk/ClientMetadata;
public fun getProviderStatus ()Ldev/openfeature/kotlin/sdk/OpenFeatureStatus;
public abstract fun getStatusFlow ()Lkotlinx/coroutines/flow/Flow;
}

public final class dev/openfeature/kotlin/sdk/Client$DefaultImpls {
public static fun getProviderStatus (Ldev/openfeature/kotlin/sdk/Client;)Ldev/openfeature/kotlin/sdk/OpenFeatureStatus;
}

public abstract interface class dev/openfeature/kotlin/sdk/ClientMetadata {
public abstract fun getName ()Ljava/lang/String;
}
Expand Down Expand Up @@ -313,6 +318,7 @@ public final class dev/openfeature/kotlin/sdk/OpenFeatureClient : dev/openfeatur
public fun getObjectDetails (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/FlagEvaluationOptions;)Ldev/openfeature/kotlin/sdk/FlagEvaluationDetails;
public fun getObjectValue (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;)Ldev/openfeature/kotlin/sdk/Value;
public fun getObjectValue (Ljava/lang/String;Ldev/openfeature/kotlin/sdk/Value;Ldev/openfeature/kotlin/sdk/FlagEvaluationOptions;)Ldev/openfeature/kotlin/sdk/Value;
public fun getProviderStatus ()Ldev/openfeature/kotlin/sdk/OpenFeatureStatus;
public fun getStatusFlow ()Lkotlinx/coroutines/flow/Flow;
public fun getStringDetails (Ljava/lang/String;Ljava/lang/String;)Ldev/openfeature/kotlin/sdk/FlagEvaluationDetails;
public fun getStringDetails (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/kotlin/sdk/FlagEvaluationOptions;)Ldev/openfeature/kotlin/sdk/FlagEvaluationDetails;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ interface Client : Features, Tracking {
val statusFlow: Flow<OpenFeatureStatus>

fun addHooks(hooks: List<Hook<*>>)
Comment thread
mstepien marked this conversation as resolved.

/**
* Get the current [OpenFeatureStatus] of the Provider handling this client's evaluations, or [OpenFeatureStatus.NotReady] if no Provider has been initialized.
*/
fun getProviderStatus(): OpenFeatureStatus = OpenFeatureStatus.NotReady
Comment thread
mstepien marked this conversation as resolved.
Outdated
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,11 @@ object OpenFeatureAPI {
} catch (e: CancellationException) {
// This happens by design and shouldn't be treated as an error
} catch (e: OpenFeatureError) {
_statusFlow.emit(OpenFeatureStatus.Error(e))
if (e is OpenFeatureError.ProviderFatalError) {
_statusFlow.emit(OpenFeatureStatus.Fatal(e))
} else {
_statusFlow.emit(OpenFeatureStatus.Error(e))
}
} catch (e: Throwable) {
_statusFlow.emit(
OpenFeatureStatus.Error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ class OpenFeatureClient(

override val statusFlow = openFeatureAPI.statusFlow

override fun getProviderStatus(): OpenFeatureStatus {
return openFeatureAPI.getStatus()
}
Comment thread
mstepien marked this conversation as resolved.
Outdated

override fun getBooleanValue(key: String, defaultValue: Boolean): Boolean {
return getBooleanDetails(key, defaultValue).value
}
Expand Down Expand Up @@ -219,7 +223,7 @@ class OpenFeatureClient(
}

private fun shortCircuitIfNotReady() {
val providerStatus = openFeatureAPI.getStatus()
val providerStatus = getProviderStatus()
if (providerStatus == OpenFeatureStatus.NotReady) {
throw OpenFeatureError.ProviderNotReadyError()
} else if (providerStatus is OpenFeatureStatus.Fatal) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
package dev.openfeature.kotlin.sdk

import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError
import dev.openfeature.kotlin.sdk.helpers.BrokenInitProvider
import dev.openfeature.kotlin.sdk.helpers.GenericSpyHookMock
import dev.openfeature.kotlin.sdk.helpers.OverlyEmittingProvider
import dev.openfeature.kotlin.sdk.helpers.SlowProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class OpenFeatureClientTests {

Expand All @@ -20,4 +29,95 @@ class OpenFeatureClientTests {
val stringValue = OpenFeatureAPI.getClient().getStringValue("test", "defaultTest")
assertEquals(stringValue, "defaultTest")
}

/**
* Spec 1.7.1: The client MUST define a provider status accessor.
*/
@Test
fun testClientGetProviderStatusShouldReturnReadyWhenProviderIsInitialized() = runTest {
OpenFeatureAPI.setProviderAndWait(NoOpProvider())
val client = OpenFeatureAPI.getClient()
assertEquals(OpenFeatureStatus.Ready, client.getProviderStatus())
Comment thread
mstepien marked this conversation as resolved.
Outdated
}

/**
* Spec 1.7.1: The client MUST define a provider status accessor.
*/
@Test
fun testClientGetProviderStatusShouldReturnErrorWhenProviderFailsToInitialize() = runTest {
OpenFeatureAPI.setProviderAndWait(BrokenInitProvider())
val client = OpenFeatureAPI.getClient()
val status = client.getProviderStatus()
assertTrue(status is OpenFeatureStatus.Error)
assertTrue(
(status as OpenFeatureStatus.Error).error is OpenFeatureError.ProviderNotReadyError
)
}

/**
* Spec 1.7.2.1: Provider status accessor must support RECONCILING state (static-context paradigm).
*/
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun testClientGetProviderStatusShouldReturnReconcilingWhileContextIsBeingUpdated() = runTest {
val dispatcher = StandardTestDispatcher(testScheduler)
val slowProvider = SlowProvider(dispatcher = dispatcher)
OpenFeatureAPI.setProvider(slowProvider, dispatcher = dispatcher)

// Wait for SlowProvider initialized (2000ms delay)
advanceTimeBy(2001)

val client = OpenFeatureAPI.getClient()
assertEquals(OpenFeatureStatus.Ready, client.getProviderStatus())

// Trigger a context update (takes 2000ms natively)
OpenFeatureAPI.setEvaluationContext(ImmutableContext(targetingKey = "user-123"), dispatcher)

// Run execution queue deterministically to ensure coroutine reaches emit(Reconciling)
runCurrent()
Comment thread
mstepien marked this conversation as resolved.
Outdated
assertEquals(OpenFeatureStatus.Reconciling, client.getProviderStatus())
Comment thread
mstepien marked this conversation as resolved.
Outdated

// Wait out the remaining 2000ms for slow context update finish
advanceTimeBy(2001)
assertEquals(OpenFeatureStatus.Ready, client.getProviderStatus())
Comment thread
mstepien marked this conversation as resolved.
Outdated
}

@Test
fun testClientGetProviderStatusShouldReturnFatalWhenProviderFailsFatal() = runTest {
val fatalProvider = object : FeatureProvider by NoOpProvider() {
override suspend fun initialize(initialContext: EvaluationContext?) {
throw OpenFeatureError.ProviderFatalError("test fatal error")
}
}
OpenFeatureAPI.setProviderAndWait(fatalProvider)
val client = OpenFeatureAPI.getClient()
val status = client.getProviderStatus()
assertTrue(status is OpenFeatureStatus.Fatal)
assertTrue(
(status as OpenFeatureStatus.Fatal).error is OpenFeatureError.ProviderFatalError
)
}

@Test
fun testClientGetProviderStatusShouldReturnNotReadyBeforeProviderIsSet() = runTest {
val client = OpenFeatureAPI.getClient()
// No provider is set, so it should be NotReady
assertEquals(OpenFeatureStatus.NotReady, client.getProviderStatus())
}

@Test
fun testClientGetProviderStatusShouldReturnStaleWhenProviderEmitsStale() = runTest {
val dispatcher = StandardTestDispatcher(testScheduler)
val emittingProvider = OverlyEmittingProvider("emitting_provider")
OpenFeatureAPI.setProviderAndWait(emittingProvider, dispatcher = dispatcher)

val client = OpenFeatureAPI.getClient()
assertEquals(OpenFeatureStatus.Ready, client.getProviderStatus())

// Call track which forces the OverlyEmittingProvider to emit ProviderStale events
client.track("test-stale-event")
runCurrent()

assertEquals(OpenFeatureStatus.Stale, client.getProviderStatus())
}
}
Loading