diff --git a/lib/sdk/server/Makefile b/lib/sdk/server/Makefile index ffa1b6f1..9dad9be3 100644 --- a/lib/sdk/server/Makefile +++ b/lib/sdk/server/Makefile @@ -9,6 +9,9 @@ test: ./gradlew test TEMP_TEST_OUTPUT=/tmp/sdk-test-service.log +TEST_SERVICE_PORT ?= 8000 +SUPPRESSION_FILE=contract-tests/test-suppressions.txt +SUPPRESSION_FILE_FDV2=contract-tests/test-suppressions-fdv2.txt # Add any extra sdk-test-harness parameters here, such as -skip for tests that are # temporarily not working. @@ -18,15 +21,19 @@ build-contract-tests: @cd contract-tests && ../gradlew installDist start-contract-test-service: - @contract-tests/service/build/install/service/bin/service + @PORT=$(TEST_SERVICE_PORT) contract-tests/service/build/install/service/bin/service start-contract-test-service-bg: @echo "Test service output will be captured in $(TEMP_TEST_OUTPUT)" @make start-contract-test-service >$(TEMP_TEST_OUTPUT) 2>&1 & run-contract-tests: + @echo "Running SDK contract test v2..." @curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v2/downloader/run.sh \ - | VERSION=v2 PARAMS="-url http://localhost:8000 -debug -stop-service-at-end $(TEST_HARNESS_PARAMS)" sh + | VERSION=v2 PARAMS="-url http://localhost:$(TEST_SERVICE_PORT) -debug -skip-from=$(SUPPRESSION_FILE) $(TEST_HARNESS_PARAMS)" sh + @echo "Running SDK contract test v3.0.0-alpha.1..." + @curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v3.0.0-alpha.1/downloader/run.sh \ + | VERSION=v3.0.0-alpha.1 PARAMS="-url http://localhost:$(TEST_SERVICE_PORT) -debug -stop-service-at-end -skip-from=$(SUPPRESSION_FILE_FDV2) $(TEST_HARNESS_PARAMS)" sh contract-tests: build-contract-tests start-contract-test-service-bg run-contract-tests diff --git a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/Representations.java b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/Representations.java index 94772cc1..265150a7 100644 --- a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/Representations.java +++ b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/Representations.java @@ -32,6 +32,7 @@ public static class SdkConfigParams { SdkConfigTagParams tags; SdkConfigServiceEndpointParams serviceEndpoints; SdkConfigHookParams hooks; + SdkConfigDataSystemParams dataSystem; } public static class SdkConfigStreamParams { @@ -73,6 +74,113 @@ public static class SdkConfigHookParams { List hooks; } + /** + * Constants for store mode values. + */ + public static class StoreMode { + /** + * Read-only mode - the data system will only read from the persistent store. + */ + public static final int READ = 0; + + /** + * Read-write mode - the data system can read from, and write to, the persistent store. + */ + public static final int READ_WRITE = 1; + } + + /** + * Constants for persistent store type values. + */ + public static class PersistentStoreType { + /** + * Redis persistent store type. + */ + public static final String REDIS = "redis"; + + /** + * DynamoDB persistent store type. + */ + public static final String DYNAMODB = "dynamodb"; + + /** + * Consul persistent store type. + */ + public static final String CONSUL = "consul"; + } + + /** + * Constants for persistent cache mode values. + */ + public static class PersistentCacheMode { + /** + * Cache disabled mode. + */ + public static final String OFF = "off"; + + /** + * Time-to-live cache mode with a specified TTL. + */ + public static final String TTL = "ttl"; + + /** + * Infinite cache mode - cache forever. + */ + public static final String INFINITE = "infinite"; + } + + public static class SdkConfigDataSystemParams { + SdkConfigDataStoreParams store; + Integer storeMode; + SdkConfigDataInitializerParams[] initializers; + SdkConfigSynchronizersParams synchronizers; + String payloadFilter; + } + + public static class SdkConfigDataStoreParams { + SdkConfigPersistentDataStoreParams persistentDataStore; + } + + public static class SdkConfigPersistentDataStoreParams { + SdkConfigPersistentStoreParams store; + SdkConfigPersistentCacheParams cache; + } + + public static class SdkConfigPersistentStoreParams { + String type; + String prefix; + String dsn; + } + + public static class SdkConfigPersistentCacheParams { + String mode; + Integer ttl; + } + + public static class SdkConfigDataInitializerParams { + SdkConfigPollingParams polling; + } + + public static class SdkConfigSynchronizersParams { + SdkConfigSynchronizerParams primary; + SdkConfigSynchronizerParams secondary; + } + + public static class SdkConfigSynchronizerParams { + SdkConfigStreamingParams streaming; + SdkConfigPollingParams polling; + } + + public static class SdkConfigPollingParams { + URI baseUri; + Long pollIntervalMs; + } + + public static class SdkConfigStreamingParams { + URI baseUri; + Long initialRetryDelayMs; + } + public static class HookConfig { String name; URI callbackUri; diff --git a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java index db70d6c2..17697fb4 100644 --- a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java +++ b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java @@ -25,7 +25,16 @@ import com.launchdarkly.sdk.server.integrations.HooksConfigurationBuilder; import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder; import com.launchdarkly.sdk.server.integrations.StreamingDataSourceBuilder; +import com.launchdarkly.sdk.server.integrations.DataSystemBuilder; +import com.launchdarkly.sdk.server.DataSystemComponents; +import com.launchdarkly.sdk.server.integrations.FDv2PollingInitializerBuilder; +import com.launchdarkly.sdk.server.integrations.FDv2PollingSynchronizerBuilder; +import com.launchdarkly.sdk.server.integrations.FDv2StreamingSynchronizerBuilder; import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider; +import com.launchdarkly.sdk.server.subsystems.DataSourceBuilder; +import com.launchdarkly.sdk.server.datasources.Initializer; +import com.launchdarkly.sdk.server.datasources.Synchronizer; +import com.launchdarkly.sdk.server.subsystems.DataSystemConfiguration; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -55,6 +64,12 @@ import sdktest.Representations.HookConfig; import sdktest.Representations.SdkConfigHookParams; import sdktest.Representations.SdkConfigParams; +import sdktest.Representations.SdkConfigDataSystemParams; +import sdktest.Representations.SdkConfigDataInitializerParams; +import sdktest.Representations.SdkConfigSynchronizersParams; +import sdktest.Representations.SdkConfigSynchronizerParams; +import sdktest.Representations.SdkConfigPollingParams; +import sdktest.Representations.SdkConfigStreamingParams; import sdktest.Representations.SecureModeHashParams; import sdktest.Representations.SecureModeHashResponse; @@ -465,6 +480,125 @@ private LDConfig buildSdkConfig(SdkConfigParams params, String tag) { builder.hooks(Components.hooks().setHooks(hookList)); } + if (params.dataSystem != null) { + DataSystemBuilder dataSystemBuilder = Components.dataSystem().custom(); + + // TODO: enable this code in the future and determine which dependencies on persistent stores need to be added to contract test build process + // Configure persistent store if provided + // if (params.dataSystem.store != null && params.dataSystem.store.persistentDataStore != null) { + // var storeConfig = params.dataSystem.store.persistentDataStore; + // var storeType = storeConfig.store.type.toLowerCase(); + // ComponentConfigurer persistentStore = null; + // + // switch (storeType) { + // case "redis": + // // Redis store configuration + // break; + // case "dynamodb": + // // DynamoDB store configuration + // break; + // case "consul": + // // Consul store configuration + // break; + // } + // + // if (persistentStore != null) { + // // Configure cache + // var cacheMode = storeConfig.cache != null ? storeConfig.cache.mode.toLowerCase() : null; + // // ... cache configuration ... + // + // // Determine store mode + // var storeMode = params.dataSystem.storeMode == 0 + // ? DataSystemConfiguration.DataStoreMode.READ_ONLY + // : DataSystemConfiguration.DataStoreMode.READ_WRITE; + // + // dataSystemBuilder.persistentStore(persistentStore, storeMode); + // } + // } + + // Configure initializers + if (params.dataSystem.initializers != null && params.dataSystem.initializers.length > 0) { + List> initializers = new ArrayList<>(); + for (SdkConfigDataInitializerParams initializer : params.dataSystem.initializers) { + if (initializer.polling != null) { + FDv2PollingInitializerBuilder pollingBuilder = DataSystemComponents.pollingInitializer(); + if (initializer.polling.baseUri != null) { + ServiceEndpointsBuilder endpointOverride = Components.serviceEndpoints().polling(initializer.polling.baseUri); + pollingBuilder.serviceEndpointsOverride(endpointOverride); + } + // Note: pollInterval is not available for initializers, only for synchronizers + if (params.dataSystem.payloadFilter != null && !params.dataSystem.payloadFilter.isEmpty()) { + pollingBuilder.payloadFilter(params.dataSystem.payloadFilter); + } + initializers.add(pollingBuilder); + } + } + if (!initializers.isEmpty()) { + dataSystemBuilder.initializers(initializers.toArray(new DataSourceBuilder[0])); + } + } + + // Configure synchronizers + if (params.dataSystem.synchronizers != null) { + List> synchronizers = new ArrayList<>(); + + // Primary synchronizer + if (params.dataSystem.synchronizers.primary != null) { + DataSourceBuilder primary = createSynchronizer(params.dataSystem.synchronizers.primary, params.dataSystem.payloadFilter); + if (primary != null) { + synchronizers.add(primary); + } + } + + // Secondary synchronizer (optional) + if (params.dataSystem.synchronizers.secondary != null) { + DataSourceBuilder secondary = createSynchronizer(params.dataSystem.synchronizers.secondary, params.dataSystem.payloadFilter); + if (secondary != null) { + synchronizers.add(secondary); + } + } + + if (!synchronizers.isEmpty()) { + dataSystemBuilder.synchronizers(synchronizers.toArray(new DataSourceBuilder[0])); + } + } + + builder.dataSystem(dataSystemBuilder); + } + return builder.build(); } + + private DataSourceBuilder createSynchronizer( + SdkConfigSynchronizerParams synchronizer, + String payloadFilter) { + if (synchronizer.polling != null) { + FDv2PollingSynchronizerBuilder pollingBuilder = DataSystemComponents.pollingSynchronizer(); + if (synchronizer.polling.baseUri != null) { + ServiceEndpointsBuilder endpointOverride = Components.serviceEndpoints().polling(synchronizer.polling.baseUri); + pollingBuilder.serviceEndpointsOverride(endpointOverride); + } + if (synchronizer.polling.pollIntervalMs != null) { + pollingBuilder.pollInterval(Duration.ofMillis(synchronizer.polling.pollIntervalMs)); + } + if (payloadFilter != null && !payloadFilter.isEmpty()) { + pollingBuilder.payloadFilter(payloadFilter); + } + return pollingBuilder; + } else if (synchronizer.streaming != null) { + FDv2StreamingSynchronizerBuilder streamingBuilder = DataSystemComponents.streamingSynchronizer(); + if (synchronizer.streaming.baseUri != null) { + ServiceEndpointsBuilder endpointOverride = Components.serviceEndpoints().streaming(synchronizer.streaming.baseUri); + streamingBuilder.serviceEndpointsOverride(endpointOverride); + } + if (synchronizer.streaming.initialRetryDelayMs != null) { + streamingBuilder.initialReconnectDelay(Duration.ofMillis(synchronizer.streaming.initialRetryDelayMs)); + } + if (payloadFilter != null && !payloadFilter.isEmpty()) { + streamingBuilder.payloadFilter(payloadFilter); + } + return streamingBuilder; + } + return null; + } } diff --git a/lib/sdk/server/contract-tests/test-suppressions-fdv2.txt b/lib/sdk/server/contract-tests/test-suppressions-fdv2.txt new file mode 100644 index 00000000..637ace30 --- /dev/null +++ b/lib/sdk/server/contract-tests/test-suppressions-fdv2.txt @@ -0,0 +1,4 @@ +streaming/validation/unrecognized data that can be safely ignored/unknown event name with JSON body +streaming/validation/unrecognized data that can be safely ignored/unknown event name with non-JSON body +streaming/validation/unrecognized data that can be safely ignored/patch event with unrecognized path kind +streaming/fdv2/fallback to FDv1 handling diff --git a/lib/sdk/server/contract-tests/test-suppressions.txt b/lib/sdk/server/contract-tests/test-suppressions.txt new file mode 100644 index 00000000..e69de29b diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSystemComponents.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSystemComponents.java index d9e01f31..89c1899a 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSystemComponents.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DataSystemComponents.java @@ -38,6 +38,7 @@ public Initializer build(DataSourceBuildInputs context) { toHttpProperties(context.getHttp()), configuredBaseUri, StandardEndpoints.FDV2_POLLING_REQUEST_PATH, + payloadFilter, context.getBaseLogger()); return new PollingInitializerImpl( @@ -64,6 +65,7 @@ public Synchronizer build(DataSourceBuildInputs context) { toHttpProperties(context.getHttp()), configuredBaseUri, StandardEndpoints.FDV2_POLLING_REQUEST_PATH, + payloadFilter, context.getBaseLogger()); return new PollingSynchronizerImpl( @@ -94,7 +96,7 @@ public Synchronizer build(DataSourceBuildInputs context) { StandardEndpoints.FDV2_STREAMING_REQUEST_PATH, context.getBaseLogger(), context.getSelectorSource(), - null, + payloadFilter, initialReconnectDelay ); } diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DefaultFDv2Requestor.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DefaultFDv2Requestor.java index 133a56aa..b08b5c2f 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DefaultFDv2Requestor.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/DefaultFDv2Requestor.java @@ -32,12 +32,14 @@ */ public class DefaultFDv2Requestor implements FDv2Requestor, Closeable { private static final String BASIS_QUERY_PARAM = "basis"; + private static final String FILTER_QUERY_PARAM = "filter"; private final OkHttpClient httpClient; private final URI pollingUri; private final Headers headers; private final LDLogger logger; private final Map etags; + private final String payloadFilter; /** * Creates a DefaultFDv2Requestor. @@ -45,12 +47,14 @@ public class DefaultFDv2Requestor implements FDv2Requestor, Closeable { * @param httpProperties HTTP configuration properties * @param baseUri base URI for the FDv2 polling endpoint * @param requestPath the request path to append to the base URI (e.g., "/sdk/poll") + * @param payloadFilter optional payload filter to add as a query parameter * @param logger logger for diagnostic output */ - public DefaultFDv2Requestor(HttpProperties httpProperties, URI baseUri, String requestPath, LDLogger logger) { + public DefaultFDv2Requestor(HttpProperties httpProperties, URI baseUri, String requestPath, String payloadFilter, LDLogger logger) { this.logger = logger; this.pollingUri = HttpHelpers.concatenateUriPath(baseUri, requestPath); this.etags = new HashMap<>(); + this.payloadFilter = payloadFilter; OkHttpClient.Builder httpBuilder = httpProperties.toHttpClientBuilder(); this.headers = httpProperties.toHeadersBuilder().build(); @@ -69,6 +73,11 @@ public CompletableFuture Poll(Selector selector) { requestUri = HttpHelpers.addQueryParam(requestUri, BASIS_QUERY_PARAM, selector.getState()); } + // Add payload filter query parameter if present + if (payloadFilter != null && !payloadFilter.isEmpty()) { + requestUri = HttpHelpers.addQueryParam(requestUri, FILTER_QUERY_PARAM, payloadFilter); + } + logger.debug("Making FDv2 polling request to: {}", requestUri); // Build the HTTP request diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2PollingInitializerBuilder.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2PollingInitializerBuilder.java index 1dcc5f6a..07af90bd 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2PollingInitializerBuilder.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2PollingInitializerBuilder.java @@ -32,6 +32,8 @@ public abstract class FDv2PollingInitializerBuilder implements DataSourceBuilder, DiagnosticDescription { protected ServiceEndpoints serviceEndpointsOverride; + protected String payloadFilter; + /** * Sets overrides for the service endpoints. In typical usage, the initializer will use the commonly defined * service endpoints, but for cases where they need to be controlled at the source level, this method can @@ -45,6 +47,18 @@ public FDv2PollingInitializerBuilder serviceEndpointsOverride(ServiceEndpointsBu return this; } + /** + * Sets the Payload Filter that will be used to filter the objects (flags, segments, etc.) + * from this initializer. + * + * @param payloadFilter the filter to be used + * @return the builder + */ + public FDv2PollingInitializerBuilder payloadFilter(String payloadFilter) { + this.payloadFilter = payloadFilter; + return this; + } + @Override public LDValue describeConfiguration(ClientContext context) { ServiceEndpoints endpoints = serviceEndpointsOverride != null diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2PollingSynchronizerBuilder.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2PollingSynchronizerBuilder.java index f3ec5219..d56e6da0 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2PollingSynchronizerBuilder.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2PollingSynchronizerBuilder.java @@ -42,6 +42,8 @@ public abstract class FDv2PollingSynchronizerBuilder implements DataSourceBuilde protected ServiceEndpoints serviceEndpointsOverride; + protected String payloadFilter; + /** * Sets the interval at which the SDK will poll for feature flag updates. *

@@ -83,6 +85,18 @@ public FDv2PollingSynchronizerBuilder serviceEndpointsOverride(ServiceEndpointsB return this; } + /** + * Sets the Payload Filter that will be used to filter the objects (flags, segments, etc.) + * from this synchronizer. + * + * @param payloadFilter the filter to be used + * @return the builder + */ + public FDv2PollingSynchronizerBuilder payloadFilter(String payloadFilter) { + this.payloadFilter = payloadFilter; + return this; + } + @Override public LDValue describeConfiguration(ClientContext context) { ServiceEndpoints endpoints = serviceEndpointsOverride != null diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2StreamingSynchronizerBuilder.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2StreamingSynchronizerBuilder.java index 5464acf8..d583aa16 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2StreamingSynchronizerBuilder.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/FDv2StreamingSynchronizerBuilder.java @@ -41,6 +41,8 @@ public abstract class FDv2StreamingSynchronizerBuilder implements DataSourceBuil protected ServiceEndpoints serviceEndpointsOverride; + protected String payloadFilter; + /** * Sets the initial reconnect delay for the streaming connection. *

@@ -73,6 +75,18 @@ public FDv2StreamingSynchronizerBuilder serviceEndpointsOverride(ServiceEndpoint return this; } + /** + * Sets the Payload Filter that will be used to filter the objects (flags, segments, etc.) + * from this synchronizer. + * + * @param payloadFilter the filter to be used + * @return the builder + */ + public FDv2StreamingSynchronizerBuilder payloadFilter(String payloadFilter) { + this.payloadFilter = payloadFilter; + return this; + } + @Override public LDValue describeConfiguration(ClientContext context) { ServiceEndpoints endpoints = serviceEndpointsOverride != null diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DefaultFDv2RequestorTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DefaultFDv2RequestorTest.java index 5ce321bb..000db065 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DefaultFDv2RequestorTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DefaultFDv2RequestorTest.java @@ -19,6 +19,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -84,7 +85,11 @@ private DefaultFDv2Requestor makeRequestor(HttpServer server) { } private DefaultFDv2Requestor makeRequestor(HttpServer server, LDConfig config) { - return new DefaultFDv2Requestor(makeHttpConfig(config), server.getUri(), REQUEST_PATH, testLogger); + return new DefaultFDv2Requestor(makeHttpConfig(config), server.getUri(), REQUEST_PATH, null, testLogger); + } + + private DefaultFDv2Requestor makeRequestorWithFilter(HttpServer server, String payloadFilter) { + return new DefaultFDv2Requestor(makeHttpConfig(LDConfig.DEFAULT), server.getUri(), REQUEST_PATH, payloadFilter, testLogger); } private HttpProperties makeHttpConfig(LDConfig config) { @@ -380,7 +385,7 @@ public void baseUriCanHaveContextPath() throws Exception { URI uri = server.getUri().resolve("/context/path"); try (DefaultFDv2Requestor requestor = new DefaultFDv2Requestor( - makeHttpConfig(LDConfig.DEFAULT), uri, REQUEST_PATH, testLogger)) { + makeHttpConfig(LDConfig.DEFAULT), uri, REQUEST_PATH, null, testLogger)) { CompletableFuture future = requestor.Poll(Selector.EMPTY); @@ -443,4 +448,75 @@ public void responseHeadersAreIncluded() throws Exception { } } } -} \ No newline at end of file + + @Test + public void payloadFilterIsAddedToRequest() throws Exception { + Handler resp = Handlers.bodyJson(EMPTY_EVENTS_JSON); + + try (HttpServer server = HttpServer.start(resp)) { + try (DefaultFDv2Requestor requestor = makeRequestorWithFilter(server, "myFilter")) { + CompletableFuture future = + requestor.Poll(Selector.EMPTY); + + future.get(5, TimeUnit.SECONDS); + + RequestInfo req = server.getRecorder().requireRequest(); + assertThat(req.getQuery(), containsString("filter=myFilter")); + } + } + } + + @Test + public void payloadFilterWithSelectorBothAddedToRequest() throws Exception { + Handler resp = Handlers.bodyJson(EMPTY_EVENTS_JSON); + + try (HttpServer server = HttpServer.start(resp)) { + try (DefaultFDv2Requestor requestor = makeRequestorWithFilter(server, "myFilter")) { + Selector selector = Selector.make(42, "test-state"); + + CompletableFuture future = + requestor.Poll(selector); + + future.get(5, TimeUnit.SECONDS); + + RequestInfo req = server.getRecorder().requireRequest(); + assertThat(req.getQuery(), containsString("basis=test-state")); + assertThat(req.getQuery(), containsString("filter=myFilter")); + } + } + } + + @Test + public void payloadFilterNotAddedWhenNull() throws Exception { + Handler resp = Handlers.bodyJson(EMPTY_EVENTS_JSON); + + try (HttpServer server = HttpServer.start(resp)) { + try (DefaultFDv2Requestor requestor = makeRequestor(server)) { + CompletableFuture future = + requestor.Poll(Selector.EMPTY); + + future.get(5, TimeUnit.SECONDS); + + RequestInfo req = server.getRecorder().requireRequest(); + assertThat(req.getQuery(), not(containsString("filter="))); + } + } + } + + @Test + public void payloadFilterNotAddedWhenEmpty() throws Exception { + Handler resp = Handlers.bodyJson(EMPTY_EVENTS_JSON); + + try (HttpServer server = HttpServer.start(resp)) { + try (DefaultFDv2Requestor requestor = makeRequestorWithFilter(server, "")) { + CompletableFuture future = + requestor.Poll(Selector.EMPTY); + + future.get(5, TimeUnit.SECONDS); + + RequestInfo req = server.getRecorder().requireRequest(); + assertThat(req.getQuery(), not(containsString("filter="))); + } + } + } +}