diff --git a/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt b/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt index 1fcc4ed..6101f27 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt @@ -106,17 +106,17 @@ class Flagsmith constructor( const val DEFAULT_ANALYTICS_FLUSH_PERIOD_SECONDS = 10 } - fun getFeatureFlags(identity: String? = null, traits: List? = null, result: (Result>) -> Unit) { + fun getFeatureFlags(identity: String? = null, traits: List? = null, transient: Boolean = false, result: (Result>) -> Unit) { // Save the last used identity as we'll refresh with this if we get update events lastUsedIdentity = identity if (identity != null) { if (traits != null) { - retrofit.postTraits(IdentityAndTraits(identity, traits)).enqueueWithResult(result = { + retrofit.postTraits(IdentityAndTraits(identity, traits, transient)).enqueueWithResult(result = { result(it.map { response -> response.flags }) }).also { lastUsedIdentity = identity } } else { - retrofit.getIdentityFlagsAndTraits(identity).enqueueWithResult { res -> + retrofit.getIdentityFlagsAndTraits(identity, transient).enqueueWithResult { res -> flagUpdateFlow.tryEmit(res.getOrNull()?.flags ?: emptyList()) result(res.map { it.flags }) } @@ -181,8 +181,8 @@ class Flagsmith constructor( }) } - fun getIdentity(identity: String, result: (Result) -> Unit) = - retrofit.getIdentityFlagsAndTraits(identity).enqueueWithResult(defaults = null, result = result) + fun getIdentity(identity: String, transient: Boolean = false, result: (Result) -> Unit) = + retrofit.getIdentityFlagsAndTraits(identity, transient).enqueueWithResult(defaults = null, result = result) .also { lastUsedIdentity = identity } fun clearCache() { diff --git a/FlagsmithClient/src/main/java/com/flagsmith/entities/IdentityAndTraits.kt b/FlagsmithClient/src/main/java/com/flagsmith/entities/IdentityAndTraits.kt index c626f5b..49f28d2 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/entities/IdentityAndTraits.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/entities/IdentityAndTraits.kt @@ -4,5 +4,6 @@ import com.google.gson.annotations.SerializedName data class IdentityAndTraits( @SerializedName(value = "identifier") val identifier: String, - @SerializedName(value = "traits") val traits: List + @SerializedName(value = "traits") val traits: List, + @SerializedName(value = "transient") val transient: Boolean? = null ) \ No newline at end of file diff --git a/FlagsmithClient/src/main/java/com/flagsmith/entities/Trait.kt b/FlagsmithClient/src/main/java/com/flagsmith/entities/Trait.kt index f03c2c8..e68a3d7 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/entities/Trait.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/entities/Trait.kt @@ -3,23 +3,25 @@ package com.flagsmith.entities import com.google.gson.annotations.SerializedName + data class Trait ( val identifier: String? = null, @SerializedName(value = "trait_key") val key: String, - @SerializedName(value = "trait_value") val traitValue: Any + @SerializedName(value = "trait_value") val traitValue: Any, + val transient: Boolean = false ) { - constructor(key: String, value: String) - : this(key = key, traitValue = value) + constructor(key: String, value: String, transient: Boolean = false) + : this(key = key, traitValue = value, transient = transient) - constructor(key: String, value: Int) - : this(key = key, traitValue = value) + constructor(key: String, value: Int, transient: Boolean = false) + : this(key = key, traitValue = value, transient = transient) - constructor(key: String, value: Double) - : this(key = key, traitValue = value) + constructor(key: String, value: Double, transient: Boolean = false) + : this(key = key, traitValue = value, transient = transient) - constructor(key: String, value: Boolean) - : this(key = key, traitValue = value) + constructor(key: String, value: Boolean, transient: Boolean = false) + : this(key = key, traitValue = value, transient = transient) @Deprecated("Use traitValue instead or one of the type-safe getters", ReplaceWith("traitValue")) val value: String @@ -42,25 +44,25 @@ data class Trait ( val booleanValue: Boolean? get() = traitValue as? Boolean - } data class TraitWithIdentity ( @SerializedName(value = "trait_key") val key: String, @SerializedName(value = "trait_value") val traitValue: Any, val identity: Identity, + val transient: Boolean = false ) { - constructor(key: String, value: String, identity: Identity) - : this(key = key, traitValue = value, identity = identity) + constructor(key: String, value: String, identity: Identity, transient: Boolean = false) + : this(key = key, traitValue = value, identity = identity, transient = transient) - constructor(key: String, value: Int, identity: Identity) - : this(key = key, traitValue = value, identity = identity) + constructor(key: String, value: Int, identity: Identity, transient: Boolean = false) + : this(key = key, traitValue = value, identity = identity, transient = transient) - constructor(key: String, value: Double, identity: Identity) - : this(key = key, traitValue = value, identity = identity) + constructor(key: String, value: Double, identity: Identity, transient: Boolean = false) + : this(key = key, traitValue = value, identity = identity, transient = transient) - constructor(key: String, value: Boolean, identity: Identity) - : this(key = key, traitValue = value, identity = identity) + constructor(key: String, value: Boolean, identity: Identity, transient: Boolean = false) + : this(key = key, traitValue = value, identity = identity, transient = transient) @Deprecated("Use traitValue instead or one of the type-safe getters", ReplaceWith("traitValue")) val value: String diff --git a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt index 9337d73..1a43011 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt @@ -19,11 +19,12 @@ import retrofit2.http.Query interface FlagsmithRetrofitService { @GET("identities/") - fun getIdentityFlagsAndTraits(@Query("identifier") identity: String) : Call + fun getIdentityFlagsAndTraits(@Query("identifier") identity: String, @Query("transient") transient: Boolean = false) : Call @GET("flags/") fun getFlags() : Call> + // todo: rename this function @POST("identities/") fun postTraits(@Body identity: IdentityAndTraits) : Call diff --git a/FlagsmithClient/src/test/java/com/flagsmith/FeatureFlagTests.kt b/FlagsmithClient/src/test/java/com/flagsmith/FeatureFlagTests.kt index a6daeda..b9c8962 100644 --- a/FlagsmithClient/src/test/java/com/flagsmith/FeatureFlagTests.kt +++ b/FlagsmithClient/src/test/java/com/flagsmith/FeatureFlagTests.kt @@ -1,6 +1,8 @@ package com.flagsmith +import com.flagsmith.entities.Trait import com.flagsmith.mockResponses.MockEndpoint +import com.flagsmith.mockResponses.MockResponses import com.flagsmith.mockResponses.mockResponseFor import kotlinx.coroutines.runBlocking import org.junit.After @@ -13,6 +15,9 @@ 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.mockserver.model.HttpResponse.response +import org.mockserver.model.JsonBody.json class FeatureFlagTests { @@ -157,4 +162,84 @@ class FeatureFlagTests { assertEquals(756.0, found?.featureStateValue) } } + + @Test + fun testGetFeatureFlagsWithTransientTraits() { + mockServer.`when`( + request() + .withPath("/identities/") + .withMethod("POST") + .withBody( + json( + """ + { + "identifier": "identity", + "traits": [ + { + "trait_key": "transient-trait", + "trait_value": "value", + "transient": true + }, + { + "trait_key": "persisted-trait", + "trait_value": "value", + "transient": false + } + ], + "transient": false + } + """.trimIndent() + ) + ) + ) + .respond( + response() + .withStatusCode(200) + .withBody(MockResponses.getTransientIdentities) + ) + + runBlocking { + val transientTrait = Trait("transient-trait", "value", true) + val persistedTrait = Trait("persisted-trait", "value", false) + val result = flagsmith.getFeatureFlagsSync( + "identity", + listOf(transientTrait, persistedTrait), + false, + ) + + assertTrue(result.isSuccess) + } + } + + @Test + fun testGetFeatureFlagsWithTransientIdentity() { + mockServer.`when`( + request() + .withPath("/identities/") + .withMethod("POST") + .withBody( + json( + """ + { + "identifier": "identity", + "traits": [], + "transient": true + } + """.trimIndent() + ) + ) + ) + .respond( + response() + .withStatusCode(200) + .withBody(MockResponses.getTransientIdentities) + ) + + runBlocking { + val result = flagsmith.getFeatureFlagsSync( + "identity", listOf(),true, + ) + assertTrue(result.isSuccess) + } + } } \ No newline at end of file diff --git a/FlagsmithClient/src/test/java/com/flagsmith/IdentityTests.kt b/FlagsmithClient/src/test/java/com/flagsmith/IdentityTests.kt new file mode 100644 index 0000000..7a6c9c4 --- /dev/null +++ b/FlagsmithClient/src/test/java/com/flagsmith/IdentityTests.kt @@ -0,0 +1,80 @@ +package com.flagsmith + +import com.flagsmith.entities.Trait +import com.flagsmith.mockResponses.MockEndpoint +import com.flagsmith.mockResponses.MockResponses +import com.flagsmith.mockResponses.mockResponseFor +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockserver.integration.ClientAndServer +import org.mockserver.model.HttpRequest.request + +class IdentityTests { + + private lateinit var mockServer: ClientAndServer + private lateinit var flagsmith: Flagsmith + + @Before + fun setup() { + mockServer = ClientAndServer.startClientAndServer() + flagsmith = Flagsmith( + environmentKey = "", + baseUrl = "http://localhost:${mockServer.localPort}", + enableAnalytics = false, + cacheConfig = FlagsmithCacheConfig(enableCache = false) + ) + } + + @After + fun tearDown() { + mockServer.stop() + } + + @Test + fun testGetIdentity() { + mockServer.mockResponseFor(MockEndpoint.GET_IDENTITIES) + runBlocking { + val result = flagsmith.getIdentitySync("person") + + mockServer.verify( + request() + .withPath("/identities/") + .withMethod("GET") + .withQueryStringParameter("identifier", "person") + ) + + assertTrue(result.isSuccess) + assertTrue(result.getOrThrow().traits.isNotEmpty()) + assertTrue(result.getOrThrow().flags.isNotEmpty()) + assertEquals( + "electric pink", + result.getOrThrow().traits.find { trait -> trait.key == "favourite-colour" }?.stringValue + ) + } + } + + @Test + fun testGetTransientIdentity() { + mockServer.mockResponseFor(MockEndpoint.GET_TRANSIENT_IDENTITIES) + runBlocking { + val result = flagsmith.getIdentitySync("transient-identity", true) + + mockServer.verify( + request() + .withPath("/identities/") + .withMethod("GET") + .withQueryStringParameter("identifier", "transient-identity") + .withQueryStringParameter("transient", "true") + ) + + assertTrue(result.isSuccess) + assertTrue(result.getOrThrow().traits.isEmpty()) + assertTrue(result.getOrThrow().flags.isNotEmpty()) + } + } +} diff --git a/FlagsmithClient/src/test/java/com/flagsmith/SynchronousFlagsmith.kt b/FlagsmithClient/src/test/java/com/flagsmith/SynchronousFlagsmith.kt index 41b9f12..8667d5a 100644 --- a/FlagsmithClient/src/test/java/com/flagsmith/SynchronousFlagsmith.kt +++ b/FlagsmithClient/src/test/java/com/flagsmith/SynchronousFlagsmith.kt @@ -10,8 +10,8 @@ import kotlin.coroutines.suspendCoroutine suspend fun Flagsmith.hasFeatureFlagSync(forFeatureId: String, identity: String? = null): Result = suspendCoroutine { cont -> this.hasFeatureFlag(forFeatureId, identity = identity) { cont.resume(it) } } -suspend fun Flagsmith.getFeatureFlagsSync(identity: String? = null, traits: List? = null) : Result> - = suspendCoroutine { cont -> this.getFeatureFlags(identity = identity, traits = traits) { cont.resume(it) } } +suspend fun Flagsmith.getFeatureFlagsSync(identity: String? = null, traits: List? = null, transient: Boolean = false) : Result> + = suspendCoroutine { cont -> this.getFeatureFlags(identity = identity, traits = traits, transient = transient) { cont.resume(it) } } suspend fun Flagsmith.getValueForFeatureSync(forFeatureId: String, identity: String? = null): Result = suspendCoroutine { cont -> this.getValueForFeature(forFeatureId, identity = identity) { cont.resume(it) } } @@ -28,6 +28,6 @@ suspend fun Flagsmith.setTraitSync(trait: Trait, identity: String) : Result, identity: String) : Result> = suspendCoroutine { cont -> this.setTraits(traits, identity) { cont.resume(it) } } -suspend fun Flagsmith.getIdentitySync(identity: String): Result - = suspendCoroutine { cont -> this.getIdentity(identity) { cont.resume(it) } } +suspend fun Flagsmith.getIdentitySync(identity: String, transient: Boolean = false): Result + = suspendCoroutine { cont -> this.getIdentity(identity, transient) { cont.resume(it) } } diff --git a/FlagsmithClient/src/test/java/com/flagsmith/TraitsTests.kt b/FlagsmithClient/src/test/java/com/flagsmith/TraitsTests.kt index 962c0c8..22eaf8a 100644 --- a/FlagsmithClient/src/test/java/com/flagsmith/TraitsTests.kt +++ b/FlagsmithClient/src/test/java/com/flagsmith/TraitsTests.kt @@ -142,19 +142,4 @@ class TraitsTests { assertEquals("person", result.getOrThrow().identity.identifier) } } - - @Test - fun testGetIdentity() { - mockServer.mockResponseFor(MockEndpoint.GET_IDENTITIES) - runBlocking { - val result = flagsmith.getIdentitySync("person") - assertTrue(result.isSuccess) - assertTrue(result.getOrThrow().traits.isNotEmpty()) - assertTrue(result.getOrThrow().flags.isNotEmpty()) - assertEquals( - "electric pink", - result.getOrThrow().traits.find { trait -> trait.key == "favourite-colour" }?.stringValue - ) - } - } -} \ No newline at end of file +} diff --git a/FlagsmithClient/src/test/java/com/flagsmith/mockResponses/MockResponses.kt b/FlagsmithClient/src/test/java/com/flagsmith/mockResponses/MockResponses.kt index 9dc70e7..5164e63 100644 --- a/FlagsmithClient/src/test/java/com/flagsmith/mockResponses/MockResponses.kt +++ b/FlagsmithClient/src/test/java/com/flagsmith/mockResponses/MockResponses.kt @@ -18,6 +18,7 @@ enum class MockEndpoint(val path: String, val body: String) { GET_FLAGS(FlagsEndpoint.path, MockResponses.getFlags), SET_TRAIT(TraitsEndpoint(Trait(key = "", traitValue = ""), "").path, MockResponses.setTrait), SET_TRAITS(TraitsBulkEndpoint(listOf(Trait(key = "", traitValue = "")), "").path, MockResponses.setTraits), + GET_TRANSIENT_IDENTITIES(IdentityFlagsAndTraitsEndpoint("").path, MockResponses.getTransientIdentities), SET_TRAIT_INTEGER(TraitsEndpoint(Trait(key = "", traitValue = ""), "").path, MockResponses.setTraitInteger), SET_TRAIT_DOUBLE(TraitsEndpoint(Trait(key = "", traitValue = ""), "").path, MockResponses.setTraitDouble), SET_TRAIT_BOOLEAN(TraitsEndpoint(Trait(key = "", traitValue = ""), "").path, MockResponses.setTraitBoolean), @@ -37,6 +38,10 @@ enum class MockEndpoint(val path: String, val body: String) { IdentityFlagsAndTraitsEndpoint("").path, MockResponses.getTraitBoolean ), + POST_TRANSIENT_TRAITS( + IdentityFlagsAndTraitsEndpoint("").path, + MockResponses.postTransientIdentities + ), } fun ClientAndServer.mockResponseFor(endpoint: MockEndpoint) { @@ -116,11 +121,58 @@ object MockResponses { "traits": [ { "trait_value": "12345", - "trait_key": "set-from-client" + "trait_key": "set-from-client", + "transient": false }, { "trait_value": "electric pink", - "trait_key": "favourite-colour" + "trait_key": "favourite-colour", + "transient": false + } + ] + } + """.trimIndent() + + val getTransientIdentities = """ + { + "flags": [ + { + "feature_state_value": null, + "feature": { + "type": "STANDARD", + "name": "no-value", + "id": 35506 + }, + "enabled": true + } + ], + "traits": [] + } + """.trimIndent() + + val postTransientIdentities = """ + { + "flags": [ + { + "feature_state_value": null, + "feature": { + "type": "STANDARD", + "name": "no-value", + "id": 35506 + }, + "enabled": true + } + ], + "traits": [ + { + "trait_key": "persisted-trait", + "trait_value": "value", + "transient": false + }, + { + "trait_key": "transient-trait", + "trait_value": "value", + "transient": true, } ] } @@ -165,7 +217,8 @@ object MockResponses { "traits": [ { "trait_value": "12345", - "trait_key": "set-from-client" + "trait_key": "set-from-client", + "transient": false } ] } @@ -178,7 +231,8 @@ object MockResponses { "traits": [ { "trait_value": "12345", - "trait_key": "set-from-client" + "trait_key": "set-from-client", + "transient": false } ] } @@ -398,4 +452,4 @@ object MockResponses { ] } """.trimIndent() -} \ No newline at end of file +} diff --git a/FlagsmithClient/src/test/java/com/flagsmith/mockResponses/endpoints/IdentityFlagsAndTraitsEndpoint.kt b/FlagsmithClient/src/test/java/com/flagsmith/mockResponses/endpoints/IdentityFlagsAndTraitsEndpoint.kt index ce62a5c..d5372f1 100644 --- a/FlagsmithClient/src/test/java/com/flagsmith/mockResponses/endpoints/IdentityFlagsAndTraitsEndpoint.kt +++ b/FlagsmithClient/src/test/java/com/flagsmith/mockResponses/endpoints/IdentityFlagsAndTraitsEndpoint.kt @@ -2,8 +2,8 @@ package com.flagsmith.mockResponses.endpoints import com.flagsmith.entities.IdentityFlagsAndTraits -data class IdentityFlagsAndTraitsEndpoint(private val identity: String) : +data class IdentityFlagsAndTraitsEndpoint(private val identity: String, private val transient: Boolean = false) : GetEndpoint( path = "/identities/", - params = listOf("identifier" to identity), + params = listOf("identifier" to identity, "transient" to transient), ) \ No newline at end of file diff --git a/FlagsmithClient/src/test/java/com/flagsmith/mockResponses/endpoints/TraitsEndpoint.kt b/FlagsmithClient/src/test/java/com/flagsmith/mockResponses/endpoints/TraitsEndpoint.kt index ab371b3..4582d13 100644 --- a/FlagsmithClient/src/test/java/com/flagsmith/mockResponses/endpoints/TraitsEndpoint.kt +++ b/FlagsmithClient/src/test/java/com/flagsmith/mockResponses/endpoints/TraitsEndpoint.kt @@ -5,13 +5,14 @@ import com.flagsmith.entities.Trait import com.flagsmith.entities.TraitWithIdentity import com.google.gson.Gson -data class TraitsEndpoint(private val trait: Trait, private val identity: String) : +data class TraitsEndpoint(private val trait: Trait, private val identity: String, private val transient: Boolean = false) : PostEndpoint( path = "/identities/", body = Gson().toJson( IdentityAndTraits( identifier = identity, - traits = listOf(trait) + traits = listOf(trait), + transient = transient, ) ), - ) \ No newline at end of file + )