diff --git a/.github/actions/contract-tests/action.yml b/.github/actions/contract-tests/action.yml index 468cd951..35431398 100644 --- a/.github/actions/contract-tests/action.yml +++ b/.github/actions/contract-tests/action.yml @@ -11,6 +11,10 @@ inputs: description: 'Github token, used for contract tests' required: false default: '' + run_fdv2_tests: + description: 'Whether to run contract tests from the feat/fdv2 branch' + required: false + default: 'false' runs: using: composite @@ -47,3 +51,29 @@ runs: test_service_port: 8000 extra_params: '-status-timeout=360 -skip-from=${{ env.SUPPRESSION_FILE }}' token: ${{ inputs.token }} + + - name: Setup Go + if: inputs.run_fdv2_tests == 'true' + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Launch Contract Tests FDv2 Flavor + if: inputs.run_fdv2_tests == 'true' + id: launch-contract-tests-fdv2 + shell: bash + run: dotnet ${{ inputs.service_dll_file }} > test-service.log 2>&1 & disown + + - name: Clone and run contract tests from feat/fdv2 branch + if: inputs.run_fdv2_tests == 'true' + shell: bash + run: | + mkdir -p /tmp/sdk-test-harness + git clone https://github.com/launchdarkly/sdk-test-harness.git /tmp/sdk-test-harness + cp $(dirname ./${{ inputs.service_project_file }})/test-supressions-fdv2.txt /tmp/sdk-test-harness/testharness-suppressions-fdv2.txt + cd /tmp/sdk-test-harness + git checkout feat/fdv2 + go build -o test-harness . + ./test-harness -url http://localhost:8000 -debug -status-timeout=360 --skip-from=testharness-suppressions-fdv2.txt --stop-service-at-end + env: + GITHUB_TOKEN: ${{ inputs.token }} diff --git a/.github/workflows/sdk-server-ci.yml b/.github/workflows/sdk-server-ci.yml index 8a0a78d4..d43b4dc8 100644 --- a/.github/workflows/sdk-server-ci.yml +++ b/.github/workflows/sdk-server-ci.yml @@ -36,6 +36,7 @@ jobs: service_project_file: ${{ env.CONTRACT_TEST_PROJECT_FILE}} service_dll_file: ${{ env.CONTRACT_TEST_DLL_FILE}} token: ${{ secrets.GITHUB_TOKEN }} + run_fdv2_tests: 'true' - uses: ./.github/actions/build-docs with: diff --git a/pkgs/sdk/server/contract-tests/Representations.cs b/pkgs/sdk/server/contract-tests/Representations.cs index 8e775bf6..43b3fdf6 100644 --- a/pkgs/sdk/server/contract-tests/Representations.cs +++ b/pkgs/sdk/server/contract-tests/Representations.cs @@ -31,6 +31,7 @@ public class SdkConfigParams public SdkConfigBigSegmentsParams BigSegments { get; set; } public SdkTagParams Tags { get; set; } public SdkHookParams Hooks { get; set; } + public SdkConfigDataSystemParams DataSystem { get; set; } } public class SdkTagParams @@ -88,6 +89,126 @@ public class SdkConfigBigSegmentsParams public long? UserCacheTimeMs { get; set; } } + /// + /// Constants for store mode values. + /// + public static class StoreMode + { + /// + /// Read-only mode - the data system will only read from the persistent store. + /// + public const int Read = 0; + + /// + /// Read-write mode - the data system can read from, and write to, the persistent store. + /// + public const int ReadWrite = 1; + } + + /// + /// Constants for persistent store type values. + /// + public static class PersistentStoreType + { + /// + /// Redis persistent store type. + /// + public const string Redis = "redis"; + + /// + /// DynamoDB persistent store type. + /// + public const string DynamoDB = "dynamodb"; + + /// + /// Consul persistent store type. + /// + public const string Consul = "consul"; + } + + /// + /// Constants for persistent cache mode values. + /// + public static class PersistentCacheMode + { + /// + /// Cache disabled mode. + /// + public const string Off = "off"; + + /// + /// Time-to-live cache mode with a specified TTL. + /// + public const string TTL = "ttl"; + + /// + /// Infinite cache mode - cache forever. + /// + public const string Infinite = "infinite"; + } + + public class SdkConfigDataSystemParams + { + public SdkConfigDataStoreParams Store { get; set; } + public int? StoreMode { get; set; } + public SdkConfigDataInitializerParams[] Initializers { get; set; } + public SdkConfigSynchronizersParams Synchronizers { get; set; } + public string PayloadFilter { get; set; } + } + + public class SdkConfigDataStoreParams + { + public SdkConfigPersistentDataStoreParams PersistentDataStore { get; set; } + } + + public class SdkConfigPersistentDataStoreParams + { + public SdkConfigPersistentStoreParams Store { get; set; } + public SdkConfigPersistentCacheParams Cache { get; set; } + } + + public class SdkConfigPersistentStoreParams + { + public string Type { get; set; } + public string Prefix { get; set; } + public string DSN { get; set; } + } + + public class SdkConfigPersistentCacheParams + { + public string Mode { get; set; } + public int? TTL { get; set; } + } + + public class SdkConfigDataInitializerParams + { + public SdkConfigPollingParams Polling { get; set; } + } + + public class SdkConfigSynchronizersParams + { + public SdkConfigSynchronizerParams Primary { get; set; } + public SdkConfigSynchronizerParams Secondary { get; set; } + } + + public class SdkConfigSynchronizerParams + { + public SdkConfigStreamingParams Streaming { get; set; } + public SdkConfigPollingParams Polling { get; set; } + } + + public class SdkConfigPollingParams + { + public Uri BaseUri { get; set; } + public long? PollIntervalMs { get; set; } + } + + public class SdkConfigStreamingParams + { + public Uri BaseUri { get; set; } + public long? InitialRetryDelayMs { get; set; } + } + public class CommandParams { public string Command { get; set; } diff --git a/pkgs/sdk/server/contract-tests/SdkClientEntity.cs b/pkgs/sdk/server/contract-tests/SdkClientEntity.cs index 2c1ca66f..f0d62a8e 100644 --- a/pkgs/sdk/server/contract-tests/SdkClientEntity.cs +++ b/pkgs/sdk/server/contract-tests/SdkClientEntity.cs @@ -7,7 +7,9 @@ using LaunchDarkly.Sdk.Json; using LaunchDarkly.Sdk.Server; using LaunchDarkly.Sdk.Server.Hooks; +using LaunchDarkly.Sdk.Server.Integrations; using LaunchDarkly.Sdk.Server.Migrations; +using LaunchDarkly.Sdk.Server.Subsystems; namespace TestService { @@ -372,9 +374,200 @@ private static Configuration BuildSdkConfig(SdkConfigParams sdkParams, ILogAdapt builder.Hooks(Components.Hooks(hooks)); } + if (sdkParams.DataSystem != null) + { + var dataSystemBuilder = Components.DataSystem().Custom(); + + // TODO: re-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 (sdkParams.DataSystem.Store?.PersistentDataStore != null) + // { + // var storeConfig = sdkParams.DataSystem.Store.PersistentDataStore; + // var storeType = storeConfig.Store.Type.ToLower(); + // IComponentConfigurer persistentStore = null; + + // PersistentDataStoreBuilder persistentStoreBuilder = null; + + // switch (storeType) + // { + // case "redis": + // var redisBuilder = Redis.DataStore(); + // if (!string.IsNullOrEmpty(storeConfig.Store.DSN)) + // { + // redisBuilder.Uri(storeConfig.Store.DSN); + // } + // if (!string.IsNullOrEmpty(storeConfig.Store.Prefix)) + // { + // redisBuilder.Prefix(storeConfig.Store.Prefix); + // } + // persistentStoreBuilder = Components.PersistentDataStore(redisBuilder); + // break; + + // case "dynamodb": + // // For DynamoDB, DSN is the table name + // var dynamoBuilder = DynamoDB.DataStore(storeConfig.Store.DSN ?? "sdk-contract-tests"); + // if (!string.IsNullOrEmpty(storeConfig.Store.Prefix)) + // { + // dynamoBuilder.Prefix(storeConfig.Store.Prefix); + // } + // // DynamoDB uses IPersistentDataStoreAsync, so use the async overload + // persistentStoreBuilder = Components.PersistentDataStore(dynamoBuilder); + // break; + + // case "consul": + // var consulBuilder = Consul.DataStore(); + // if (!string.IsNullOrEmpty(storeConfig.Store.DSN)) + // { + // consulBuilder.Address(storeConfig.Store.DSN); + // } + // if (!string.IsNullOrEmpty(storeConfig.Store.Prefix)) + // { + // consulBuilder.Prefix(storeConfig.Store.Prefix); + // } + // persistentStoreBuilder = Components.PersistentDataStore(consulBuilder); + // break; + // } + + // if (persistentStoreBuilder != null) + // { + // // Configure cache + // var cacheMode = storeConfig.Cache?.Mode?.ToLower(); + // if (cacheMode == "off") + // { + // persistentStoreBuilder.NoCaching(); + // } + // else if (cacheMode == "ttl" && storeConfig.Cache.TTL.HasValue) + // { + // persistentStoreBuilder.CacheTime(TimeSpan.FromSeconds(storeConfig.Cache.TTL.Value)); + // } + // else if (cacheMode == "infinite") + // { + // persistentStoreBuilder.CacheForever(); + // } + + // // Determine store mode + // var storeMode = sdkParams.DataSystem.StoreMode == 0 + // ? DataSystemConfiguration.DataStoreMode.ReadOnly + // : DataSystemConfiguration.DataStoreMode.ReadWrite; + + // dataSystemBuilder.PersistentStore(persistentStoreBuilder, storeMode); + // } + // } + + // Configure initializers + if (sdkParams.DataSystem.Initializers != null && sdkParams.DataSystem.Initializers.Length > 0) + { + var initializers = new List>(); + foreach (var initializer in sdkParams.DataSystem.Initializers) + { + if (initializer.Polling != null) + { + var pollingBuilder = DataSystemComponents.Polling(); + if (initializer.Polling.BaseUri != null) + { + var endpointOverride = Components.ServiceEndpoints().Polling(initializer.Polling.BaseUri); + pollingBuilder.ServiceEndpointsOverride(endpointOverride); + } + if (initializer.Polling.PollIntervalMs.HasValue) + { + pollingBuilder.PollInterval(TimeSpan.FromMilliseconds(initializer.Polling.PollIntervalMs.Value)); + } + if (!string.IsNullOrEmpty(sdkParams.DataSystem.PayloadFilter)) + { + // PayloadFilter is not yet supported in FDv2 builders, so we skip it for now + // TODO: Add PayloadFilter support when available + } + initializers.Add(pollingBuilder); + } + } + if (initializers.Count > 0) + { + dataSystemBuilder.Initializers(initializers.ToArray()); + } + } + + // Configure synchronizers + if (sdkParams.DataSystem.Synchronizers != null) + { + var synchronizers = new List>(); + + // Primary synchronizer + if (sdkParams.DataSystem.Synchronizers.Primary != null) + { + var primary = CreateSynchronizer(sdkParams.DataSystem.Synchronizers.Primary, sdkParams.DataSystem.PayloadFilter); + if (primary != null) + { + synchronizers.Add(primary); + } + } + + // Secondary synchronizer (optional) + if (sdkParams.DataSystem.Synchronizers.Secondary != null) + { + var secondary = CreateSynchronizer(sdkParams.DataSystem.Synchronizers.Secondary, sdkParams.DataSystem.PayloadFilter); + if (secondary != null) + { + synchronizers.Add(secondary); + } + } + + if (synchronizers.Count > 0) + { + dataSystemBuilder.Synchronizers(synchronizers.ToArray()); + } + } + + builder.DataSystem(dataSystemBuilder); + } + return builder.Build(); } + private static IComponentConfigurer CreateSynchronizer( + SdkConfigSynchronizerParams synchronizer, + string payloadFilter) + { + if (synchronizer.Polling != null) + { + var pollingBuilder = DataSystemComponents.Polling(); + if (synchronizer.Polling.BaseUri != null) + { + var endpointOverride = Components.ServiceEndpoints().Polling(synchronizer.Polling.BaseUri); + pollingBuilder.ServiceEndpointsOverride(endpointOverride); + } + if (synchronizer.Polling.PollIntervalMs.HasValue) + { + pollingBuilder.PollInterval(TimeSpan.FromMilliseconds(synchronizer.Polling.PollIntervalMs.Value)); + } + if (!string.IsNullOrEmpty(payloadFilter)) + { + // PayloadFilter is not yet supported in FDv2 builders, so we skip it for now + // TODO: Add PayloadFilter support when available + } + return pollingBuilder; + } + else if (synchronizer.Streaming != null) + { + var streamingBuilder = DataSystemComponents.Streaming(); + if (synchronizer.Streaming.BaseUri != null) + { + var endpointOverride = Components.ServiceEndpoints().Streaming(synchronizer.Streaming.BaseUri); + streamingBuilder.ServiceEndpointsOverride(endpointOverride); + } + if (synchronizer.Streaming.InitialRetryDelayMs.HasValue) + { + streamingBuilder.InitialReconnectDelay(TimeSpan.FromMilliseconds(synchronizer.Streaming.InitialRetryDelayMs.Value)); + } + if (!string.IsNullOrEmpty(payloadFilter)) + { + // PayloadFilter is not yet supported in FDv2 builders, so we skip it for now + // TODO: Add PayloadFilter support when available + } + return streamingBuilder; + } + return null; + } + private MigrationVariationResponse DoMigrationVariation(MigrationVariationParams migrationVariation) { var defaultStage = MigrationStageExtensions.FromDataModelString(migrationVariation.DefaultStage); diff --git a/pkgs/sdk/server/contract-tests/test-supressions-fdv2.txt b/pkgs/sdk/server/contract-tests/test-supressions-fdv2.txt new file mode 100644 index 00000000..241f5661 --- /dev/null +++ b/pkgs/sdk/server/contract-tests/test-supressions-fdv2.txt @@ -0,0 +1,10 @@ +streaming/ +evaluation/ +hooks/ +migrations/ +secure mode hash/ +context type/ +tags/ +big segments/ +events/ +service endpoints/ \ No newline at end of file diff --git a/pkgs/sdk/server/src/Properties/AssemblyInfo.cs b/pkgs/sdk/server/src/Properties/AssemblyInfo.cs index ab67d42e..fd617be8 100644 --- a/pkgs/sdk/server/src/Properties/AssemblyInfo.cs +++ b/pkgs/sdk/server/src/Properties/AssemblyInfo.cs @@ -15,6 +15,9 @@ // tests must be run against the Debug configuration of this assembly) [assembly: InternalsVisibleTo("LaunchDarkly.ServerSdk.Tests")] +// Allow contract tests to see internal classes +[assembly: InternalsVisibleTo("ContractTestService")] + // Allow mock/proxy objects in unit tests to access internal classes [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] #endif