diff --git a/.gitignore b/.gitignore index 557c14a..a6005d0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store -/build +build /captures .externalNativeBuild .cxx diff --git a/FlagsmithClient/.gitignore b/FlagsmithClient/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/FlagsmithClient/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/FlagsmithClient/build.gradle b/FlagsmithClient/build.gradle index 0766ccb..5ccb0df 100644 --- a/FlagsmithClient/build.gradle +++ b/FlagsmithClient/build.gradle @@ -45,9 +45,10 @@ dependencies { implementation 'com.github.kittinunf.fuel:fuel-gson:2.3.1' testImplementation 'junit:junit:4.13.2' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1' + testImplementation 'org.mock-server:mockserver-netty-no-dependencies:5.14.0' androidTestImplementation 'androidx.test.ext:junit:1.1.4' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1' } afterEvaluate { diff --git a/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt b/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt index e5c2463..65b4e27 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt @@ -5,6 +5,7 @@ import com.flagsmith.internal.FlagsmithApi import com.flagsmith.internal.* import com.flagsmith.entities.* import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.result.Result as FuelResult /** * Flagsmith @@ -20,21 +21,18 @@ import com.github.kittinunf.fuel.Fuel */ class Flagsmith constructor( private val environmentKey: String, - private val baseUrl: String? = null, - val context: Context? = null, + private val baseUrl: String = "https://edge.api.flagsmith.com/api/v1", + private val context: Context? = null, private val enableAnalytics: Boolean = DEFAULT_ENABLE_ANALYTICS, private val analyticsFlushPeriod: Int = DEFAULT_ANALYTICS_FLUSH_PERIOD_SECONDS ) { - private var analytics: FlagsmithAnalytics? = null + private val analytics: FlagsmithAnalytics? = + if (!enableAnalytics) null + else if (context != null) FlagsmithAnalytics(context, analyticsFlushPeriod) + else throw IllegalArgumentException("Flagsmith requires a context to use the analytics feature") init { - if (enableAnalytics && context != null) { - this.analytics = FlagsmithAnalytics(context, analyticsFlushPeriod) - } - if (enableAnalytics && context == null) { - throw IllegalArgumentException("Flagsmith requires a context to use the analytics feature") - } - FlagsmithApi.baseUrl = baseUrl ?: "https://edge.api.flagsmith.com/api/v1" + FlagsmithApi.baseUrl = baseUrl FlagsmithApi.environmentKey = environmentKey } @@ -45,97 +43,76 @@ class Flagsmith constructor( fun getFeatureFlags(identity: String? = null, result: (Result>) -> Unit) { if (identity != null) { - Fuel.request( - FlagsmithApi.getIdentityFlagsAndTraits(identity = identity)) - .responseObject(IdentityFlagsAndTraitsDeserializer()) { _, _, res -> - res.fold( - success = { value -> result(Result.success(value.flags)) }, - failure = { err -> result(Result.failure(err)) } - ) - } + getIdentityFlagsAndTraits(identity) { res -> + result(res.map { it.flags }) + } } else { - Fuel.request(FlagsmithApi.getFlags()) + Fuel.request(FlagsmithApi.GetFlags) .responseObject(FlagListDeserializer()) { _, _, res -> - res.fold( - success = { value -> result(Result.success(value)) }, - failure = { err -> result(Result.failure(err)) } - ) + result(res.convertToResult()) } } } - fun hasFeatureFlag(forFeatureId: String, identity: String? = null, result:(Result) -> Unit) { - getFeatureFlags(identity) { res -> - res.fold( - onSuccess = { flags -> - val foundFlag = flags.find { flag -> flag.feature.name == forFeatureId && flag.enabled } - analytics?.trackEvent(forFeatureId) - result(Result.success(foundFlag != null)) - }, - onFailure = { err -> result(Result.failure(err))} - ) - } + fun hasFeatureFlag( + featureId: String, + identity: String? = null, + result: (Result) -> Unit + ) = getFeatureFlag(featureId, identity) { res -> + result(res.map { flag -> flag != null }) } - fun getValueForFeature(searchFeatureId: String, identity: String? = null, result: (Result) -> Unit) { - getFeatureFlags(identity) { res -> - res.fold( - onSuccess = { flags -> - val foundFlag = flags.find { flag -> flag.feature.name == searchFeatureId && flag.enabled } - analytics?.trackEvent(searchFeatureId) - result(Result.success(foundFlag?.featureStateValue)) - }, - onFailure = { err -> result(Result.failure(err))} - ) - } + fun getValueForFeature( + featureId: String, + identity: String? = null, + result: (Result) -> Unit + ) = getFeatureFlag(featureId, identity) { res -> + result(res.map { flag -> flag?.featureStateValue }) } - fun getTrait(id: String, identity: String, result: (Result) -> Unit) { - Fuel.request( - FlagsmithApi.getIdentityFlagsAndTraits(identity = identity)) - .responseObject(IdentityFlagsAndTraitsDeserializer()) { _, _, res -> - res.fold( - success = { value -> - val trait = value.traits.find { it.key == id } - result(Result.success(trait)) - }, - failure = { err -> result(Result.failure(err)) } - ) - } - } + fun getTrait(id: String, identity: String, result: (Result) -> Unit) = + getIdentityFlagsAndTraits(identity) { res -> + result(res.map { value -> value.traits.find { it.key == id } }) + } - fun getTraits(identity: String, result: (Result>) -> Unit) { - Fuel.request( - FlagsmithApi.getIdentityFlagsAndTraits(identity = identity)) - .responseObject(IdentityFlagsAndTraitsDeserializer()) { _, _, res -> - res.fold( - success = { value -> result(Result.success(value.traits)) }, - failure = { err -> result(Result.failure(err)) } - ) - } - } + fun getTraits(identity: String, result: (Result>) -> Unit) = + getIdentityFlagsAndTraits(identity) { res -> + result(res.map { it.traits }) + } fun setTrait(trait: Trait, identity: String, result: (Result) -> Unit) { - Fuel.request( - FlagsmithApi.setTrait(trait = trait, identity = identity)) + Fuel.request(FlagsmithApi.SetTrait(trait = trait, identity = identity)) .responseObject(TraitWithIdentityDeserializer()) { _, _, res -> - res.fold( - success = { value -> result(Result.success(value)) }, - failure = { err -> result(Result.failure(err)) } - ) + result(res.convertToResult()) } } - fun getIdentity(identity: String, result: (Result) -> Unit){ - Fuel.request( - FlagsmithApi.getIdentityFlagsAndTraits(identity = identity)) - .responseObject(IdentityFlagsAndTraitsDeserializer()) { _, _, res -> - res.fold( - success = { value -> - result(Result.success(value)) - }, - failure = { err -> result(Result.failure(err)) } - ) - } + fun getIdentity(identity: String, result: (Result) -> Unit) = + getIdentityFlagsAndTraits(identity, result) + + private fun getFeatureFlag( + featureId: String, + identity: String?, + result: (Result) -> Unit + ) = getFeatureFlags(identity) { res -> + result(res.map { flags -> + val foundFlag = flags.find { flag -> flag.feature.name == featureId && flag.enabled } + analytics?.trackEvent(featureId) + foundFlag + }) } + + private fun getIdentityFlagsAndTraits( + identity: String, + result: (Result) -> Unit + ) = Fuel.request(FlagsmithApi.GetIdentityFlagsAndTraits(identity = identity)) + .responseObject(IdentityFlagsAndTraitsDeserializer()) { _, _, res -> + result(res.convertToResult()) + } + + private fun FuelResult.convertToResult(): Result = + fold( + success = { value -> Result.success(value) }, + failure = { err -> Result.failure(err) } + ) } \ No newline at end of file diff --git a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithAnalytics.kt b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithAnalytics.kt index 1a516bd..fef5c85 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithAnalytics.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithAnalytics.kt @@ -5,7 +5,6 @@ import android.content.SharedPreferences import android.os.Handler import android.os.Looper import android.util.Log -import com.flagsmith.Flagsmith import com.github.kittinunf.fuel.Fuel import org.json.JSONException import org.json.JSONObject @@ -21,7 +20,7 @@ class FlagsmithAnalytics constructor( private val timerRunnable = object : Runnable { override fun run() { if (currentEvents.isNotEmpty()) { - Fuel.request(FlagsmithApi.postAnalytics(eventMap = currentEvents)) + Fuel.request(FlagsmithApi.PostAnalytics(eventMap = currentEvents)) .response { _, _, res -> res.fold( success = { resetMap() }, @@ -44,7 +43,7 @@ class FlagsmithAnalytics constructor( /// Counts the instances of a `Flag` being queried. fun trackEvent(flagName: String) { val currentFlagCount = currentEvents[flagName] ?: 0 - currentEvents[flagName] = currentFlagCount + 1; + currentEvents[flagName] = currentFlagCount + 1 // Update events cache setMap(currentEvents) diff --git a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithApi.kt b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithApi.kt index d7bd788..f96c9c3 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithApi.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithApi.kt @@ -9,11 +9,11 @@ import com.github.kittinunf.fuel.core.Parameters import com.github.kittinunf.fuel.util.FuelRouting import com.google.gson.Gson -sealed class FlagsmithApi: FuelRouting { - class getIdentityFlagsAndTraits(val identity: String): FlagsmithApi() - class getFlags(): FlagsmithApi() - class setTrait(val trait: Trait, val identity: String): FlagsmithApi() - class postAnalytics(val eventMap: Map): FlagsmithApi() +sealed class FlagsmithApi : FuelRouting { + class GetIdentityFlagsAndTraits(val identity: String) : FlagsmithApi() + object GetFlags : FlagsmithApi() + class SetTrait(val trait: Trait, val identity: String) : FlagsmithApi() + class PostAnalytics(val eventMap: Map) : FlagsmithApi() companion object { var environmentKey: String? = null @@ -29,52 +29,48 @@ sealed class FlagsmithApi: FuelRouting { } } override val body: String? - get() { - return when(this) { - is setTrait -> Gson().toJson(TraitWithIdentity(key = trait.key, value = trait.value, identity = Identity(identity))) - is postAnalytics -> Gson().toJson(eventMap) - else -> null - } + get() = when (this) { + is SetTrait -> Gson().toJson( + TraitWithIdentity( + key = trait.key, + value = trait.value, + identity = Identity(identity) + ) + ) + is PostAnalytics -> Gson().toJson(eventMap) + else -> null } - override val bytes: ByteArray? - get() = null + override val bytes: ByteArray? = null override val headers: Map? - get() { - val headers = mutableMapOf("X-Environment-Key" to listOf(environmentKey ?: "")) + get() = mutableMapOf( + "X-Environment-Key" to listOf(environmentKey ?: "") + ).apply { if (method == Method.POST) { - headers["Content-Type"] = listOf("application/json") + this += "Content-Type" to listOf("application/json") } - return headers } override val method: Method - get() { - return when(this) { - is getIdentityFlagsAndTraits -> Method.GET - is getFlags -> Method.GET - is setTrait -> Method.POST - is postAnalytics -> Method.POST - } + get() = when (this) { + is GetIdentityFlagsAndTraits -> Method.GET + is GetFlags -> Method.GET + is SetTrait -> Method.POST + is PostAnalytics -> Method.POST } override val params: Parameters? - get() { - return when(this) { - is getIdentityFlagsAndTraits -> listOf("identifier" to this.identity) - is setTrait -> listOf("identifier" to this.identity) - else -> null - } + get() = when (this) { + is GetIdentityFlagsAndTraits -> listOf("identifier" to this.identity) + else -> null } override val path: String - get() { - return when(this) { - is getIdentityFlagsAndTraits -> "/identities" - is getFlags -> "/flags" - is setTrait -> "/traits" - is postAnalytics -> "/analytics/flags" - } + get() = when (this) { + is GetIdentityFlagsAndTraits -> "/identities" + is GetFlags -> "/flags" + is SetTrait -> "/traits" + is PostAnalytics -> "/analytics/flags" } } \ No newline at end of file diff --git a/FlagsmithClient/src/test/java/com/flagsmith/FeatureFlagTests.kt b/FlagsmithClient/src/test/java/com/flagsmith/FeatureFlagTests.kt index 6411029..0159797 100644 --- a/FlagsmithClient/src/test/java/com/flagsmith/FeatureFlagTests.kt +++ b/FlagsmithClient/src/test/java/com/flagsmith/FeatureFlagTests.kt @@ -1,15 +1,41 @@ package com.flagsmith -import junit.framework.Assert.* +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.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test +import org.mockserver.integration.ClientAndServer class FeatureFlagTests { - private val flagsmith = Flagsmith(environmentKey = System.getenv("ENVIRONMENT_KEY") ?: "", enableAnalytics = false) + 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 + ) + } + + @After + fun tearDown() { + mockServer.stop() + } @Test fun testHasFeatureFlagWithFlag() { + mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) runBlocking { val result = flagsmith.hasFeatureFlagSync("no-value") assertTrue(result.isSuccess) @@ -19,6 +45,7 @@ class FeatureFlagTests { @Test fun testHasFeatureFlagWithoutFlag() { + mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) runBlocking { val result = flagsmith.hasFeatureFlagSync("doesnt-exist") assertTrue(result.isSuccess) @@ -28,48 +55,53 @@ class FeatureFlagTests { @Test fun testGetFeatureFlags() { + mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) runBlocking { val result = flagsmith.getFeatureFlagsSync() assertTrue(result.isSuccess) val found = result.getOrThrow().find { flag -> flag.feature.name == "with-value" } assertNotNull(found) - assertEquals(found?.featureStateValue, 7.0) + assertEquals(7.0, found?.featureStateValue) } } @Test fun testGetFeatureFlagsWithIdentity() { + mockServer.mockResponseFor(MockEndpoint.GET_IDENTITIES) runBlocking { val result = flagsmith.getFeatureFlagsSync(identity = "person") assertTrue(result.isSuccess) val found = result.getOrThrow().find { flag -> flag.feature.name == "with-value" } assertNotNull(found) - assertEquals(found?.featureStateValue, 756.0) + assertEquals(756.0, found?.featureStateValue) } } @Test fun testGetValueForFeatureExisting() { + mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) runBlocking { val result = flagsmith.getValueForFeatureSync("with-value", identity = null) assertTrue(result.isSuccess) - assertEquals(result.getOrThrow(), 7.0) + assertEquals(7.0, result.getOrThrow()) } } @Test fun testGetValueForFeatureExistingOverriddenWithIdentity() { + mockServer.mockResponseFor(MockEndpoint.GET_IDENTITIES) runBlocking { val result = flagsmith.getValueForFeatureSync("with-value", identity = "person") assertTrue(result.isSuccess) - assertEquals(result.getOrThrow(), 756.0) + assertEquals(756.0, result.getOrThrow()) } } @Test fun testGetValueForFeatureNotExisting() { + mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) runBlocking { val result = flagsmith.getValueForFeatureSync("not-existing", identity = null) assertTrue(result.isSuccess) @@ -79,8 +111,10 @@ class FeatureFlagTests { @Test fun testHasFeatureForNoIdentity() { + mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) runBlocking { - val result = flagsmith.hasFeatureFlagSync("with-value-just-person-enabled", identity = null) + val result = + flagsmith.hasFeatureFlagSync("with-value-just-person-enabled", identity = null) assertTrue(result.isSuccess) assertFalse(result.getOrThrow()) } @@ -88,8 +122,10 @@ class FeatureFlagTests { @Test fun testHasFeatureWithIdentity() { + mockServer.mockResponseFor(MockEndpoint.GET_IDENTITIES) runBlocking { - val result = flagsmith.hasFeatureFlagSync("with-value-just-person-enabled", identity = "person") + val result = + flagsmith.hasFeatureFlagSync("with-value-just-person-enabled", identity = "person") assertTrue(result.isSuccess) assertTrue(result.getOrThrow()) } diff --git a/FlagsmithClient/src/test/java/com/flagsmith/TraitsTests.kt b/FlagsmithClient/src/test/java/com/flagsmith/TraitsTests.kt index fc0617e..73f0c16 100644 --- a/FlagsmithClient/src/test/java/com/flagsmith/TraitsTests.kt +++ b/FlagsmithClient/src/test/java/com/flagsmith/TraitsTests.kt @@ -1,26 +1,54 @@ package com.flagsmith import com.flagsmith.entities.Trait -import junit.framework.Assert.* +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.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test +import org.mockserver.integration.ClientAndServer class TraitsTests { - private val flagsmith = Flagsmith(environmentKey = System.getenv("ENVIRONMENT_KEY") ?: "", enableAnalytics = false) + 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 + ) + } + + @After + fun tearDown() { + mockServer.stop() + } @Test fun testGetTraitsDefinedForPerson() { + mockServer.mockResponseFor(MockEndpoint.GET_IDENTITIES) runBlocking { val result = flagsmith.getTraitsSync("person") assertTrue(result.isSuccess) assertTrue(result.getOrThrow().isNotEmpty()) - assertEquals(result.getOrThrow().find { trait -> trait.key == "favourite-colour" }?.value, "electric pink") + assertEquals( + "electric pink", + result.getOrThrow().find { trait -> trait.key == "favourite-colour" }?.value + ) } } @Test fun testGetTraitsNotDefinedForPerson() { + mockServer.mockResponseFor(MockEndpoint.GET_IDENTITIES) runBlocking { val result = flagsmith.getTraitsSync("person") assertTrue(result.isSuccess) @@ -31,15 +59,17 @@ class TraitsTests { @Test fun testGetTraitById() { + mockServer.mockResponseFor(MockEndpoint.GET_IDENTITIES) runBlocking { val result = flagsmith.getTraitSync("favourite-colour", "person") assertTrue(result.isSuccess) - assertEquals(result.getOrThrow()?.value, "electric pink") + assertEquals("electric pink", result.getOrThrow()?.value) } } @Test fun testGetUndefinedTraitById() { + mockServer.mockResponseFor(MockEndpoint.GET_IDENTITIES) runBlocking { val result = flagsmith.getTraitSync("favourite-cricketer", "person") assertTrue(result.isSuccess) @@ -49,23 +79,29 @@ class TraitsTests { @Test fun testSetTrait() { + mockServer.mockResponseFor(MockEndpoint.SET_TRAIT) runBlocking { - val result = flagsmith.setTraitSync(Trait(key = "set-from-client", value = "12345"), "person") + val result = + flagsmith.setTraitSync(Trait(key = "set-from-client", value = "12345"), "person") assertTrue(result.isSuccess) - assertEquals(result.getOrThrow().key, "set-from-client") - assertEquals(result.getOrThrow().value, "12345") - assertEquals(result.getOrThrow().identity.identifier, "person") + assertEquals("set-from-client", result.getOrThrow().key) + assertEquals("12345", result.getOrThrow().value) + 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(result.getOrThrow().traits.find { trait -> trait.key == "favourite-colour" }?.value, "electric pink") + assertEquals( + "electric pink", + result.getOrThrow().traits.find { trait -> trait.key == "favourite-colour" }?.value + ) } } } \ 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 new file mode 100644 index 0000000..0d5ff91 --- /dev/null +++ b/FlagsmithClient/src/test/java/com/flagsmith/mockResponses/MockResponses.kt @@ -0,0 +1,109 @@ +package com.flagsmith.mockResponses + +import org.mockserver.integration.ClientAndServer +import org.mockserver.model.HttpRequest.request +import org.mockserver.model.HttpResponse.response +import org.mockserver.model.MediaType + +enum class MockEndpoint(val path: String, val body: String) { + GET_IDENTITIES("/identities", MockResponses.getIdentities), + GET_FLAGS("/flags", MockResponses.getFlags), + SET_TRAIT("/traits", MockResponses.setTrait) +} + +fun ClientAndServer.mockResponseFor(endpoint: MockEndpoint) { + `when`(request().withPath(endpoint.path)) + .respond( + response() + .withContentType(MediaType.APPLICATION_JSON) + .withBody(endpoint.body) + ) +} + +object MockResponses { + val getIdentities = """ + { + "flags": [ + { + "feature_state_value": null, + "feature": { + "type": "STANDARD", + "name": "no-value", + "id": 35506 + }, + "enabled": true + }, + { + "feature_state_value": 756, + "feature": { + "type": "STANDARD", + "name": "with-value", + "id": 35507 + }, + "enabled": true + }, + { + "feature_state_value": "", + "feature": { + "type": "STANDARD", + "name": "with-value-just-person-enabled", + "id": 35508 + }, + "enabled": true + } + ], + "traits": [ + { + "trait_value": "12345", + "trait_key": "set-from-client" + }, + { + "trait_value": "electric pink", + "trait_key": "favourite-colour" + } + ] + } + """.trimIndent() + + val getFlags = """ + [ + { + "enabled": true, + "feature": { + "type": "STANDARD", + "id": 35506, + "name": "no-value" + }, + "feature_state_value": null + }, + { + "enabled": true, + "feature": { + "type": "STANDARD", + "id": 35507, + "name": "with-value" + }, + "feature_state_value": 7 + }, + { + "enabled": false, + "feature": { + "type": "STANDARD", + "id": 35508, + "name": "with-value-just-person-enabled" + }, + "feature_state_value": null + } + ] + """.trimIndent() + + val setTrait = """ + { + "trait_key": "set-from-client", + "trait_value": "12345", + "identity": { + "identifier": "person" + } + } + """.trimIndent() +} \ No newline at end of file