aboutsummaryrefslogtreecommitdiffstats
path: root/flags/src/test
diff options
context:
space:
mode:
authorHåkon Hallingstad <hakon@oath.com>2018-12-30 20:10:16 +0100
committerHåkon Hallingstad <hakon@oath.com>2018-12-30 20:10:16 +0100
commitaf82f15b8ec3a7c19d1b9ba48b53edf9feb6de48 (patch)
treeae2e16385bea0218a7bb3232cff0d6ea0104c528 /flags/src/test
parent4e810c250f3013982a3fc935de9f083eacef1d7c (diff)
Configserver flags REST API
Adds a new ZooKeeper backed flag source. It is defined in a new module configserver-flags to allow as many as possible config server modules to depend on it by minimizing dependencies. The content of the ZK backed flag source can be viewed and modified through REST API on the config server/controller. The data stored per flag looks like { "rules": [ { "conditions": [ { "type": "whitelist", "dimension": "hostname", "values": ["host1"] } ], "value": true } ] } typical for enabling a feature flag on host1. 2 types of conditions are so far supported: whitelist and blacklist. All the conditions must match in order for the value to apply. If the value is null (or absent), the default value will be used. At the time the flag's value is retrieved, it is resolved against the conditions with the current zone, hostname, and/or application. The same data structure is used for FileFlagSource for files in /etc/vespa/flags with the ".2" extension. The FlagSource component injected in the config server is changed to: 1. Return the flag value if specified in /etc/vespa/flags, or otherwise 2. return flag value from ZooKeeper (same as REST API) The current flags (module) is also changed: - All flags must be defined in com.yahoo.vespa.flags.Flags. This allows the ZK backed flag source additional sanity checking when modifying flags. - If it makes sense to have different flag value depending on e.g. the application, then at some point before the value is retrieved, one has to bind the flag to that application (using with() to set up the fetch vector). Future changes would be to 0. make a merged FlagSource in host admin, 1. add support for viewing and modifying feature flags in dashboard, 2. in hv tool.
Diffstat (limited to 'flags/src/test')
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java47
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java131
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java66
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/OrderedFlagSourceTest.java50
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java38
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/json/FlagDataTest.java82
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java130
7 files changed, 462 insertions, 82 deletions
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java b/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java
index 9e7508706ed..7d9d7868308 100644
--- a/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java
+++ b/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java
@@ -5,9 +5,11 @@ import com.yahoo.vespa.test.file.TestFileSystem;
import org.junit.Test;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Optional;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
@@ -22,18 +24,18 @@ public class FileFlagSourceTest {
@Test
public void testFeatureLikeFlags() throws IOException {
- FeatureFlag featureFlag = new FeatureFlag(id, false, source);
- FeatureFlag byDefaultTrue = new FeatureFlag(id, true, source);
+ Flag<Boolean> featureFlag = new Flag<>(id, false, source, Flags.BOOLEAN_SERIALIZER);
+ Flag<Boolean> byDefaultTrue = new Flag<>(id, true, source, Flags.BOOLEAN_SERIALIZER);
assertFalse(featureFlag.value());
assertTrue(byDefaultTrue.value());
- writeFlag(id.toString(), "True\n");
+ writeFlag(id.toString(), "true\n");
assertTrue(featureFlag.value());
assertTrue(byDefaultTrue.value());
- writeFlag(id.toString(), "False\n");
+ writeFlag(id.toString(), "false\n");
assertFalse(featureFlag.value());
assertFalse(byDefaultTrue.value());
@@ -41,35 +43,48 @@ public class FileFlagSourceTest {
@Test
public void testIntegerLikeFlags() throws IOException {
- StringFlag stringFlag = new StringFlag(id, "default", source);
- IntFlag intFlag = new IntFlag(id, -1, source);
- LongFlag longFlag = new LongFlag(id, -2L, source);
+ Flag<Integer> intFlag = new Flag<>(id, -1, source, Flags.INT_SERIALIZER);
+ Flag<Long> longFlag = new Flag<>(id, -2L, source, Flags.LONG_SERIALIZER);
- assertFalse(source.getString(id).isPresent());
- assertEquals("default", stringFlag.value());
- assertEquals(-1, intFlag.value());
- assertEquals(-2L, longFlag.value());
+ assertFalse(fetch().isPresent());
+ assertFalse(fetch().isPresent());
+ assertEquals(-1, (int) intFlag.value());
+ assertEquals(-2L, (long) longFlag.value());
writeFlag(id.toString(), "1\n");
- assertTrue(source.getString(id).isPresent());
+ assertTrue(fetch().isPresent());
+ assertTrue(fetch().isPresent());
+ assertEquals(1, (int) intFlag.value());
+ assertEquals(1L, (long) longFlag.value());
+ }
+
+ @Test
+ public void testStringFlag() throws IOException {
+ Flag<String> stringFlag = new Flag<>(id, "default", source, Flags.STRING_SERIALIZER);
+ assertFalse(fetch().isPresent());
+ assertEquals("default", stringFlag.value());
+
+ writeFlag(id.toString(), "\"1\\n\"\n");
assertEquals("1\n", stringFlag.value());
- assertEquals(1, intFlag.value());
- assertEquals(1L, longFlag.value());
}
@Test
public void parseFailure() throws IOException {
- FeatureFlag featureFlag = new FeatureFlag(id, false, source);
+ Flag<Boolean> featureFlag = new Flag<>(id, false, source, Flags.BOOLEAN_SERIALIZER);
writeFlag(featureFlag.id().toString(), "garbage");
try {
featureFlag.value();
- } catch (IllegalArgumentException e) {
+ } catch (UncheckedIOException e) {
assertThat(e.getMessage(), containsString("garbage"));
}
}
+ private Optional<RawFlag> fetch() {
+ return source.fetch(id, new FetchVector());
+ }
+
private void writeFlag(String flagId, String value) throws IOException {
Path featurePath = fileSystem.getPath(FileFlagSource.FLAGS_DIRECTORY).resolve(flagId);
Files.createDirectories(featurePath.getParent());
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java b/flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java
new file mode 100644
index 00000000000..4f7d797e07d
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java
@@ -0,0 +1,131 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.node.BooleanNode;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.emptyOrNullString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author hakonhall
+ */
+public class FlagsTest {
+ @Test
+ public void testBoolean() {
+ final boolean defaultValue = false;
+ FlagSource source = mock(FlagSource.class);
+ Flag<Boolean> booleanFlag = Flags.defineBoolean("id", defaultValue, "description",
+ "modification effect", FetchVector.Dimension.ZONE_ID, FetchVector.Dimension.HOSTNAME)
+ .with(FetchVector.Dimension.ZONE_ID, "a-zone")
+ .bindTo(source);
+ assertThat(booleanFlag.id().toString(), equalTo("id"));
+
+ when(source.fetch(eq(new FlagId("id")), any())).thenReturn(Optional.empty());
+ // default value without raw flag
+ assertThat(booleanFlag.value(), equalTo(defaultValue));
+
+ ArgumentCaptor<FetchVector> vector = ArgumentCaptor.forClass(FetchVector.class);
+ verify(source).fetch(any(), vector.capture());
+ // hostname is set by default
+ assertThat(vector.getValue().getValue(FetchVector.Dimension.HOSTNAME).isPresent(), is(true));
+ assertThat(vector.getValue().getValue(FetchVector.Dimension.HOSTNAME).get(), is(not(emptyOrNullString())));
+ // zone is set because it was set on the unbound flag above
+ assertThat(vector.getValue().getValue(FetchVector.Dimension.ZONE_ID), is(Optional.of("a-zone")));
+ // application is not set
+ assertThat(vector.getValue().getValue(FetchVector.Dimension.APPLICATION_ID), is(Optional.empty()));
+
+ RawFlag rawFlag = mock(RawFlag.class);
+ when(source.fetch(eq(new FlagId("id")), any())).thenReturn(Optional.of(rawFlag));
+ when(rawFlag.asJsonNode()).thenReturn(BooleanNode.getTrue());
+
+ // raw flag deserializes to true
+ assertThat(booleanFlag.with(FetchVector.Dimension.APPLICATION_ID, "an-app").value(), equalTo(true));
+
+ verify(source, times(2)).fetch(any(), vector.capture());
+ // application was set on the (bound) flag.
+ assertThat(vector.getValue().getValue(FetchVector.Dimension.APPLICATION_ID), is(Optional.of("an-app")));
+ }
+
+ @Test
+ public void testString() {
+ testGeneric(Flags.defineString("string-id", "default value", "description",
+ "modification effect", FetchVector.Dimension.ZONE_ID, FetchVector.Dimension.HOSTNAME),
+ "default value", "other value");
+ }
+
+ @Test
+ public void testInt() {
+ testGeneric(Flags.defineInt("int-id", 2, "desc", "mod"), 2, 3);
+ }
+
+ @Test
+ public void testLong() {
+ testGeneric(Flags.defineLong("long-id", 1L, "desc", "mod"), 1L, 2L);
+ }
+
+ @Test
+ public void testJacksonClass() {
+ ExampleJacksonClass defaultInstance = new ExampleJacksonClass();
+ ExampleJacksonClass instance = new ExampleJacksonClass();
+ instance.integer = -2;
+ instance.string = "foo";
+
+ testGeneric(Flags.defineJackson("jackson-id", ExampleJacksonClass.class, defaultInstance,
+ "description", "modification effect", FetchVector.Dimension.HOSTNAME),
+ defaultInstance, instance);
+ }
+
+ private <T> void testGeneric(UnboundFlag<T> unboundFlag, T defaultValue, T value) {
+ FlagSource source = mock(FlagSource.class);
+ Flag<T> flag = unboundFlag.bindTo(source);
+
+ when(source.fetch(any(), any())).thenReturn(Optional.empty());
+ assertThat(flag.value(), equalTo(defaultValue));
+
+ when(source.fetch(any(), any())).thenReturn(Optional.of(JsonNodeRawFlag.fromJacksonClass(value)));
+ assertThat(flag.value(), equalTo(value));
+
+ assertTrue(Flags.getFlag(unboundFlag.id()).isPresent());
+ }
+
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ private static class ExampleJacksonClass {
+ @JsonProperty("integer")
+ public int integer = 1;
+
+ @JsonProperty("string")
+ public String string = "2";
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ExampleJacksonClass that = (ExampleJacksonClass) o;
+ return integer == that.integer &&
+ Objects.equals(string, that.string);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(integer, string);
+ }
+ }
+}
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java b/flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java
deleted file mode 100644
index 8bd486f5b47..00000000000
--- a/flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.flags;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import org.junit.Test;
-
-import java.util.Objects;
-import java.util.Optional;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-public class JacksonFlagTest {
- private final FlagId id = new FlagId("id");
- private final ExampleJacksonClass defaultValue = new ExampleJacksonClass();
- private final FlagSource source = mock(FlagSource.class);
- private final JacksonFlag<ExampleJacksonClass> jacksonFlag = new JacksonFlag<>(id.toString(), ExampleJacksonClass.class, defaultValue, source);
-
- @Test
- public void unsetThenSet() {
- when(source.getString(id)).thenReturn(Optional.empty());
- ExampleJacksonClass value = jacksonFlag.value();
- assertEquals(1, value.integer);
- assertEquals("2", value.string);
- assertEquals("3", value.dummy);
-
- when(source.getString(id)).thenReturn(Optional.of("{\"integer\": 4, \"string\": \"foo\", \"stray\": 6}"));
- value = jacksonFlag.value();
- assertEquals(4, value.integer);
- assertEquals("foo", value.string);
- assertEquals("3", value.dummy);
-
- assertEquals(4, value.integer);
- assertEquals("foo", value.string);
- assertEquals("3", value.dummy);
- }
-
- @JsonIgnoreProperties(ignoreUnknown = true)
- private static class ExampleJacksonClass {
- @JsonProperty("integer")
- public int integer = 1;
-
- @JsonProperty("string")
- public String string = "2";
-
- @JsonProperty("dummy")
- public String dummy = "3";
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ExampleJacksonClass that = (ExampleJacksonClass) o;
- return integer == that.integer &&
- Objects.equals(string, that.string) &&
- Objects.equals(dummy, that.dummy);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(integer, string, dummy);
- }
- }
-} \ No newline at end of file
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/OrderedFlagSourceTest.java b/flags/src/test/java/com/yahoo/vespa/flags/OrderedFlagSourceTest.java
new file mode 100644
index 00000000000..5465f89e2eb
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/OrderedFlagSourceTest.java
@@ -0,0 +1,50 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import org.junit.Test;
+
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author hakonhall
+ */
+public class OrderedFlagSourceTest {
+ @Test
+ public void test() {
+ FlagSource source1 = mock(FlagSource.class);
+ FlagSource source2 = mock(FlagSource.class);
+ OrderedFlagSource orderedSource = new OrderedFlagSource(source1, source2);
+
+ FlagId id = new FlagId("id");
+ FetchVector vector = new FetchVector();
+
+ when(source1.fetch(any(), any())).thenReturn(Optional.empty());
+ when(source2.fetch(any(), any())).thenReturn(Optional.empty());
+ assertFalse(orderedSource.fetch(id, vector).isPresent());
+ verify(source1, times(1)).fetch(any(), any());
+ verify(source2, times(1)).fetch(any(), any());
+
+ RawFlag rawFlag = mock(RawFlag.class);
+
+ when(source1.fetch(any(), any())).thenReturn(Optional.empty());
+ when(source2.fetch(any(), any())).thenReturn(Optional.of(rawFlag));
+ assertEquals(orderedSource.fetch(id, vector), Optional.of(rawFlag));
+ verify(source1, times(2)).fetch(any(), any());
+ verify(source2, times(2)).fetch(any(), any());
+
+ when(source1.fetch(any(), any())).thenReturn(Optional.of(rawFlag));
+ when(source2.fetch(any(), any())).thenReturn(Optional.empty());
+ assertEquals(orderedSource.fetch(id, vector), Optional.of(rawFlag));
+ verify(source1, times(3)).fetch(any(), any());
+ // Not invoked as source1 provided raw flag
+ verify(source2, times(2)).fetch(any(), any());
+ }
+} \ No newline at end of file
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java b/flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java
new file mode 100644
index 00000000000..96cbce71fa8
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java
@@ -0,0 +1,38 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+import com.yahoo.vespa.flags.FetchVector;
+import org.junit.Test;
+
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author hakonhall
+ */
+public class ConditionTest {
+ @Test
+ public void testWhitelist() {
+ String hostname1 = "host1";
+ Condition condition = new Condition(Condition.Type.WHITELIST, FetchVector.Dimension.HOSTNAME,
+ Stream.of(hostname1).collect(Collectors.toSet()));
+ assertFalse(condition.test(new FetchVector()));
+ assertFalse(condition.test(new FetchVector().with(FetchVector.Dimension.APPLICATION_ID, "foo")));
+ assertFalse(condition.test(new FetchVector().with(FetchVector.Dimension.HOSTNAME, "bar")));
+ assertTrue(condition.test(new FetchVector().with(FetchVector.Dimension.HOSTNAME, hostname1)));
+ }
+
+ @Test
+ public void testBlacklist() {
+ String hostname1 = "host1";
+ Condition condition = new Condition(Condition.Type.BLACKLIST, FetchVector.Dimension.HOSTNAME,
+ Stream.of(hostname1).collect(Collectors.toSet()));
+ assertTrue(condition.test(new FetchVector()));
+ assertTrue(condition.test(new FetchVector().with(FetchVector.Dimension.APPLICATION_ID, "foo")));
+ assertTrue(condition.test(new FetchVector().with(FetchVector.Dimension.HOSTNAME, "bar")));
+ assertFalse(condition.test(new FetchVector().with(FetchVector.Dimension.HOSTNAME, hostname1)));
+ }
+}
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/json/FlagDataTest.java b/flags/src/test/java/com/yahoo/vespa/flags/json/FlagDataTest.java
new file mode 100644
index 00000000000..2eb12e53ddc
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/json/FlagDataTest.java
@@ -0,0 +1,82 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+import com.yahoo.vespa.flags.FetchVector;
+import com.yahoo.vespa.flags.RawFlag;
+import org.junit.Test;
+
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author hakonhall
+ */
+public class FlagDataTest {
+ private final String json = "{\n" +
+ " \"rules\": [\n" +
+ " {\n" +
+ " \"conditions\": [\n" +
+ " {\n" +
+ " \"type\": \"whitelist\",\n" +
+ " \"dimension\": \"hostname\",\n" +
+ " \"values\": [ \"host1\", \"host2\" ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"blacklist\",\n" +
+ " \"dimension\": \"application\",\n" +
+ " \"values\": [ \"app1\", \"app2\" ]\n" +
+ " }\n" +
+ " ],\n" +
+ " \"value\": true\n" +
+ " },\n" +
+ " {\n" +
+ " \"conditions\": [\n" +
+ " {\n" +
+ " \"type\": \"whitelist\",\n" +
+ " \"dimension\": \"zone\",\n" +
+ " \"values\": [ \"zone1\", \"zone2\" ]\n" +
+ " }\n" +
+ " ],\n" +
+ " \"value\": false\n" +
+ " }\n" +
+ " ],\n" +
+ " \"attributes\": {\n" +
+ " \"zone\": \"zone1\"\n" +
+ " }\n" +
+ "}";
+
+ private final FetchVector vector = new FetchVector();
+
+ @Test
+ public void test() {
+ // Second rule matches with the default zone matching
+ verify(Optional.of("false"), vector);
+
+ // First rule matches only if both conditions match
+ verify(Optional.of("false"), vector
+ .with(FetchVector.Dimension.HOSTNAME, "host1")
+ .with(FetchVector.Dimension.APPLICATION_ID, "app2"));
+ verify(Optional.of("true"), vector
+ .with(FetchVector.Dimension.HOSTNAME, "host1")
+ .with(FetchVector.Dimension.APPLICATION_ID, "app3"));
+
+ // No rules apply if zone is overridden to an unknown zone
+ verify(Optional.empty(), vector.with(FetchVector.Dimension.ZONE_ID, "unknown zone"));
+ }
+
+ private void verify(Optional<String> expectedValue, FetchVector vector) {
+ FlagData data = FlagData.deserialize(json);
+ Optional<RawFlag> rawFlag = data.resolve(vector);
+
+ if (expectedValue.isPresent()) {
+ assertTrue(rawFlag.isPresent());
+ assertEquals(expectedValue.get(), rawFlag.get().asJson());
+ } else {
+ assertFalse(rawFlag.isPresent());
+ }
+
+ }
+} \ No newline at end of file
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java b/flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java
new file mode 100644
index 00000000000..f3f8c147212
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java
@@ -0,0 +1,130 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.JsonNodeType;
+import com.yahoo.vespa.flags.json.wire.WireCondition;
+import com.yahoo.vespa.flags.json.wire.WireFlagData;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anEmptyMap;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+
+/**
+ * @author hakonhall
+ */
+public class SerializationTest {
+ @Test
+ public void emptyJson() throws IOException {
+ String json = "{}";
+ WireFlagData wireData = WireFlagData.deserialize(json);
+ assertThat(wireData.defaultFetchVector, nullValue());
+ assertThat(wireData.rules, nullValue());
+ assertThat(wireData.serializeToJson(), equalTo(json));
+
+ assertThat(FlagData.deserialize(json).serializeToJson(), equalTo("{}"));
+ }
+
+ @Test
+ public void deserialization() throws IOException {
+ String json = "{\n" +
+ " \"rules\": [\n" +
+ " {\n" +
+ " \"conditions\": [\n" +
+ " {\n" +
+ " \"type\": \"whitelist\",\n" +
+ " \"dimension\": \"application\",\n" +
+ " \"values\": [ \"a1\", \"a2\" ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"blacklist\",\n" +
+ " \"dimension\": \"hostname\",\n" +
+ " \"values\": [ \"h1\" ]\n" +
+ " }\n" +
+ " ],\n" +
+ " \"value\": true\n" +
+ " }\n" +
+ " ],\n" +
+ " \"attributes\": {\n" +
+ " \"zone\": \"z1\",\n" +
+ " \"application\": \"a1\",\n" +
+ " \"hostname\": \"h1\"\n" +
+ " }\n" +
+ "}";
+
+ WireFlagData wireData = WireFlagData.deserialize(json);
+
+ // rule
+ assertThat(wireData.rules.size(), equalTo(1));
+ assertThat(wireData.rules.get(0).andConditions.size(), equalTo(2));
+ assertThat(wireData.rules.get(0).value.getNodeType(), equalTo(JsonNodeType.BOOLEAN));
+ assertThat(wireData.rules.get(0).value.asBoolean(), equalTo(true));
+ // first condition
+ WireCondition whitelistCondition = wireData.rules.get(0).andConditions.get(0);
+ assertThat(whitelistCondition.type, equalTo("whitelist"));
+ assertThat(whitelistCondition.dimension, equalTo("application"));
+ assertThat(whitelistCondition.values, equalTo(new HashSet<>(Arrays.asList("a1", "a2"))));
+ // second condition
+ WireCondition blacklistCondition = wireData.rules.get(0).andConditions.get(1);
+ assertThat(blacklistCondition.type, equalTo("blacklist"));
+ assertThat(blacklistCondition.dimension, equalTo("hostname"));
+ assertThat(blacklistCondition.values, equalTo(new HashSet<>(Arrays.asList("h1"))));
+
+ // attributes
+ assertThat(wireData.defaultFetchVector, notNullValue());
+ assertThat(wireData.defaultFetchVector.get("zone"), equalTo("z1"));
+ assertThat(wireData.defaultFetchVector.get("application"), equalTo("a1"));
+ assertThat(wireData.defaultFetchVector.get("hostname"), equalTo("h1"));
+
+ // Verify serialization of RawFlag == serialization by ObjectMapper
+ ObjectMapper mapper = new ObjectMapper();
+ String serializedWithObjectMapper = mapper.writeValueAsString(mapper.readTree(json));
+ assertThat(wireData.serializeToJson(), equalTo(serializedWithObjectMapper));
+
+ // Unfortunately the order of attributes members are different...
+ // assertThat(FlagData.deserialize(json).serializeToJson(), equalTo(serializedWithObjectMapper));
+ }
+
+ @Test
+ public void jsonWithStrayFields() {
+ String json = "{\n" +
+ " \"foo\": true,\n" +
+ " \"rules\": [\n" +
+ " {\n" +
+ " \"conditions\": [\n" +
+ " {\n" +
+ " \"type\": \"whitelist\",\n" +
+ " \"dimension\": \"zone\",\n" +
+ " \"bar\": \"zoo\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"other\": true\n" +
+ " }\n" +
+ " ],\n" +
+ " \"attributes\": {\n" +
+ " }\n" +
+ "}";
+
+ WireFlagData wireData = WireFlagData.deserialize(json);
+
+ assertThat(wireData.rules.size(), equalTo(1));
+ assertThat(wireData.rules.get(0).andConditions.size(), equalTo(1));
+ WireCondition whitelistCondition = wireData.rules.get(0).andConditions.get(0);
+ assertThat(whitelistCondition.type, equalTo("whitelist"));
+ assertThat(whitelistCondition.dimension, equalTo("zone"));
+ assertThat(whitelistCondition.values, nullValue());
+ assertThat(wireData.rules.get(0).value, nullValue());
+ assertThat(wireData.defaultFetchVector, anEmptyMap());
+
+ assertThat(wireData.serializeToJson(), equalTo("{\"rules\":[{\"conditions\":[{\"type\":\"whitelist\",\"dimension\":\"zone\"}]}],\"attributes\":{}}"));
+
+ assertThat(FlagData.deserialize(json).serializeToJson(), equalTo("{\"rules\":[{\"conditions\":[{\"type\":\"whitelist\",\"dimension\":\"zone\"}]}]}"));
+ }
+}