Repository: Unleash/unleash-client-java
Pull request #139 (2021-06-07 11:12:07)

Feat/custom stickiness

chriswk: Hmm, just realised I did the refactoring of package names here as well. I think if you look at the diff for ed15b43 that should be the custom stickiness changes.

Patch:
diff --git a/pom.xml b/pom.xml
index 4c70f059..701596f5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,9 +8,9 @@
     <properties>
         <version.slf4j>1.7.30</version.slf4j>
         <version.log4j2>2.13.3</version.log4j2>
-        <version.junit5>5.7.1</version.junit5>
+        <version.junit5>5.7.2</version.junit5>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-        <version.unleash.specification>3.3.0</version.unleash.specification>
+        <version.unleash.specification>4.0.0</version.unleash.specification>
         <arguments />
     </properties>
 
@@ -143,7 +143,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-plugin</artifactId>
-                <version>2.22.2</version>
+                <version>3.0.0-M5</version>
                 <configuration>
                     <forkCount>1</forkCount>
                 </configuration>
@@ -177,7 +177,7 @@
             <plugin>
                 <groupId>org.jacoco</groupId>
                 <artifactId>jacoco-maven-plugin</artifactId>
-                <version>0.8.6</version>
+                <version>0.8.7</version>
                 <executions>
                     <execution>
                         <id>prepare-agent</id>
diff --git a/src/main/java/no/finn/unleash/Variant.java b/src/main/java/no/finn/unleash/Variant.java
index dc31c629..5ebd652f 100644
--- a/src/main/java/no/finn/unleash/Variant.java
+++ b/src/main/java/no/finn/unleash/Variant.java
@@ -11,17 +11,28 @@ public class Variant {
     private final String name;
     @Nullable private final Payload payload;
     private final boolean enabled;
+    @Nullable private final String stickiness;
 
     public Variant(String name, @Nullable Payload payload, boolean enabled) {
+        this(name, payload, enabled, null);
+    }
+
+    public Variant(String name, @Nullable Payload payload, boolean enabled, String stickiness) {
         this.name = name;
         this.payload = payload;
         this.enabled = enabled;
+        this.stickiness = stickiness;
     }
 
     public Variant(String name, @Nullable String payload, boolean enabled) {
+        this(name, payload, enabled, null);
+    }
+
+    public Variant(String name, @Nullable String payload, boolean enabled, String stickiness) {
         this.name = name;
         this.payload = new Payload("string", payload);
         this.enabled = enabled;
+        this.stickiness = stickiness;
     }
 
     public String getName() {
@@ -36,6 +47,11 @@ public class Variant {
         return enabled;
     }
 
+    @Nullable
+    public String getStickiness() {
+        return stickiness;
+    }
+
     @Override
     public String toString() {
         return "Variant{"
diff --git a/src/main/java/no/finn/unleash/repository/HttpToggleFetcher.java b/src/main/java/no/finn/unleash/repository/HttpToggleFetcher.java
index 2d12f066..f7173615 100644
--- a/src/main/java/no/finn/unleash/repository/HttpToggleFetcher.java
+++ b/src/main/java/no/finn/unleash/repository/HttpToggleFetcher.java
@@ -24,9 +24,11 @@ public final class HttpToggleFetcher implements ToggleFetcher {
 
     public HttpToggleFetcher(UnleashConfig unleashConfig) {
         this.unleashConfig = unleashConfig;
-        this.toggleUrl = unleashConfig
-            .getUnleashURLs()
-            .getFetchTogglesURL(unleashConfig.getProjectName(), unleashConfig.getNamePrefix());
+        this.toggleUrl =
+                unleashConfig
+                        .getUnleashURLs()
+                        .getFetchTogglesURL(
+                                unleashConfig.getProjectName(), unleashConfig.getNamePrefix());
     }
 
     @Override
diff --git a/src/main/java/no/finn/unleash/strategy/FlexibleRolloutStrategy.java b/src/main/java/no/finn/unleash/strategy/FlexibleRolloutStrategy.java
index 7fae031d..0c586772 100644
--- a/src/main/java/no/finn/unleash/strategy/FlexibleRolloutStrategy.java
+++ b/src/main/java/no/finn/unleash/strategy/FlexibleRolloutStrategy.java
@@ -37,11 +37,13 @@ public class FlexibleRolloutStrategy implements Strategy {
                 return context.getSessionId();
             case "random":
                 return Optional.of(randomGenerator.get());
-            default:
+            case "default":
                 String value =
                         context.getUserId()
                                 .orElse(context.getSessionId().orElse(this.randomGenerator.get()));
                 return Optional.of(value);
+            default:
+                return context.getByName(stickiness);
         }
     }
 
@@ -52,13 +54,10 @@ public class FlexibleRolloutStrategy implements Strategy {
         final int percentage = StrategyUtils.getPercentage(parameters.get(PERCENTAGE));
         final String groupId = parameters.getOrDefault(GROUP_ID, "");
 
-        if (stickinessId.isPresent()) {
-            final int normalizedUserId =
-                    StrategyUtils.getNormalizedNumber(stickinessId.get(), groupId);
-            return percentage > 0 && normalizedUserId <= percentage;
-        } else {
-            return false;
-        }
+        return stickinessId
+                .map(stick -> StrategyUtils.getNormalizedNumber(stick, groupId))
+                .map(norm -> percentage > 0 && norm <= percentage)
+                .orElse(false);
     }
 
     private String getStickiness(Map<String, String> parameters) {
diff --git a/src/main/java/no/finn/unleash/util/UnleashConfig.java b/src/main/java/no/finn/unleash/util/UnleashConfig.java
index c02560f6..c435a12b 100644
--- a/src/main/java/no/finn/unleash/util/UnleashConfig.java
+++ b/src/main/java/no/finn/unleash/util/UnleashConfig.java
@@ -245,7 +245,8 @@ public class UnleashConfig {
 
                 // Only apply PasswordAuthentication to requests to the proxy itself - if not set
                 // just ignore
-                if (getRequestingHost().equalsIgnoreCase(proxyHost) && Integer.parseInt(proxyPort) == getRequestingPort()) {
+                if (getRequestingHost().equalsIgnoreCase(proxyHost)
+                        && Integer.parseInt(proxyPort) == getRequestingPort()) {
                     return new PasswordAuthentication(proxyUser, proxyPassword.toCharArray());
                 }
             }
@@ -267,14 +268,17 @@ public class UnleashConfig {
 
         @Override
         protected @Nullable PasswordAuthentication getPasswordAuthentication() {
-            if (getRequestorType() == RequestorType.PROXY && proxy.type() == Proxy.Type.HTTP && proxy.address() instanceof InetSocketAddress) {
+            if (getRequestorType() == RequestorType.PROXY
+                    && proxy.type() == Proxy.Type.HTTP
+                    && proxy.address() instanceof InetSocketAddress) {
                 final String proxyHost = ((InetSocketAddress) proxy.address()).getHostName();
                 final int proxyPort = ((InetSocketAddress) proxy.address()).getPort();
 
                 // Only apply PasswordAuthentication to requests to the proxy
                 // itself - if not set
                 // just ignore
-                if (getRequestingHost().equalsIgnoreCase(proxyHost) && proxyPort == getRequestingPort()) {
+                if (getRequestingHost().equalsIgnoreCase(proxyHost)
+                        && proxyPort == getRequestingPort()) {
                     return new PasswordAuthentication(proxyUser, proxyPassword.toCharArray());
                 }
             }
@@ -435,7 +439,8 @@ public class UnleashConfig {
             this.proxy = proxy;
 
             if (proxyUser != null && proxyPassword != null) {
-                this.proxyAuthenticator = new CustomProxyAuthenticator(proxy, proxyUser, proxyPassword);
+                this.proxyAuthenticator =
+                        new CustomProxyAuthenticator(proxy, proxyUser, proxyPassword);
             }
             return this;
         }
diff --git a/src/main/java/no/finn/unleash/util/UnleashURLs.java b/src/main/java/no/finn/unleash/util/UnleashURLs.java
index 702365fc..25507c50 100644
--- a/src/main/java/no/finn/unleash/util/UnleashURLs.java
+++ b/src/main/java/no/finn/unleash/util/UnleashURLs.java
@@ -1,11 +1,9 @@
 package no.finn.unleash.util;
 
-import no.finn.unleash.lang.Nullable;
-
 import java.net.MalformedURLException;
 import java.net.URI;
-import java.net.URISyntaxException;
 import java.net.URL;
+import no.finn.unleash.lang.Nullable;
 
 public class UnleashURLs {
     private final URL fetchTogglesURL;
@@ -43,20 +41,19 @@ public class UnleashURLs {
 
         try {
             return URI.create(fetchTogglesURL + suffix.toString()).normalize().toURL();
-        } catch (IllegalArgumentException|MalformedURLException e) {
-            throw new IllegalArgumentException("fetchTogglesURL [" + fetchTogglesURL + suffix + "] was not URL friendly.", e);
+        } catch (IllegalArgumentException | MalformedURLException e) {
+            throw new IllegalArgumentException(
+                    "fetchTogglesURL [" + fetchTogglesURL + suffix + "] was not URL friendly.", e);
         }
     }
 
     private void appendParam(StringBuilder suffix, String key, @Nullable String value) {
-        if(value == null) return;
-        if(suffix.length() == 0){
+        if (value == null) return;
+        if (suffix.length() == 0) {
             suffix.append("?");
         } else {
             suffix.append("&");
         }
         suffix.append(key).append("=").append(value);
     }
-
-
 }
diff --git a/src/main/java/no/finn/unleash/variant/VariantDefinition.java b/src/main/java/no/finn/unleash/variant/VariantDefinition.java
index 9b3890e3..eb1a4b98 100644
--- a/src/main/java/no/finn/unleash/variant/VariantDefinition.java
+++ b/src/main/java/no/finn/unleash/variant/VariantDefinition.java
@@ -11,20 +11,31 @@ public class VariantDefinition {
     private final int weight;
     @Nullable private final Payload payload;
     @Nullable private final List<VariantOverride> overrides;
+    @Nullable private final String stickiness;
 
     public VariantDefinition(
             String name,
             int weight,
             @Nullable Payload payload,
             @Nullable List<VariantOverride> overrides) {
+        this(name, weight, payload, overrides, null);
+    }
+
+    public VariantDefinition(
+            String name,
+            int weight,
+            @Nullable Payload payload,
+            @Nullable List<VariantOverride> overrides,
+            @Nullable String stickiness) {
         this.name = name;
         this.weight = weight;
         this.payload = payload;
         this.overrides = overrides;
+        this.stickiness = stickiness;
     }
 
     VariantDefinition(String name, int weight) {
-        this(name, weight, null, Collections.emptyList());
+        this(name, weight, null, Collections.emptyList(), null);
     }
 
     public String getName() {
@@ -47,7 +58,11 @@ public class VariantDefinition {
         }
     }
 
+    public @Nullable String getStickiness() {
+        return stickiness;
+    }
+
     Variant toVariant() {
-        return new Variant(name, payload, true);
+        return new Variant(name, payload, true, stickiness);
     }
 }
diff --git a/src/main/java/no/finn/unleash/variant/VariantUtil.java b/src/main/java/no/finn/unleash/variant/VariantUtil.java
index 483200f6..9cac061a 100644
--- a/src/main/java/no/finn/unleash/variant/VariantUtil.java
+++ b/src/main/java/no/finn/unleash/variant/VariantUtil.java
@@ -2,6 +2,7 @@ package no.finn.unleash.variant;
 
 import java.util.List;
 import java.util.Optional;
+import java.util.Random;
 import java.util.function.Predicate;
 import no.finn.unleash.FeatureToggle;
 import no.finn.unleash.UnleashContext;
@@ -61,6 +62,17 @@ public final class VariantUtil {
                                                 .orElse(Double.toString(Math.random()))));
     }
 
+    private static String randomString() {
+        int randSeed = new Random().nextInt(100000);
+        return "" + randSeed;
+    }
+
+    private static String getSeed(UnleashContext unleashContext, Optional<String> stickiness) {
+        return stickiness
+                .map(s -> unleashContext.getByName(s).orElse(randomString()))
+                .orElse(getIdentifier(unleashContext));
+    }
+
     public static Variant selectVariant(
             @Nullable FeatureToggle featureToggle, UnleashContext context, Variant defaultVariant) {
         if (featureToggle == null) {
@@ -76,10 +88,14 @@ public final class VariantUtil {
         if (variantOverride.isPresent()) {
             return variantOverride.get().toVariant();
         }
-
+        Optional<String> customStickiness =
+                variants.stream()
+                        .filter(f -> f.getStickiness() != null)
+                        .map(VariantDefinition::getStickiness)
+                        .findFirst();
         int target =
                 StrategyUtils.getNormalizedNumber(
-                        getIdentifier(context), featureToggle.getName(), totalWeight);
+                        getSeed(context, customStickiness), featureToggle.getName(), totalWeight);
 
         int counter = 0;
         for (final VariantDefinition definition : featureToggle.getVariants()) {


Test patch:
diff --git a/src/test/java/no/finn/unleash/ManualTesting.java b/src/test/java/no/finn/unleash/ManualTesting.java
index 4e44cb58..3c87e411 100644
--- a/src/test/java/no/finn/unleash/ManualTesting.java
+++ b/src/test/java/no/finn/unleash/ManualTesting.java
@@ -29,7 +29,9 @@ public class ManualTesting {
                         .appName("java-test")
                         .instanceId("instance y")
                         .unleashAPI("https://unleash.herokuapp.com/api/")
-                        .customHttpHeader("Authorization", "3bd74da5b341d868443134377ba5d802ea1e6fa2d2a948276ade1f092bec8d92")
+                        .customHttpHeader(
+                                "Authorization",
+                                "3bd74da5b341d868443134377ba5d802ea1e6fa2d2a948276ade1f092bec8d92")
                         .subscriber(
                                 new UnleashSubscriber() {
                                     @Override
diff --git a/src/test/java/no/finn/unleash/event/SubscriberTest.java b/src/test/java/no/finn/unleash/event/SubscriberTest.java
index 537cefbb..a2edf838 100644
--- a/src/test/java/no/finn/unleash/event/SubscriberTest.java
+++ b/src/test/java/no/finn/unleash/event/SubscriberTest.java
@@ -19,8 +19,6 @@ import no.finn.unleash.UnleashException;
 import no.finn.unleash.metric.ClientMetrics;
 import no.finn.unleash.metric.ClientRegistration;
 import no.finn.unleash.repository.FeatureToggleResponse;
-import no.finn.unleash.repository.HttpToggleFetcher;
-import no.finn.unleash.repository.ToggleBootstrapHandler;
 import no.finn.unleash.util.UnleashConfig;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -65,14 +63,18 @@ public class SubscriberTest {
         assertThat(testSubscriber.toggleEnabled).isFalse();
         assertThat(testSubscriber.errors).hasSize(2);
 
-//        assertThat(testSubscriber.events).filteredOn(e -> e instanceof ToggleBootstrapHandler.ToggleBootstrapRead).hasSize(1);
+        //        assertThat(testSubscriber.events).filteredOn(e -> e instanceof
+        // ToggleBootstrapHandler.ToggleBootstrapRead).hasSize(1);
         assertThat(testSubscriber.events).filteredOn(e -> e instanceof UnleashReady).hasSize(1);
         assertThat(testSubscriber.events).filteredOn(e -> e instanceof ToggleEvaluated).hasSize(3);
-        assertThat(testSubscriber.events).filteredOn(e -> e instanceof FeatureToggleResponse).hasSize(2);
-        assertThat(testSubscriber.events).filteredOn(e -> e instanceof ClientRegistration).hasSize(1);
+        assertThat(testSubscriber.events)
+                .filteredOn(e -> e instanceof FeatureToggleResponse)
+                .hasSize(2);
+        assertThat(testSubscriber.events)
+                .filteredOn(e -> e instanceof ClientRegistration)
+                .hasSize(1);
         assertThat(testSubscriber.events).filteredOn(e -> e instanceof ClientMetrics).hasSize(1);
         assertThat(testSubscriber.events).hasSize(9);
-
     }
 
     private class TestSubscriber implements UnleashSubscriber {
diff --git a/src/test/java/no/finn/unleash/repository/HttpToggleFetcherTest.java b/src/test/java/no/finn/unleash/repository/HttpToggleFetcherTest.java
index 5e4f9f9c..19e3b33b 100644
--- a/src/test/java/no/finn/unleash/repository/HttpToggleFetcherTest.java
+++ b/src/test/java/no/finn/unleash/repository/HttpToggleFetcherTest.java
@@ -346,6 +346,6 @@ public class HttpToggleFetcherTest {
                 UnleashConfig.builder().appName("test").unleashAPI(uri).projectName(name).build();
         org.assertj.core.api.Assertions.assertThatThrownBy(() -> new HttpToggleFetcher(config))
                 .isInstanceOf(IllegalArgumentException.class)
-                .hasMessageContaining("?project="+name+"] was not URL friendly.");
+                .hasMessageContaining("?project=" + name + "] was not URL friendly.");
     }
 }
diff --git a/src/test/java/no/finn/unleash/strategy/FlexibleRolloutStrategyTest.java b/src/test/java/no/finn/unleash/strategy/FlexibleRolloutStrategyTest.java
index 9b71ca59..4e48eedb 100644
--- a/src/test/java/no/finn/unleash/strategy/FlexibleRolloutStrategyTest.java
+++ b/src/test/java/no/finn/unleash/strategy/FlexibleRolloutStrategyTest.java
@@ -1,5 +1,6 @@
 package no.finn.unleash.strategy;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.*;
 
 import java.util.HashMap;
@@ -141,4 +142,46 @@ class FlexibleRolloutStrategyTest {
         boolean enabled = strategy.isEnabled(params, context);
         assertFalse(enabled);
     }
+
+    @Test
+    public void should_not_be_enabled_for_custom_field_402() {
+        Supplier<String> randomGenerator = () -> "2";
+        FlexibleRolloutStrategy strategy = new FlexibleRolloutStrategy(randomGenerator);
+        Map<String, String> params = new HashMap<>();
+        params.put("rollout", "50");
+        params.put("stickiness", "customField");
+        params.put("groupId", "Feature.flexible.rollout.custom.stickiness_50");
+        UnleashContext context = UnleashContext.builder().addProperty("customField", "402").build();
+        boolean enabled = strategy.isEnabled(params, context);
+        assertThat(enabled).isFalse();
+    }
+
+    @Test
+    public void should_not_be_enabled_for_custom_field_388_and_39() {
+        Supplier<String> randomGenerator = () -> "2";
+        FlexibleRolloutStrategy strategy = new FlexibleRolloutStrategy(randomGenerator);
+        Map<String, String> params = new HashMap<>();
+        params.put("rollout", "50");
+        params.put("stickiness", "customField");
+        params.put("groupId", "Feature.flexible.rollout.custom.stickiness_50");
+        UnleashContext context = UnleashContext.builder().addProperty("customField", "388").build();
+        boolean enabled = strategy.isEnabled(params, context);
+        assertThat(enabled).isTrue();
+        context = UnleashContext.builder().addProperty("customField", "39").build();
+        enabled = strategy.isEnabled(params, context);
+        assertThat(enabled).isTrue();
+    }
+
+    @Test
+    public void should_not_be_enabled_with_custom_stickiness_if_custom_field_is_missing() {
+        Supplier<String> randomGenerator = () -> "2";
+        FlexibleRolloutStrategy strategy = new FlexibleRolloutStrategy(randomGenerator);
+        Map<String, String> params = new HashMap<>();
+        params.put("rollout", "50");
+        params.put("stickiness", "customField");
+        params.put("groupId", "Feature.flexible.rollout.custom.stickiness_50");
+        UnleashContext context = UnleashContext.builder().build();
+        boolean enabled = strategy.isEnabled(params, context);
+        assertThat(enabled).isFalse();
+    }
 }
diff --git a/src/test/java/no/finn/unleash/util/UnleashConfigTest.java b/src/test/java/no/finn/unleash/util/UnleashConfigTest.java
index edb5788e..cece9624 100644
--- a/src/test/java/no/finn/unleash/util/UnleashConfigTest.java
+++ b/src/test/java/no/finn/unleash/util/UnleashConfigTest.java
@@ -223,7 +223,8 @@ public class UnleashConfigTest {
 
         Field authenticator = Authenticator.class.getDeclaredField("theAuthenticator");
         authenticator.setAccessible(true);
-        SystemProxyAuthenticator proxyAuthenticator = (SystemProxyAuthenticator) authenticator.get(null);
+        SystemProxyAuthenticator proxyAuthenticator =
+                (SystemProxyAuthenticator) authenticator.get(null);
 
         Field requestingAuthType =
                 proxyAuthenticator
@@ -281,7 +282,8 @@ public class UnleashConfigTest {
 
         Field authenticator = Authenticator.class.getDeclaredField("theAuthenticator");
         authenticator.setAccessible(true);
-        CustomProxyAuthenticator proxyAuthenticator = (CustomProxyAuthenticator) authenticator.get(null);
+        CustomProxyAuthenticator proxyAuthenticator =
+                (CustomProxyAuthenticator) authenticator.get(null);
 
         Field requestingAuthType =
                 proxyAuthenticator
@@ -309,11 +311,11 @@ public class UnleashConfigTest {
         requestingPort.setAccessible(true);
         requestingPort.set(proxyAuthenticator, 8080);
 
-        PasswordAuthentication passwordAuthentication = proxyAuthenticator.getPasswordAuthentication();
+        PasswordAuthentication passwordAuthentication =
+                proxyAuthenticator.getPasswordAuthentication();
 
         assertThat(passwordAuthentication.getUserName()).isEqualTo(proxyUser);
-        assertThat(new
-            String(passwordAuthentication.getPassword())).isEqualTo(proxyPassword);
+        assertThat(new String(passwordAuthentication.getPassword())).isEqualTo(proxyPassword);
         assertThat(config.isProxyAuthenticationByJvmProperties()).isEqualTo(false);
     }
 }
diff --git a/src/test/java/no/finn/unleash/util/UnleashURLsTest.java b/src/test/java/no/finn/unleash/util/UnleashURLsTest.java
index 922d40ee..0b524818 100644
--- a/src/test/java/no/finn/unleash/util/UnleashURLsTest.java
+++ b/src/test/java/no/finn/unleash/util/UnleashURLsTest.java
@@ -40,29 +40,29 @@ public class UnleashURLsTest {
     public void should_set_build_fetch_url_if_project_and_prefix_are_null() {
         UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com/api/"));
         assertThat(urls.getFetchTogglesURL(null, null).toString())
-            .isEqualTo("http://unleash.com/api/client/features");
+                .isEqualTo("http://unleash.com/api/client/features");
     }
 
     @Test
     public void should_set_build_fetch_url_with_project() {
         UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com/api/"));
         assertThat(urls.getFetchTogglesURL("myProject", null).toString())
-            .isEqualTo("http://unleash.com/api/client/features?project=myProject");
+                .isEqualTo("http://unleash.com/api/client/features?project=myProject");
     }
 
     @Test
     public void should_set_build_fetch_url_with_nameprefix() {
         UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com/api/"));
         assertThat(urls.getFetchTogglesURL(null, "prefix.").toString())
-            .isEqualTo("http://unleash.com/api/client/features?namePrefix=prefix.");
+                .isEqualTo("http://unleash.com/api/client/features?namePrefix=prefix.");
     }
 
-
     @Test
     public void should_set_build_fetch_url_with_project_and_nameprefix() {
         UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com/api/"));
         assertThat(urls.getFetchTogglesURL("aproject", "prefix.").toString())
-            .isEqualTo("http://unleash.com/api/client/features?project=aproject&namePrefix=prefix.");
+                .isEqualTo(
+                        "http://unleash.com/api/client/features?project=aproject&namePrefix=prefix.");
     }
 
     @Test()
diff --git a/src/test/java/no/finn/unleash/variant/VariantUtilTest.java b/src/test/java/no/finn/unleash/variant/VariantUtilTest.java
index 948ee72a..596cf896 100644
--- a/src/test/java/no/finn/unleash/variant/VariantUtilTest.java
+++ b/src/test/java/no/finn/unleash/variant/VariantUtilTest.java
@@ -5,6 +5,10 @@ import static no.finn.unleash.Variant.DISABLED_VARIANT;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 import no.finn.unleash.ActivationStrategy;
 import no.finn.unleash.FeatureToggle;
 import no.finn.unleash.UnleashContext;
@@ -168,4 +172,35 @@ public class VariantUtilTest {
 
         assertThat(variant.getName()).isEqualTo(v2.getName());
     }
+
+    @Test
+    public void should_distribute_variants_according_to_stickiness() {
+        VariantDefinition v1 = new VariantDefinition("blue", 1, null, null, "customField");
+        VariantDefinition v2 = new VariantDefinition("red", 1, null, null, "customField");
+        VariantDefinition v3 = new VariantDefinition("green", 1, null, null, "customField");
+        VariantDefinition v4 = new VariantDefinition("yellow", 1, null, null, "customField");
+        FeatureToggle variantToggle =
+                new FeatureToggle(
+                        "toggle-with-variants",
+                        true,
+                        asList(defaultStrategy),
+                        asList(v1, v2, v3, v4));
+        UnleashContext context =
+                UnleashContext.builder()
+                        .userId("11")
+                        .addProperty("env", "prod")
+                        .sessionId("1222221")
+                        .build();
+        Map<String, List<Variant>> variantResults =
+                IntStream.range(0, 10000)
+                        .mapToObj(
+                                i ->
+                                        VariantUtil.selectVariant(
+                                                variantToggle, context, DISABLED_VARIANT))
+                        .collect(Collectors.groupingBy(Variant::getName));
+        assertThat(variantResults)
+                .allSatisfy(
+                        (name, variantResult) ->
+                                assertThat(variantResult).hasSizeBetween(2400, 2600));
+    }
 }