Repository: Unleash/unleash-client-java
Pull request #152 (2022-02-01 13:54:38)

feat: Add new constraint operators
- Adds String operators, startsWith, endsWith, contains
  with support for case insentivity if desired

- Adds Number operators LT, LTE, EQ, GTE, GT for comparing numbers

- Adds Date operators for supporting DATE_AFTER and DATE_BEFORE

- Adds Semver operators for support SEMVER_LT, SEMVER_EQ, SEMVER_GT


Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 00000000..8c2ef2ae
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,58 @@
+name: Build
+
+on:
+  push:
+    branches:
+      - master
+      - main
+
+
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        version: [8,11]
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Setup Java
+        uses: actions/setup-java@v1
+        with:
+          java-version: ${{ matrix.version }}
+      - name: Build, test, coverage
+        run: ./mvnw clean test jacoco:report coveralls:report
+      - name: Notify Slack of pipeline completion
+        uses: 8398a7/action-slack@v2
+        with:
+          status: ${{ job.status }}
+          author_name: Github Action
+          text: Build on Java ${{ matrix.version }}
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          SLACK_WEBHOOK_URL: ${{ secrets.slack_webhook }}
+        if: always()
+  java17:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Setup Java
+        uses: actions/setup-java@v1
+        with:
+          java-version: 17
+      - name: Build, test, coverage
+        run: ./mvnw clean test jacoco:report coveralls:report
+        env:
+          MAVEN_OPTS: "--add-opens java.base/java.net=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
+      - name: Notify Slack of pipeline completion
+        uses: 8398a7/action-slack@v2
+        with:
+          status: ${{ job.status }}
+          author_name: Github Action
+          text: Build on Java 17
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          SLACK_WEBHOOK_URL: ${{ secrets.slack_webhook }}
+        if: always()
diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
deleted file mode 100644
index e2946c66..00000000
--- a/.github/workflows/master.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-name: Build
-
-on:
-  push:
-    branches:
-      - master
-      - main
-
-
-
-jobs:
-  build:
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        version: [8,11,15]
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v2
-      - name: Setup Java
-        uses: actions/setup-java@v1
-        with:
-          java-version: ${{ matrix.version }}
-      - name: Build, test, coverage
-        run: ./mvnw clean test jacoco:report coveralls:report
-      - name: Notify Slack of pipeline completion
-        uses: 8398a7/action-slack@v2
-        with:
-          status: ${{ job.status }}
-          author_name: Github Action
-          text: Build on Java ${{ matrix.version }}
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          SLACK_WEBHOOK_URL: ${{ secrets.slack_webhook }}
-        if: always()
diff --git a/.github/workflows/publish_javadoc.yml b/.github/workflows/publish_javadoc.yml
index 783c5013..31f5a164 100644
--- a/.github/workflows/publish_javadoc.yml
+++ b/.github/workflows/publish_javadoc.yml
@@ -16,7 +16,7 @@ jobs:
       - name: Setup Java
         uses: actions/setup-java@v1
         with:
-          java-version: 15
+          java-version: 17
       - name: Build
         run: ./mvnw javadoc:javadoc
       - name: Deploy docs to pages
diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml
index d6858c14..eb7443e1 100644
--- a/.github/workflows/pull_requests.yml
+++ b/.github/workflows/pull_requests.yml
@@ -7,7 +7,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        version: [8,11,15]
+        version: [8,11]
     steps:
       - name: Checkout
         uses: actions/checkout@v2
@@ -16,4 +16,18 @@ jobs:
         with:
           java-version: ${{ matrix.version }}
       - name: Build, test, coverage
-        run: ./mvnw clean test jacoco:report coveralls:report
\ No newline at end of file
+        run: ./mvnw clean test jacoco:report coveralls:report
+  java17:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Setup Java
+        uses: actions/setup-java@v1
+        with:
+          java-version: 17
+      - name: Build, test, coverage
+        run: ./mvnw clean test jacoco:report coveralls:report
+        env:
+          MAVEN_OPTS: "--add-opens java.base/java.net=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 10e1dd2c..59a6ac11 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 5.1.0
+- feat: Adds support for more constraint operators (String, Number, Semver, Dates)
+  - See https://docs.getunleash.io/advanced/strategy_constraints#constraint-structure
+
 ## 5.0.4
 - fix: Bootstrap no longer fails hard when passed invalid json. It now returns an empty list of features if the bootstrap json could not be parsed.
 ## 5.0.0
diff --git a/pom.xml b/pom.xml
index c63b94e7..a16fc5e0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <groupId>io.getunleash</groupId>
     <artifactId>unleash-client-java</artifactId>
@@ -10,8 +11,8 @@
         <version.log4j2>2.17.0</version.log4j2>
         <version.junit5>5.8.2</version.junit5>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-        <version.unleash.specification>4.0.0</version.unleash.specification>
-        <arguments />
+        <version.unleash.specification>4.1.0</version.unleash.specification>
+        <arguments/>
     </properties>
 
     <name>io.getunleash:unleash-client-java</name>
@@ -96,7 +97,7 @@
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
-            <version>3.21.0</version>
+            <version>3.22.0</version>
             <scope>test</scope>
         </dependency>
 
@@ -113,6 +114,25 @@
             <version>2.27.2</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <version>2.13.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+            <version>2.13.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.13.1</version>
+            <scope>test</scope>
+        </dependency>
+
 
         <dependency>
             <groupId>com.github.JensPiegsa</groupId>
@@ -131,10 +151,15 @@
 
     <build>
         <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-site-plugin</artifactId>
+                <version>3.10.0</version>
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
-                <version>3.8.1</version>
+                <version>3.9.0</version>
                 <configuration>
                     <source>1.8</source>
                     <target>1.8</target>
@@ -151,7 +176,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jar-plugin</artifactId>
-                <version>3.2.0</version>
+                <version>3.2.2</version>
                 <configuration>
                     <archive>
                         <manifest>
@@ -289,7 +314,7 @@
             <plugin>
                 <groupId>com.diffplug.spotless</groupId>
                 <artifactId>spotless-maven-plugin</artifactId>
-                <version>2.7.0</version>
+                <version>2.20.1</version>
                 <configuration>
                     <formats>
                         <format>
@@ -297,8 +322,8 @@
                                 <include>*.md</include>
                                 <include>.gitignore</include>
                             </includes>
-                            <trimTrailingWhitespace />
-                            <endWithNewline />
+                            <trimTrailingWhitespace/>
+                            <endWithNewline/>
                             <indent>
                                 <spaces>true</spaces>
                                 <tabs>false</tabs>
@@ -307,8 +332,8 @@
                         </format>
                     </formats>
                     <java>
-                        <removeUnusedImports />
-                        <toggleOffOn />
+                        <removeUnusedImports/>
+                        <toggleOffOn/>
                         <googleJavaFormat>
                             <style>AOSP</style>
                         </googleJavaFormat>
@@ -335,6 +360,25 @@
             </plugin>
         </plugins>
     </build>
+    <reporting>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-project-info-reports-plugin</artifactId>
+                <version>3.2.1</version>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <version>3.3.1</version>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>3.0.0-M5</version>
+            </plugin>
+        </plugins>
+    </reporting>
     <profiles>
         <profile>
             <id>sonatype-release</id>
@@ -342,7 +386,8 @@
                 <activeByDefault>false</activeByDefault>
             </activation>
             <properties>
-                <sonatypeOssDistMgmtSnapshotsUrl>https://s01.oss.sonatype.org/content/repositories/snapshots</sonatypeOssDistMgmtSnapshotsUrl>
+                <sonatypeOssDistMgmtSnapshotsUrl>https://s01.oss.sonatype.org/content/repositories/snapshots
+                </sonatypeOssDistMgmtSnapshotsUrl>
             </properties>
             <repositories>
                 <repository>
diff --git a/src/main/java/io/getunleash/Constraint.java b/src/main/java/io/getunleash/Constraint.java
index 311fd920..1e15f14c 100644
--- a/src/main/java/io/getunleash/Constraint.java
+++ b/src/main/java/io/getunleash/Constraint.java
@@ -1,16 +1,56 @@
 package io.getunleash;
 
+import io.getunleash.lang.Nullable;
+import java.util.Collections;
 import java.util.List;
 
 public class Constraint {
     private final String contextName;
     private final Operator operator;
-    private final List<String> values;
+    @Nullable private final List<String> values;
+    @Nullable private final String value;
+    private final boolean inverted;
+    private final boolean caseInsensitive;
 
     public Constraint(String contextName, Operator operator, List<String> values) {
+        this(contextName, operator, null, values, false, false);
+    }
+
+    public Constraint(String contextName, Operator operator, String value) {
+        this(contextName, operator, value, Collections.emptyList(), false, false);
+    }
+
+    public Constraint(
+            String contextName, Operator operator, List<String> values, boolean inverted) {
+        this(contextName, operator, null, values, inverted, false);
+    }
+
+    public Constraint(String contextName, Operator operator, String value, boolean inverted) {
+        this(contextName, operator, value, Collections.emptyList(), inverted, false);
+    }
+
+    public Constraint(
+            String contextName,
+            Operator operator,
+            List<String> values,
+            boolean inverted,
+            boolean caseInsensitive) {
+        this(contextName, operator, null, values, inverted, caseInsensitive);
+    }
+
+    public Constraint(
+            String contextName,
+            Operator operator,
+            @Nullable String value,
+            @Nullable List<String> values,
+            boolean inverted,
+            boolean caseInsensitive) {
         this.contextName = contextName;
         this.operator = operator;
         this.values = values;
+        this.inverted = inverted;
+        this.caseInsensitive = caseInsensitive;
+        this.value = value;
     }
 
     public String getContextName() {
@@ -22,6 +62,18 @@ public class Constraint {
     }
 
     public List<String> getValues() {
-        return values;
+        return values != null ? values : Collections.emptyList();
+    }
+
+    public @Nullable String getValue() {
+        return value;
+    }
+
+    public boolean isInverted() {
+        return inverted;
+    }
+
+    public boolean isCaseInsensitive() {
+        return caseInsensitive;
     }
 }
diff --git a/src/main/java/io/getunleash/Operator.java b/src/main/java/io/getunleash/Operator.java
index 045ff9f0..03ceb187 100644
--- a/src/main/java/io/getunleash/Operator.java
+++ b/src/main/java/io/getunleash/Operator.java
@@ -2,5 +2,18 @@ package io.getunleash;
 
 public enum Operator {
     IN,
-    NOT_IN
+    NOT_IN,
+    STR_ENDS_WITH,
+    STR_STARTS_WITH,
+    STR_CONTAINS,
+    NUM_EQ,
+    NUM_GT,
+    NUM_GTE,
+    NUM_LT,
+    NUM_LTE,
+    DATE_AFTER,
+    DATE_BEFORE,
+    SEMVER_EQ,
+    SEMVER_GT,
+    SEMVER_LT
 }
diff --git a/src/main/java/io/getunleash/UnleashContext.java b/src/main/java/io/getunleash/UnleashContext.java
index f4edff21..fa619cbf 100644
--- a/src/main/java/io/getunleash/UnleashContext.java
+++ b/src/main/java/io/getunleash/UnleashContext.java
@@ -2,6 +2,8 @@ package io.getunleash;
 
 import io.getunleash.lang.Nullable;
 import io.getunleash.util.UnleashConfig;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
@@ -12,12 +14,13 @@ public class UnleashContext {
     private final Optional<String> userId;
     private final Optional<String> sessionId;
     private final Optional<String> remoteAddress;
+    private final Optional<ZonedDateTime> currentTime;
 
     private final Map<String, String> properties;
 
     public UnleashContext(
             String userId, String sessionId, String remoteAddress, Map<String, String> properties) {
-        this(null, null, userId, sessionId, remoteAddress, properties);
+        this(null, null, userId, sessionId, remoteAddress, null, properties);
     }
 
     public UnleashContext(
@@ -27,11 +30,23 @@ public class UnleashContext {
             @Nullable String sessionId,
             @Nullable String remoteAddress,
             Map<String, String> properties) {
+        this(appName, environment, userId, sessionId, remoteAddress, null, properties);
+    }
+
+    public UnleashContext(
+            @Nullable String appName,
+            @Nullable String environment,
+            @Nullable String userId,
+            @Nullable String sessionId,
+            @Nullable String remoteAddress,
+            @Nullable ZonedDateTime currentTime,
+            Map<String, String> properties) {
         this.appName = Optional.ofNullable(appName);
         this.environment = Optional.ofNullable(environment);
         this.userId = Optional.ofNullable(userId);
         this.sessionId = Optional.ofNullable(sessionId);
         this.remoteAddress = Optional.ofNullable(remoteAddress);
+        this.currentTime = Optional.ofNullable(currentTime);
         this.properties = properties;
     }
 
@@ -59,6 +74,10 @@ public class UnleashContext {
         return environment;
     }
 
+    public Optional<ZonedDateTime> getCurrentTime() {
+        return currentTime;
+    }
+
     public Optional<String> getByName(String contextName) {
         switch (contextName) {
             case "environment":
@@ -97,6 +116,7 @@ public class UnleashContext {
         @Nullable private String userId;
         @Nullable private String sessionId;
         @Nullable private String remoteAddress;
+        @Nullable private ZonedDateTime currentTime;
 
         private final Map<String, String> properties = new HashMap<>();
 
@@ -108,6 +128,7 @@ public class UnleashContext {
             context.userId.ifPresent(val -> this.userId = val);
             context.sessionId.ifPresent(val -> this.sessionId = val);
             context.remoteAddress.ifPresent(val -> this.remoteAddress = val);
+            context.currentTime.ifPresent(val -> this.currentTime = val);
             context.properties.forEach(this.properties::put);
         }
 
@@ -136,6 +157,21 @@ public class UnleashContext {
             return this;
         }
 
+        public Builder currentTime(ZonedDateTime currentTime) {
+            this.currentTime = currentTime;
+            return this;
+        }
+
+        /**
+         * Defaults to now() in UTC timezone
+         *
+         * @return this
+         */
+        public Builder now() {
+            this.currentTime = ZonedDateTime.now(ZoneOffset.UTC);
+            return this;
+        }
+
         public Builder addProperty(String name, String value) {
             properties.put(name, value);
             return this;
@@ -143,7 +179,13 @@ public class UnleashContext {
 
         public UnleashContext build() {
             return new UnleashContext(
-                    appName, environment, userId, sessionId, remoteAddress, properties);
+                    appName,
+                    environment,
+                    userId,
+                    sessionId,
+                    remoteAddress,
+                    currentTime,
+                    properties);
         }
     }
 }
diff --git a/src/main/java/io/getunleash/metric/UnleashMetricsSender.java b/src/main/java/io/getunleash/metric/UnleashMetricsSender.java
index e0604750..9a7c6dee 100644
--- a/src/main/java/io/getunleash/metric/UnleashMetricsSender.java
+++ b/src/main/java/io/getunleash/metric/UnleashMetricsSender.java
@@ -46,8 +46,7 @@ public class UnleashMetricsSender {
                 Type type,
                 JsonSerializationContext jsonSerializationContext) {
             return new JsonPrimitive(ISO_INSTANT.format(localDateTime.toInstant(ZoneOffset.UTC)));
-        }
-        ;
+        };
     }
 
     static class AtomicLongSerializer implements JsonSerializer<AtomicLong> {
diff --git a/src/main/java/io/getunleash/strategy/ConstraintUtil.java b/src/main/java/io/getunleash/strategy/ConstraintUtil.java
index 9eabc063..80b38ec6 100644
--- a/src/main/java/io/getunleash/strategy/ConstraintUtil.java
+++ b/src/main/java/io/getunleash/strategy/ConstraintUtil.java
@@ -1,13 +1,55 @@
 package io.getunleash.strategy;
 
+import static io.getunleash.Operator.DATE_AFTER;
+import static io.getunleash.Operator.DATE_BEFORE;
+import static io.getunleash.Operator.IN;
+import static io.getunleash.Operator.NOT_IN;
+import static io.getunleash.Operator.NUM_EQ;
+import static io.getunleash.Operator.NUM_GT;
+import static io.getunleash.Operator.NUM_GTE;
+import static io.getunleash.Operator.NUM_LT;
+import static io.getunleash.Operator.NUM_LTE;
+import static io.getunleash.Operator.SEMVER_EQ;
+import static io.getunleash.Operator.SEMVER_GT;
+import static io.getunleash.Operator.SEMVER_LT;
+import static io.getunleash.Operator.STR_CONTAINS;
+import static io.getunleash.Operator.STR_ENDS_WITH;
+import static io.getunleash.Operator.STR_STARTS_WITH;
+
 import io.getunleash.Constraint;
 import io.getunleash.Operator;
 import io.getunleash.UnleashContext;
 import io.getunleash.lang.Nullable;
+import io.getunleash.strategy.constraints.ConstraintOperator;
+import io.getunleash.strategy.constraints.DateConstraintOperator;
+import io.getunleash.strategy.constraints.NumberConstraintOperator;
+import io.getunleash.strategy.constraints.SemverConstraintOperator;
+import io.getunleash.strategy.constraints.StringConstraintOperator;
+import java.util.HashMap;
 import java.util.List;
-import java.util.Optional;
+import java.util.Locale;
+import java.util.Map;
 
 public class ConstraintUtil {
+    private static final Map<Operator, ConstraintOperator> operators = new HashMap<>();
+
+    static {
+        operators.put(STR_CONTAINS, new StringConstraintOperator(Locale.ROOT));
+        operators.put(STR_ENDS_WITH, new StringConstraintOperator(Locale.ROOT));
+        operators.put(STR_STARTS_WITH, new StringConstraintOperator(Locale.ROOT));
+        operators.put(IN, new StringConstraintOperator(Locale.ROOT));
+        operators.put(NOT_IN, new StringConstraintOperator(Locale.ROOT));
+        operators.put(NUM_LT, new NumberConstraintOperator());
+        operators.put(NUM_LTE, new NumberConstraintOperator());
+        operators.put(NUM_EQ, new NumberConstraintOperator());
+        operators.put(NUM_GTE, new NumberConstraintOperator());
+        operators.put(NUM_GT, new NumberConstraintOperator());
+        operators.put(SEMVER_LT, new SemverConstraintOperator());
+        operators.put(SEMVER_EQ, new SemverConstraintOperator());
+        operators.put(SEMVER_GT, new SemverConstraintOperator());
+        operators.put(DATE_BEFORE, new DateConstraintOperator());
+        operators.put(DATE_AFTER, new DateConstraintOperator());
+    }
 
     public static boolean validate(@Nullable List<Constraint> constraints, UnleashContext context) {
         if (constraints != null && constraints.size() > 0) {
@@ -18,10 +60,7 @@ public class ConstraintUtil {
     }
 
     private static boolean validateConstraint(Constraint constraint, UnleashContext context) {
-        Optional<String> contextValue = context.getByName(constraint.getContextName());
-        boolean isIn =
-                contextValue.isPresent()
-                        && constraint.getValues().contains(contextValue.get().trim());
-        return (constraint.getOperator() == Operator.IN) == isIn;
+        ConstraintOperator operator = operators.get(constraint.getOperator());
+        return constraint.isInverted() ^ operator.evaluate(constraint, context);
     }
 }
diff --git a/src/main/java/io/getunleash/strategy/constraints/ConstraintOperator.java b/src/main/java/io/getunleash/strategy/constraints/ConstraintOperator.java
new file mode 100644
index 00000000..62a4f77e
--- /dev/null
+++ b/src/main/java/io/getunleash/strategy/constraints/ConstraintOperator.java
@@ -0,0 +1,8 @@
+package io.getunleash.strategy.constraints;
+
+import io.getunleash.Constraint;
+import io.getunleash.UnleashContext;
+
+public interface ConstraintOperator {
+    boolean evaluate(Constraint constraint, UnleashContext context);
+}
diff --git a/src/main/java/io/getunleash/strategy/constraints/DateConstraintOperator.java b/src/main/java/io/getunleash/strategy/constraints/DateConstraintOperator.java
new file mode 100644
index 00000000..96da1758
--- /dev/null
+++ b/src/main/java/io/getunleash/strategy/constraints/DateConstraintOperator.java
@@ -0,0 +1,34 @@
+package io.getunleash.strategy.constraints;
+
+import io.getunleash.Constraint;
+import io.getunleash.Operator;
+import io.getunleash.UnleashContext;
+import java.time.ZonedDateTime;
+
+public class DateConstraintOperator implements ConstraintOperator {
+
+    @Override
+    public boolean evaluate(Constraint constraint, UnleashContext context) {
+        ZonedDateTime dateToMatch =
+                context.getByName(constraint.getContextName())
+                        .map(DateParser::parseDate)
+                        .orElseGet(() -> context.getCurrentTime().orElseGet(ZonedDateTime::now));
+        try {
+            ZonedDateTime value = DateParser.parseDate(constraint.getValue());
+            return eval(constraint.getOperator(), value, dateToMatch);
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    private boolean eval(Operator op, ZonedDateTime value, ZonedDateTime toMatch) {
+        switch (op) {
+            case DATE_AFTER:
+                return toMatch.isAfter(value);
+            case DATE_BEFORE:
+                return toMatch.isBefore(value);
+            default:
+                return false;
+        }
+    }
+}
diff --git a/src/main/java/io/getunleash/strategy/constraints/DateParser.java b/src/main/java/io/getunleash/strategy/constraints/DateParser.java
new file mode 100644
index 00000000..1be4389f
--- /dev/null
+++ b/src/main/java/io/getunleash/strategy/constraints/DateParser.java
@@ -0,0 +1,49 @@
+package io.getunleash.strategy.constraints;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class DateParser {
+    private static final List<DateTimeFormatter> formatters = new ArrayList<>();
+
+    static {
+        formatters.add(DateTimeFormatter.ISO_INSTANT);
+        formatters.add(DateTimeFormatter.ISO_DATE_TIME);
+        formatters.add(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+        formatters.add(DateTimeFormatter.ISO_ZONED_DATE_TIME);
+    }
+
+    public static ZonedDateTime parseDate(String date) {
+        if (date != null && date.length() > 0) {
+            return formatters.stream()
+                    .map(
+                            f -> {
+                                try {
+                                    return ZonedDateTime.parse(date, f);
+                                } catch (DateTimeParseException dateTimeParseException) {
+                                    return null;
+                                }
+                            })
+                    .filter(Objects::nonNull)
+                    .findFirst()
+                    .orElseGet(
+                            () -> {
+                                try {
+                                    return LocalDateTime.parse(
+                                                    date, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
+                                            .atZone(ZoneOffset.UTC);
+                                } catch (DateTimeParseException dateTimeParseException) {
+                                    return null;
+                                }
+                            });
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/src/main/java/io/getunleash/strategy/constraints/NumberConstraintOperator.java b/src/main/java/io/getunleash/strategy/constraints/NumberConstraintOperator.java
new file mode 100644
index 00000000..bb703332
--- /dev/null
+++ b/src/main/java/io/getunleash/strategy/constraints/NumberConstraintOperator.java
@@ -0,0 +1,66 @@
+package io.getunleash.strategy.constraints;
+
+import io.getunleash.Constraint;
+import io.getunleash.Operator;
+import io.getunleash.UnleashContext;
+import java.util.Objects;
+
+public class NumberConstraintOperator implements ConstraintOperator {
+    @Override
+    public boolean evaluate(Constraint constraint, UnleashContext context) {
+        return context.getByName(constraint.getContextName())
+                .map(
+                        cVal -> {
+                            try {
+                                return Double.parseDouble(cVal);
+                            } catch (NumberFormatException nfe) {
+                                return null;
+                            }
+                        })
+                .map(
+                        cVal -> {
+                            try {
+                                if (constraint.getValues().size() > 0) {
+                                    return constraint.getValues().stream()
+                                            .map(
+                                                    v -> {
+                                                        try {
+                                                            return Double.parseDouble(v);
+                                                        } catch (NumberFormatException nfe) {
+                                                            return null;
+                                                        }
+                                                    })
+                                            .filter(Objects::nonNull)
+                                            .anyMatch(v -> eval(constraint.getOperator(), v, cVal));
+                                } else if (constraint.getValue() != null
+                                        && constraint.getValue().length() > 0) {
+                                    Double value = Double.parseDouble(constraint.getValue());
+                                    return eval(constraint.getOperator(), value, cVal);
+                                } else {
+                                    return null;
+                                }
+                            } catch (NumberFormatException nfe) {
+                                return null;
+                            }
+                        })
+                .orElse(false);
+    }
+
+    private boolean eval(Operator operator, Double value, Double contextValue) {
+
+        switch (operator) {
+            case NUM_LT:
+                return contextValue < value;
+            case NUM_LTE:
+                return contextValue <= value;
+            case NUM_EQ:
+                return contextValue.equals(value);
+            case NUM_GTE:
+                return contextValue >= value;
+            case NUM_GT:
+                return contextValue > value;
+            default:
+                return false;
+        }
+    }
+}
diff --git a/src/main/java/io/getunleash/strategy/constraints/SemanticVersion.java b/src/main/java/io/getunleash/strategy/constraints/SemanticVersion.java
new file mode 100644
index 00000000..e73101b1
--- /dev/null
+++ b/src/main/java/io/getunleash/strategy/constraints/SemanticVersion.java
@@ -0,0 +1,183 @@
+package io.getunleash.strategy.constraints;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Simple implementation of semantic version parsing and comparison according to the Semantic
+ * Versions 2.0.0 standard (http://semver.org).
+ */
+public class SemanticVersion implements Comparable<SemanticVersion> {
+    private static Pattern VERSION_REGEX =
+            Pattern.compile(
+                    "^(?<major>0|[1-9]\\d*)(\\.(?<minor>0|[1-9]\\d*))?(\\.(?<patch>0|[1-9]\\d*))?"
+                            + "(\\-(?<prerel>[0-9A-Za-z\\-\\.]+))?(\\+(?<build>[0-9A-Za-z\\-\\.]+))?$");
+
+    public static class InvalidVersionException extends Exception {
+        public InvalidVersionException(String message) {
+            super(message);
+        }
+    }
+
+    private final int major;
+    private final int minor;
+    private final int patch;
+    private final String preRelease;
+    private final String[] preReleaseComponents;
+    private final String build;
+
+    public SemanticVersion(int major, int minor, int patch, String preRelease, String build) {
+        this.major = major;
+        this.minor = minor;
+        this.patch = patch;
+        this.preRelease = preRelease;
+        this.preReleaseComponents = preRelease == null ? null : preRelease.split("\\.");
+        this.build = build;
+    }
+
+    public int getMajor() {
+        return major;
+    }
+
+    public int getMinor() {
+        return minor;
+    }
+
+    public int getPatch() {
+        return patch;
+    }
+
+    public String getPreRelease() {
+        return preRelease;
+    }
+
+    public String[] getPreReleaseComponents() {
+        return preReleaseComponents;
+    }
+
+    public String getBuild() {
+        return build;
+    }
+
+    /**
+     * Attempts to parse a string as a semantic version according to the Semver 2.0.0 specification.
+     *
+     * @param input the input string
+     * @return a SemanticVersion instance
+     * @throws InvalidVersionException if the version could not be parsed
+     */
+    public static SemanticVersion parse(String input) throws InvalidVersionException {
+        return parse(input, false);
+    }
+
+    /**
+     * Attempts to parse a string as a semantic version according to the Semver 2.0.0 specification,
+     * except that the minor and patch versions may optionally be omitted.
+     *
+     * @param input the input string
+     * @param allowMissingMinorAndPatch true if the parser should tolerate the absence of a minor
+     *     and/or patch version; if absent, they will be treated as zero
+     * @return a SemanticVersion instance
+     * @throws InvalidVersionException if the version could not be parsed
+     */
+    public static SemanticVersion parse(String input, boolean allowMissingMinorAndPatch)
+            throws InvalidVersionException {
+        Matcher matcher = VERSION_REGEX.matcher(input);
+        if (!matcher.matches()) {
+            throw new InvalidVersionException("Invalid semantic version");
+        }
+        int major, minor, patch;
+        try {
+            major = Integer.parseInt(matcher.group("major"));
+            if (!allowMissingMinorAndPatch) {
+                if (matcher.group("minor") == null || matcher.group("patch") == null) {
+                    throw new InvalidVersionException("Invalid semantic version");
+                }
+            }
+            minor = matcher.group("minor") == null ? 0 : Integer.parseInt(matcher.group("minor"));
+            patch = matcher.group("patch") == null ? 0 : Integer.parseInt(matcher.group("patch"));
+        } catch (NumberFormatException e) {
+            // COVERAGE: This should be impossible, because our regex should only match if these
+            // strings are numeric.
+            throw new InvalidVersionException("Invalid semantic version");
+        }
+        String prerelease = matcher.group("prerel");
+        String build = matcher.group("build");
+        return new SemanticVersion(major, minor, patch, prerelease, build);
+    }
+
+    @Override
+    public int compareTo(SemanticVersion other) {
+        return comparePrecedence(other);
+    }
+
+    /**
+     * Compares this object with another SemanticVersion according to Semver 2.0.0 precedence rules.
+     *
+     * @param other another SemanticVersion
+     * @return 0 if equal, -1 if the current object has lower precedence, or 1 if the current object
+     *     has higher precedence
+     */
+    public int comparePrecedence(SemanticVersion other) {
+        if (other == null) {
+            return 1;
+        }
+        if (major != other.major) {
+            return Integer.compare(major, other.major);
+        }
+        if (minor != other.minor) {
+            return Integer.compare(minor, other.minor);
+        }
+        if (patch != other.patch) {
+            return Integer.compare(patch, other.patch);
+        }
+        if (preRelease == null && other.preRelease == null) {
+            return 0;
+        }
+        // *no* prerelease component always has higher precedence than *any* prerelease component
+        if (preRelease == null) {
+            return 1;
+        }
+        if (other.preRelease == null) {
+            return -1;
+        }
+        return compareIdentifiers(preReleaseComponents, other.preReleaseComponents);
+    }
+
+    private int compareIdentifiers(String[] ids1, String[] ids2) {
+        for (int i = 0; ; i++) {
+            if (i >= ids1.length) {
+                // x.y is always less than x.y.z
+                return (i >= ids2.length) ? 0 : -1;
+            }
+            if (i >= ids2.length) {
+                return 1;
+            }
+            // each sub-identifier is compared numerically if both are numeric; if both are
+            // non-numeric,
+            // they're compared as strings; otherwise, the numeric one is the lesser one
+            int n1 = 0, n2 = 0, d;
+            boolean isNum1, isNum2;
+            try {
+                n1 = Integer.parseInt(ids1[i]);
+                isNum1 = true;
+            } catch (NumberFormatException e) {
+                isNum1 = false;
+            }
+            try {
+                n2 = Integer.parseInt(ids2[i]);
+                isNum2 = true;
+            } catch (NumberFormatException e) {
+                isNum2 = false;
+            }
+            if (isNum1 && isNum2) {
+                d = Integer.compare(n1, n2);
+            } else {
+                d = isNum1 ? -1 : (isNum2 ? 1 : ids1[i].compareTo(ids2[i]));
+            }
+            if (d != 0) {
+                return d;
+            }
+        }
+    }
+}
diff --git a/src/main/java/io/getunleash/strategy/constraints/SemverConstraintOperator.java b/src/main/java/io/getunleash/strategy/constraints/SemverConstraintOperator.java
new file mode 100644
index 00000000..6821047e
--- /dev/null
+++ b/src/main/java/io/getunleash/strategy/constraints/SemverConstraintOperator.java
@@ -0,0 +1,79 @@
+package io.getunleash.strategy.constraints;
+
+import io.getunleash.Constraint;
+import io.getunleash.Operator;
+import io.getunleash.UnleashContext;
+import java.util.Objects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SemverConstraintOperator implements ConstraintOperator {
+    private static final Logger LOGGER = LoggerFactory.getLogger(SemverConstraintOperator.class);
+
+    @Override
+    public boolean evaluate(Constraint constraint, UnleashContext context) {
+        return context.getByName(constraint.getContextName())
+                .map(
+                        contextValue -> {
+                            try {
+                                return SemanticVersion.parse(contextValue);
+                            } catch (
+                                    SemanticVersion.InvalidVersionException
+                                            invalidVersionException) {
+                                LOGGER.info(
+                                        "Couldn't parse version [{}] from context - This is dynamic on evaluation, might not be your fault",
+                                        contextValue);
+                                return null;
+                            }
+                        })
+                .map(
+                        contextVersion -> {
+                            try {
+                                if (constraint.getValues().size() > 0) {
+                                    return constraint.getValues().stream()
+                                            .map(
+                                                    v -> {
+                                                        try {
+                                                            return SemanticVersion.parse(v);
+                                                        } catch (
+                                                                SemanticVersion
+                                                                                .InvalidVersionException
+                                                                        e) {
+                                                            return null;
+                                                        }
+                                                    })
+                                            .filter(Objects::nonNull)
+                                            .anyMatch(
+                                                    v ->
+                                                            eval(
+                                                                    constraint.getOperator(),
+                                                                    v,
+                                                                    contextVersion));
+                                } else if (constraint.getValue() != null
+                                        && constraint.getValue().length() > 0) {
+                                    SemanticVersion value =
+                                            SemanticVersion.parse(constraint.getValue());
+                                    return eval(constraint.getOperator(), value, contextVersion);
+                                } else {
+                                    return null;
+                                }
+                            } catch (SemanticVersion.InvalidVersionException ive) {
+                                return null;
+                            }
+                        })
+                .orElse(false);
+    }
+
+    private boolean eval(Operator operator, SemanticVersion value, SemanticVersion contextVersion) {
+        switch (operator) {
+            case SEMVER_LT:
+                return contextVersion.compareTo(value) < 0;
+            case SEMVER_EQ:
+                return contextVersion.compareTo(value) == 0;
+            case SEMVER_GT:
+                return contextVersion.compareTo(value) > 0;
+            default:
+                return false;
+        }
+    }
+}
diff --git a/src/main/java/io/getunleash/strategy/constraints/StringConstraintOperator.java b/src/main/java/io/getunleash/strategy/constraints/StringConstraintOperator.java
new file mode 100644
index 00000000..5a300b42
--- /dev/null
+++ b/src/main/java/io/getunleash/strategy/constraints/StringConstraintOperator.java
@@ -0,0 +1,111 @@
+package io.getunleash.strategy.constraints;
+
+import io.getunleash.Constraint;
+import io.getunleash.UnleashContext;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+
+public class StringConstraintOperator implements ConstraintOperator {
+    private Locale comparisonLocale;
+
+    public StringConstraintOperator(Locale comparisonLocale) {
+        this.comparisonLocale = comparisonLocale;
+    }
+
+    @Override
+    public boolean evaluate(Constraint constraint, UnleashContext context) {
+        List<String> values = constraint.getValues();
+        Optional<String> contextValue = context.getByName(constraint.getContextName());
+        boolean caseInsensitive = constraint.isCaseInsensitive();
+        switch (constraint.getOperator()) {
+            case IN:
+                return isIn(values, contextValue, caseInsensitive);
+            case NOT_IN:
+                return !isIn(values, contextValue, caseInsensitive);
+            case STR_CONTAINS:
+                return contains(values, contextValue, caseInsensitive);
+            case STR_STARTS_WITH:
+                return startsWith(values, contextValue, caseInsensitive);
+            case STR_ENDS_WITH:
+                return endsWith(values, contextValue, caseInsensitive);
+            default:
+                return false;
+        }
+    }
+
+    private boolean endsWith(
+            List<String> values, Optional<String> contextValue, boolean caseInsensitive) {
+        return contextValue
+                .map(
+                        c ->
+                                values.stream()
+                                        .anyMatch(
+                                                v -> {
+                                                    if (caseInsensitive) {
+                                                        return c.toLowerCase(comparisonLocale)
+                                                                .endsWith(
+                                                                        v.toLowerCase(
+                                                                                comparisonLocale));
+                                                    } else {
+                                                        return c.endsWith(v);
+                                                    }
+                                                }))
+                .orElse(false);
+    }
+
+    private boolean startsWith(
+            List<String> values, Optional<String> contextValue, boolean caseInsensitive) {
+        return contextValue
+                .map(
+                        c ->
+                                values.stream()
+                                        .anyMatch(
+                                                v -> {
+                                                    if (caseInsensitive) {
+                                                        return v.toLowerCase(comparisonLocale)
+                                                                .startsWith(
+                                                                        c.toLowerCase(
+                                                                                comparisonLocale));
+                                                    } else {
+                                                        return c.startsWith(v);
+                                                    }
+                                                }))
+                .orElse(false);
+    }
+
+    private boolean contains(
+            List<String> values, Optional<String> contextValue, boolean caseInsensitive) {
+        return contextValue
+                .map(
+                        c ->
+                                values.stream()
+                                        .anyMatch(
+                                                v -> {
+                                                    if (caseInsensitive) {
+                                                        return c.toLowerCase(comparisonLocale)
+                                                                .contains(
+                                                                        v.toLowerCase(
+                                                                                comparisonLocale));
+                                                    } else {
+                                                        return c.contains(v);
+                                                    }
+                                                }))
+                .orElse(false);
+    }
+
+    private boolean isIn(List<String> values, Optional<String> value, boolean caseInsensitive) {
+        return value.map(
+                        v ->
+                                values.stream()
+                                        .anyMatch(
+                                                c -> {
+                                                    if (caseInsensitive) {
+                                                        return c.equalsIgnoreCase(v);
+                                                    } else {
+                                                        return c.equals(v);
+                                                    }
+                                                }))
+                .orElse(false);
+    }
+}


Test patch:
diff --git a/src/test/java/io/getunleash/RunOnJavaVersions.java b/src/test/java/io/getunleash/RunOnJavaVersions.java
new file mode 100644
index 00000000..a3000200
--- /dev/null
+++ b/src/test/java/io/getunleash/RunOnJavaVersions.java
@@ -0,0 +1,11 @@
+package io.getunleash;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@Retention(RetentionPolicy.RUNTIME)
+@ExtendWith(RunOnJavaVersionsCondition.class)
+public @interface RunOnJavaVersions {
+    String[] javaVersions();
+}
diff --git a/src/test/java/io/getunleash/RunOnJavaVersionsCondition.java b/src/test/java/io/getunleash/RunOnJavaVersionsCondition.java
new file mode 100644
index 00000000..2d240bab
--- /dev/null
+++ b/src/test/java/io/getunleash/RunOnJavaVersionsCondition.java
@@ -0,0 +1,40 @@
+package io.getunleash;
+
+import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation;
+
+import java.util.Arrays;
+import java.util.Optional;
+import org.junit.jupiter.api.extension.ConditionEvaluationResult;
+import org.junit.jupiter.api.extension.ExecutionCondition;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+public class RunOnJavaVersionsCondition implements ExecutionCondition {
+    @Override
+    public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext extensionContext) {
+        Optional<RunOnJavaVersions> annotation =
+                findAnnotation(extensionContext.getElement(), RunOnJavaVersions.class);
+        return annotation
+                .map(
+                        a -> {
+                            String runtimeVersion = getJavaVersion();
+                            if (runtimeVersion != null && !runtimeVersion.isEmpty()) {
+                                if (Arrays.stream(a.javaVersions())
+                                        .anyMatch(runtimeVersion::startsWith)) {
+                                    return ConditionEvaluationResult.enabled(
+                                            "Runtime java version is included");
+                                } else {
+                                    return ConditionEvaluationResult.disabled(
+                                            "Java version is not among the versions we run this test for");
+                                }
+                            } else {
+                                return ConditionEvaluationResult.enabled(
+                                        "Couldn't find a java version to compare for");
+                            }
+                        })
+                .orElse(ConditionEvaluationResult.enabled("No annotation found"));
+    }
+
+    private String getJavaVersion() {
+        return System.getProperty("java.version");
+    }
+}
diff --git a/src/test/java/io/getunleash/integration/ClientSpecificationTest.java b/src/test/java/io/getunleash/integration/ClientSpecificationTest.java
index bbed913a..8f21e39b 100644
--- a/src/test/java/io/getunleash/integration/ClientSpecificationTest.java
+++ b/src/test/java/io/getunleash/integration/ClientSpecificationTest.java
@@ -17,6 +17,7 @@ import io.getunleash.DefaultUnleash;
 import io.getunleash.Unleash;
 import io.getunleash.UnleashContext;
 import io.getunleash.Variant;
+import io.getunleash.strategy.constraints.DateParser;
 import io.getunleash.util.UnleashConfig;
 import java.io.BufferedReader;
 import java.io.File;
@@ -158,6 +159,7 @@ public class ClientSpecificationTest {
                         .sessionId(context.getSessionId())
                         .remoteAddress(context.getRemoteAddress())
                         .environment(context.getEnvironment())
+                        .currentTime(DateParser.parseDate(context.getCurrentTime()))
                         .appName(context.getAppName());
 
         if (context.getProperties() != null) {
diff --git a/src/test/java/io/getunleash/integration/UnleashContextDefinition.java b/src/test/java/io/getunleash/integration/UnleashContextDefinition.java
index 1b8ba4e9..7d1e06ca 100644
--- a/src/test/java/io/getunleash/integration/UnleashContextDefinition.java
+++ b/src/test/java/io/getunleash/integration/UnleashContextDefinition.java
@@ -8,6 +8,7 @@ public class UnleashContextDefinition {
     private final String remoteAddress;
     private final String environment;
     private final String appName;
+    private final String currentTime;
 
     // Custom context fields used in tests
     private final Map<String, String> properties;
@@ -18,12 +19,14 @@ public class UnleashContextDefinition {
             String remoteAddress,
             String environment,
             String appName,
+            String currentTime,
             Map<String, String> properties) {
         this.userId = userId;
         this.sessionId = sessionId;
         this.remoteAddress = remoteAddress;
         this.environment = environment;
         this.appName = appName;
+        this.currentTime = currentTime;
         this.properties = properties;
     }
 
@@ -47,6 +50,10 @@ public class UnleashContextDefinition {
         return appName;
     }
 
+    public String getCurrentTime() {
+        return currentTime;
+    }
+
     public Map<String, String> getProperties() {
         return properties;
     }
diff --git a/src/test/java/io/getunleash/strategy/constraints/DateConstraintOperatorTest.java b/src/test/java/io/getunleash/strategy/constraints/DateConstraintOperatorTest.java
new file mode 100644
index 00000000..5349a3ea
--- /dev/null
+++ b/src/test/java/io/getunleash/strategy/constraints/DateConstraintOperatorTest.java
@@ -0,0 +1,171 @@
+package io.getunleash.strategy.constraints;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.getunleash.Constraint;
+import io.getunleash.Operator;
+import io.getunleash.UnleashContext;
+import io.getunleash.strategy.DefaultStrategy;
+import io.getunleash.strategy.Strategy;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class DateConstraintOperatorTest {
+    DateTimeFormatter ISO = DateTimeFormatter.ISO_INSTANT;
+
+    @Test
+    public void shouldSupportAfterDate() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "releaseDate",
+                                Operator.DATE_AFTER,
+                                ZonedDateTime.now().minusHours(1).format(ISO)));
+        Map<String, String> parameters = new HashMap<>();
+        // If there's not a date in the defined context field, compare with Instant.now()
+        UnleashContext enabled = UnleashContext.builder().environment("dev").build();
+        assertThat(strategy.isEnabled(parameters, enabled, constraintList)).isTrue();
+        // If there's a date in the defined context field, compare with that
+        UnleashContext equal =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("releaseDate", ZonedDateTime.now().format(ISO))
+                        .build();
+        assertThat(strategy.isEnabled(parameters, equal, constraintList)).isTrue();
+
+        UnleashContext disabled =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("releaseDate", ZonedDateTime.now().minusHours(2).format(ISO))
+                        .build();
+        assertThat(strategy.isEnabled(parameters, disabled, constraintList)).isFalse();
+    }
+
+    @Test
+    public void afterDateSupportsInversion() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "releaseDate",
+                                Operator.DATE_AFTER,
+                                ZonedDateTime.now().minusHours(1).format(ISO),
+                                true));
+        Map<String, String> parameters = new HashMap<>();
+        // If there's not a date in the defined context field, compare with Instant.now()
+        UnleashContext enabled = UnleashContext.builder().environment("dev").build();
+        assertThat(strategy.isEnabled(parameters, enabled, constraintList)).isFalse();
+        // If there's a date in the defined context field, compare with that
+        UnleashContext equal =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("releaseDate", ZonedDateTime.now().format(ISO))
+                        .build();
+        assertThat(strategy.isEnabled(parameters, equal, constraintList)).isFalse();
+
+        UnleashContext disabled =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("releaseDate", ZonedDateTime.now().minusHours(2).format(ISO))
+                        .build();
+        assertThat(strategy.isEnabled(parameters, disabled, constraintList)).isTrue();
+    }
+
+    @Test
+    public void shouldSupportBeforeDate() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "releaseDate",
+                                Operator.DATE_BEFORE,
+                                ZonedDateTime.now().plusDays(2).format(ISO)));
+        Map<String, String> parameters = new HashMap<>();
+        // If there's not a date in the defined context field, compare with Instant.now()
+        UnleashContext now = UnleashContext.builder().environment("dev").build();
+        assertThat(strategy.isEnabled(parameters, now, constraintList)).isTrue();
+        // If there's a date in the defined context field, compare with that
+        UnleashContext equal =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("releaseDate", ZonedDateTime.now().plusDays(1).format(ISO))
+                        .build();
+        assertThat(strategy.isEnabled(parameters, equal, constraintList)).isTrue();
+
+        UnleashContext fourDaysFromNow =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("releaseDate", ZonedDateTime.now().plusDays(4).format(ISO))
+                        .build();
+        assertThat(strategy.isEnabled(parameters, fourDaysFromNow, constraintList)).isFalse();
+    }
+
+    @Test
+    public void beforeDateShouldSupportInversion() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "releaseDate",
+                                Operator.DATE_BEFORE,
+                                ZonedDateTime.now().plusDays(2).format(ISO),
+                                Collections.emptyList(),
+                                true,
+                                false));
+        Map<String, String> parameters = new HashMap<>();
+        // If there's not a date in the defined context field, compare with Instant.now()
+        UnleashContext now = UnleashContext.builder().environment("dev").build();
+        assertThat(strategy.isEnabled(parameters, now, constraintList)).isFalse();
+        // If there's a date in the defined context field, compare with that
+        UnleashContext equal =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("releaseDate", ZonedDateTime.now().plusDays(1).format(ISO))
+                        .build();
+        assertThat(strategy.isEnabled(parameters, equal, constraintList)).isFalse();
+
+        UnleashContext fourDaysFromNow =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("releaseDate", ZonedDateTime.now().plusDays(4).format(ISO))
+                        .build();
+        assertThat(strategy.isEnabled(parameters, fourDaysFromNow, constraintList)).isTrue();
+    }
+
+    @Test
+    public void dateParserSupportsNotPassingInZone() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint("releaseDate", Operator.DATE_BEFORE, "2022-11-05T11:05:00"));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext equal =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("releaseDate", "2022-11-04T11:05:00")
+                        .build();
+        assertThat(strategy.isEnabled(parameters, equal, constraintList)).isTrue();
+    }
+
+    @Test
+    public void dateBefore() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> c =
+                Collections.singletonList(
+                        new Constraint(
+                                "currentTime", Operator.DATE_BEFORE, "2022-01-29T13:00:00.000Z"));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext context =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("currentTime", "2022-01-30T13:00:00.000Z")
+                        .build();
+        assertThat(strategy.isEnabled(parameters, context, c)).isFalse();
+    }
+}
diff --git a/src/test/java/io/getunleash/strategy/constraints/NumberConstraintOperatorTest.java b/src/test/java/io/getunleash/strategy/constraints/NumberConstraintOperatorTest.java
new file mode 100644
index 00000000..19e948cf
--- /dev/null
+++ b/src/test/java/io/getunleash/strategy/constraints/NumberConstraintOperatorTest.java
@@ -0,0 +1,198 @@
+package io.getunleash.strategy.constraints;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.getunleash.Constraint;
+import io.getunleash.Operator;
+import io.getunleash.UnleashContext;
+import io.getunleash.strategy.DefaultStrategy;
+import io.getunleash.strategy.Strategy;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class NumberConstraintOperatorTest {
+
+    @Test
+    public void shouldSupportEqForNumbers() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "userAge", Operator.NUM_EQ, Collections.singletonList("42")));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext ctx =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "42").build();
+        assertThat(strategy.isEnabled(parameters, ctx, constraintList)).isTrue();
+    }
+
+    @Test
+    public void shouldSupportInvertingNumberEq() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "userAge", Operator.NUM_EQ, Collections.singletonList("42"), true));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext ctx =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "42").build();
+        assertThat(strategy.isEnabled(parameters, ctx, constraintList)).isFalse();
+    }
+
+    @Test
+    public void shouldSupportGreaterThanComparisonForNumbers() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "userAge", Operator.NUM_GT, Collections.singletonList("42")));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext enabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "45").build();
+        assertThat(strategy.isEnabled(parameters, enabled, constraintList)).isTrue();
+        UnleashContext disabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "35").build();
+        assertThat(strategy.isEnabled(parameters, disabled, constraintList)).isFalse();
+    }
+
+    @Test
+    public void shouldSupportInvertingGreaterThanComparisonForNumbers() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "userAge", Operator.NUM_GT, Collections.singletonList("42"), true));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext disabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "45").build();
+        assertThat(strategy.isEnabled(parameters, disabled, constraintList)).isFalse();
+        UnleashContext enabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "35").build();
+        assertThat(strategy.isEnabled(parameters, enabled, constraintList)).isTrue();
+    }
+
+    @Test
+    public void shouldSupportLesserThanForNumbers() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "userAge",
+                                Operator.NUM_LT,
+                                Collections.singletonList("42"),
+                                false));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext enabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "35").build();
+        assertThat(strategy.isEnabled(parameters, enabled, constraintList)).isTrue();
+        UnleashContext disabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "45").build();
+        assertThat(strategy.isEnabled(parameters, disabled, constraintList)).isFalse();
+    }
+
+    @Test
+    public void shouldSupportInvertingLesserThanForNumbers() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "userAge", Operator.NUM_LT, Collections.singletonList("42"), true));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext disabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "35").build();
+        UnleashContext enabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "45").build();
+        assertThat(strategy.isEnabled(parameters, enabled, constraintList)).isTrue();
+        assertThat(strategy.isEnabled(parameters, disabled, constraintList)).isFalse();
+    }
+
+    @Test
+    public void shouldSupportLessThanOrEqualForNumbers() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "userAge",
+                                Operator.NUM_LTE,
+                                Collections.singletonList("42"),
+                                false));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext enabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "35").build();
+        assertThat(strategy.isEnabled(parameters, enabled, constraintList)).isTrue();
+        UnleashContext equal =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "42").build();
+        assertThat(strategy.isEnabled(parameters, equal, constraintList)).isTrue();
+        UnleashContext disabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "45").build();
+        assertThat(strategy.isEnabled(parameters, disabled, constraintList)).isFalse();
+    }
+
+    @Test
+    public void shouldSupportInvertingLessThanOrEqualForNumbers() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "userAge",
+                                Operator.NUM_LTE,
+                                Collections.singletonList("42"),
+                                true));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext enabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "35").build();
+        assertThat(strategy.isEnabled(parameters, enabled, constraintList)).isFalse();
+        UnleashContext equal =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "42").build();
+        assertThat(strategy.isEnabled(parameters, equal, constraintList)).isFalse();
+        UnleashContext disabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "45").build();
+        assertThat(strategy.isEnabled(parameters, disabled, constraintList)).isTrue();
+    }
+
+    @Test
+    public void shouldSupportGreaterThanOrEqualForNumbers() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "userAge",
+                                Operator.NUM_GTE,
+                                Collections.singletonList("42"),
+                                false));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext enabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "45").build();
+        assertThat(strategy.isEnabled(parameters, enabled, constraintList)).isTrue();
+        UnleashContext equal =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "42").build();
+        assertThat(strategy.isEnabled(parameters, equal, constraintList)).isTrue();
+        UnleashContext disabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "35").build();
+        assertThat(strategy.isEnabled(parameters, disabled, constraintList)).isFalse();
+    }
+
+    @Test
+    public void shouldSupportInvertingGreaterThanOrEqualForNumbers() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "userAge",
+                                Operator.NUM_GTE,
+                                Collections.singletonList("42"),
+                                true));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext enabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "45").build();
+        assertThat(strategy.isEnabled(parameters, enabled, constraintList)).isFalse();
+        UnleashContext equal =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "42").build();
+        assertThat(strategy.isEnabled(parameters, equal, constraintList)).isFalse();
+        UnleashContext disabled =
+                UnleashContext.builder().environment("dev").addProperty("userAge", "35").build();
+        assertThat(strategy.isEnabled(parameters, disabled, constraintList)).isTrue();
+    }
+}
diff --git a/src/test/java/io/getunleash/strategy/constraints/SemanticVersionTest.java b/src/test/java/io/getunleash/strategy/constraints/SemanticVersionTest.java
new file mode 100644
index 00000000..8904e91c
--- /dev/null
+++ b/src/test/java/io/getunleash/strategy/constraints/SemanticVersionTest.java
@@ -0,0 +1,48 @@
+package io.getunleash.strategy.constraints;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+class SemanticVersionTest {
+
+    @Test
+    public void obeysSgnRequirementForComparator() throws SemanticVersion.InvalidVersionException {
+        SemanticVersion a = SemanticVersion.parse("1.0.0");
+        SemanticVersion b = SemanticVersion.parse("1.0.1");
+        assertThat(a).isLessThan(b);
+        assertThat(b).isGreaterThan(a);
+    }
+
+    @Test
+    public void isTransitive() throws SemanticVersion.InvalidVersionException {
+        SemanticVersion a = SemanticVersion.parse("1.0.0");
+        SemanticVersion b = SemanticVersion.parse("1.0.1");
+        SemanticVersion c = SemanticVersion.parse("1.1.0");
+        assertThat(a).isLessThan(b);
+        assertThat(b).isLessThan(c);
+        assertThat(a).isLessThan(c);
+    }
+
+    @Test
+    public void rcIsGreaterThanAlphaAndBetaButSmallerThanRelease()
+            throws SemanticVersion.InvalidVersionException {
+        SemanticVersion rc = SemanticVersion.parse("1.0.0-rc");
+        SemanticVersion alpha = SemanticVersion.parse("1.0.0-alpha.1");
+        SemanticVersion beta = SemanticVersion.parse("1.0.0-beta.1");
+        SemanticVersion release = SemanticVersion.parse("1.0.0");
+
+        assertThat(rc).isGreaterThan(alpha);
+        assertThat(rc).isGreaterThan(beta);
+        assertThat(rc).isLessThan(release);
+    }
+
+    @Test
+    public void alphaVersionsAlsoCounted() throws SemanticVersion.InvalidVersionException {
+        SemanticVersion alpha = SemanticVersion.parse("1.0.0-alpha.1");
+        SemanticVersion alpha2 = SemanticVersion.parse("1.0.0-alpha.2");
+        assertThat(alpha.compareTo(alpha2)).isLessThan(0);
+        assertThat(alpha).isLessThan(alpha2);
+    }
+}
diff --git a/src/test/java/io/getunleash/strategy/constraints/SemverConstraintOperatorTest.java b/src/test/java/io/getunleash/strategy/constraints/SemverConstraintOperatorTest.java
new file mode 100644
index 00000000..4b80173c
--- /dev/null
+++ b/src/test/java/io/getunleash/strategy/constraints/SemverConstraintOperatorTest.java
@@ -0,0 +1,147 @@
+package io.getunleash.strategy.constraints;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.getunleash.Constraint;
+import io.getunleash.Operator;
+import io.getunleash.UnleashContext;
+import io.getunleash.strategy.DefaultStrategy;
+import io.getunleash.strategy.Strategy;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class SemverConstraintOperatorTest {
+    @Test
+    public void shouldSupportSemverEq() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "version", Operator.SEMVER_EQ, Collections.singletonList("1.2.4")));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext correctVersion =
+                UnleashContext.builder().environment("dev").addProperty("version", "1.2.4").build();
+        assertThat(strategy.isEnabled(parameters, correctVersion, constraintList)).isTrue();
+        UnleashContext wrongVersion =
+                UnleashContext.builder().environment("dev").addProperty("version", "2.2.4").build();
+        assertThat(strategy.isEnabled(parameters, wrongVersion, constraintList)).isFalse();
+    }
+
+    @Test
+    public void shouldSupportInvertingSemverEq() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "version",
+                                Operator.SEMVER_EQ,
+                                Collections.singletonList("1.2.4"),
+                                true));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext correctVersion =
+                UnleashContext.builder().environment("dev").addProperty("version", "1.2.4").build();
+        assertThat(strategy.isEnabled(parameters, correctVersion, constraintList)).isFalse();
+        UnleashContext wrongVersion =
+                UnleashContext.builder().environment("dev").addProperty("version", "2.2.4").build();
+        assertThat(strategy.isEnabled(parameters, wrongVersion, constraintList)).isTrue();
+    }
+
+    @Test
+    public void shouldSupportSemverLt() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "version", Operator.SEMVER_LT, Collections.singletonList("2.0.0")));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext correctVersion =
+                UnleashContext.builder().environment("dev").addProperty("version", "1.2.4").build();
+        assertThat(strategy.isEnabled(parameters, correctVersion, constraintList)).isTrue();
+        UnleashContext wrongVersion =
+                UnleashContext.builder().environment("dev").addProperty("version", "2.2.4").build();
+        assertThat(strategy.isEnabled(parameters, wrongVersion, constraintList)).isFalse();
+    }
+
+    @Test
+    public void shouldSupportInvertingSemverLt() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "version",
+                                Operator.SEMVER_LT,
+                                Collections.singletonList("2.0.0"),
+                                true));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext correctVersion =
+                UnleashContext.builder().environment("dev").addProperty("version", "1.2.4").build();
+        assertThat(strategy.isEnabled(parameters, correctVersion, constraintList)).isFalse();
+        UnleashContext wrongVersion =
+                UnleashContext.builder().environment("dev").addProperty("version", "2.2.4").build();
+        assertThat(strategy.isEnabled(parameters, wrongVersion, constraintList)).isTrue();
+    }
+
+    @Test
+    public void shouldSupportSemverGt() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "version", Operator.SEMVER_GT, Collections.singletonList("2.0.0")));
+        Map<String, String> parameters = new HashMap<>();
+        // If there's not a date in the defined context field, compare with Instant.now()
+        UnleashContext newerVersion =
+                UnleashContext.builder().environment("dev").addProperty("version", "2.2.4").build();
+        assertThat(strategy.isEnabled(parameters, newerVersion, constraintList)).isTrue();
+        UnleashContext olderVersion =
+                UnleashContext.builder().environment("dev").addProperty("version", "1.2.4").build();
+        assertThat(strategy.isEnabled(parameters, olderVersion, constraintList)).isFalse();
+    }
+
+    @Test
+    public void shouldSupportInvertingSemverGt() {
+
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "version",
+                                Operator.SEMVER_GT,
+                                Collections.singletonList("2.0.0"),
+                                true));
+        Map<String, String> parameters = new HashMap<>();
+        // If there's not a date in the defined context field, compare with Instant.now()
+        UnleashContext newerVersion =
+                UnleashContext.builder().environment("dev").addProperty("version", "2.2.4").build();
+        assertThat(strategy.isEnabled(parameters, newerVersion, constraintList)).isFalse();
+        UnleashContext olderVersion =
+                UnleashContext.builder().environment("dev").addProperty("version", "1.2.4").build();
+        assertThat(strategy.isEnabled(parameters, olderVersion, constraintList)).isTrue();
+    }
+
+    @Test
+    public void shouldSupportSemverInRange() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Arrays.asList(
+                        new Constraint(
+                                "version", Operator.SEMVER_GT, Collections.singletonList("2.0.0")),
+                        new Constraint(
+                                "version", Operator.SEMVER_LT, Collections.singletonList("3.0.0")));
+        Map<String, String> parameters = new HashMap<>();
+        // If there's not a date in the defined context field, compare with Instant.now()
+        UnleashContext inRange =
+                UnleashContext.builder().environment("dev").addProperty("version", "2.2.4").build();
+        assertThat(strategy.isEnabled(parameters, inRange, constraintList)).isTrue();
+        UnleashContext lessThanRange =
+                UnleashContext.builder().environment("dev").addProperty("version", "1.2.4").build();
+        assertThat(strategy.isEnabled(parameters, lessThanRange, constraintList)).isFalse();
+        UnleashContext moreThanRange =
+                UnleashContext.builder().environment("dev").addProperty("version", "3.2.4").build();
+        assertThat(strategy.isEnabled(parameters, moreThanRange, constraintList)).isFalse();
+    }
+}
diff --git a/src/test/java/io/getunleash/strategy/constraints/StringConstraintOperatorTest.java b/src/test/java/io/getunleash/strategy/constraints/StringConstraintOperatorTest.java
new file mode 100644
index 00000000..34234ee8
--- /dev/null
+++ b/src/test/java/io/getunleash/strategy/constraints/StringConstraintOperatorTest.java
@@ -0,0 +1,214 @@
+package io.getunleash.strategy.constraints;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.getunleash.Constraint;
+import io.getunleash.Operator;
+import io.getunleash.UnleashContext;
+import io.getunleash.strategy.DefaultStrategy;
+import io.getunleash.strategy.Strategy;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class StringConstraintOperatorTest {
+    @Test
+    public void shouldBeEnabledWhenEmailStartsWith() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "email",
+                                Operator.STR_STARTS_WITH,
+                                Collections.singletonList("example")));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext ctx =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("email", "example@getunleash.ai")
+                        .build();
+        assertThat(strategy.isEnabled(new HashMap<>(), ctx, constraintList)).isTrue();
+    }
+
+    @Test
+    public void shouldBeEnabledWhenEmailStartsWithMultiple() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "email",
+                                Operator.STR_STARTS_WITH,
+                                Arrays.asList("other", "example")));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext ctx =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("email", "example@getunleash.ai")
+                        .build();
+        assertThat(strategy.isEnabled(parameters, ctx, constraintList)).isTrue();
+    }
+
+    @Test
+    public void shouldBeEnabledWhenEmailEndsWith() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "email",
+                                Operator.STR_ENDS_WITH,
+                                Collections.singletonList("@getunleash.ai")));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext ctx =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("email", "example@getunleash.ai")
+                        .build();
+        assertThat(strategy.isEnabled(parameters, ctx, constraintList)).isTrue();
+    }
+
+    @Test
+    public void shouldBeEnabledWhenEmailEndsWithIgnoringCase() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "email",
+                                Operator.STR_ENDS_WITH,
+                                Collections.singletonList("@getunleash.ai"),
+                                false,
+                                true));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext ctx =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("email", "example@GETunleash.ai")
+                        .build();
+        assertThat(strategy.isEnabled(parameters, ctx, constraintList)).isTrue();
+    }
+
+    @Test
+    public void shouldNotBeEnabledWhenEmailEndsWithCaseSensitive() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "email",
+                                Operator.STR_ENDS_WITH,
+                                Collections.singletonList("@getunleash.ai")));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext ctx =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("email", "example@GETunleash.ai")
+                        .build();
+        assertThat(strategy.isEnabled(parameters, ctx, constraintList)).isFalse();
+    }
+
+    // Testing inversion of evaluation result
+    @Test
+    public void shouldBeEnabledWhenEmailDoesNotEndWith() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "email",
+                                Operator.STR_ENDS_WITH,
+                                Collections.singletonList("@getunleash.ai"),
+                                true,
+                                false));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext ctx =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("email", "example@something.ai")
+                        .build();
+        assertThat(strategy.isEnabled(parameters, ctx, constraintList)).isTrue();
+    }
+
+    @Test
+    public void shouldBeEnabledWhenEmailEndsWithMultipleEmails() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "email",
+                                Operator.STR_ENDS_WITH,
+                                Arrays.asList("@getunleash.ai", "somerandom-email.com")));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext enabled =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("email", "example@getunleash.ai")
+                        .build();
+        assertThat(strategy.isEnabled(parameters, enabled, constraintList)).isTrue();
+        UnleashContext enabled2 =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("email", "example@somerandom-email.com")
+                        .build();
+        assertThat(strategy.isEnabled(parameters, enabled2, constraintList)).isTrue();
+        UnleashContext disabled =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("email", "example@some-email.com")
+                        .build();
+
+        assertThat(strategy.isEnabled(parameters, disabled, constraintList)).isFalse();
+    }
+
+    @Test
+    public void shouldNotBeDisabledWhenEmailDoesNotEndWith() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "email",
+                                Operator.STR_ENDS_WITH,
+                                Collections.singletonList("@getunleash.ai")));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext ctx =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("email", "example@something-else")
+                        .build();
+        assertThat(strategy.isEnabled(parameters, ctx, constraintList)).isFalse();
+    }
+
+    @Test
+    public void shouldBeEnabledWhenEmailContains() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "email", Operator.STR_CONTAINS, Collections.singletonList("some")));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext ctx =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("email", "example-some@getunleash.ai")
+                        .build();
+        assertThat(strategy.isEnabled(parameters, ctx, constraintList)).isTrue();
+    }
+
+    @Test
+    public void shouldSupportInvertingStringContains() {
+        Strategy strategy = new DefaultStrategy();
+        List<Constraint> constraintList =
+                Collections.singletonList(
+                        new Constraint(
+                                "email",
+                                Operator.STR_CONTAINS,
+                                Collections.singletonList("ample"),
+                                true));
+        Map<String, String> parameters = new HashMap<>();
+        UnleashContext ctx =
+                UnleashContext.builder()
+                        .environment("dev")
+                        .addProperty("email", "example@GETunleash.ai")
+                        .build();
+        assertThat(strategy.isEnabled(parameters, ctx, constraintList)).isFalse();
+    }
+}
diff --git a/src/test/java/io/getunleash/util/UnleashConfigTest.java b/src/test/java/io/getunleash/util/UnleashConfigTest.java
index ad950daa..387d8cea 100644
--- a/src/test/java/io/getunleash/util/UnleashConfigTest.java
+++ b/src/test/java/io/getunleash/util/UnleashConfigTest.java
@@ -8,6 +8,7 @@ import static org.mockito.Mockito.when;
 
 import io.getunleash.CustomHttpHeadersProvider;
 import io.getunleash.DefaultCustomHttpHeadersProviderImpl;
+import io.getunleash.RunOnJavaVersions;
 import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.Field;
@@ -199,6 +200,7 @@ public class UnleashConfigTest {
     }
 
     @Test
+    @RunOnJavaVersions(javaVersions = {"1.8", "11"})
     public void should_enable_proxy_based_on_jvm_settings()
             throws IllegalAccessException, NoSuchFieldException {
 
@@ -259,6 +261,7 @@ public class UnleashConfigTest {
     }
 
     @Test
+    @RunOnJavaVersions(javaVersions = {"1.8", "11"})
     public void should_use_specified_proxy() throws IllegalAccessException, NoSuchFieldException {
 
         String proxyHost = "proxy-host";
@@ -277,7 +280,6 @@ public class UnleashConfigTest {
                                 proxyUser,
                                 proxyPassword)
                         .build();
-
         Field authenticator = Authenticator.class.getDeclaredField("theAuthenticator");
         authenticator.setAccessible(true);
         UnleashConfig.CustomProxyAuthenticator proxyAuthenticator =