Repository: Unleash/unleash-client-java
Pull request #33 (2017-02-05 19:40:05)

Add support for UnleashContext #29
The idea is to simplify common strategies via a shared unelash context. The context should be possible to provide just in time, or via a request bound probider. 

**Just in time example:**

```java
UnleashContext context = UnleashContext.builder().userId("user@mail.com").build();
unleash.isEnabled("someToggle", unleashContext);
```
The main benefit of just in time is that it is more explicit. It is also makes it easier when ThreadLocal is not bound to the current request. 


**Provider example:**
```java
UnleashContextProvider provider = new CustomUnleashContextProvider();
UnleashConfig config = UnleashConfig.builder().contextProvider(provider);
Unleash unleash = new DefaultUnleash(config):

// context will be provided to the strategy impl via the provided contextProvider. 
unleash.isEnabled("someToggle");
```


this is still WIP!

todo:

- [x] Implement UserContext
- [x] Implement common strategies: 
  - [x] UserWithIdStrategy
  - [x] GradualRolloutRandom Strategy
  - [x] GradualRolloutUserWithId Strategy
  - [x] GradualRolloutSessionId Strategy
  - [x] RemoteAddress Strategy
  - [x] ApplicationHostname Strategy
- [x] Add support for providing unleashContext (via config and inside UnleashDefault)
- [x] Unleash should add all "default" strategies automatically. 
- [x] Write some documentation on how to provide the UnleashContext ("just in time" vs provider)
- [x] Do some testing before merging.




Patch:
diff --git a/README.md b/README.md
index ebbe46cb..42ade6a2 100644
--- a/README.md
+++ b/README.md
@@ -3,25 +3,37 @@
 [![Build Status](https://travis-ci.org/Unleash/unleash-client-java.svg?branch=master)](https://travis-ci.org/Unleash/unleash-client-java)
 [![Coverage Status](https://coveralls.io/repos/github/Unleash/unleash-client-java/badge.svg?branch=master)](https://coveralls.io/github/Unleash/unleash-client-java?branch=master)
 
-## Create a new a Unleash instance
+## Getting started
+You will require unleash on your class path, pop it in to your pom:
 
-It is really easy to get a new instance of Unleash. In your app you typically just want one instance, 
+```xml
+<dependency>
+    <groupId>no.finn.unleash</groupId>
+    <artifactId>unleash-client-java</artifactId>
+    <version>Latest version here</version>
+</dependency>
+```
+
+
+### Create a new a Unleash instance
+
+It is easy to get a new instance of Unleash. In your app you typically just want one instance, 
 and inject that where you need it. You will typically use a dependency injection frameworks such as  
 Spring or Guice to manage this. 
 
-You create a new instance with the following command:
+To create a new instance of Unleash you need to pass in a config object:
 ```java
 
 UnleashConfig config = UnleashConfig.builder()
             .appName("java-test")
             .instanceId("instance x")
-            .unleashAPI("http://unleash.herokuapp.com")
+            .unleashAPI("http://unleash.herokuapp.com/api/")
             .build();
 
 Unleash unleash = new DefaultUnleash(config);
 ```
 
-## Awesome feature toggle API
+### Awesome feature toggle API
 
 It is really simple to use unleash.
 
@@ -33,17 +45,120 @@ if(unleash.isEnabled("AwesomeFeature")) {
 }
 ```
 
-Calling `unleash.isEnabled("AwesomeFeature")` is the equvivalent of calling `unleash.isEnabled("AwesomeFeature", false)`. Which means that it will return `false` if it cannot find the named toggle. 
+Calling `unleash.isEnabled("AwesomeFeature")` is the equvivalent of calling `unleash.isEnabled("AwesomeFeature", false)`. 
+Which means that it will return `false` if it cannot find the named toggle. 
+
+If you want it to default to `true` instead, you can pass `true` as the second argument:
 
-If you want it to default to true, you can pass `true` as the second argument:
 ```java
 unleash.isEnabled("AwesomeFeature", true)
 ```
 
-By default unleash-client fetches the feature toggles from unleash-server every 10s, and stores the result in `unleash-repo.json` which is located in the `java.io.tmpdir` directory. This means that if the unleash-server becomes unavailable, the unleash-client will still be able to toggle the features based on the values stored in `unleash-repo.json`. As a result of this, the second argument of `isEnabled` will be returned in two cases:
+### Activation strategies
+
+The Java client comes with implementations for the built-in activation strategies 
+provided by unleash. 
+
+- DefaultStrategy
+- UserWithIdStrategy
+- GradualRolloutRandomStrategy
+- GradualRolloutUserWithIdStrategy
+- GradualRolloutSessionIdStrategy
+- RemoteAddressStrategy
+- ApplicationHostnameStrategy
+
+Read more about the strategies in [activation-strategy.md](https://github.com/Unleash/unleash/blob/master/docs/activation-strategies.md).
+
+#### Custom strategies
+You may also specify and implement your own strategy. The specification must be registered in the Unleash UI and 
+you must register the strategy implementation when you wire up unleash. 
+
+```java
+Strategy s1 = new MyAwesomeStrategy();
+Strategy s2 = new MySuperAwesomeStrategy();
+Unleash unleash return new DefaultUnleash(config, s1, s2);
+
+```
+
+### Unleash context
+
+In order to use some of the common activation strategies you must provide a [unleash-context](https://github.com/Unleash/unleash/blob/master/docs/unleash-context.md).
+This client SDK provides two ways of provide the unleash-context:
+
+#### 1. As part of isEnabled call
+This is the simplest and most explicit way of providing the unleash context. 
+You just add it as an argument to the `isEnabled` call. 
+
+
+```
+UnleashContext context = UnleashContext.builder()
+  .userId("user@mail.com").build();
+
+unleash.isEnabled("someToggle", unleashContext);
+``` 
+
+
+#### 2. Via a UnleashContextProvider
+This is a bit more advanced approach, where you configure a unleash-context provider. 
+By doing this you do not have rebuild or pass the unleash-context object to every 
+place you are calling `unleash.isEnabled`. 
+
+The provider typically binds the context to the same thread as the request. 
+If you are using Spring the `UnleashContextProvider` will typically be a 
+'request scoped' bean. 
+
+
+```
+UnleashContextProvider contextProvider = new MyAwesomeContextProvider();
+
+UnleashConfig config = new UnleashConfig.Builder()
+            .appName("java-test")
+            .instanceId("instance x")
+            .unleashAPI("http://unleash.herokuapp.com/api/")
+            .unleashContextProvider(contextProvider)
+            .build();
+
+// Anywhere in the code unleash will get the unleash context from your registered provider. 
+unleash.isEnabled("someToggle");
+``` 
+
+## Local backup
+By default unleash-client fetches the feature toggles from unleash-server every 10s, and stores the 
+result in `unleash-repo.json` which is located in the `java.io.tmpdir` directory. This means that if 
+the unleash-server becomes unavailable, the unleash-client will still be able to toggle the features 
+based on the values stored in `unleash-repo.json`. As a result of this, the second argument of 
+`isEnabled` will be returned in two cases:
+
 * When `unleash-repo.json` does not exists
 * When the named feature toggle does not exist in `unleash-repo.json`
 
+
+## Unit testing
+You might want to control the state of the toggles during unit-testing.
+Unleash do come with a ```FakeUnleash``` implementation for doing this. 
+
+Some examples on how to use it below:
+
+
+```java
+\\example 1
+FakeUnleash fakeUnleash = new FakeUnleash();
+fakeUnleash.enableAll();
+
+assertThat(fakeUnleash.isEnabled("unknown"), is(true));
+assertThat(fakeUnleash.isEnabled("unknown2"), is(true));
+
+\\example 2
+FakeUnleash fakeUnleash = new FakeUnleash();
+fakeUnleash.enable("t1", "t2");
+
+assertThat(fakeUnleash.isEnabled("t1"), is(true));
+assertThat(fakeUnleash.isEnabled("t2"), is(true));
+assertThat(fakeUnleash.isEnabled("unknown"), is(false));
+```
+
+Se more in [FakeUnleashTest.java](https://github.com/Unleash/unleash-client-java/blob/master/src/test/java/no/finn/unleash/FakeUnleashTest.java)
+
 ## Development
 
 Build:
diff --git a/src/main/java/no/finn/unleash/DefaultUnleash.java b/src/main/java/no/finn/unleash/DefaultUnleash.java
index fde971eb..08764523 100644
--- a/src/main/java/no/finn/unleash/DefaultUnleash.java
+++ b/src/main/java/no/finn/unleash/DefaultUnleash.java
@@ -1,6 +1,8 @@
 package no.finn.unleash;
 
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import no.finn.unleash.metric.UnleashMetricService;
@@ -9,21 +11,35 @@ import no.finn.unleash.repository.FeatureToggleRepository;
 import no.finn.unleash.repository.ToggleBackupHandlerFile;
 import no.finn.unleash.repository.HttpToggleFetcher;
 import no.finn.unleash.repository.ToggleRepository;
+import no.finn.unleash.strategy.ApplicationHostnameStrategy;
 import no.finn.unleash.strategy.DefaultStrategy;
+import no.finn.unleash.strategy.GradualRolloutRandomStrategy;
+import no.finn.unleash.strategy.GradualRolloutSessionIdStrategy;
+import no.finn.unleash.strategy.GradualRolloutUserIdStrategy;
+import no.finn.unleash.strategy.RemoteAddressStrategy;
 import no.finn.unleash.strategy.Strategy;
 import no.finn.unleash.strategy.UnknownStrategy;
+import no.finn.unleash.strategy.UserWithIdStrategy;
 import no.finn.unleash.util.UnleashConfig;
 import no.finn.unleash.util.UnleashScheduledExecutor;
 import no.finn.unleash.util.UnleashScheduledExecutorImpl;
 
 public final class DefaultUnleash implements Unleash {
-    private static final DefaultStrategy DEFAULT_STRATEGY = new DefaultStrategy();
+    private static final List<Strategy> BUILTIN_STRATEGIES = Arrays.asList(new DefaultStrategy(),
+            new ApplicationHostnameStrategy(),
+            new GradualRolloutRandomStrategy(),
+            new GradualRolloutSessionIdStrategy(),
+            new GradualRolloutUserIdStrategy(),
+            new RemoteAddressStrategy(),
+            new UserWithIdStrategy());
+
     private static final UnknownStrategy UNKNOWN_STRATEGY = new UnknownStrategy();
     private static final UnleashScheduledExecutor unleashScheduledExecutor = new UnleashScheduledExecutorImpl();
 
     private final UnleashMetricService metricService;
     private final ToggleRepository toggleRepository;
     private final Map<String, Strategy> strategyMap;
+    private final UnleashContextProvider contextProvider;
 
 
     private static FeatureToggleRepository defaultToggleRepository(UnleashConfig unleashConfig) {
@@ -41,6 +57,7 @@ public final class DefaultUnleash implements Unleash {
     public DefaultUnleash(UnleashConfig unleashConfig, ToggleRepository toggleRepository, Strategy... strategies) {
         this.toggleRepository = toggleRepository;
         this.strategyMap = buildStrategyMap(strategies);
+        this.contextProvider = unleashConfig.getContextProvider();
         this.metricService = new UnleashMetricServiceImpl(unleashConfig, unleashScheduledExecutor);
         metricService.register(strategyMap.keySet());
     }
@@ -52,6 +69,11 @@ public final class DefaultUnleash implements Unleash {
 
     @Override
     public boolean isEnabled(final String toggleName, final boolean defaultSetting) {
+        return isEnabled(toggleName, contextProvider.getContext(), defaultSetting);
+    }
+
+    @Override
+    public boolean isEnabled(final String toggleName, final UnleashContext context ,final boolean defaultSetting) {
         boolean enabled = false;
         FeatureToggle featureToggle = toggleRepository.getToggle(toggleName);
 
@@ -61,7 +83,7 @@ public final class DefaultUnleash implements Unleash {
             enabled = false;
         } else {
             enabled = featureToggle.getStrategies().stream()
-                    .filter(as -> getStrategy(as.getName()).isEnabled(as.getParameters()))
+                    .filter(as -> getStrategy(as.getName()).isEnabled(as.getParameters(), context))
                     .findFirst()
                     .isPresent();
         }
@@ -73,7 +95,7 @@ public final class DefaultUnleash implements Unleash {
     private Map<String, Strategy> buildStrategyMap(Strategy[] strategies) {
         Map<String, Strategy> map = new HashMap<>();
 
-        map.put(DEFAULT_STRATEGY.getName(), DEFAULT_STRATEGY);
+        BUILTIN_STRATEGIES.forEach(strategy -> map.put(strategy.getName(), strategy));
 
         if (strategies != null) {
             for (Strategy strategy : strategies) {
@@ -86,10 +108,6 @@ public final class DefaultUnleash implements Unleash {
 
 
     private Strategy getStrategy(String strategy) {
-        if (strategyMap.containsKey(strategy)) {
-            return strategyMap.get(strategy);
-        } else {
-            return UNKNOWN_STRATEGY;
-        }
+        return strategyMap.containsKey(strategy) ? strategyMap.get(strategy) : UNKNOWN_STRATEGY;
     }
 }
diff --git a/src/main/java/no/finn/unleash/Unleash.java b/src/main/java/no/finn/unleash/Unleash.java
index 1ade0761..2bce6d0f 100644
--- a/src/main/java/no/finn/unleash/Unleash.java
+++ b/src/main/java/no/finn/unleash/Unleash.java
@@ -4,4 +4,12 @@ public interface Unleash {
     boolean isEnabled(String toggleName);
 
     boolean isEnabled(String toggleName, boolean defaultSetting);
+
+    default boolean isEnabled(String toggleName, UnleashContext context) {
+        return isEnabled(toggleName, context, false);
+    }
+
+    default boolean isEnabled(String toggleName, UnleashContext context, boolean defaultSetting) {
+        return isEnabled(toggleName, defaultSetting);
+    }
 }
diff --git a/src/main/java/no/finn/unleash/UnleashContext.java b/src/main/java/no/finn/unleash/UnleashContext.java
new file mode 100644
index 00000000..ac6036f2
--- /dev/null
+++ b/src/main/java/no/finn/unleash/UnleashContext.java
@@ -0,0 +1,74 @@
+package no.finn.unleash;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+public class UnleashContext {
+    private final Optional<String> userId;
+    private final Optional<String> sessionId;
+    private final Optional<String> remoteAddress;
+
+    private final Map<String, String> properties;
+
+    public UnleashContext(String userId, String sessionId, String remoteAddress, Map<String, String> properties) {
+        this.userId = Optional.ofNullable(userId);
+        this.sessionId = Optional.ofNullable(sessionId);
+        this.remoteAddress = Optional.ofNullable(remoteAddress);
+        this.properties = properties;
+    }
+
+    public Optional<String> getUserId() {
+        return userId;
+    }
+
+    public Optional<String> getSessionId() {
+        return sessionId;
+    }
+
+    public Optional<String> getRemoteAddress() {
+        return remoteAddress;
+    }
+
+    public Map<String, String> getProperties() {
+        return properties;
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+        private String userId;
+        private String sessionId;
+        private String remoteAddress;
+
+        private final Map<String, String> properties = new HashMap<>();
+
+        public Builder userId(String userId) {
+            this.userId = userId;
+            return this;
+        }
+
+        public Builder sessionId(String sessionId) {
+            this.sessionId = sessionId;
+            return this;
+        }
+
+        public Builder remoteAddress(String remoteAddress) {
+            this.remoteAddress = remoteAddress;
+            return this;
+        }
+
+        public Builder addProperty(String name, String value) {
+            properties.put(name, value);
+            return this;
+        }
+
+        public UnleashContext build() {
+            return new UnleashContext(userId, sessionId, remoteAddress, properties);
+        }
+    }
+
+
+}
diff --git a/src/main/java/no/finn/unleash/UnleashContextProvider.java b/src/main/java/no/finn/unleash/UnleashContextProvider.java
new file mode 100644
index 00000000..213129da
--- /dev/null
+++ b/src/main/java/no/finn/unleash/UnleashContextProvider.java
@@ -0,0 +1,9 @@
+package no.finn.unleash;
+
+public interface UnleashContextProvider {
+    UnleashContext getContext();
+
+    static UnleashContextProvider getDefaultProvider() {
+        return () -> UnleashContext.builder().build();
+    }
+}
diff --git a/src/main/java/no/finn/unleash/strategy/ApplicationHostnameStrategy.java b/src/main/java/no/finn/unleash/strategy/ApplicationHostnameStrategy.java
new file mode 100644
index 00000000..23b8025f
--- /dev/null
+++ b/src/main/java/no/finn/unleash/strategy/ApplicationHostnameStrategy.java
@@ -0,0 +1,43 @@
+package no.finn.unleash.strategy;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+
+public class ApplicationHostnameStrategy implements Strategy {
+    public static final String HOST_NAMES_PARAM = "hostNames";
+    protected final String NAME = "applicationHostname";
+    private final String hostname;
+
+    public ApplicationHostnameStrategy() {
+        this.hostname = resolveHostname();
+    }
+
+    private String resolveHostname() {
+        String hostname =  System.getProperty("hostname");
+        if(hostname == null) {
+            try {
+                hostname = InetAddress.getLocalHost().getHostName();
+            } catch (UnknownHostException e) {
+                hostname = "undefined";
+            }
+        }
+        return hostname;
+    }
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+
+    @Override
+    public boolean isEnabled(Map<String, String> parameters) {
+        return Optional.ofNullable(parameters.get(HOST_NAMES_PARAM))
+                .map(hostString -> hostString.toLowerCase())
+                .map(hostString -> Arrays.asList(hostString.split(",\\s*")))
+                .map(hostList -> hostList.contains(hostname.toLowerCase()))
+                .orElse(false);
+    }
+}
diff --git a/src/main/java/no/finn/unleash/strategy/GradualRolloutRandomStrategy.java b/src/main/java/no/finn/unleash/strategy/GradualRolloutRandomStrategy.java
new file mode 100644
index 00000000..d842cda1
--- /dev/null
+++ b/src/main/java/no/finn/unleash/strategy/GradualRolloutRandomStrategy.java
@@ -0,0 +1,34 @@
+package no.finn.unleash.strategy;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Random;
+
+import no.finn.unleash.strategy.Strategy;
+
+public final class GradualRolloutRandomStrategy implements Strategy {
+    protected static final String PERCENTAGE = "percentage";
+    private static final String STRATEGY_NAME = "gradualRolloutRandom";
+
+    private final Random random;
+
+    public GradualRolloutRandomStrategy() {
+        random = new Random();
+    }
+
+    protected GradualRolloutRandomStrategy(long seed) {
+        random = new Random(seed);
+    }
+
+    @Override
+    public String getName() {
+        return STRATEGY_NAME;
+    }
+
+    @Override
+    public boolean isEnabled(final Map<String, String> parameters) {
+        int percentage = StrategyUtils.getPercentage(parameters.get(PERCENTAGE));
+        int randomNumber = random.nextInt(100) + 1;
+        return percentage >= randomNumber;
+    }
+}
diff --git a/src/main/java/no/finn/unleash/strategy/GradualRolloutSessionIdStrategy.java b/src/main/java/no/finn/unleash/strategy/GradualRolloutSessionIdStrategy.java
new file mode 100644
index 00000000..a91ff635
--- /dev/null
+++ b/src/main/java/no/finn/unleash/strategy/GradualRolloutSessionIdStrategy.java
@@ -0,0 +1,54 @@
+package no.finn.unleash.strategy;
+
+import java.util.Map;
+import java.util.Optional;
+
+import no.finn.unleash.UnleashContext;
+
+/**
+ * Implements a gradual roll-out strategy based on session id.
+ *
+ * Using this strategy you can target only users bound to a session and gradually expose your
+ * feature to higher percentage of the logged in user.
+ *
+ * This strategy takes two parameters:
+ *  - percentage :  a number between 0 and 100. The percentage you want to enable the feature for.
+ *  - groupId :     a groupId used for rolling out the feature. By using the same groupId for different
+ *                  toggles you can correlate the user experience across toggles.
+ *
+ */
+public final class GradualRolloutSessionIdStrategy implements Strategy {
+    protected static final String PERCENTAGE = "percentage";
+    protected static final String GROUP_ID = "groupId";
+
+    private static final String NAME = "gradualRolloutSessionId";
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+
+    @Override
+    public boolean isEnabled(Map<String, String> parameters) {
+        return false;
+    }
+
+    @Override
+    public boolean isEnabled(final Map<String, String> parameters, UnleashContext unleashContext) {
+        Optional<String> sessionId = unleashContext.getSessionId();
+
+        if(!sessionId.isPresent()) {
+            return false;
+        }
+
+        final int percentage = StrategyUtils.getPercentage(parameters.get(PERCENTAGE));
+        final String groupId = Optional.ofNullable(parameters.get(GROUP_ID)).orElse("");
+
+        final int normalizedSessionId = StrategyUtils.getNormalizedNumber(sessionId.get(), groupId);
+
+        return percentage > 0 && normalizedSessionId <= percentage;
+    }
+
+
+}
+
diff --git a/src/main/java/no/finn/unleash/strategy/GradualRolloutUserIdStrategy.java b/src/main/java/no/finn/unleash/strategy/GradualRolloutUserIdStrategy.java
new file mode 100644
index 00000000..e3c1a109
--- /dev/null
+++ b/src/main/java/no/finn/unleash/strategy/GradualRolloutUserIdStrategy.java
@@ -0,0 +1,54 @@
+package no.finn.unleash.strategy;
+
+import java.util.Map;
+import java.util.Optional;
+
+import no.finn.unleash.UnleashContext;
+
+/**
+ * Implements a gradual roll-out strategy based on userId.
+ *
+ * Using this strategy you can target only logged in users and gradually expose your
+ * feature to higher percentage of the logged in user.
+ *
+ * This strategy takes two parameters:
+ *  - percentage :  a number between 0 and 100. The percentage you want to enable the feature for.
+ *  - groupId :     a groupId used for rolling out the feature. By using the same groupId for different
+ *                  toggles you can correlate the user experience across toggles.
+ *
+ */
+public final class GradualRolloutUserIdStrategy implements Strategy {
+    protected static final String PERCENTAGE = "percentage";
+    protected static final String GROUP_ID = "groupId";
+
+    private static final String NAME = "gradualRolloutUserId";
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+
+    @Override
+    public boolean isEnabled(Map<String, String> parameters) {
+        return false;
+    }
+
+    @Override
+    public boolean isEnabled(final Map<String, String> parameters, UnleashContext unleashContext) {
+        Optional<String> userId = unleashContext.getUserId();
+
+        if(!userId.isPresent()) {
+            return false;
+        }
+
+        final int percentage = StrategyUtils.getPercentage(parameters.get(PERCENTAGE));
+        final String groupId = Optional.ofNullable(parameters.get(GROUP_ID)).orElse("");
+
+        final int normalizedUserId = StrategyUtils.getNormalizedNumber(userId.get(), groupId);
+
+        return percentage > 0 && normalizedUserId <= percentage;
+    }
+
+
+}
+
diff --git a/src/main/java/no/finn/unleash/strategy/RemoteAddressStrategy.java b/src/main/java/no/finn/unleash/strategy/RemoteAddressStrategy.java
new file mode 100644
index 00000000..7959ef88
--- /dev/null
+++ b/src/main/java/no/finn/unleash/strategy/RemoteAddressStrategy.java
@@ -0,0 +1,33 @@
+package no.finn.unleash.strategy;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+
+import no.finn.unleash.UnleashContext;
+
+public final class RemoteAddressStrategy implements Strategy {
+
+    protected static final String PARAM = "IPs";
+    private static final String STRATEGY_NAME = "remoteAddress";
+
+
+    @Override
+    public String getName() {
+        return STRATEGY_NAME;
+    }
+
+    @Override
+    public boolean isEnabled(Map<String, String> parameters) {
+        return false;
+    }
+
+    @Override
+    public boolean isEnabled(Map<String, String> parameters, UnleashContext context) {
+        return Optional.ofNullable(parameters.get(PARAM))
+                .map(ips -> Arrays.asList(ips.split(",\\s*")))
+                .map(ips -> ips.contains(context.getRemoteAddress().orElse(null)))
+                .orElse(false);
+    }
+
+}
diff --git a/src/main/java/no/finn/unleash/strategy/Strategy.java b/src/main/java/no/finn/unleash/strategy/Strategy.java
index 81bc2136..24d2b3c8 100644
--- a/src/main/java/no/finn/unleash/strategy/Strategy.java
+++ b/src/main/java/no/finn/unleash/strategy/Strategy.java
@@ -1,9 +1,14 @@
 package no.finn.unleash.strategy;
 
 import java.util.Map;
+import no.finn.unleash.UnleashContext;
 
 public interface Strategy {
     String getName();
 
     boolean isEnabled(Map<String, String> parameters);
+
+    default boolean isEnabled(Map<String, String> parameters, UnleashContext unleashContext) {
+        return isEnabled(parameters);
+    }
 }
diff --git a/src/main/java/no/finn/unleash/strategy/StrategyUtils.java b/src/main/java/no/finn/unleash/strategy/StrategyUtils.java
new file mode 100644
index 00000000..1f3714c9
--- /dev/null
+++ b/src/main/java/no/finn/unleash/strategy/StrategyUtils.java
@@ -0,0 +1,55 @@
+package no.finn.unleash.strategy;
+
+public final class StrategyUtils {
+    private static final int ONE_HUNDRED = 100;
+
+    public static boolean isNotEmpty(final CharSequence cs) {
+        return !isEmpty(cs);
+    }
+
+    public static boolean isEmpty(final CharSequence cs) {
+        return cs == null || cs.length() == 0;
+    }
+
+    public static boolean isNumeric(final CharSequence cs) {
+        if (isEmpty(cs)) {
+            return false;
+        }
+        final int sz = cs.length();
+        for (int i = 0; i < sz; i++) {
+            if (Character.isDigit(cs.charAt(i)) == false) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Takes to string inputs concat them, produce a hashCode and return a normalized value between 0 and 100;
+     *
+     * @param identifier
+     * @param groupId
+     * @return
+     */
+    public static int getNormalizedNumber(String identifier, String groupId) {
+        int hashCode = Math.abs((groupId + ':' + identifier).hashCode());
+        return hashCode % ONE_HUNDRED + 1;
+    }
+
+    /**
+     * Takes a numeric string value and converts it to a integer between 0 and 100.
+     *
+     * returns 0 if the string is not numeric.
+     *
+     * @param percentage - A numeric string value
+     * @return a integer between 0 and 100
+     */
+    public static int getPercentage(String percentage) {
+        if (isNotEmpty(percentage) && isNumeric(percentage)) {
+            int p = Integer.parseInt(percentage);
+            return p;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/src/main/java/no/finn/unleash/strategy/UserWithIdStrategy.java b/src/main/java/no/finn/unleash/strategy/UserWithIdStrategy.java
new file mode 100644
index 00000000..5878c9a0
--- /dev/null
+++ b/src/main/java/no/finn/unleash/strategy/UserWithIdStrategy.java
@@ -0,0 +1,33 @@
+package no.finn.unleash.strategy;
+
+import java.util.Map;
+import java.util.Optional;
+
+import no.finn.unleash.UnleashContext;
+
+import static java.util.Arrays.asList;
+
+public final class UserWithIdStrategy implements Strategy {
+
+    protected static final String PARAM = "userIds";
+    private static final String STRATEGY_NAME = "userWithId";
+
+    @Override
+    public String getName() {
+        return STRATEGY_NAME;
+    }
+
+    @Override
+    public boolean isEnabled(Map<String, String> parameters) {
+        return false;
+    }
+
+    @Override
+    public boolean isEnabled(Map<String, String> parameters, UnleashContext unleashContext) {
+        return unleashContext.getUserId()
+                           .map(currentUserId -> Optional.ofNullable(parameters.get(PARAM))
+                                                         .map(userIdString -> asList(userIdString.split(",\\s?")))
+                                                         .filter(f -> f.contains(currentUserId)).isPresent())
+                           .orElse(false);
+    }
+}
diff --git a/src/main/java/no/finn/unleash/util/UnleashConfig.java b/src/main/java/no/finn/unleash/util/UnleashConfig.java
index a7b5d42f..24d6100a 100644
--- a/src/main/java/no/finn/unleash/util/UnleashConfig.java
+++ b/src/main/java/no/finn/unleash/util/UnleashConfig.java
@@ -2,6 +2,7 @@ package no.finn.unleash.util;
 
 import java.io.File;
 import java.net.URI;
+import no.finn.unleash.UnleashContextProvider;
 
 public class UnleashConfig {
     private final URI unleashAPI;
@@ -12,6 +13,7 @@ public class UnleashConfig {
     private final long fetchTogglesInterval;
     private final long sendMetricsInterval;
     private final boolean disableMetrics;
+    private final UnleashContextProvider contextProvider;
 
     public UnleashConfig(
             URI unleashAPI,
@@ -20,7 +22,9 @@ public class UnleashConfig {
             String backupFile,
             long fetchTogglesInterval,
             long sendMetricsInterval,
-            boolean disableMetrics) {
+            boolean disableMetrics,
+            UnleashContextProvider contextProvider) {
+
 
         if(appName == null) {
             throw new IllegalStateException("You are required to specify the unleash appName");
@@ -38,6 +42,7 @@ public class UnleashConfig {
         this.fetchTogglesInterval = fetchTogglesInterval;
         this.sendMetricsInterval = sendMetricsInterval;
         this.disableMetrics = disableMetrics;
+        this.contextProvider = contextProvider;
     }
 
     public URI getUnleashAPI() {
@@ -76,6 +81,10 @@ public class UnleashConfig {
         return this.backupFile;
     }
 
+    public UnleashContextProvider getContextProvider() {
+        return contextProvider;
+    }
+
     public static class Builder {
         private URI unleashAPI;
         private String appName;
@@ -84,6 +93,7 @@ public class UnleashConfig {
         private long fetchTogglesInterval = 10;
         private long sendMetricsInterval = 60;
         private boolean disableMetrics = false;
+        private UnleashContextProvider contextProvider = UnleashContextProvider.getDefaultProvider();
 
 
         public Builder unleashAPI(URI unleashAPI) {
@@ -126,6 +136,11 @@ public class UnleashConfig {
             return this;
         }
 
+        public Builder unleashContextProvider(UnleashContextProvider contextProvider) {
+            this.contextProvider = contextProvider;
+            return this;
+        }
+
         private String getBackupFile() {
             if(backupFile != null) {
                 return backupFile;
@@ -143,7 +158,8 @@ public class UnleashConfig {
                     getBackupFile(),
                     fetchTogglesInterval,
                     sendMetricsInterval,
-                    disableMetrics);
+                    disableMetrics,
+                    contextProvider);
         }
     }
 }


Test patch:
diff --git a/src/test/java/no/finn/unleash/ManualTesting.java b/src/test/java/no/finn/unleash/ManualTesting.java
index e332a2b3..a89ba8d0 100644
--- a/src/test/java/no/finn/unleash/ManualTesting.java
+++ b/src/test/java/no/finn/unleash/ManualTesting.java
@@ -23,9 +23,14 @@ public class ManualTesting {
         UnleashConfig unleashConfig = new UnleashConfig.Builder()
                 .appName("java-test")
                 .instanceId("instance y")
-                .unleashAPI("https://unleash-new-ui.herokuapp.com/api/")
+                .unleashAPI("https://unleash.herokuapp.com/api/")
                 .fetchTogglesInterval(1)
-                .sendMetricsInterval(10)
+                .sendMetricsInterval(2)
+                .unleashContextProvider(() -> UnleashContext.builder()
+                        .sessionId(new Random().nextInt(10000) + "")
+                        .userId(new Random().nextInt(10000) + "")
+                        .remoteAddress("192.168.1.1")
+                        .build())
                 .build();
 
         Unleash unleash = new DefaultUnleash(unleashConfig, strategy);
@@ -52,6 +57,7 @@ public class ManualTesting {
             while(currentRound < maxRounds) {
                 currentRound++;
                 long startTime = System.nanoTime();
+
                 boolean enabled = unleash.isEnabled("Demo");
                 long timeUsed = System.nanoTime() - startTime;
 
diff --git a/src/test/java/no/finn/unleash/UnleashTest.java b/src/test/java/no/finn/unleash/UnleashTest.java
index a801e93c..9701e9a7 100644
--- a/src/test/java/no/finn/unleash/UnleashTest.java
+++ b/src/test/java/no/finn/unleash/UnleashTest.java
@@ -2,6 +2,7 @@ package no.finn.unleash;
 
 import no.finn.unleash.repository.ToggleRepository;
 import no.finn.unleash.strategy.Strategy;
+import no.finn.unleash.strategy.UserWithIdStrategy;
 import no.finn.unleash.util.UnleashConfig;
 import org.junit.Before;
 import org.junit.Test;
@@ -9,6 +10,8 @@ import org.junit.Test;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
 
 import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertThat;
@@ -17,16 +20,22 @@ import static org.mockito.Mockito.*;
 public class UnleashTest {
 
     private ToggleRepository toggleRepository;
+    private UnleashContextProvider contextProvider;
     private Unleash unleash;
 
     @Before
     public void setup() {
         toggleRepository = mock(ToggleRepository.class);
+        contextProvider = mock(UnleashContextProvider.class);
+        when(contextProvider.getContext()).thenReturn(UnleashContext.builder().build());
+
         UnleashConfig config = new UnleashConfig.Builder()
                 .appName("test")
                 .unleashAPI("http://localhost:4242")
+                .unleashContextProvider(contextProvider)
                 .build();
-        unleash = new DefaultUnleash(config, toggleRepository);
+
+        unleash = new DefaultUnleash(config, toggleRepository, new UserWithIdStrategy());
     }
 
     @Test
@@ -73,7 +82,7 @@ public class UnleashTest {
 
         unleash.isEnabled("test");
 
-        verify(customStrategy, times(1)).isEnabled(anyMap());
+        verify(customStrategy, times(1)).isEnabled(anyMap(), any(UnleashContext.class));
     }
 
     @Test
@@ -88,6 +97,38 @@ public class UnleashTest {
         assertThat(unleash.isEnabled("test"), is(true));
     }
 
+    @Test
+    public void should_support_context_provider() {
+        UnleashContext context = UnleashContext.builder().userId("111").build();
+        when(contextProvider.getContext()).thenReturn(context);
+
+        //Set up a toggle using UserWithIdStrategy
+        Map<String, String> params = new HashMap<>();
+        params.put("userIds", "123, 111, 121");
+        ActivationStrategy strategy = new ActivationStrategy("userWithId", params);
+        FeatureToggle featureToggle = new FeatureToggle("test", true, Arrays.asList(strategy));
+
+        when(toggleRepository.getToggle("test")).thenReturn(featureToggle);
+
+        assertThat(unleash.isEnabled("test"), is(true));
+    }
+    
+    @Test
+    public void should_support_context_as_part_of_is_enabled_call() {
+        UnleashContext context = UnleashContext.builder().userId("13").build();
+
+        //Set up a toggle using UserWithIdStrategy
+        Map<String, String> params = new HashMap<>();
+        params.put("userIds", "123, 111, 121, 13");
+        ActivationStrategy strategy = new ActivationStrategy("userWithId", params);
+        FeatureToggle featureToggle = new FeatureToggle("test", true, Arrays.asList(strategy));
+
+        when(toggleRepository.getToggle("test")).thenReturn(featureToggle);
+
+        assertThat(unleash.isEnabled("test", context), is(true));
+    }
+
+
     @Test
     public void inactive_feature_toggle() {
         ActivationStrategy strategy1 = new ActivationStrategy("unknown", null);
diff --git a/src/test/java/no/finn/unleash/strategy/ApplicationHostnameStrategyTest.java b/src/test/java/no/finn/unleash/strategy/ApplicationHostnameStrategyTest.java
new file mode 100644
index 00000000..87b10f6e
--- /dev/null
+++ b/src/test/java/no/finn/unleash/strategy/ApplicationHostnameStrategyTest.java
@@ -0,0 +1,107 @@
+package no.finn.unleash.strategy;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.After;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class ApplicationHostnameStrategyTest {
+
+    @After
+    public void remove_hostname_property() {
+        System.getProperties().remove("hostname");
+    }
+
+    @Test
+    public void should_be_disabled_if_no_HostNames_in_params() {
+        Strategy strategy = new ApplicationHostnameStrategy();
+        Map<String, String> params = new HashMap<>();
+        params.put("hostNames", null);
+
+        assertFalse(strategy.isEnabled(params));
+    }
+
+    @Test
+    public void should_be_disabled_if_hostname_not_in_list() {
+        Strategy strategy = new ApplicationHostnameStrategy();
+
+        Map<String, String> params = new HashMap<>();
+        params.put("hostNames", "MegaHost,MiniHost, happyHost");
+
+        assertFalse(strategy.isEnabled(params));
+    }
+
+    @Test
+    public void should_be_enabled_for_hostName(){
+        String hostName  = "my-super-host";
+        System.setProperty("hostname", hostName);
+
+        Strategy strategy = new ApplicationHostnameStrategy();
+
+        Map<String, String> params = new HashMap<>();
+        params.put("hostNames", "MegaHost," + hostName + ",MiniHost, happyHost");
+        assertTrue(strategy.isEnabled(params));
+    }
+
+    @Test
+    public void should_handle_weird_casing(){
+        String hostName  = "my-super-host";
+        System.setProperty("hostname", hostName);
+
+        Strategy strategy = new ApplicationHostnameStrategy();
+
+        Map<String, String> params = new HashMap<>();
+
+        params.put("hostNames", "MegaHost," + hostName.toUpperCase() + ",MiniHost, happyHost");
+        assertTrue(strategy.isEnabled(params));
+    }
+
+    @Test
+    public void so_close_but_no_cigar(){
+        String hostName  = "my-super-host";
+        System.setProperty("hostname", hostName);
+
+        Strategy strategy = new ApplicationHostnameStrategy();
+
+        Map<String, String> params = new HashMap<>();
+
+        params.put("hostNames", "MegaHost, MiniHost, SuperhostOne");
+        assertFalse(strategy.isEnabled(params));
+    }
+
+    @Test
+    public void should_be_enabled_for_InetAddress() throws UnknownHostException {
+        String hostName  = InetAddress.getLocalHost().getHostName();
+        System.setProperty("hostname", hostName);
+
+        Strategy strategy = new ApplicationHostnameStrategy();
+
+        Map<String, String> params = new HashMap<>();
+        params.put("hostNames", "MegaHost," + hostName + ",MiniHost, happyHost");
+        assertTrue(strategy.isEnabled(params));
+    }
+
+    @Test
+    public void should_be_enabled_for_dashed_host() throws UnknownHostException {
+        String hostName  = "super-wiEred-host";
+        System.setProperty("hostname", hostName);
+
+        Strategy strategy = new ApplicationHostnameStrategy();
+
+        Map<String, String> params = new HashMap<>();
+        params.put("hostNames", "MegaHost," + hostName + ",MiniHost, happyHost");
+        assertTrue(strategy.isEnabled(params));
+    }
+
+    @Test
+    public void null_test(){
+        Strategy strategy = new ApplicationHostnameStrategy();
+        assertFalse(strategy.isEnabled(new HashMap<>()));
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/no/finn/unleash/strategy/GradualRolloutRandomStrategyTest.java b/src/test/java/no/finn/unleash/strategy/GradualRolloutRandomStrategyTest.java
new file mode 100644
index 00000000..3115c174
--- /dev/null
+++ b/src/test/java/no/finn/unleash/strategy/GradualRolloutRandomStrategyTest.java
@@ -0,0 +1,106 @@
+package no.finn.unleash.strategy;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public final class GradualRolloutRandomStrategyTest {
+
+    private static GradualRolloutRandomStrategy gradualRolloutRandomStrategy;
+
+    @Before
+    public void setUp() {
+        long seed = new Random().nextLong();
+        System.out.println("GradualRolloutRandomStrategyTest running with seed: " + seed);
+        gradualRolloutRandomStrategy = new GradualRolloutRandomStrategy(seed);
+    }
+
+    @Test
+    public void should_not_be_enabled_when_percentage_not_set() {
+        final Map<String, String> parameters = new HashMap<>();
+
+        final boolean enabled = gradualRolloutRandomStrategy.isEnabled(parameters);
+
+        assertFalse(enabled);
+    }
+
+    @Test
+    public void should_not_be_enabled_when_percentage_is_not_a_not_a_number() {
+        final Map<String, String> parameters = new HashMap<String, String>() {{
+            put("percentage", "foo");
+        }};
+
+        final boolean enabled = gradualRolloutRandomStrategy.isEnabled(parameters);
+
+        assertFalse(enabled);
+    }
+
+    @Test
+    public void should_not_be_enabled_when_percentage_is_not_a_not_a_valid_percentage_value() {
+        final Map<String, String> parameters = new HashMap<String, String>() {{
+            put("percentage", "ab");
+        }};
+
+        final boolean enabled = gradualRolloutRandomStrategy.isEnabled(parameters);
+
+        assertFalse(enabled);
+    }
+
+    @Test
+    public void should_never_be_enabled_when_0_percent() {
+        final Map<String, String> parameters = new HashMap<String, String>() {{
+            put("percentage", "0");
+        }};
+
+        for (int i = 0; i < 1000; i++) {
+            final boolean enabled = gradualRolloutRandomStrategy.isEnabled(parameters);
+            assertFalse(enabled);
+        }
+
+    }
+
+    @Test
+    public void should_always_be_enabled_when_100_percent() {
+        final Map<String, String> parameters = new HashMap<String, String>() {{
+            put("percentage", "100");
+        }};
+
+        for (int i = 0; i <= 100; i++) {
+            final boolean enabled = gradualRolloutRandomStrategy.isEnabled(parameters);
+            assertTrue("Should be enabled for p=" + i, enabled);
+        }
+    }
+
+    @Test
+    public void should_diverage_at_most_with_one_percent_point() {
+        int percentage = 55;
+        final int min= percentage - 1;
+        final int max = percentage + 1;
+
+        final Map<String, String> parameters = new HashMap<String, String>() {{
+            put("percentage", ""+percentage);
+        }};
+
+        int rounds = 20000;
+        int countEnabled = 0;
+
+        for (int i = 0; i < rounds; i++) {
+            final boolean enabled = gradualRolloutRandomStrategy.isEnabled(parameters);
+            if(enabled) {
+                countEnabled = countEnabled + 1;
+            }
+        }
+
+        long measuredPercentage = Math.round(((double) countEnabled / rounds * 100));
+
+        assertTrue(measuredPercentage >= min);
+        assertTrue(measuredPercentage <= max);
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/no/finn/unleash/strategy/GradualRolloutSessionIdStrategyTest.java b/src/test/java/no/finn/unleash/strategy/GradualRolloutSessionIdStrategyTest.java
new file mode 100644
index 00000000..3fc276dc
--- /dev/null
+++ b/src/test/java/no/finn/unleash/strategy/GradualRolloutSessionIdStrategyTest.java
@@ -0,0 +1,168 @@
+package no.finn.unleash.strategy;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+import com.google.common.collect.ImmutableList;
+import no.finn.unleash.UnleashContext;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+public class GradualRolloutSessionIdStrategyTest {
+    private static final long SEED = 89235015401L;
+    private static final long MIN = 10000000L;
+    private static final long MAX = 9999999999L;
+
+    Random rand = new Random(SEED);
+    List<Integer> percentages;
+
+    @Before
+    public void init() {
+        percentages = ImmutableList.<Integer>builder()
+                .add(1)
+                .add(2)
+                .add(5)
+                .add(10)
+                .add(25)
+                .add(50)
+                .add(90)
+                .add(99)
+                .add(100)
+                .build();
+    }
+
+    @Test
+    public void should_have_a_name() {
+        GradualRolloutSessionIdStrategy gradualRolloutStrategy = new GradualRolloutSessionIdStrategy();
+        assertThat(gradualRolloutStrategy.getName(), is("gradualRolloutSessionId"));
+    }
+
+    @Test
+    public void should_require_context() {
+        GradualRolloutSessionIdStrategy gradualRolloutStrategy = new GradualRolloutSessionIdStrategy();
+        assertThat(gradualRolloutStrategy.isEnabled(new HashMap<>()), is(false));
+    }
+
+    @Test
+    public void should_be_disabled_when_missing_user_id() {
+        UnleashContext context = UnleashContext.builder().build();
+        GradualRolloutSessionIdStrategy gradualRolloutStrategy = new GradualRolloutSessionIdStrategy();
+
+        assertThat(gradualRolloutStrategy.isEnabled(new HashMap<>(), context), is(false));
+    }
+
+    @Test
+    public void should_have_same_result_for_multiple_executions() {
+        UnleashContext context = UnleashContext.builder().sessionId("1574576830").build();
+        GradualRolloutSessionIdStrategy gradualRolloutStrategy = new GradualRolloutSessionIdStrategy();
+
+        Map<String, String> params = buildParams(1, "innfinn");
+        boolean firstRunResult = gradualRolloutStrategy.isEnabled(params, context);
+
+        for (int i = 0; i < 10; i++) {
+            boolean subsequentRunResult = gradualRolloutStrategy.isEnabled(params, context);
+            assertThat(
+                    "loginId will return same result when unchanged parameters",
+                    firstRunResult,
+                    is(equalTo(subsequentRunResult))
+            );
+        }
+    }
+
+    @Test
+    public void should_be_enabled_when_using_100percent_rollout() {
+        UnleashContext context = UnleashContext.builder().sessionId("1574576830").build();
+        GradualRolloutSessionIdStrategy gradualRolloutStrategy = new GradualRolloutSessionIdStrategy();
+
+        Map<String, String> params = buildParams(100, "innfinn");
+        boolean result = gradualRolloutStrategy.isEnabled(params, context);
+
+        assertThat(result, is(true));
+    }
+
+
+    @Test
+    public void should_not_be_enabled_when_0percent_rollout() {
+        UnleashContext context = UnleashContext.builder().sessionId("1574576830").build();
+        GradualRolloutSessionIdStrategy gradualRolloutStrategy = new GradualRolloutSessionIdStrategy();
+
+        Map<String, String> params = buildParams(0, "innfinn");
+        boolean actual = gradualRolloutStrategy.isEnabled(params, context);
+
+        assertFalse("should not be enabled when 0% rollout", actual);
+    }
+
+    @Test
+    public void should_be_enabled_above_minimum_percentage() {
+        String sessionId = "1574576830";
+        String groupId = "";
+        int minimumPercentage = StrategyUtils.getNormalizedNumber(sessionId, groupId);
+
+        UnleashContext context = UnleashContext.builder().sessionId(sessionId).build();
+
+        GradualRolloutSessionIdStrategy gradualRolloutStrategy = new GradualRolloutSessionIdStrategy();
+
+        for(int p = minimumPercentage ; p <=100 ; p++) {
+            Map<String, String> params = buildParams(p, groupId);
+            boolean actual = gradualRolloutStrategy.isEnabled(params, context);
+            assertTrue("should be enabled when " + p + "% rollout", actual);
+        }
+    }
+
+
+    @Ignore // Intended for manual execution
+    @Test
+    public void generateReportForListOfLoginIDs() {
+        final int numberOfIDs = 200000;
+
+        for (Integer percentage : percentages) {
+            int numberOfEnabledUsers = checkRandomLoginIDs(numberOfIDs, percentage);
+            double p = ((double) numberOfEnabledUsers / (double) numberOfIDs) * 100.0;
+            System.out.println("Testing " + percentage + "% --> " + numberOfEnabledUsers + " of " + numberOfIDs + " got new feature (" + p + "%)");
+        }
+    }
+
+
+    protected int checkRandomLoginIDs(int numberOfIDs, int percentage) {
+        int numberOfEnabledUsers = 0;
+        for (int i = 0; i < numberOfIDs; i++) {
+            Long sessionId = getRandomLoginId();
+            UnleashContext context = UnleashContext.builder().sessionId(sessionId.toString()).build();
+
+            GradualRolloutSessionIdStrategy gradualRolloutStrategy = new GradualRolloutSessionIdStrategy();
+
+            Map<String, String> params = buildParams(percentage, "");
+            boolean enabled = gradualRolloutStrategy.isEnabled(params, context);
+            if (enabled) {
+                numberOfEnabledUsers++;
+            }
+        }
+        return numberOfEnabledUsers;
+    }
+
+    private Map<String, String> buildParams(int percentage, String groupId) {
+        Map<String, String> params = new HashMap();
+        params.put(GradualRolloutSessionIdStrategy.PERCENTAGE, String.valueOf(percentage));
+        params.put(GradualRolloutSessionIdStrategy.GROUP_ID, groupId);
+
+        return params;
+    }
+
+
+    private Long getRandomLoginId() {
+        long bits, val;
+        long bound = (MAX - MIN) + 1L;
+        do {
+            bits = (rand.nextLong() << 1) >>> 1;
+            val = bits % bound;
+        } while (bits - val + (bound - 1L) < 0L);
+        return val;
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/no/finn/unleash/strategy/GradualRolloutUserIdStrategyTest.java b/src/test/java/no/finn/unleash/strategy/GradualRolloutUserIdStrategyTest.java
new file mode 100644
index 00000000..9a2ddebc
--- /dev/null
+++ b/src/test/java/no/finn/unleash/strategy/GradualRolloutUserIdStrategyTest.java
@@ -0,0 +1,197 @@
+package no.finn.unleash.strategy;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+import com.google.common.collect.ImmutableList;
+import no.finn.unleash.UnleashContext;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+public class GradualRolloutUserIdStrategyTest {
+    private static final long SEED = 89235015401L;
+    private static final long MIN = 10000000L;
+    private static final long MAX = 9999999999L;
+
+    Random rand = new Random(SEED);
+    List<Integer> percentages;
+
+    @Before
+    public void init() {
+        percentages = ImmutableList.<Integer>builder()
+                .add(1)
+                .add(2)
+                .add(5)
+                .add(10)
+                .add(25)
+                .add(50)
+                .add(90)
+                .add(99)
+                .add(100)
+                .build();
+    }
+
+    @Test
+    public void should_have_a_name() {
+        GradualRolloutUserIdStrategy gradualRolloutStrategy = new GradualRolloutUserIdStrategy();
+        assertThat(gradualRolloutStrategy.getName(), is("gradualRolloutUserId"));
+    }
+
+    @Test
+    public void should_require_context() {
+        GradualRolloutUserIdStrategy gradualRolloutStrategy = new GradualRolloutUserIdStrategy();
+        assertThat(gradualRolloutStrategy.isEnabled(new HashMap<>()), is(false));
+    }
+
+    @Test
+    public void should_be_disabled_when_missing_user_id() {
+        UnleashContext context = UnleashContext.builder().build();
+        GradualRolloutUserIdStrategy gradualRolloutStrategy = new GradualRolloutUserIdStrategy();
+
+        assertThat(gradualRolloutStrategy.isEnabled(new HashMap<>(), context), is(false));
+    }
+
+    @Test
+    public void should_have_same_result_for_multiple_executions() {
+        UnleashContext context = UnleashContext.builder().userId("1574576830").build();
+        GradualRolloutUserIdStrategy gradualRolloutStrategy = new GradualRolloutUserIdStrategy();
+
+        Map<String, String> params = buildParams(1, "innfinn");
+        boolean firstRunResult = gradualRolloutStrategy.isEnabled(params, context);
+
+        for (int i = 0; i < 10; i++) {
+            boolean subsequentRunResult = gradualRolloutStrategy.isEnabled(params, context);
+            assertThat(
+                    "loginId will return same result when unchanged parameters",
+                    firstRunResult,
+                    is(equalTo(subsequentRunResult))
+            );
+        }
+    }
+
+    @Test
+    public void should_be_enabled_when_using_100percent_rollout() {
+        UnleashContext context = UnleashContext.builder().userId("1574576830").build();
+        GradualRolloutUserIdStrategy gradualRolloutStrategy = new GradualRolloutUserIdStrategy();
+
+        Map<String, String> params = buildParams(100, "innfinn");
+        boolean result = gradualRolloutStrategy.isEnabled(params, context);
+
+        assertThat(result, is(true));
+    }
+
+
+    @Test
+    public void should_not_be_enabled_when_0percent_rollout() {
+        UnleashContext context = UnleashContext.builder().userId("1574576830").build();
+        GradualRolloutUserIdStrategy gradualRolloutStrategy = new GradualRolloutUserIdStrategy();
+
+        Map<String, String> params = buildParams(0, "innfinn");
+        boolean actual = gradualRolloutStrategy.isEnabled(params, context);
+
+        assertFalse("should not be enabled when 0% rollout", actual);
+    }
+
+    @Test
+    public void should_be_enabled_above_minimum_percentage() {
+        String userId = "1574576830";
+        String groupId = "";
+        int minimumPercentage = StrategyUtils.getNormalizedNumber(userId, groupId);
+
+        UnleashContext context = UnleashContext.builder().userId(userId).build();
+
+        GradualRolloutUserIdStrategy gradualRolloutStrategy = new GradualRolloutUserIdStrategy();
+
+        for(int p = minimumPercentage ; p <=100 ; p++) {
+            Map<String, String> params = buildParams(p, groupId);
+            boolean actual = gradualRolloutStrategy.isEnabled(params, context);
+            assertTrue("should be enabled when " + p + "% rollout", actual);
+        }
+    }
+
+    @Test
+    public void should_at_most_miss_with_one_percent_when_rolling_out_to_specified_percentage() {
+        String groupId = "group1";
+        int percentage = 25;
+        int rounds = 20000;
+        int enabledCount = 0;
+
+        Map<String, String> params = buildParams(percentage, groupId);
+
+        GradualRolloutUserIdStrategy gradualRolloutStrategy = new GradualRolloutUserIdStrategy();
+
+
+        for(int userId=0; userId < rounds; userId++) {
+            UnleashContext context = UnleashContext.builder().userId("user"+ userId).build();
+
+            if(gradualRolloutStrategy.isEnabled(params, context)) {
+                enabledCount++;
+            }
+        }
+
+        double actualPercentage = ((double) enabledCount / (double) rounds) * 100.0;
+
+        assertTrue("Expected " + percentage + "%, but was "+ actualPercentage + "%" ,
+                (percentage-1) < actualPercentage);
+
+        assertTrue("Expected " + percentage + "%, but was "+ actualPercentage + "%" ,
+                (percentage+1) > actualPercentage);
+    }
+
+
+    @Ignore // Intended for manual execution
+    @Test
+    public void generateReportForListOfLoginIDs() {
+        final int numberOfIDs = 200000;
+
+        for (Integer percentage : percentages) {
+            int numberOfEnabledUsers = checkRandomLoginIDs(numberOfIDs, percentage);
+            double p = ((double) numberOfEnabledUsers / (double) numberOfIDs) * 100.0;
+            System.out.println("Testing " + percentage + "% --> " + numberOfEnabledUsers + " of " + numberOfIDs + " got new feature (" + p + "%)");
+        }
+    }
+
+
+    protected int checkRandomLoginIDs(int numberOfIDs, int percentage) {
+        int numberOfEnabledUsers = 0;
+        for (int i = 0; i < numberOfIDs; i++) {
+            Long userId = getRandomLoginId();
+            UnleashContext context = UnleashContext.builder().userId(userId.toString()).build();
+
+            GradualRolloutUserIdStrategy gradualRolloutStrategy = new GradualRolloutUserIdStrategy();
+
+            Map<String, String> params = buildParams(percentage, "");
+            boolean enabled = gradualRolloutStrategy.isEnabled(params, context);
+            if (enabled) {
+                numberOfEnabledUsers++;
+            }
+        }
+        return numberOfEnabledUsers;
+    }
+
+    private Map<String, String> buildParams(int percentage, String groupId) {
+        Map<String, String> params = new HashMap();
+        params.put(GradualRolloutUserIdStrategy.PERCENTAGE, String.valueOf(percentage));
+        params.put(GradualRolloutUserIdStrategy.GROUP_ID, groupId);
+
+        return params;
+    }
+
+
+    private Long getRandomLoginId() {
+        long bits, val;
+        long bound = (MAX - MIN) + 1L;
+        do {
+            bits = (rand.nextLong() << 1) >>> 1;
+            val = bits % bound;
+        } while (bits - val + (bound - 1L) < 0L);
+        return val;
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/no/finn/unleash/strategy/RemoteAddressStrategyTest.java b/src/test/java/no/finn/unleash/strategy/RemoteAddressStrategyTest.java
new file mode 100644
index 00000000..8245c47c
--- /dev/null
+++ b/src/test/java/no/finn/unleash/strategy/RemoteAddressStrategyTest.java
@@ -0,0 +1,76 @@
+package no.finn.unleash.strategy;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import no.finn.unleash.UnleashContext;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+@RunWith(Parameterized.class)
+public class RemoteAddressStrategyTest {
+    private static final String FIRST_IP = "127.0.0.1";
+    private static final String SECOND_IP = "10.0.0.1";
+    private static final String THIRD_IP = "196.0.0.1";
+    private static final List<String> ALL = Arrays.asList(FIRST_IP, SECOND_IP, THIRD_IP);
+
+    @Parameter(value=0)
+    public String actualIp;
+
+    @Parameter(value=1)
+    public String parameterString;
+
+    @Parameter(value=2)
+    public boolean expected;
+
+    private RemoteAddressStrategy strategy;
+
+    @Parameters(name="{index}: actualIp: {0}, parameter: {1}, expected: {2}")
+    public static Iterable<Object[]> data() {
+        return Arrays.asList(new Object[][] {
+                {FIRST_IP, FIRST_IP, true},
+                {FIRST_IP, String.join(",", ALL), true},
+                {SECOND_IP, String.join(",", ALL), true},
+                {THIRD_IP, String.join(",", ALL), true},
+                {FIRST_IP, String.join(", ", ALL), true},
+                {SECOND_IP, String.join(", ", ALL), true},
+                {THIRD_IP, String.join(", ", ALL), true},
+                {SECOND_IP, String.join(",  ", ALL), true},
+                {SECOND_IP, String.join(".", ALL), false},
+                {FIRST_IP, SECOND_IP, false},
+        });
+    }
+
+    @Before
+    public void setUp() {
+        strategy = new RemoteAddressStrategy();
+    }
+
+    @Test
+    public void should_have_a_name() {
+        assertThat(strategy.getName(), is("remoteAddress"));
+    }
+
+    @Test
+    public void test() {
+        UnleashContext context = UnleashContext.builder().remoteAddress(actualIp).build();
+        Map<String, String> parameters = setupParameterMap(parameterString);
+
+        assertThat(strategy.isEnabled(parameters, context), is(expected));
+    }
+
+    private Map<String, String> setupParameterMap(String ipString) {
+        Map<String, String> parameters = new HashMap<>();
+        parameters.put(RemoteAddressStrategy.PARAM, ipString);
+        return parameters;
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/no/finn/unleash/strategy/StrategyUsingContext.java b/src/test/java/no/finn/unleash/strategy/StrategyUsingContext.java
new file mode 100644
index 00000000..3a8e9d4f
--- /dev/null
+++ b/src/test/java/no/finn/unleash/strategy/StrategyUsingContext.java
@@ -0,0 +1,33 @@
+package no.finn.unleash.strategy;
+
+import java.util.List;
+import java.util.Map;
+import no.finn.unleash.UnleashContext;
+
+import static java.util.Arrays.asList;
+
+public class StrategyUsingContext implements Strategy {
+
+
+    @Override
+    public String getName() {
+        return "usingContext";
+    }
+
+    @Override
+    public boolean isEnabled(Map<String, String> parameters) {
+        return false;
+    }
+
+    @Override
+    public boolean isEnabled(Map<String, String> parameters, UnleashContext unleashContext) {
+        String userIdString = parameters.get("userIds");
+        List<String> userIds = asList(userIdString.split(",\\s?"));
+        if(unleashContext.getUserId().isPresent()) {
+            String userId = unleashContext.getUserId().get();
+            return userIds.contains(userId);
+        } else {
+            return false;
+        }
+    }
+}
diff --git a/src/test/java/no/finn/unleash/strategy/StrategyWithContextTest.java b/src/test/java/no/finn/unleash/strategy/StrategyWithContextTest.java
new file mode 100644
index 00000000..1143b55a
--- /dev/null
+++ b/src/test/java/no/finn/unleash/strategy/StrategyWithContextTest.java
@@ -0,0 +1,44 @@
+package no.finn.unleash.strategy;
+
+import java.util.HashMap;
+import java.util.Map;
+import no.finn.unleash.UnleashContext;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class StrategyWithContextTest {
+    private StrategyUsingContext strategy;
+
+    @Before
+    public void setup() {
+        strategy = new StrategyUsingContext();
+    }
+
+    @Test
+    public void should_be_enabled_for_known_user() {
+        //Params
+        Map<String, String> params = new HashMap<>();
+        params.put("userIds", "123");
+
+        //Context
+        UnleashContext context = UnleashContext.builder().userId("123").build();
+
+        assertTrue(strategy.isEnabled(params, context));
+    }
+
+    @Test
+    public void should_not_enabled_for_unknown_user() {
+        //Params
+        Map<String, String> params = new HashMap<>();
+        params.put("userIds", "123");
+
+        //Context
+        UnleashContext context = UnleashContext.builder().userId("other").build();
+
+        assertFalse(strategy.isEnabled(params, context));
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/no/finn/unleash/strategy/UserWithIdStrategyTest.java b/src/test/java/no/finn/unleash/strategy/UserWithIdStrategyTest.java
new file mode 100644
index 00000000..9de5a34c
--- /dev/null
+++ b/src/test/java/no/finn/unleash/strategy/UserWithIdStrategyTest.java
@@ -0,0 +1,119 @@
+package no.finn.unleash.strategy;
+
+import java.util.HashMap;
+import java.util.Map;
+import no.finn.unleash.UnleashContext;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public class UserWithIdStrategyTest {
+    private UserWithIdStrategy strategy;
+
+    @Before
+    public void setup() {
+        strategy = new UserWithIdStrategy();
+    }
+
+    @Test
+    public void should_have_expected_strategy_name() {
+        assertThat(strategy.getName(), is("userWithId"));
+
+    }
+
+    @Test
+    public void should_match_one_userId() {
+        Map<String, String> parameters = new HashMap<>();
+
+        UnleashContext context = UnleashContext.builder().userId("123").build();
+        parameters.put(strategy.PARAM, "123");
+
+        assertTrue(strategy.isEnabled(parameters, context));
+    }
+
+    @Test
+    public void should_match_first_userId_in_list() {
+        Map<String, String> parameters = new HashMap<>();
+
+        UnleashContext context = UnleashContext.builder().userId("123").build();
+        parameters.put(strategy.PARAM, "123, 122, 121");
+
+        assertTrue(strategy.isEnabled(parameters, context));
+    }
+
+    @Test
+    public void should_match_middle_userId_in_list() {
+        Map<String, String> parameters = new HashMap<>();
+
+        UnleashContext context = UnleashContext.builder().userId("123").build();
+        parameters.put(strategy.PARAM, "123, 122, 121");
+
+        assertTrue(strategy.isEnabled(parameters, context));
+    }
+
+    @Test
+    public void should_match_last_userId_in_list() {
+        Map<String, String> parameters = new HashMap<>();
+
+        UnleashContext context = UnleashContext.builder().userId("123").build();
+        parameters.put(strategy.PARAM, "123, 122, 121");
+
+        assertTrue(strategy.isEnabled(parameters, context));
+    }
+
+    @Test
+    public void should_not_match_subparts_of_ids() {
+        Map<String, String> parameters = new HashMap<>();
+
+        UnleashContext context = UnleashContext.builder().userId("12").build();
+        parameters.put(strategy.PARAM, "123, 122, 121, 212");
+
+        assertFalse(strategy.isEnabled(parameters, context));
+    }
+
+    @Test
+    public void should_not_match_csv_without_space() {
+        Map<String, String> parameters = new HashMap<>();
+
+        UnleashContext context = UnleashContext.builder().userId("123").build();
+        parameters.put(strategy.PARAM, "123,122,121");
+
+        assertTrue(strategy.isEnabled(parameters, context));
+    }
+
+    @Test
+    public void should_match_real_ids() {
+        Map<String, String> parameters = new HashMap<>();
+
+        UnleashContext context = UnleashContext.builder().userId("298261117").build();
+        parameters.put(strategy.PARAM,
+                "160118738, 1823311338, 1422637466, 2125981185, 298261117, 1829486714, 463568019, 271166598");
+
+        assertTrue(strategy.isEnabled(parameters, context));
+    }
+
+    @Test
+    public void should_not_match_real_ids() {
+        Map<String, String> parameters = new HashMap<>();
+
+        UnleashContext context = UnleashContext.builder().userId("32667774").build();
+        parameters.put(strategy.PARAM,
+                "160118738, 1823311338, 1422637466, 2125981185, 298261117, 1829486714, 463568019, 271166598");
+
+        assertFalse(strategy.isEnabled(parameters, context));
+    }
+
+    @Test
+    public void should_not_be_enabled_without_id() {
+        Map<String, String> parameters = new HashMap<>();
+
+        parameters.put(strategy.PARAM, "160118738, 1823311338");
+
+        assertFalse(strategy.isEnabled(parameters));
+    }
+
+}
\ No newline at end of file