diff --git a/src/main/java/com/flagsmith/FlagsmithClient.java b/src/main/java/com/flagsmith/FlagsmithClient.java index 054648ae..c29ed283 100644 --- a/src/main/java/com/flagsmith/FlagsmithClient.java +++ b/src/main/java/com/flagsmith/FlagsmithClient.java @@ -39,6 +39,7 @@ public class FlagsmithClient { private FlagsmithSdk flagsmithSdk; private EnvironmentModel environment; private PollingManager pollingManager; + private Map identitiesWithOverridesByIdentifier; private FlagsmithClient() { } @@ -57,6 +58,16 @@ public void updateEnvironment() { // if we didn't get an environment from the API, // then don't overwrite the copy we already have. if (updatedEnvironment != null) { + List identityOverrides = updatedEnvironment.getIdentityOverrides(); + + if (identityOverrides != null) { + Map identitiesWithOverridesByIdentifier = new HashMap<>(); + for (IdentityModel identity : identityOverrides) { + identitiesWithOverridesByIdentifier.put(identity.getIdentifier(), identity); + } + this.identitiesWithOverridesByIdentifier = identitiesWithOverridesByIdentifier; + } + this.environment = updatedEnvironment; } else { logger.error(getEnvironmentUpdateErrorMessage()); @@ -138,7 +149,7 @@ public List getIdentitySegments(String identifier, Map if (environment == null) { throw new FlagsmithClientError("Local evaluation required to obtain identity segments."); } - IdentityModel identityModel = buildIdentityModel( + IdentityModel identityModel = getIdentityModel( identifier, (traits != null ? traits : new HashMap<>())); List segmentModels = SegmentEvaluator.getIdentitySegments( environment, identityModel); @@ -187,7 +198,7 @@ private Flags getIdentityFlagsFromDocument(String identifier, Map featureStates = Engine.getIdentityFeatureStates(environment, identity); return Flags.fromFeatureStateModels( @@ -245,11 +256,11 @@ private Flags getIdentityFlagsFromApi(String identifier, Map tra } } - private IdentityModel buildIdentityModel(String identifier, Map traits) + private IdentityModel getIdentityModel(String identifier, Map traits) throws FlagsmithClientError { if (environment == null) { throw new FlagsmithClientError( - "Unable to build identity model when no local environment present."); + "Unable to build identity model when no local environment present."); } List traitsList = traits.entrySet().stream().map((entry) -> { @@ -260,6 +271,14 @@ private IdentityModel buildIdentityModel(String identifier, Map return trait; }).collect(Collectors.toList()); + if (identitiesWithOverridesByIdentifier != null) { + IdentityModel identityOverride = identitiesWithOverridesByIdentifier.get(identifier); + if (identityOverride != null) { + identityOverride.updateTraits(traitsList); + return identityOverride; + } + } + IdentityModel identity = new IdentityModel(); identity.setIdentityTraits(traitsList); identity.setEnvironmentApiKey(environment.getApiKey()); diff --git a/src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java b/src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java index 6988de72..58f1ac3b 100644 --- a/src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java +++ b/src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java @@ -1,14 +1,13 @@ package com.flagsmith.flagengine.environments; import com.fasterxml.jackson.annotation.JsonProperty; -import com.flagsmith.flagengine.environments.integrations.IntegrationModel; import com.flagsmith.flagengine.features.FeatureStateModel; +import com.flagsmith.flagengine.identities.IdentityModel; import com.flagsmith.flagengine.projects.ProjectModel; import com.flagsmith.utils.models.BaseModel; import java.util.List; import lombok.Data; - @Data public class EnvironmentModel extends BaseModel { private Integer id; @@ -20,12 +19,6 @@ public class EnvironmentModel extends BaseModel { @JsonProperty("feature_states") private List featureStates; - @JsonProperty("amplitude_config") - private IntegrationModel amplitudeConfig; - @JsonProperty("segment_config") - private IntegrationModel segmentConfig; - @JsonProperty("mixpanel_config") - private IntegrationModel mixpanelConfig; - @JsonProperty("heap_config") - private IntegrationModel heapConfig; + @JsonProperty("identity_overrides") + private List identityOverrides; } diff --git a/src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java b/src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java index e6069fe2..2a5b5b71 100644 --- a/src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java +++ b/src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java @@ -29,7 +29,7 @@ public class IdentityModel extends BaseModel { @JsonProperty("identity_traits") private List identityTraits = new ArrayList<>(); @JsonProperty("identity_features") - private Set identityFeatures = new HashSet<>(); + private List identityFeatures = new ArrayList<>(); @JsonProperty("composite_key") private String compositeKey; diff --git a/src/test/java/com/flagsmith/FlagsmithClientTest.java b/src/test/java/com/flagsmith/FlagsmithClientTest.java index 5a01e817..d8d5e1a8 100644 --- a/src/test/java/com/flagsmith/FlagsmithClientTest.java +++ b/src/test/java/com/flagsmith/FlagsmithClientTest.java @@ -22,11 +22,11 @@ import com.flagsmith.exceptions.FlagsmithRuntimeError; import com.flagsmith.flagengine.environments.EnvironmentModel; import com.flagsmith.flagengine.features.FeatureStateModel; +import com.flagsmith.flagengine.identities.IdentityModel; import com.flagsmith.flagengine.identities.traits.TraitModel; import com.flagsmith.interfaces.FlagsmithCache; import com.flagsmith.models.BaseFlag; import com.flagsmith.models.DefaultFlag; -import com.flagsmith.models.Flag; import com.flagsmith.models.Flags; import com.flagsmith.models.Segment; import com.flagsmith.responses.FlagsAndTraitsResponse; @@ -44,9 +44,7 @@ import okhttp3.ResponseBody; import okhttp3.mock.MockInterceptor; import okio.Buffer; -import org.junit.Rule; import org.junit.jupiter.api.Test; -import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.invocation.Invocation; @@ -579,6 +577,31 @@ public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentReturnsNullAndEn assertEquals(client.getEnvironment(), null); } + @Test + public void testUpdateEnvironment_StoresIdentityOverrides_WhenGetEnvironmentReturnsEnvironmentWithOverrides() { + // Given + EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); + + FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockApiWrapper.getEnvironment()).thenReturn(environmentModel); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockApiWrapper) + .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) + .setApiKey("ser.dummy-key") + .build(); + + // When + client.updateEnvironment(); + + // Then + // Identity overrides are correctly stored + IdentityModel actualIdentity = client.getIdentitiesWithOverridesByIdentifier().get("overridden-identity"); + + assertEquals(actualIdentity.getIdentityFeatures().size(), 1); + assertEquals(actualIdentity.getIdentityFeatures().iterator().next().getValue(), "overridden-value"); + } + @Test public void testClose_StopsPollingManager() { // Given @@ -654,6 +677,35 @@ public void testLocalEvaluation_ReturnsConsistentResults() throws FlagsmithClien } } + @Test + public void testLocalEvaluation_ReturnsIdentityOverrides() throws FlagsmithClientError { + // Given + EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); + + FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); + + FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockedApiWrapper.getEnvironment()) + .thenReturn(environmentModel) + .thenReturn(null); + when(mockedApiWrapper.getConfig()).thenReturn(config); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockedApiWrapper) + .withConfiguration(config) + .setApiKey("ser.dummy-key") + .build(); + + Flags flagsWithoutOverride = client.getIdentityFlags("test"); + + // When + Flags flagsWithOverride = client.getIdentityFlags("overridden-identity"); + + // Then + assertEquals(flagsWithoutOverride.getFeatureValue("some_feature"), "some-value"); + assertEquals(flagsWithOverride.getFeatureValue("some_feature"), "overridden-value"); + } + @Test public void testGetEnvironmentFlags_UsesDefaultFlags_IfLocalEvaluationEnvironmentNull() throws FlagsmithClientError { diff --git a/src/test/java/com/flagsmith/FlagsmithTestHelper.java b/src/test/java/com/flagsmith/FlagsmithTestHelper.java index 29921fb4..d2b3176a 100644 --- a/src/test/java/com/flagsmith/FlagsmithTestHelper.java +++ b/src/test/java/com/flagsmith/FlagsmithTestHelper.java @@ -273,62 +273,110 @@ public static IdentityModel featureUser(String identifier) { return user; } + public static IdentityModel identityOverride() { + final FeatureModel overriddenFeature = new FeatureModel(); + overriddenFeature.setId(1); + overriddenFeature.setName("some_feature"); + overriddenFeature.setType("STANDARD"); + + final FeatureStateModel overriddenFeatureState = new FeatureStateModel(); + overriddenFeatureState.setFeature(overriddenFeature); + overriddenFeatureState.setFeaturestateUuid("d5d0767b-6287-4bb4-9d53-8b87e5458642"); + overriddenFeatureState.setValue("overridden-value"); + overriddenFeatureState.setEnabled(true); + overriddenFeatureState.setMultivariateFeatureStateValues(new ArrayList<>()); + + List identityFeatures = new ArrayList<>(); + identityFeatures.add(overriddenFeatureState); + + final IdentityModel identity = new IdentityModel(); + identity.setIdentifier("overridden-identity"); + identity.setIdentityUuid("65bc5ac6-5859-4cfe-97e6-d5ec2e80c1fb"); + identity.setCompositeKey("B62qaMZNwfiqT76p38ggrQ_identity_overridden_identity"); + identity.setEnvironmentApiKey("B62qaMZNwfiqT76p38ggrQ"); + identity.setIdentityFeatures(identityFeatures); + return identity; + } + public static String environmentString() { return "{\n" + - " \"api_key\": \"B62qaMZNwfiqT76p38ggrQ\",\n" + - " \"project\": {\n" + - " \"name\": \"Test project\",\n" + - " \"organisation\": {\n" + - " \"feature_analytics\": false,\n" + - " \"name\": \"Test Org\",\n" + - " \"id\": 1,\n" + - " \"persist_trait_data\": true,\n" + - " \"stop_serving_flags\": false\n" + - " },\n" + - " \"id\": 1,\n" + - " \"hide_disabled_flags\": false,\n" + - " \"segments\": [" + - " {\n" + - " \"id\": 1,\n" + - " \"name\": \"Test segment\",\n" + - " \"rules\": [\n" + - " {\n" + - " \"type\": \"ALL\",\n" + - " \"rules\": [\n" + - " {\n" + - " \"type\": \"ALL\",\n" + - " \"rules\": [],\n" + - " \"conditions\": [\n" + - " {\n" + - " \"operator\": \"EQUAL\",\n" + - " \"property_\": \"foo\",\n" + - " \"value\": \"bar\"\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " }]\n" + - " },\n" + - " \"segment_overrides\": [],\n" + - " \"id\": 1,\n" + - " \"feature_states\": [\n" + - " {\n" + - " \"multivariate_feature_state_values\": [],\n" + - " \"feature_state_value\": \"some-value\",\n" + - " \"id\": 1,\n" + - " \"featurestate_uuid\": \"40eb539d-3713-4720-bbd4-829dbef10d51\",\n" + - " \"feature\": {\n" + - " \"name\": \"some_feature\",\n" + - " \"type\": \"STANDARD\",\n" + - " \"id\": 1\n" + - " },\n" + - " \"segment_id\": null,\n" + - " \"enabled\": true\n" + - " }\n" + - " ]\n" + - "}"; + " \"api_key\": \"B62qaMZNwfiqT76p38ggrQ\",\n" + + " \"project\": {\n" + + " \"name\": \"Test project\",\n" + + " \"organisation\": {\n" + + " \"feature_analytics\": false,\n" + + " \"name\": \"Test Org\",\n" + + " \"id\": 1,\n" + + " \"persist_trait_data\": true,\n" + + " \"stop_serving_flags\": false\n" + + " },\n" + + " \"id\": 1,\n" + + " \"hide_disabled_flags\": false,\n" + + " \"segments\": [\n" + + " {\n" + + " \"id\": 1,\n" + + " \"name\": \"Test segment\",\n" + + " \"rules\": [\n" + + " {\n" + + " \"type\": \"ALL\",\n" + + " \"rules\": [\n" + + " {\n" + + " \"type\": \"ALL\",\n" + + " \"rules\": [],\n" + + " \"conditions\": [\n" + + " {\n" + + " \"operator\": \"EQUAL\",\n" + + " \"property_\": \"foo\",\n" + + " \"value\": \"bar\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"segment_overrides\": [],\n" + + " \"id\": 1,\n" + + " \"feature_states\": [\n" + + " {\n" + + " \"multivariate_feature_state_values\": [],\n" + + " \"feature_state_value\": \"some-value\",\n" + + " \"id\": 1,\n" + + " \"featurestate_uuid\": \"40eb539d-3713-4720-bbd4-829dbef10d51\",\n" + + " \"feature\": {\n" + + " \"name\": \"some_feature\",\n" + + " \"type\": \"STANDARD\",\n" + + " \"id\": 1\n" + + " },\n" + + " \"segment_id\": null,\n" + + " \"enabled\": true\n" + + " }\n" + + " ],\n" + + " \"identity_overrides\": [\n" + + " {\n" + + " \"identity_uuid\": \"65bc5ac6-5859-4cfe-97e6-d5ec2e80c1fb\",\n" + + " \"identifier\": \"overridden-identity\",\n" + + " \"composite_key\": \"B62qaMZNwfiqT76p38ggrQ_identity_overridden_identity\",\n" + + " \"identity_features\": [\n" + + " {\n" + + " \"feature_state_value\": \"overridden-value\",\n" + + " \"multivariate_feature_state_values\": [],\n" + + " \"featurestate_uuid\": \"d5d0767b-6287-4bb4-9d53-8b87e5458642\",\n" + + " \"feature\": {\n" + + " \"name\": \"some_feature\",\n" + + " \"type\": \"STANDARD\",\n" + + " \"id\": 1\n" + + " },\n" + + " \"enabled\": true\n" + + " }\n" + + " ],\n" + + " \"identity_traits\": [],\n" + + " \"environment_api_key\": \"B62qaMZNwfiqT76p38ggrQ\"\n" + + " }\n" + + " ]\n" + + "}"; } public static EnvironmentModel environmentModel() { diff --git a/src/test/java/com/flagsmith/flagengine/unit/environments/EnvironmentTest.java b/src/test/java/com/flagsmith/flagengine/unit/environments/EnvironmentTest.java index f85f25e9..0ff35024 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/environments/EnvironmentTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/environments/EnvironmentTest.java @@ -66,11 +66,6 @@ public void test_get_flags_for_environment_returns_feature_states_for_environmen Assertions.assertTrue(environmentModel.getFeatureStates().size() == 3); - Assertions.assertNull(environmentModel.getAmplitudeConfig()); - Assertions.assertNull(environmentModel.getMixpanelConfig()); - Assertions.assertNull(environmentModel.getHeapConfig()); - Assertions.assertNull(environmentModel.getSegmentConfig()); - FeatureStateModel featureState = FeatureStateHelper.getFeatureStateForFeatureByName( environmentModel.getFeatureStates(), "feature_with_string_value"