From 1a8f40f73fa4bea12a3f857545c500c16cf9b165 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 2 Aug 2019 13:31:07 -0700 Subject: [PATCH] add polling mode tests and end-to-end streaming test --- .../com/launchdarkly/client/FeatureFlag.java | 11 -- .../launchdarkly/client/FeatureRequestor.java | 16 +- .../java/com/launchdarkly/client/Segment.java | 11 -- .../client/FeatureRequestorTest.java | 166 ++++++++++++++++++ .../client/LDClientEndToEndTest.java | 116 ++++++++++++ .../com/launchdarkly/client/TestHttpUtil.java | 63 +++++++ 6 files changed, 348 insertions(+), 35 deletions(-) create mode 100644 src/test/java/com/launchdarkly/client/FeatureRequestorTest.java create mode 100644 src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java create mode 100644 src/test/java/com/launchdarkly/client/TestHttpUtil.java diff --git a/src/main/java/com/launchdarkly/client/FeatureFlag.java b/src/main/java/com/launchdarkly/client/FeatureFlag.java index 7cc7dde91..c9ed7f31c 100644 --- a/src/main/java/com/launchdarkly/client/FeatureFlag.java +++ b/src/main/java/com/launchdarkly/client/FeatureFlag.java @@ -17,9 +17,6 @@ class FeatureFlag implements VersionedData { private final static Logger logger = LoggerFactory.getLogger(FeatureFlag.class); - private static final Type mapType = new TypeToken>() { - }.getType(); - private String key; private int version; private boolean on; @@ -35,14 +32,6 @@ class FeatureFlag implements VersionedData { private Long debugEventsUntilDate; private boolean deleted; - static FeatureFlag fromJson(LDConfig config, String json) { - return config.gson.fromJson(json, FeatureFlag.class); - } - - static Map fromJsonMap(LDConfig config, String json) { - return config.gson.fromJson(json, mapType); - } - // We need this so Gson doesn't complain in certain java environments that restrict unsafe allocation FeatureFlag() {} diff --git a/src/main/java/com/launchdarkly/client/FeatureRequestor.java b/src/main/java/com/launchdarkly/client/FeatureRequestor.java index 180270295..f4a031a5e 100644 --- a/src/main/java/com/launchdarkly/client/FeatureRequestor.java +++ b/src/main/java/com/launchdarkly/client/FeatureRequestor.java @@ -37,24 +37,14 @@ static class AllData { this.config = config; } - Map getAllFlags() throws IOException, HttpErrorException { - String body = get(GET_LATEST_FLAGS_PATH); - return FeatureFlag.fromJsonMap(config, body); - } - FeatureFlag getFlag(String featureKey) throws IOException, HttpErrorException { String body = get(GET_LATEST_FLAGS_PATH + "/" + featureKey); - return FeatureFlag.fromJson(config, body); - } - - Map getAllSegments() throws IOException, HttpErrorException { - String body = get(GET_LATEST_SEGMENTS_PATH); - return Segment.fromJsonMap(config, body); + return config.gson.fromJson(body, FeatureFlag.class); } Segment getSegment(String segmentKey) throws IOException, HttpErrorException { String body = get(GET_LATEST_SEGMENTS_PATH + "/" + segmentKey); - return Segment.fromJson(config, body); + return config.gson.fromJson(body, Segment.class); } AllData getAllData() throws IOException, HttpErrorException { @@ -71,7 +61,7 @@ AllData getAllData() throws IOException, HttpErrorException { private String get(String path) throws IOException, HttpErrorException { Request request = getRequestBuilder(sdkKey) - .url(config.baseURI.toString() + path) + .url(config.baseURI.resolve(path).toURL()) .get() .build(); diff --git a/src/main/java/com/launchdarkly/client/Segment.java b/src/main/java/com/launchdarkly/client/Segment.java index eb7fc24d8..2febf6b65 100644 --- a/src/main/java/com/launchdarkly/client/Segment.java +++ b/src/main/java/com/launchdarkly/client/Segment.java @@ -9,9 +9,6 @@ import com.google.gson.reflect.TypeToken; class Segment implements VersionedData { - - private static final Type mapType = new TypeToken>() { }.getType(); - private String key; private List included; private List excluded; @@ -20,14 +17,6 @@ class Segment implements VersionedData { private int version; private boolean deleted; - static Segment fromJson(LDConfig config, String json) { - return config.gson.fromJson(json, Segment.class); - } - - static Map fromJsonMap(LDConfig config, String json) { - return config.gson.fromJson(json, mapType); - } - // We need this so Gson doesn't complain in certain java environments that restrict unsafe allocation Segment() {} diff --git a/src/test/java/com/launchdarkly/client/FeatureRequestorTest.java b/src/test/java/com/launchdarkly/client/FeatureRequestorTest.java new file mode 100644 index 000000000..f179a93dc --- /dev/null +++ b/src/test/java/com/launchdarkly/client/FeatureRequestorTest.java @@ -0,0 +1,166 @@ +package com.launchdarkly.client; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +import static com.launchdarkly.client.TestHttpUtil.baseConfig; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; + +public class FeatureRequestorTest { + private static final String sdkKey = "sdk-key"; + private static final String flag1Key = "flag1"; + private static final String flag1Json = "{\"key\":\"" + flag1Key + "\"}"; + private static final String flagsJson = "{\"" + flag1Key + "\":" + flag1Json + "}"; + private static final String segment1Key = "segment1"; + private static final String segment1Json = "{\"key\":\"" + segment1Key + "\"}"; + private static final String segmentsJson = "{\"" + segment1Key + "\":" + segment1Json + "}"; + private static final String allDataJson = "{\"flags\":" + flagsJson + ",\"segments\":" + segmentsJson + "}"; + + @Test + public void requestAllData() throws Exception { + MockResponse resp = new MockResponse(); + resp.setHeader("Content-Type", "application/json"); + resp.setBody(allDataJson); + + try (MockWebServer server = TestHttpUtil.makeStartedServer(resp)) { + FeatureRequestor r = new FeatureRequestor(sdkKey, basePollingConfig(server).build()); + + FeatureRequestor.AllData data = r.getAllData(); + + RecordedRequest req = server.takeRequest(); + assertEquals("/sdk/latest-all", req.getPath()); + verifyHeaders(req); + + assertNotNull(data); + assertNotNull(data.flags); + assertNotNull(data.segments); + assertEquals(1, data.flags.size()); + assertEquals(1, data.flags.size()); + verifyFlag(data.flags.get(flag1Key), flag1Key); + verifySegment(data.segments.get(segment1Key), segment1Key); + } + } + + @Test + public void requestFlag() throws Exception { + MockResponse resp = new MockResponse(); + resp.setHeader("Content-Type", "application/json"); + resp.setBody(flag1Json); + + try (MockWebServer server = TestHttpUtil.makeStartedServer(resp)) { + FeatureRequestor r = new FeatureRequestor(sdkKey, basePollingConfig(server).build()); + + FeatureFlag flag = r.getFlag(flag1Key); + + RecordedRequest req = server.takeRequest(); + assertEquals("/sdk/latest-flags/" + flag1Key, req.getPath()); + verifyHeaders(req); + + verifyFlag(flag, flag1Key); + } + } + + @Test + public void requestSegment() throws Exception { + MockResponse resp = new MockResponse(); + resp.setHeader("Content-Type", "application/json"); + resp.setBody(segment1Json); + + try (MockWebServer server = TestHttpUtil.makeStartedServer(resp)) { + FeatureRequestor r = new FeatureRequestor(sdkKey, basePollingConfig(server).build()); + + Segment segment = r.getSegment(segment1Key); + + RecordedRequest req = server.takeRequest(); + assertEquals("/sdk/latest-segments/" + segment1Key, req.getPath()); + verifyHeaders(req); + + verifySegment(segment, segment1Key); + } + } + + @Test + public void requestFlagNotFound() throws Exception { + MockResponse notFoundResp = new MockResponse().setResponseCode(404); + + try (MockWebServer server = TestHttpUtil.makeStartedServer(notFoundResp)) { + FeatureRequestor r = new FeatureRequestor(sdkKey, basePollingConfig(server).build()); + + try { + r.getFlag(flag1Key); + Assert.fail("expected exception"); + } catch (HttpErrorException e) { + assertEquals(404, e.getStatus()); + } + } + } + + @Test + public void requestSegmentNotFound() throws Exception { + MockResponse notFoundResp = new MockResponse().setResponseCode(404); + + try (MockWebServer server = TestHttpUtil.makeStartedServer(notFoundResp)) { + FeatureRequestor r = new FeatureRequestor(sdkKey, basePollingConfig(server).build()); + + try { + r.getSegment(segment1Key); + Assert.fail("expected exception"); + } catch (HttpErrorException e) { + assertEquals(404, e.getStatus()); + } + } + } + + @Test + public void requestsAreCached() throws Exception { + MockResponse cacheableResp = new MockResponse(); + cacheableResp.setHeader("Content-Type", "application/json"); + cacheableResp.setHeader("ETag", "aaa"); + cacheableResp.setHeader("Cache-Control", "max-age=1000"); + cacheableResp.setBody(flag1Json); + + try (MockWebServer server = TestHttpUtil.makeStartedServer(cacheableResp)) { + FeatureRequestor r = new FeatureRequestor(sdkKey, basePollingConfig(server).build()); + + FeatureFlag flag1a = r.getFlag(flag1Key); + + RecordedRequest req1 = server.takeRequest(); + assertEquals("/sdk/latest-flags/" + flag1Key, req1.getPath()); + verifyHeaders(req1); + + verifyFlag(flag1a, flag1Key); + + FeatureFlag flag1b = r.getFlag(flag1Key); + verifyFlag(flag1b, flag1Key); + assertNull(server.takeRequest(0, TimeUnit.SECONDS)); // there was no second request, due to the cache hit + } + } + + private LDConfig.Builder basePollingConfig(MockWebServer server) { + return baseConfig(server) + .stream(false); + } + + private void verifyHeaders(RecordedRequest req) { + assertEquals(sdkKey, req.getHeader("Authorization")); + assertEquals("JavaClient/" + LDClient.CLIENT_VERSION, req.getHeader("User-Agent")); + } + + private void verifyFlag(FeatureFlag flag, String key) { + assertNotNull(flag); + assertEquals(key, flag.getKey()); + } + + private void verifySegment(Segment segment, String key) { + assertNotNull(segment); + assertEquals(key, segment.getKey()); + } +} diff --git a/src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java b/src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java new file mode 100644 index 000000000..135c2b40f --- /dev/null +++ b/src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java @@ -0,0 +1,116 @@ +package com.launchdarkly.client; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +import static com.launchdarkly.client.TestHttpUtil.baseConfig; +import static com.launchdarkly.client.TestHttpUtil.makeStartedServer; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import okhttp3.mockwebserver.SocketPolicy; + +public class LDClientEndToEndTest { + private static final Gson gson = new Gson(); + private static final String sdkKey = "sdk-key"; + private static final String flagKey = "flag1"; + private static final FeatureFlag flag = new FeatureFlagBuilder(flagKey) + .offVariation(0).variations(new JsonPrimitive(true)) + .build(); + private static final LDUser user = new LDUser("user-key"); + + @Test + public void clientStartsInPollingMode() throws Exception { + MockResponse resp = new MockResponse() + .setHeader("Content-Type", "application/json") + .setBody(makeAllDataJson()); + + try (MockWebServer server = makeStartedServer(resp)) { + LDConfig config = baseConfig(server) + .stream(false) + .sendEvents(false) + .build(); + + try (LDClient client = new LDClient(sdkKey, config)) { + assertTrue(client.initialized()); + assertTrue(client.boolVariation(flagKey, user, false)); + } + } + } + + @Test + public void clientFailsInPollingModeWith401Error() throws Exception { + MockResponse resp = new MockResponse().setResponseCode(401); + + try (MockWebServer server = makeStartedServer(resp)) { + LDConfig config = baseConfig(server) + .stream(false) + .sendEvents(false) + .build(); + + try (LDClient client = new LDClient(sdkKey, config)) { + assertFalse(client.initialized()); + assertFalse(client.boolVariation(flagKey, user, false)); + } + } + } + + @Test + public void clientStartsInStreamingMode() throws Exception { + String eventData = "event: put\n" + + "data: {\"data\":" + makeAllDataJson() + "}\n\n"; + + MockResponse resp = new MockResponse() + .setHeader("Content-Type", "text/event-stream") + .setChunkedBody(eventData, 1000) + .setSocketPolicy(SocketPolicy.KEEP_OPEN); + + try (MockWebServer server = makeStartedServer(resp)) { + LDConfig config = baseConfig(server) + .sendEvents(false) + .build(); + + try (LDClient client = new LDClient(sdkKey, config)) { + assertTrue(client.initialized()); + assertTrue(client.boolVariation(flagKey, user, false)); + } + } + } + + @Test + public void clientFailsInStreamingModeWith401Error() throws Exception { + MockResponse resp = new MockResponse().setResponseCode(401); + + try (MockWebServer server = makeStartedServer(resp)) { + LDConfig config = baseConfig(server) + .sendEvents(false) + .build(); + + try (LDClient client = new LDClient(sdkKey, config)) { + assertFalse(client.initialized()); + assertFalse(client.boolVariation(flagKey, user, false)); + } + } + } + + public String makeAllDataJson() { + JsonObject flagsData = new JsonObject(); + flagsData.add(flagKey, gson.toJsonTree(flag)); + JsonObject allData = new JsonObject(); + allData.add("flags", flagsData); + allData.add("segments", new JsonObject()); + return gson.toJson(allData); + } +} diff --git a/src/test/java/com/launchdarkly/client/TestHttpUtil.java b/src/test/java/com/launchdarkly/client/TestHttpUtil.java new file mode 100644 index 000000000..5c1f6371d --- /dev/null +++ b/src/test/java/com/launchdarkly/client/TestHttpUtil.java @@ -0,0 +1,63 @@ +package com.launchdarkly.client; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetAddress; +import java.net.URI; +import java.security.GeneralSecurityException; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.internal.tls.HeldCertificate; +import okhttp3.mockwebserver.internal.tls.SslClient; + +class TestHttpUtil { + static MockWebServer makeStartedServer(MockResponse... responses) throws IOException { + MockWebServer server = new MockWebServer(); + for (MockResponse r: responses) { + server.enqueue(r); + } + server.start(); + return server; + } + + static LDConfig.Builder baseConfig(MockWebServer server) { + URI uri = server.url("").uri(); + return new LDConfig.Builder() + .baseURI(uri) + .streamURI(uri) + .eventsURI(uri); + } + + static class HttpsServerWithSelfSignedCert implements Closeable { + final MockWebServer server; + final HeldCertificate cert; + final SslClient sslClient; + + public HttpsServerWithSelfSignedCert() throws IOException, GeneralSecurityException { + cert = new HeldCertificate.Builder() + .serialNumber("1") + .commonName(InetAddress.getByName("localhost").getCanonicalHostName()) + .subjectAlternativeName("localhost") + .build(); + + sslClient = new SslClient.Builder() + .certificateChain(cert.keyPair, cert.certificate) + .addTrustedCertificate(cert.certificate) + .build(); + + server = new MockWebServer(); + server.useHttps(sslClient.socketFactory, false); + + server.start(); + } + + public URI uri() { + return server.url("/").uri(); + } + + public void close() throws IOException { + server.close(); + } + } +}