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 cd7593e7..a2e03b1c 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 @@ -34,6 +34,21 @@ public static class SdkConfigParams { SdkConfigServiceEndpointParams serviceEndpoints; SdkConfigHookParams hooks; SdkConfigDataSystemParams dataSystem; + SdkConfigOverridesParams overrides; + } + + /** + * Configuration for the SDK's file-based flag overrides (the "flag-overrides" capability). + */ + public static class SdkConfigOverridesParams { + /** Absolute paths of the override files, in precedence order. */ + String[] filePaths; + /** "fail" (the default) or "ignore". */ + String duplicateKeysHandling; + /** "polling" or "watching"; null means the SDK default. */ + String changeDetection; + /** The polling interval in milliseconds; the SDK may raise it to its minimum. */ + Long pollIntervalMs; } public static class SdkConfigStreamParams { 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 f6bb9d96..53da61ac 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 @@ -32,6 +32,9 @@ 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.integrations.FileData; +import com.launchdarkly.sdk.server.integrations.FileOverrideSourceBuilder; +import com.launchdarkly.sdk.server.integrations.FileOverrides; import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider; import com.launchdarkly.sdk.server.subsystems.DataSourceBuilder; import com.launchdarkly.sdk.server.datasources.Initializer; @@ -68,6 +71,7 @@ import sdktest.Representations.SdkConfigParams; import sdktest.Representations.SdkConfigDataSystemParams; import sdktest.Representations.SdkConfigDataInitializerParams; +import sdktest.Representations.SdkConfigOverridesParams; import sdktest.Representations.SdkConfigSynchronizerParams; import sdktest.Representations.SdkConfigPollingParams; import sdktest.Representations.SdkConfigStreamingParams; @@ -587,12 +591,53 @@ private LDConfig buildSdkConfig(SdkConfigParams params, String tag) { dataSystemBuilder.fDv1FallbackSynchronizer(fdv1Polling); } + if (params.overrides != null) { + dataSystemBuilder.overrides(makeOverridesConfig(params.overrides)); + } + builder.dataSystem(dataSystemBuilder); + } else if (params.overrides != null) { + throw new IllegalArgumentException("flag overrides require the data system to be configured"); } return builder.build(); } + private static FileOverrideSourceBuilder makeOverridesConfig(SdkConfigOverridesParams params) { + FileOverrideSourceBuilder overrides = FileOverrides.source(); + if (params.filePaths != null) { + overrides.filePaths(params.filePaths); + } + if (params.duplicateKeysHandling != null) { + switch (params.duplicateKeysHandling) { + case "fail": + overrides.duplicateKeysHandling(FileData.DuplicateKeysHandling.FAIL); + break; + case "ignore": + overrides.duplicateKeysHandling(FileData.DuplicateKeysHandling.IGNORE); + break; + default: + throw new IllegalArgumentException("unknown duplicate keys handling: " + params.duplicateKeysHandling); + } + } + if (params.changeDetection != null) { + switch (params.changeDetection) { + case "polling": + overrides.changeDetection(FileOverrides.ChangeDetection.POLLING); + break; + case "watching": + overrides.changeDetection(FileOverrides.ChangeDetection.WATCHING); + break; + default: + throw new IllegalArgumentException("unknown change detection mode: " + params.changeDetection); + } + } + if (params.pollIntervalMs != null) { + overrides.pollInterval(Duration.ofMillis(params.pollIntervalMs)); + } + return overrides; + } + private DataSourceBuilder createSynchronizer( SdkConfigSynchronizerParams synchronizer, String payloadFilter) { diff --git a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java index 45eeead7..51f9fb4e 100644 --- a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java +++ b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java @@ -21,7 +21,7 @@ import sdktest.Representations.Status; public class TestService { - private static final int PORT = 8000; + private static final int DEFAULT_PORT = 8000; private static final String[] CAPABILITIES = new String[]{ "all-flags-client-side-only", "all-flags-details-only-for-tracked-flags", @@ -47,7 +47,8 @@ public class TestService { "fdv1-fallback", "instance-id", "retry-conformance-fdv1-streaming", - "retry-conformance-fdv1-polling" + "retry-conformance-fdv1-polling", + "flag-overrides" }; static final Gson gson = new GsonBuilder().serializeNulls().create(); @@ -83,10 +84,15 @@ public static void main(String[] args) throws Exception { .addRegex("POST", Pattern.compile("/clients/(.*)"), ctx -> service.postClientCommand(diableKeepAlive(ctx))) .addRegex("DELETE", Pattern.compile("/clients/(.*)"), ctx -> service.deleteClient(diableKeepAlive(ctx))); - HttpServer server = HttpServer.start(PORT, router); + int port = DEFAULT_PORT; + String portEnv = System.getenv("PORT"); + if (portEnv != null && !portEnv.isEmpty()) { + port = Integer.parseInt(portEnv); + } + HttpServer server = HttpServer.start(port, router); server.getRecorder().setEnabled(false); // don't accumulate a request log - System.out.println("Listening on port " + PORT); + System.out.println("Listening on port " + port); // need to explicitly sleep because HttpServer now starts as a daemon thread while (true) {