diff --git a/LICENSE b/LICENSE index 3f60711f..0cda54e6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 ConfigCat +Copyright (c) 2020 ConfigCat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/ConfigCat.Client.Tests/BaseUrlTests.cs b/src/ConfigCat.Client.Tests/BaseUrlTests.cs index e51eb176..04dd0544 100644 --- a/src/ConfigCat.Client.Tests/BaseUrlTests.cs +++ b/src/ConfigCat.Client.Tests/BaseUrlTests.cs @@ -3,6 +3,7 @@ namespace ConfigCat.Client.Tests { + [TestCategory(TestCategories.Integration)] [TestClass] public class BaseUrlTests { diff --git a/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs index 6da9c44e..026e4f05 100644 --- a/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs @@ -6,7 +6,7 @@ namespace ConfigCat.Client.Tests [TestClass] public class BasicConfigEvaluatorTests : ConfigEvaluatorTestsBase { - protected override string SampleJsonFileName => "sample_v4.json"; + protected override string SampleJsonFileName => "sample_v5.json"; protected override string MatrixResultFileName => "testmatrix.csv"; diff --git a/src/ConfigCat.Client.Tests/ConfigCacheTests.cs b/src/ConfigCat.Client.Tests/ConfigCacheTests.cs index 6a1f893d..6caf4a9a 100644 --- a/src/ConfigCat.Client.Tests/ConfigCacheTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCacheTests.cs @@ -1,8 +1,11 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; namespace ConfigCat.Client.Tests { + [TestCategory(TestCategories.Integration)] [TestClass] public class ConfigCacheTests { @@ -13,20 +16,27 @@ public void ConfigCache_Override_AutoPoll_Works() { ProjectConfig cachedConfig = ProjectConfig.Empty; Mock configCacheMock = new Mock(); - configCacheMock.Setup(c => c.Set(It.IsAny())).Callback((config) => + + configCacheMock.Setup(c => c.SetAsync(It.IsAny(), It.IsAny())).Callback((key, config) => { cachedConfig = config; }); - configCacheMock.Setup(c => c.Get()).Returns(() => cachedConfig); - - var client = ConfigCatClientBuilder.Initialize(SDKKEY).WithAutoPoll().WithConfigCache(configCacheMock.Object).Create(); - + configCacheMock.Setup(c => c.GetAsync(It.IsAny(), CancellationToken.None)).ReturnsAsync(() => cachedConfig); + + var client = ConfigCatClientBuilder + .Initialize(SDKKEY) + .WithLogger(new ConsoleLogger(LogLevel.Debug)) + .WithAutoPoll() + .WithConfigCache(configCacheMock.Object) + .Create(); + var actual = client.GetValue("stringDefaultCat", "N/A"); + Assert.AreEqual("Cat", actual); - configCacheMock.Verify(c => c.Set(It.IsAny()), Times.AtLeastOnce); - configCacheMock.Verify(c => c.Get(), Times.AtLeastOnce); + configCacheMock.Verify(c => c.SetAsync(It.IsAny(), It.IsAny()), Times.AtLeastOnce); + configCacheMock.Verify(c => c.GetAsync(It.IsAny(), CancellationToken.None), Times.AtLeastOnce); } [TestMethod] @@ -34,29 +44,30 @@ public void ConfigCache_Override_ManualPoll_Works() { ProjectConfig cachedConfig = ProjectConfig.Empty; Mock configCacheMock = new Mock(); - configCacheMock.Setup(c => c.Set(It.IsAny())).Callback((config) => + configCacheMock.Setup(c => c.SetAsync(It.IsAny(), It.IsAny())).Callback((key, config) => { cachedConfig = config; }); - configCacheMock.Setup(c => c.Get()).Returns(() => cachedConfig); + configCacheMock.Setup(c => c.GetAsync(It.IsAny(), CancellationToken.None)).ReturnsAsync(() => cachedConfig); var client = ConfigCatClientBuilder.Initialize(SDKKEY).WithManualPoll().WithConfigCache(configCacheMock.Object).Create(); - configCacheMock.Verify(c => c.Set(It.IsAny()), Times.Never); - configCacheMock.Verify(c => c.Get(), Times.Never); + configCacheMock.Verify(c => c.SetAsync(It.IsAny(), It.IsAny()), Times.Never); + configCacheMock.Verify(c => c.GetAsync(It.IsAny(), CancellationToken.None), Times.Never); var actual = client.GetValue("stringDefaultCat", "N/A"); + Assert.AreEqual("N/A", actual); - configCacheMock.Verify(c => c.Set(It.IsAny()), Times.Never); - configCacheMock.Verify(c => c.Get(), Times.Once); + configCacheMock.Verify(c => c.SetAsync(It.IsAny(), It.IsAny()), Times.Never); + configCacheMock.Verify(c => c.GetAsync(It.IsAny(), CancellationToken.None), Times.Once); client.ForceRefresh(); actual = client.GetValue("stringDefaultCat", "N/A"); Assert.AreEqual("Cat", actual); - configCacheMock.Verify(c => c.Set(It.IsAny()), Times.Once); - configCacheMock.Verify(c => c.Get(), Times.Exactly(3)); + configCacheMock.Verify(c => c.SetAsync(It.IsAny(), It.IsAny()), Times.Once); + configCacheMock.Verify(c => c.GetAsync(It.IsAny(), CancellationToken.None), Times.Exactly(3)); } [TestMethod] @@ -64,20 +75,20 @@ public void ConfigCache_Override_LazyLoad_Works() { ProjectConfig cachedConfig = ProjectConfig.Empty; Mock configCacheMock = new Mock(); - configCacheMock.Setup(c => c.Set(It.IsAny())).Callback((config) => + configCacheMock.Setup(c => c.SetAsync(It.IsAny(), It.IsAny())).Callback((key, config) => { cachedConfig = config; }); - configCacheMock.Setup(c => c.Get()).Returns(() => cachedConfig); + configCacheMock.Setup(c => c.GetAsync(It.IsAny(), CancellationToken.None)).ReturnsAsync(() => cachedConfig); var client = ConfigCatClientBuilder.Initialize(SDKKEY).WithLazyLoad().WithConfigCache(configCacheMock.Object).Create(); var actual = client.GetValue("stringDefaultCat", "N/A"); Assert.AreEqual("Cat", actual); - configCacheMock.Verify(c => c.Set(It.IsAny()), Times.AtLeastOnce); - configCacheMock.Verify(c => c.Get(), Times.AtLeastOnce); + configCacheMock.Verify(c => c.SetAsync(It.IsAny(), It.IsAny()), Times.AtLeastOnce); + configCacheMock.Verify(c => c.GetAsync(It.IsAny(), CancellationToken.None), Times.AtLeastOnce); } } } diff --git a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj index 29736a74..ea1ff8c7 100644 --- a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj +++ b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj @@ -14,7 +14,7 @@ - + @@ -26,22 +26,22 @@ - + Always - + Always - + Always - + Always - + Always - + Always diff --git a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs index c8a1eb50..3e69fd0a 100644 --- a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using System.Linq; using System.Collections.Generic; +using ConfigCat.Client.Cache; namespace ConfigCat.Client.Tests { @@ -13,18 +14,18 @@ namespace ConfigCat.Client.Tests [TestClass] public class ConfigCatClientTests { - Mock configService = new Mock(); + Mock configServiceMock = new Mock(); Mock loggerMock = new Mock(); - Mock evaluateMock = new Mock(); - Mock deserializerMock = new Mock(); + Mock evaluatorMock = new Mock(); + Mock configDeserializerMock = new Mock(); [TestInitialize] public void TestInitialize() { - configService.Reset(); + configServiceMock.Reset(); loggerMock.Reset(); - evaluateMock.Reset(); - deserializerMock.Reset(); + evaluatorMock.Reset(); + configDeserializerMock.Reset(); } [ExpectedException(typeof(ArgumentException))] @@ -170,11 +171,11 @@ public void GetValue_ConfigServiceThrowException_ShouldReturnDefaultValue() const string defaultValue = "Victory for the Firstborn!"; - configService + configServiceMock .Setup(m => m.GetConfigAsync()) .Throws(); - var client = new ConfigCatClient(configService.Object, loggerMock.Object, evaluateMock.Object, deserializerMock.Object); + var client = new ConfigCatClient(configServiceMock.Object, loggerMock.Object, evaluatorMock.Object, configDeserializerMock.Object); // Act @@ -192,11 +193,11 @@ public async Task GetValueAsync_ConfigServiceThrowException_ShouldReturnDefaultV const string defaultValue = "Victory for the Firstborn!"; - configService + configServiceMock .Setup(m => m.GetConfigAsync()) .Throws(); - var client = new ConfigCatClient(configService.Object, loggerMock.Object, evaluateMock.Object, deserializerMock.Object); + var client = new ConfigCatClient(configServiceMock.Object, loggerMock.Object, evaluatorMock.Object, configDeserializerMock.Object); // Act @@ -214,11 +215,11 @@ public void GetValue_EvaluateServiceThrowException_ShouldReturnDefaultValue() const string defaultValue = "Victory for the Firstborn!"; - evaluateMock + evaluatorMock .Setup(m => m.Evaluate(It.IsAny(), It.IsAny(), defaultValue, null)) .Throws(); - var client = new ConfigCatClient(configService.Object, loggerMock.Object, evaluateMock.Object, deserializerMock.Object); + var client = new ConfigCatClient(configServiceMock.Object, loggerMock.Object, evaluatorMock.Object, configDeserializerMock.Object); // Act @@ -236,11 +237,11 @@ public async Task GetValueAsync_EvaluateServiceThrowException_ShouldReturnDefaul const string defaultValue = "Victory for the Firstborn!"; - evaluateMock + evaluatorMock .Setup(m => m.Evaluate(It.IsAny(), It.IsAny(), defaultValue, null)) .Throws(); - var client = new ConfigCatClient(configService.Object, loggerMock.Object, evaluateMock.Object, deserializerMock.Object); + var client = new ConfigCatClient(configServiceMock.Object, loggerMock.Object, evaluatorMock.Object, configDeserializerMock.Object); // Act @@ -256,11 +257,6 @@ public void GetAllKeys_ConfigServiceThrowException_ShouldReturnsWithEmptyArray() { // Arrange - var configServiceMock = new Mock(); - var loggerMock = new Mock(); - var evaluatorMock = new Mock(); - var configDeserializerMock = new Mock(); - configServiceMock.Setup(m => m.GetConfigAsync()).Throws(); IConfigCatClient instance = new ConfigCatClient( @@ -285,11 +281,6 @@ public void GetAllKeys_DeserializerThrowException_ShouldReturnsWithEmptyArray() { // Arrange - var configServiceMock = new Mock(); - var loggerMock = new Mock(); - var evaluatorMock = new Mock(); - var configDeserializerMock = new Mock(); - configServiceMock.Setup(m => m.GetConfigAsync()).ReturnsAsync(ProjectConfig.Empty); IDictionary o = new Dictionary(); configDeserializerMock @@ -318,11 +309,6 @@ public void GetAllKeys_DeserializeFailed_ShouldReturnsWithEmptyArray() { // Arrange - var configServiceMock = new Mock(); - var loggerMock = new Mock(); - var evaluatorMock = new Mock(); - var configDeserializerMock = new Mock(); - configServiceMock.Setup(m => m.GetConfigAsync()).ReturnsAsync(ProjectConfig.Empty); IDictionary o = new Dictionary(); configDeserializerMock @@ -353,11 +339,11 @@ public void GetVariationId_EvaluateServiceThrowException_ShouldReturnDefaultValu const string defaultValue = "Victory for the Firstborn!"; - evaluateMock + evaluatorMock .Setup(m => m.EvaluateVariationId(It.IsAny(), It.IsAny(), defaultValue, null)) .Throws(); - var client = new ConfigCatClient(configService.Object, loggerMock.Object, evaluateMock.Object, deserializerMock.Object); + var client = new ConfigCatClient(configServiceMock.Object, loggerMock.Object, evaluatorMock.Object, configDeserializerMock.Object); // Act @@ -375,11 +361,11 @@ public async Task GetVariationIdAsync_EvaluateServiceThrowException_ShouldReturn const string defaultValue = "Victory for the Firstborn!"; - evaluateMock + evaluatorMock .Setup(m => m.EvaluateVariationId(It.IsAny(), It.IsAny(), defaultValue, null)) .Throws(); - var client = new ConfigCatClient(configService.Object, loggerMock.Object, evaluateMock.Object, deserializerMock.Object); + var client = new ConfigCatClient(configServiceMock.Object, loggerMock.Object, evaluatorMock.Object, configDeserializerMock.Object); // Act @@ -395,11 +381,6 @@ public void GetVariationId_DeserializeFailed_ShouldReturnsWithEmptyArray() { // Arrange - var configServiceMock = new Mock(); - var loggerMock = new Mock(); - var evaluatorMock = new Mock(); - var configDeserializerMock = new Mock(); - configServiceMock.Setup(m => m.GetConfigAsync()).ReturnsAsync(ProjectConfig.Empty); IDictionary o = new Dictionary(); configDeserializerMock @@ -428,11 +409,6 @@ public async Task GetVariationIdAsync_DeserializeFailed_ShouldReturnsWithEmptyAr { // Arrange - var configServiceMock = new Mock(); - var loggerMock = new Mock(); - var evaluatorMock = new Mock(); - var configDeserializerMock = new Mock(); - configServiceMock.Setup(m => m.GetConfigAsync()).ReturnsAsync(ProjectConfig.Empty); IDictionary o = new Dictionary(); configDeserializerMock @@ -461,11 +437,11 @@ public void GetAllVariationId_ConfigServiceThrowException_ShouldReturnEmptyEnume { // Arrange - configService + configServiceMock .Setup(m => m.GetConfigAsync()) .Throws(); - var client = new ConfigCatClient(configService.Object, loggerMock.Object, evaluateMock.Object, deserializerMock.Object); + var client = new ConfigCatClient(configServiceMock.Object, loggerMock.Object, evaluatorMock.Object, configDeserializerMock.Object); // Act @@ -481,11 +457,11 @@ public async Task GetAllVariationIdAsync_ConfigServiceThrowException_ShouldRetur { // Arrange - configService + configServiceMock .Setup(m => m.GetConfigAsync()) .Throws(); - var client = new ConfigCatClient(configService.Object, loggerMock.Object, evaluateMock.Object, deserializerMock.Object); + var client = new ConfigCatClient(configServiceMock.Object, loggerMock.Object, evaluatorMock.Object, configDeserializerMock.Object); // Act @@ -495,5 +471,52 @@ public async Task GetAllVariationIdAsync_ConfigServiceThrowException_ShouldRetur Assert.AreEqual(Enumerable.Empty(), actual); } + + [TestMethod] + public void Dispose_ConfigServiceIsDisposable_ShouldInvokeDispose() + { + // Arrange + + var myMock = new FakeConfigService(Mock.Of(), new CacheParameters(), Mock.Of()); + + IConfigCatClient instance = new ConfigCatClient( + myMock, + loggerMock.Object, + evaluatorMock.Object, + configDeserializerMock.Object); + + // Act + + instance.Dispose(); + + // Assert + + Assert.AreEqual(1, myMock.DisposeCount); + } + + internal class FakeConfigService : ConfigServiceBase, IConfigService + { + public byte DisposeCount { get; private set; } + + public FakeConfigService(IConfigFetcher configFetcher, CacheParameters cacheParameters, ILogger log) : base(configFetcher, cacheParameters, log) + { + } + + protected override void Dispose(bool disposing) + { + DisposeCount++; + base.Dispose(disposing); + } + + public Task GetConfigAsync() + { + return Task.FromResult(ProjectConfig.Empty); + } + + public Task RefreshConfigAsync() + { + return Task.CompletedTask; + } + } } } diff --git a/src/ConfigCat.Client.Tests/ConfigServiceTests.cs b/src/ConfigCat.Client.Tests/ConfigServiceTests.cs index c3f2a8cc..fe6cf6a8 100644 --- a/src/ConfigCat.Client.Tests/ConfigServiceTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigServiceTests.cs @@ -1,12 +1,14 @@ using System; using System.Threading; using System.Threading.Tasks; +using ConfigCat.Client.Cache; using ConfigCat.Client.ConfigService; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; namespace ConfigCat.Client.Tests { + [DoNotParallelize] [TestClass] public class ConfigServiceTests { @@ -26,17 +28,19 @@ public void TestInitialize() cacheMock.Reset(); } + [DoNotParallelize] [TestMethod] public async Task LazyLoadConfigService_GetConfigAsync_ReturnsExpiredContent_ShouldInvokeFetchAndCacheSet() { // Arrange this.cacheMock - .Setup(m => m.Get()) - .Returns(cachedPc); + .Setup(m => m.GetAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(cachedPc); this.cacheMock - .Setup(m => m.Set(fetchedPc)) + .Setup(m => m.SetAsync(It.IsAny(), fetchedPc)) + .Returns(Task.CompletedTask) .Verifiable(); this.fetcherMock @@ -46,7 +50,7 @@ public async Task LazyLoadConfigService_GetConfigAsync_ReturnsExpiredContent_Sho var service = new LazyLoadConfigService( fetcherMock.Object, - cacheMock.Object, + new CacheParameters { ConfigCache = cacheMock.Object }, loggerMock.Object, defaultExpire); @@ -62,6 +66,7 @@ public async Task LazyLoadConfigService_GetConfigAsync_ReturnsExpiredContent_Sho this.cacheMock.VerifyAll(); } + [TestMethod] public async Task LazyLoadConfigService_GetConfigAsync_ReturnsNotExpiredContent_ShouldNotInvokeFetchAndCacheSet() { @@ -70,12 +75,12 @@ public async Task LazyLoadConfigService_GetConfigAsync_ReturnsNotExpiredContent_ var cachedPc = new ProjectConfig(null, DateTime.UtcNow, null); this.cacheMock - .Setup(m => m.Get()) - .Returns(cachedPc); + .Setup(m => m.GetAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(cachedPc); var service = new LazyLoadConfigService( fetcherMock.Object, - cacheMock.Object, + new CacheParameters { ConfigCache = cacheMock.Object }, loggerMock.Object, defaultExpire); @@ -88,7 +93,7 @@ public async Task LazyLoadConfigService_GetConfigAsync_ReturnsNotExpiredContent_ Assert.AreEqual(cachedPc, projectConfig); this.fetcherMock.Verify(m => m.Fetch(It.IsAny()), Times.Never); - this.cacheMock.Verify(m => m.Set(It.IsAny()), Times.Never); + this.cacheMock.Verify(m => m.SetAsync(It.IsAny(), It.IsAny()), Times.Never); } [TestMethod] @@ -99,8 +104,8 @@ public async Task LazyLoadConfigService_RefreshConfigAsync_ShouldNotInvokeCacheG byte callOrder = 1; this.cacheMock - .Setup(m => m.Get()) - .Returns(cachedPc) + .Setup(m => m.GetAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(cachedPc) .Callback(() => Assert.AreEqual(1, callOrder++)) .Verifiable(); @@ -111,13 +116,14 @@ public async Task LazyLoadConfigService_RefreshConfigAsync_ShouldNotInvokeCacheG .Verifiable(); this.cacheMock - .Setup(m => m.Set(fetchedPc)) + .Setup(m => m.SetAsync(It.IsAny(), fetchedPc)) + .Returns(Task.CompletedTask) .Callback(() => Assert.AreEqual(3, callOrder)) .Verifiable(); var service = new LazyLoadConfigService( fetcherMock.Object, - cacheMock.Object, + new CacheParameters { ConfigCache = cacheMock.Object }, loggerMock.Object, defaultExpire); @@ -132,27 +138,28 @@ public async Task LazyLoadConfigService_RefreshConfigAsync_ShouldNotInvokeCacheG } [TestMethod] - public async Task AutoPollConfigService_GetConfigAsync_WithoutTimer_ShouldInvokeFetchAndCacheSetAndCacheGet2x() + public async Task AutoPollConfigService_GetConfigAsync_WithoutTimerWithCachedConfig_ShouldInvokeCacheGet1xAndSetNeverFetchNever() { // Arrange var localPc = cachedPc; this.cacheMock - .Setup(m => m.Get()) - .Returns(localPc); + .Setup(m => m.GetAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(localPc); this.fetcherMock .Setup(m => m.Fetch(localPc)) .Returns(Task.FromResult(fetchedPc)); this.cacheMock - .Setup(m => m.Set(fetchedPc)) - .Callback(() => localPc = fetchedPc); + .Setup(m => m.SetAsync(It.IsAny(), fetchedPc)) + .Callback(() => localPc = fetchedPc) + .Returns(Task.CompletedTask); var service = new AutoPollConfigService( fetcherMock.Object, - cacheMock.Object, + new CacheParameters { ConfigCache = cacheMock.Object }, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1), loggerMock.Object, @@ -164,9 +171,9 @@ public async Task AutoPollConfigService_GetConfigAsync_WithoutTimer_ShouldInvoke // Assert - this.cacheMock.Verify(m => m.Get(), Times.Exactly(2)); - this.cacheMock.Verify(m => m.Set(fetchedPc), Times.Once); - this.fetcherMock.Verify(m => m.Fetch(cachedPc), Times.Once); + this.cacheMock.Verify(m => m.GetAsync(It.IsAny(), CancellationToken.None), Times.Once); + this.cacheMock.Verify(m => m.SetAsync(It.IsAny(), fetchedPc), Times.Never); + this.fetcherMock.Verify(m => m.Fetch(cachedPc), Times.Never); } [TestMethod] @@ -177,20 +184,21 @@ public async Task AutoPollConfigService_GetConfigAsync_WithTimer_ShouldInvokeFet var wd = new ManualResetEventSlim(false); this.cacheMock - .Setup(m => m.Get()) - .Returns(cachedPc); + .Setup(m => m.GetAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(cachedPc); this.fetcherMock .Setup(m => m.Fetch(cachedPc)) .Returns(Task.FromResult(fetchedPc)); this.cacheMock - .Setup(m => m.Set(fetchedPc)) - .Callback(() => wd.Set()); + .Setup(m => m.SetAsync(It.IsAny(), fetchedPc)) + .Callback(() => wd.Set()) + .Returns(Task.FromResult(0)); var service = new AutoPollConfigService( fetcherMock.Object, - cacheMock.Object, + new CacheParameters { ConfigCache = cacheMock.Object }, TimeSpan.FromMinutes(1), TimeSpan.Zero, loggerMock.Object, @@ -201,11 +209,11 @@ public async Task AutoPollConfigService_GetConfigAsync_WithTimer_ShouldInvokeFet wd.Wait(TimeSpan.FromMinutes(1)); await service.GetConfigAsync(); - + service.Dispose(); // Assert - this.cacheMock.Verify(m => m.Get(), Times.Exactly(2)); - this.cacheMock.Verify(m => m.Set(fetchedPc), Times.Once); + this.cacheMock.Verify(m => m.GetAsync(It.IsAny(), CancellationToken.None), Times.Exactly(2)); + this.cacheMock.Verify(m => m.SetAsync(It.IsAny(), fetchedPc), Times.Once); this.fetcherMock.Verify(m => m.Fetch(cachedPc), Times.Once); } @@ -215,19 +223,20 @@ public async Task AutoPollConfigService_RefreshConfigAsync_ShouldOnceInvokeCache // Arrange this.cacheMock - .Setup(m => m.Get()) - .Returns(cachedPc); + .Setup(m => m.GetAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(cachedPc); this.fetcherMock .Setup(m => m.Fetch(cachedPc)) .Returns(Task.FromResult(fetchedPc)); this.cacheMock - .Setup(m => m.Set(fetchedPc)); + .Setup(m => m.SetAsync(It.IsAny(), fetchedPc)) + .Returns(Task.CompletedTask); var service = new AutoPollConfigService( fetcherMock.Object, - cacheMock.Object, + new CacheParameters { ConfigCache = cacheMock.Object }, TimeSpan.FromMinutes(1), TimeSpan.Zero, loggerMock.Object, @@ -239,8 +248,8 @@ public async Task AutoPollConfigService_RefreshConfigAsync_ShouldOnceInvokeCache // Assert - this.cacheMock.Verify(m => m.Get(), Times.Once); - this.cacheMock.Verify(m => m.Set(fetchedPc), Times.Once); + this.cacheMock.Verify(m => m.GetAsync(It.IsAny(), CancellationToken.None), Times.Once); + this.cacheMock.Verify(m => m.SetAsync(It.IsAny(), fetchedPc), Times.Once); this.fetcherMock.Verify(m => m.Fetch(cachedPc), Times.Once); } @@ -252,19 +261,20 @@ public async Task AutoPollConfigService_RefreshConfigAsync_ConfigChanged_ShouldR byte eventChanged = 0; this.cacheMock - .Setup(m => m.Get()) - .Returns(cachedPc); + .Setup(m => m.GetAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(cachedPc); this.fetcherMock .Setup(m => m.Fetch(cachedPc)) .Returns(Task.FromResult(fetchedPc)); this.cacheMock - .Setup(m => m.Set(fetchedPc)); + .Setup(m => m.SetAsync(It.IsAny(), fetchedPc)) + .Returns(Task.CompletedTask); var service = new AutoPollConfigService( fetcherMock.Object, - cacheMock.Object, + new CacheParameters { ConfigCache = cacheMock.Object }, TimeSpan.FromMinutes(1), TimeSpan.Zero, loggerMock.Object, @@ -290,8 +300,8 @@ public void AutoPollConfigService_Dispose_ShouldStopTimer() long e1, e2; this.cacheMock - .Setup(m => m.Get()) - .Returns(cachedPc); + .Setup(m => m.GetAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(cachedPc)); this.fetcherMock .Setup(m => m.Fetch(cachedPc)) @@ -300,11 +310,11 @@ public void AutoPollConfigService_Dispose_ShouldStopTimer() var service = new AutoPollConfigService( fetcherMock.Object, - cacheMock.Object, + new CacheParameters { ConfigCache = cacheMock.Object, CacheKey = "" }, TimeSpan.FromSeconds(0.2d), TimeSpan.Zero, loggerMock.Object, - true); + false); // Act Thread.Sleep(TimeSpan.FromSeconds(1)); @@ -328,8 +338,8 @@ public void AutoPollConfigService_WithoutTimer_InvokeDispose_ShouldDisposeServic long e1; this.cacheMock - .Setup(m => m.Get()) - .Returns(cachedPc); + .Setup(m => m.GetAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(cachedPc); this.fetcherMock .Setup(m => m.Fetch(cachedPc)) @@ -338,7 +348,7 @@ public void AutoPollConfigService_WithoutTimer_InvokeDispose_ShouldDisposeServic var service = new AutoPollConfigService( fetcherMock.Object, - cacheMock.Object, + new CacheParameters { ConfigCache = cacheMock.Object }, TimeSpan.FromSeconds(0.2d), TimeSpan.Zero, loggerMock.Object, @@ -359,12 +369,12 @@ public async Task ManualPollConfigService_GetConfigAsync_ShouldInvokeCacheGet() // Arrange this.cacheMock - .Setup(m => m.Get()) - .Returns(cachedPc); + .Setup(m => m.GetAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(cachedPc); var service = new ManualPollConfigService( fetcherMock.Object, - cacheMock.Object, + new CacheParameters { ConfigCache = cacheMock.Object }, loggerMock.Object); // Act @@ -375,9 +385,9 @@ public async Task ManualPollConfigService_GetConfigAsync_ShouldInvokeCacheGet() Assert.AreEqual(cachedPc, projectConfig); - this.cacheMock.Verify(m => m.Get(), Times.Once); + this.cacheMock.Verify(m => m.GetAsync(It.IsAny(), CancellationToken.None), Times.Once); this.fetcherMock.Verify(m => m.Fetch(It.IsAny()), Times.Never); - this.cacheMock.Verify(m => m.Set(It.IsAny()), Times.Never); + this.cacheMock.Verify(m => m.SetAsync(It.IsAny(), It.IsAny()), Times.Never); } [TestMethod] @@ -388,8 +398,8 @@ public async Task ManualPollConfigService_RefreshConfigAsync_ShouldInvokeCacheGe byte callOrder = 1; this.cacheMock - .Setup(m => m.Get()) - .Returns(cachedPc) + .Setup(m => m.GetAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(cachedPc) .Callback(() => Assert.AreEqual(1, callOrder++)); this.fetcherMock @@ -398,12 +408,13 @@ public async Task ManualPollConfigService_RefreshConfigAsync_ShouldInvokeCacheGe .Callback(() => Assert.AreEqual(2, callOrder++)); this.cacheMock - .Setup(m => m.Set(fetchedPc)) - .Callback(() => Assert.AreEqual(3, callOrder++)); + .Setup(m => m.SetAsync(It.IsAny(), fetchedPc)) + .Callback(() => Assert.AreEqual(3, callOrder++)) + .Returns(Task.CompletedTask); var service = new ManualPollConfigService( fetcherMock.Object, - cacheMock.Object, + new CacheParameters { ConfigCache = cacheMock.Object }, loggerMock.Object); // Act @@ -412,9 +423,9 @@ public async Task ManualPollConfigService_RefreshConfigAsync_ShouldInvokeCacheGe // Assert - this.cacheMock.Verify(m => m.Get(), Times.Once); + this.cacheMock.Verify(m => m.GetAsync(It.IsAny(), CancellationToken.None), Times.Once); this.fetcherMock.Verify(m => m.Fetch(It.IsAny()), Times.Once); - this.cacheMock.Verify(m => m.Set(It.IsAny()), Times.Once); + this.cacheMock.Verify(m => m.SetAsync(It.IsAny(), It.IsAny()), Times.Once); } [TestMethod] @@ -434,7 +445,7 @@ public void ConfigService_InvokeDisposeManyTimes_ShouldInvokeFetcherDisposeExact var configServiceMock = new Mock( MockBehavior.Loose, configFetcherMock.Object, - new InMemoryConfigCache(), + new CacheParameters { ConfigCache = new InMemoryConfigCache() }, loggerMock.Object) { CallBase = true @@ -465,7 +476,7 @@ public void ConfigService_WithNonDisposableConfigFetcher_DisposeShouldWork() var configServiceMock = new Mock( MockBehavior.Loose, configFetcherMock.Object, - new InMemoryConfigCache(), + new CacheParameters { ConfigCache = new InMemoryConfigCache() }, new MyCounterLogger()) { CallBase = true @@ -478,6 +489,4 @@ public void ConfigService_WithNonDisposableConfigFetcher_DisposeShouldWork() configService.Dispose(); } } -} - - +} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/CustomHttpClientHandlerTests.cs b/src/ConfigCat.Client.Tests/CustomHttpClientHandlerTests.cs index 9b6c3cb2..d7e68f7f 100644 --- a/src/ConfigCat.Client.Tests/CustomHttpClientHandlerTests.cs +++ b/src/ConfigCat.Client.Tests/CustomHttpClientHandlerTests.cs @@ -4,6 +4,7 @@ namespace ConfigCat.Client.Tests { + [TestCategory(TestCategories.Integration)] [TestClass] public class CustomHttpClientHandlerTests { @@ -22,6 +23,7 @@ public void AutoPoll_WithHttpClientHandlerOverride_ShouldReturnCatUseCustomImple // Arrange var client = ConfigCatClientBuilder.Initialize(SDKKEY) + .WithDataGovernance(DataGovernance.EuOnly) .WithAutoPoll() .WithHttpClientHandler(httpClientHandler) .Create(); @@ -42,6 +44,7 @@ public void ManualPoll_WithHttpClientHandlerOverride_ShouldReturnCatUseCustomImp // Arrange var client = ConfigCatClientBuilder.Initialize(SDKKEY) + .WithDataGovernance(DataGovernance.EuOnly) .WithManualPoll() .WithHttpClientHandler(httpClientHandler) .Create(); @@ -63,6 +66,7 @@ public void LazyLoad_WithHttpClientHandlerOverride_ShouldReturnCatUseCustomImple // Arrange var client = ConfigCatClientBuilder.Initialize(SDKKEY) + .WithDataGovernance(DataGovernance.EuOnly) .WithLazyLoad() .WithHttpClientHandler(httpClientHandler) .Create(); diff --git a/src/ConfigCat.Client.Tests/DataGovernanceTests.cs b/src/ConfigCat.Client.Tests/DataGovernanceTests.cs new file mode 100644 index 00000000..fb444e83 --- /dev/null +++ b/src/ConfigCat.Client.Tests/DataGovernanceTests.cs @@ -0,0 +1,410 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using ConfigCat.Client.Evaluate; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Moq.Protected; +using Newtonsoft.Json; + +namespace ConfigCat.Client.Tests +{ + [TestClass] + public class DataGovernanceTests + { + private static readonly Uri GlobalCdnUri = new Uri(ConfigurationBase.BaseUrlGlobal); + private static readonly Uri EuOnlyCdnUri = new Uri(ConfigurationBase.BaseUrlEu); + private static readonly Uri CustomCdnUri = new Uri("https://custom-cdn.example.com"); + private static readonly Uri ForcedCdnUri = new Uri("https://forced-cdn.example.com"); + + [DataRow(DataGovernance.Global, ConfigurationBase.BaseUrlGlobal)] + [DataRow(DataGovernance.EuOnly, ConfigurationBase.BaseUrlEu)] + [DataRow(null, ConfigurationBase.BaseUrlGlobal)] + [DataTestMethod] + public async Task WithDataGovernanceSetting_ShouldUseProperCdnUrl(DataGovernance dataGovernance, string expectedUrl) + { + // Arrange + + var configuration = new AutoPollConfiguration + { + SdkKey = "DEMO", + DataGovernance = dataGovernance + }; + + byte requestCount = 0; + var requests = new SortedList(); + + var handlerMock = new Mock(MockBehavior.Strict); + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .Callback((message, _) => + { + requests.Add(requestCount++, message); + }) + .ReturnsAsync(new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("{\"p\": {\"u\": \"http://example.com\", \"r\": 0}}"), + }) + .Verifiable(); + + IConfigFetcher fetcher = new HttpConfigFetcher( + configuration.CreateUri(), + "DEMO", + Mock.Of(), + handlerMock.Object, + configuration.IsCustomBaseUrl); + + // Act + + await fetcher.Fetch(ProjectConfig.Empty); + + // Assert + + handlerMock.VerifyAll(); + Assert.AreEqual(1, requestCount); + Assert.AreEqual(new Uri(expectedUrl).Host, requests[0].RequestUri.Host); + } + + [TestMethod] + public async Task ClientIsGlobalAndOrgSettingIsGlobal_AllRequestsInvokeGlobalCdn() + { + // Arrange + + var fetchConfig = new AutoPollConfiguration + { + SdkKey = "SDK-KEY", + DataGovernance = DataGovernance.Global + }; + + var responsesRegistry = new Dictionary + { + { GlobalCdnUri.Host, CreateResponse() } + }; + + // Act + + var requests = await Fetch(fetchConfig, responsesRegistry, 3); + + // Assert + + Assert.IsTrue(requests.Values.All(message => message.RequestUri.Host == GlobalCdnUri.Host)); + } + + [TestMethod] + public async Task ClientIsEuOnlyAndOrgSettingIsGlobal_FirstRequestInvokesEuAfterAllRequestsInvokeGlobal() + { + // Arrange + + var fetchConfig = new AutoPollConfiguration + { + SdkKey = "SDK-KEY", + DataGovernance = DataGovernance.EuOnly + }; + + var responsesRegistry = new Dictionary + { + {GlobalCdnUri.Host, CreateResponse()}, + {EuOnlyCdnUri.Host, CreateResponse()} + }; + + // Act + + var requests = await Fetch(fetchConfig, responsesRegistry, 3); + + // Assert + + Assert.AreEqual(3, requests.Count); + Assert.AreEqual(EuOnlyCdnUri.Host, requests[1].RequestUri.Host); + Assert.IsTrue(requests.Values.Skip(1).All(message => message.RequestUri.Host == GlobalCdnUri.Host)); + } + + [TestMethod] + public async Task ClientIsGlobalAndOrgSettingIsEuOnly_FirstRequestInvokesGlobalAndRedirectToEuAfterAllRequestsInvokeEu() + { + // Arrange + + var fetchConfig = new AutoPollConfiguration + { + SdkKey = "SDK-KEY", + DataGovernance = DataGovernance.Global + }; + + var responsesRegistry = new Dictionary + { + {GlobalCdnUri.Host, CreateResponse(ConfigurationBase.BaseUrlEu, RedirectMode.Should, false)}, + {EuOnlyCdnUri.Host, CreateResponse(ConfigurationBase.BaseUrlEu, RedirectMode.No, true)} + }; + + // Act + + var requests = await Fetch(fetchConfig, responsesRegistry, 3); + + // Assert + + Assert.AreEqual(3 + 1, requests.Count); + Assert.AreEqual(GlobalCdnUri.Host, requests[1].RequestUri.Host); + Assert.AreEqual(EuOnlyCdnUri.Host, requests[2].RequestUri.Host); + Assert.IsTrue(requests.Values.Skip(2).All(m => m.RequestUri.Host == EuOnlyCdnUri.Host)); + } + + [TestMethod] + public async Task ClientIsEuOnlyAndOrgSettingIsEuOnly_AllRequestsInvokeEu() + { + // Arrange + + var fetchConfig = new AutoPollConfiguration + { + SdkKey = "SDK-KEY", + DataGovernance = DataGovernance.EuOnly + }; + + var responsesRegistry = new Dictionary + { + {EuOnlyCdnUri.Host, CreateResponse(ConfigurationBase.BaseUrlEu)} + }; + + // Act + + var requests = await Fetch(fetchConfig, responsesRegistry, 3); + + // Assert + + Assert.AreEqual(3, requests.Count); + Assert.IsTrue(requests.Values.All(m => m.RequestUri.Host == EuOnlyCdnUri.Host)); + } + + [TestMethod] + public async Task ClientIsGlobalAndHasCustomBaseUri_AllRequestInvokeCustomUri() + { + // Arrange + + var responsesRegistry = new Dictionary + { + {CustomCdnUri.Host, CreateResponse()} + }; + + var fetchConfig = new AutoPollConfiguration + { + DataGovernance = DataGovernance.Global, + BaseUrl = CustomCdnUri + }; + + // Act + + var requests = await Fetch(fetchConfig, responsesRegistry, 3); + + // Assert + + Assert.AreEqual(3, requests.Count); + Assert.IsTrue(requests.Values.All(m => m.RequestUri.Host == CustomCdnUri.Host)); + } + + [TestMethod] + public async Task ClientIsEuOnlyAndHasCustomBaseUri_AllRequestInvokeCustomUri() + { + // Arrange + + var responsesRegistry = new Dictionary + { + {CustomCdnUri.Host, CreateResponse()} + }; + + var fetchConfig = new AutoPollConfiguration + { + DataGovernance = DataGovernance.EuOnly, + BaseUrl = CustomCdnUri + }; + + // Act + + var requests = await Fetch(fetchConfig, responsesRegistry, 3); + + // Assert + + Assert.AreEqual(3, requests.Count); + Assert.IsTrue(requests.Values.All(m => m.RequestUri.Host == CustomCdnUri.Host)); + } + + [TestMethod] + public async Task ClientIsGlobalAndOrgIsForced_AllRequestInvokeForcedUri() + { + // Arrange + + var responsesRegistry = new Dictionary + { + {GlobalCdnUri.Host, CreateResponse(ForcedCdnUri.ToString(), RedirectMode.Force, false)}, + {ForcedCdnUri.Host, CreateResponse(ForcedCdnUri.ToString(), RedirectMode.Force, true)} + }; + + var fetchConfig = new AutoPollConfiguration + { + DataGovernance = DataGovernance.Global + }; + + // Act + + var requests = await Fetch(fetchConfig, responsesRegistry, 3); + + // Assert + + Assert.AreEqual(3 + 1, requests.Count); + Assert.AreEqual(GlobalCdnUri.Host, requests[1].RequestUri.Host); + Assert.IsTrue(requests.Values.Skip(1).All(m => m.RequestUri.Host == ForcedCdnUri.Host)); + } + + [TestMethod] + public async Task ClientIsEuOnlyAndOrgIsForced_AllRequestInvokeForcedUri() + { + // Arrange + + var responsesRegistry = new Dictionary + { + {EuOnlyCdnUri.Host, CreateResponse(ForcedCdnUri.ToString(), RedirectMode.Force, false)}, + {ForcedCdnUri.Host, CreateResponse(ForcedCdnUri.ToString(), RedirectMode.Force, true)} + }; + + var fetchConfig = new AutoPollConfiguration + { + DataGovernance = DataGovernance.EuOnly + }; + + // Act + + var requests = await Fetch(fetchConfig, responsesRegistry, 3); + + // Assert + + Assert.AreEqual(3 + 1, requests.Count); + Assert.AreEqual(EuOnlyCdnUri.Host, requests[1].RequestUri.Host); + Assert.IsTrue(requests.Values.Skip(1).All(m => m.RequestUri.Host == ForcedCdnUri.Host)); + } + + [TestMethod] + public async Task ClientIsGlobalAndHasCustomBaseUriAndOrgIsForced_FirstRequestInvokeCustomAndRedirectToForceUriAndAllRequestInvokeForcedUri() + { + // Arrange + + var responsesRegistry = new Dictionary + { + {CustomCdnUri.Host, CreateResponse(ForcedCdnUri.ToString(), RedirectMode.Force, false)}, + {ForcedCdnUri.Host, CreateResponse(ForcedCdnUri.ToString(), RedirectMode.Force, true)} + }; + + var fetchConfig = new AutoPollConfiguration + { + DataGovernance = DataGovernance.Global, + BaseUrl = CustomCdnUri + }; + + // Act + + var responses = await Fetch(fetchConfig, responsesRegistry, 3); + + // Assert + + Assert.AreEqual(3 + 1, responses.Count); + Assert.AreEqual(CustomCdnUri.Host, responses[1].RequestUri.Host); + Assert.IsTrue(responses.Values.Skip(1).All(m => m.RequestUri.Host == ForcedCdnUri.Host)); + } + + [TestMethod] + public async Task TestCircuitBreaker_WhenClientIsGlobalRedirectToEuAndRedirectToGlobal_MaximumInvokeCountShouldBeThree() + { + // Arrange + + var responsesRegistry = new Dictionary + { + {GlobalCdnUri.Host, CreateResponse(EuOnlyCdnUri.ToString(), RedirectMode.Should, false)}, + {EuOnlyCdnUri.Host, CreateResponse(GlobalCdnUri.ToString(), RedirectMode.Should, false)} + }; + + var fetchConfig = new AutoPollConfiguration + { + DataGovernance = DataGovernance.Global + }; + + // Act + + var responses = await Fetch(fetchConfig, responsesRegistry); + + // Assert + + Assert.AreEqual(3, responses.Count); + } + + + internal async Task> Fetch( + ConfigurationBase fetchConfig, + Dictionary responsesRegistry, + byte fetchInvokeCount = 1) + { + // Arrange + + byte requestCount = 1; + var requests = new SortedList(); + + var handlerMock = new Mock(MockBehavior.Strict); + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .Callback((message, _) => + { + requests.Add(requestCount++, message); + }) + .Returns((message, _) => Task.FromResult(new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonConvert.SerializeObject(responsesRegistry[message.RequestUri.Host])) + })) + .Verifiable(); + + IConfigFetcher fetcher = new HttpConfigFetcher( + fetchConfig.CreateUri(), + "DEMO", + Mock.Of(), + handlerMock.Object, + fetchConfig.IsCustomBaseUrl); + + // Act + + byte i = 0; + do + { + await fetcher.Fetch(ProjectConfig.Empty); + i++; + } while (fetchInvokeCount > i); + + // Assert + + return requests; + } + + private static SettingsWithPreferences CreateResponse(string url = ConfigurationBase.BaseUrlGlobal, RedirectMode redirectMode = RedirectMode.No, bool withSettings = true) + { + return new SettingsWithPreferences + { + Preferences = new Preferences + { + Url = url, + RedirectMode = redirectMode + }, + Settings = withSettings ? new Dictionary { { "myKey", new Setting { RawValue = "foo", SettingType = SettingTypeEnum.String } } } : null + }; + } + } +} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/HttpConfigFetcherTests.cs b/src/ConfigCat.Client.Tests/HttpConfigFetcherTests.cs index a13eea58..f249e949 100644 --- a/src/ConfigCat.Client.Tests/HttpConfigFetcherTests.cs +++ b/src/ConfigCat.Client.Tests/HttpConfigFetcherTests.cs @@ -15,7 +15,7 @@ public async Task HttpConfigFetcher_WithCustomHttpClientHandler_ShouldUsePassedH var myHandler = new MyFakeHttpClientHandler(); - var instance = new HttpConfigFetcher(new Uri("http://example.com"), "1.0", new MyCounterLogger(), myHandler); + var instance = new HttpConfigFetcher(new Uri("http://example.com"), "1.0", new MyCounterLogger(), myHandler, false); // Act @@ -33,7 +33,7 @@ public void HttpConfigFetcher_WithCustomHttpClientHandler_HandlersDisposeShouldN var myHandler = new MyFakeHttpClientHandler(); - var instance = new HttpConfigFetcher(new Uri("http://example.com"), "1.0", new MyCounterLogger(), myHandler); + var instance = new HttpConfigFetcher(new Uri("http://example.com"), "1.0", new MyCounterLogger(), myHandler, false); // Act @@ -45,13 +45,33 @@ public void HttpConfigFetcher_WithCustomHttpClientHandler_HandlersDisposeShouldN } [TestMethod] - public async Task HttpConfigFetcher_ThrowAnException_ShouldReturPassedConfig() + public async Task HttpConfigFetcher_ResponseHttpCodeIsUnexpected_ShouldReturnsPassedConfig() + { + // Arrange + + var myHandler = new MyFakeHttpClientHandler(HttpStatusCode.Forbidden); + + var instance = new HttpConfigFetcher(new Uri("http://example.com"), "1.0", new MyCounterLogger(), myHandler, false); + + var lastConfig = new ProjectConfig("{ }", DateTime.UtcNow, "\"ETAG\""); + + // Act + + var actual = await instance.Fetch(lastConfig); + + // Assert + + Assert.AreEqual(lastConfig, actual); + } + + [TestMethod] + public async Task HttpConfigFetcher_ThrowAnException_ShouldReturnPassedConfig() { // Arrange var myHandler = new ExceptionThrowerHttpClientHandler(new WebException()); - var instance = new HttpConfigFetcher(new Uri("http://example.com"), "1.0", new MyCounterLogger(), myHandler); + var instance = new HttpConfigFetcher(new Uri("http://example.com"), "1.0", new MyCounterLogger(), myHandler, false); var lastConfig = new ProjectConfig("{ }", DateTime.UtcNow, "\"ETAG\""); diff --git a/src/ConfigCat.Client.Tests/MyFakeHttpClientHandler.cs b/src/ConfigCat.Client.Tests/MyFakeHttpClientHandler.cs index fab05e8d..3a8f575f 100644 --- a/src/ConfigCat.Client.Tests/MyFakeHttpClientHandler.cs +++ b/src/ConfigCat.Client.Tests/MyFakeHttpClientHandler.cs @@ -1,4 +1,5 @@ -using System.Net; +using System.Collections.Generic; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -7,15 +8,26 @@ namespace ConfigCat.Client.Tests { internal class MyFakeHttpClientHandler : HttpClientHandler { + private readonly HttpStatusCode httpStatusCode; + public byte SendInvokeCount { get; private set; } = 0; public bool Disposed { get; private set; } = false; + public SortedList Requests = new SortedList(); + + public MyFakeHttpClientHandler(HttpStatusCode httpStatusCode = HttpStatusCode.NotModified) + { + this.httpStatusCode = httpStatusCode; + } + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { SendInvokeCount++; - var response = new HttpResponseMessage(HttpStatusCode.NotModified); + Requests.Add(SendInvokeCount, request); + + var response = new HttpResponseMessage(this.httpStatusCode); return Task.FromResult(response); } diff --git a/src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs index 520ddf9d..61aa6e3f 100644 --- a/src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/NumericConfigEvaluatorTests.cs @@ -5,7 +5,7 @@ namespace ConfigCat.Client.Tests [TestClass] public class NumericConfigEvaluatorTests : ConfigEvaluatorTestsBase { - protected override string SampleJsonFileName => "sample_number_v4.json"; + protected override string SampleJsonFileName => "sample_number_v5.json"; protected override string MatrixResultFileName => "testmatrix_number.csv"; } diff --git a/src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs index 2e2c6334..38193193 100644 --- a/src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/SemanticVersion2ConfigEvaluatorTests.cs @@ -5,7 +5,7 @@ namespace ConfigCat.Client.Tests [TestClass] public class SemanticVersion2ConfigEvaluatorTests : ConfigEvaluatorTestsBase { - protected override string SampleJsonFileName => "sample_semantic_2_v4.json"; + protected override string SampleJsonFileName => "sample_semantic_2_v5.json"; protected override string MatrixResultFileName => "testmatrix_semantic_2.csv"; } diff --git a/src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs index 71831e7e..113bc4da 100644 --- a/src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/SemanticVersionConfigEvaluatorTests.cs @@ -5,7 +5,7 @@ namespace ConfigCat.Client.Tests [TestClass] public class SemanticVersionConfigEvaluatorTests : ConfigEvaluatorTestsBase { - protected override string SampleJsonFileName => "sample_semantic_v4.json"; + protected override string SampleJsonFileName => "sample_semantic_v5.json"; protected override string MatrixResultFileName => "testmatrix_semantic.csv"; } diff --git a/src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs index 842ba30f..deabbb5d 100644 --- a/src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/SensitiveConfigEvaluatorTests.cs @@ -5,7 +5,7 @@ namespace ConfigCat.Client.Tests [TestClass] public class SensitiveEvaluatorTests : ConfigEvaluatorTestsBase { - protected override string SampleJsonFileName => "sample_sensitive_v4.json"; + protected override string SampleJsonFileName => "sample_sensitive_v5.json"; protected override string MatrixResultFileName => "testmatrix_sensitive.csv"; } diff --git a/src/ConfigCat.Client.Tests/TestCategories.cs b/src/ConfigCat.Client.Tests/TestCategories.cs new file mode 100644 index 00000000..a4596aca --- /dev/null +++ b/src/ConfigCat.Client.Tests/TestCategories.cs @@ -0,0 +1,7 @@ +namespace ConfigCat.Client.Tests +{ + public class TestCategories + { + public const string Integration = nameof(Integration); + } +} diff --git a/src/ConfigCat.Client.Tests/VariationIdEvaluatorTests.cs b/src/ConfigCat.Client.Tests/VariationIdEvaluatorTests.cs index 3dcc235a..74d475b2 100644 --- a/src/ConfigCat.Client.Tests/VariationIdEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/VariationIdEvaluatorTests.cs @@ -9,7 +9,7 @@ namespace ConfigCat.Client.Tests [TestClass] public class VariationIdEvaluatorTests : ConfigEvaluatorTestsBase { - protected override string SampleJsonFileName => "sample_variationid_v4.json"; + protected override string SampleJsonFileName => "sample_variationid_v5.json"; protected override string MatrixResultFileName => "testmatrix_variationid.csv"; diff --git a/src/ConfigCat.Client.Tests/data/sample_number_v4.json b/src/ConfigCat.Client.Tests/data/sample_number_v4.json deleted file mode 100644 index a6f77509..00000000 --- a/src/ConfigCat.Client.Tests/data/sample_number_v4.json +++ /dev/null @@ -1 +0,0 @@ -{"numberWithPercentage":{"v":"Default","t":1,"p":[{"o":0,"v":"80%","p":80},{"o":1,"v":"20%","p":20}],"r":[{"o":0,"a":"Custom1","t":10,"c":"sajt","v":"=sajt"},{"o":1,"a":"Custom1","t":12,"c":"2.1","v":"<2.1"},{"o":2,"a":"Custom1","t":13,"c":"2,1","v":"<=2,1"},{"o":3,"a":"Custom1","t":10,"c":"3.5","v":"=3.5"},{"o":4,"a":"Custom1","t":14,"c":"5","v":">5"},{"o":5,"a":"Custom1","t":15,"c":"5","v":">=5"},{"o":6,"a":"Custom1","t":11,"c":"4.2","v":"<>4.2"}]},"number":{"v":"Default","t":1,"p":[],"r":[{"o":0,"a":"Custom1","t":11,"c":"5","v":"<>5"}]}} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/sample_number_v5.json b/src/ConfigCat.Client.Tests/data/sample_number_v5.json new file mode 100644 index 00000000..1c8398a5 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/sample_number_v5.json @@ -0,0 +1,85 @@ +{ + "f" : { + "numberWithPercentage": { + "v": "Default", + "t": 1, + "p": [ + { + "o": 0, + "v": "80%", + "p": 80 + }, + { + "o": 1, + "v": "20%", + "p": 20 + } + ], + "r": [ + { + "o": 0, + "a": "Custom1", + "t": 10, + "c": "sajt", + "v": "=sajt" + }, + { + "o": 1, + "a": "Custom1", + "t": 12, + "c": "2.1", + "v": "<2.1" + }, + { + "o": 2, + "a": "Custom1", + "t": 13, + "c": "2,1", + "v": "<=2,1" + }, + { + "o": 3, + "a": "Custom1", + "t": 10, + "c": "3.5", + "v": "=3.5" + }, + { + "o": 4, + "a": "Custom1", + "t": 14, + "c": "5", + "v": ">5" + }, + { + "o": 5, + "a": "Custom1", + "t": 15, + "c": "5", + "v": ">=5" + }, + { + "o": 6, + "a": "Custom1", + "t": 11, + "c": "4.2", + "v": "<>4.2" + } + ] + }, + "number": { + "v": "Default", + "t": 1, + "p": [], + "r": [ + { + "o": 0, + "a": "Custom1", + "t": 11, + "c": "5", + "v": "<>5" + } + ] + } + } +} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/sample_semantic_2_v4.json b/src/ConfigCat.Client.Tests/data/sample_semantic_2_v4.json deleted file mode 100644 index 3d97f125..00000000 --- a/src/ConfigCat.Client.Tests/data/sample_semantic_2_v4.json +++ /dev/null @@ -1 +0,0 @@ -{"precedenceTests":{"v":"DEFAULT-FROM-CC-APP","t":1,"p":[],"r":[{"o":0,"a":"AppVersion","t":6,"c":"1.9.1-2","v":"< 1.9.1-2"},{"o":1,"a":"AppVersion","t":6,"c":"1.9.1-10","v":"< 1.9.1-10"},{"o":2,"a":"AppVersion","t":6,"c":"1.9.1-10a","v":"< 1.9.1-10a"},{"o":3,"a":"AppVersion","t":6,"c":"1.9.1-1a","v":"< 1.9.1-1a"},{"o":4,"a":"AppVersion","t":6,"c":"1.9.1-alpha","v":"< 1.9.1-alpha"},{"o":5,"a":"AppVersion","t":6,"c":"1.9.99-alpha","v":"< 1.9.99-alpha"},{"o":6,"a":"AppVersion","t":4,"c":"1.9.99-alpha","v":"= 1.9.99-alpha"},{"o":7,"a":"AppVersion","t":6,"c":"1.9.99-beta","v":"< 1.9.99-beta"},{"o":8,"a":"AppVersion","t":6,"c":"1.9.99-rc","v":"< 1.9.99-rc"},{"o":9,"a":"AppVersion","t":6,"c":"1.9.99-rc.1","v":"< 1.9.99-rc.1"},{"o":10,"a":"AppVersion","t":6,"c":"1.9.99-rc.2","v":"< 1.9.99-rc.2"},{"o":11,"a":"AppVersion","t":6,"c":"1.9.99-rc.20","v":"< 1.9.99-rc.20"},{"o":12,"a":"AppVersion","t":6,"c":"1.9.99-rc.20a","v":"< 1.9.99-rc.20a"},{"o":13,"a":"AppVersion","t":6,"c":"1.9.99-rc.2a","v":"< 1.9.99-rc.2a"},{"o":14,"a":"AppVersion","t":6,"c":"1.9.99","v":"< 1.9.99"},{"o":15,"a":"AppVersion","t":6,"c":"1.9.100","v":"< 1.9.100"},{"o":16,"a":"AppVersion","t":6,"c":"1.10.0-alpha","v":"< 1.10.0-alpha"},{"o":17,"a":"AppVersion","t":7,"c":"1.10.0-alpha","v":"<= 1.10.0-alpha"},{"o":18,"a":"AppVersion","t":6,"c":"1.10.0","v":"< 1.10.0"},{"o":19,"a":"AppVersion","t":7,"c":"1.10.0","v":"<= 1.10.0"},{"o":20,"a":"AppVersion","t":7,"c":"1.10.1","v":"<= 1.10.1"},{"o":21,"a":"AppVersion","t":7,"c":"1.10.3","v":"<= 1.10.3"},{"o":22,"a":"AppVersion","t":6,"c":"2.0.0","v":"< 2.0.0"},{"o":23,"a":"AppVersion","t":4,"c":"2.0.0","v":"= 2.0.0"},{"o":24,"a":"AppVersion","t":4,"c":"3.0.0+build3","v":"= 3.0.0+build3"},{"o":25,"a":"AppVersion","t":4,"c":"4.0.0+001","v":"= 4.0.0+001"},{"o":26,"a":"AppVersion","t":4,"c":"5.0.0+20130313144700","v":"= 5.0.0+20130313144700"},{"o":27,"a":"AppVersion","t":4,"c":"6.0.0+exp.sha.5114f85","v":"= 6.0.0+exp.sha.5114f85"},{"o":28,"a":"AppVersion","t":4,"c":"7.0.0-patch","v":"= 7.0.0-patch"},{"o":29,"a":"AppVersion","t":4,"c":"8.0.0-patch+anothermetadata","v":"= 8.0.0-patch+anothermetadata"},{"o":30,"a":"AppVersion","t":4,"c":"9.0.0-patch+metadata","v":"= 9.0.0-patch+metadata"},{"o":31,"a":"AppVersion","t":8,"c":"103.0.0","v":"> 103.0.0"},{"o":32,"a":"AppVersion","t":9,"c":"103.0.0","v":">= 103.0.0"},{"o":33,"a":"AppVersion","t":9,"c":"101.0.0","v":">= 101.0.0"},{"o":34,"a":"AppVersion","t":8,"c":"90.103.0","v":"> 90.103.0"},{"o":35,"a":"AppVersion","t":9,"c":"90.103.0","v":">= 90.103.0"},{"o":36,"a":"AppVersion","t":9,"c":"90.101.0","v":">= 90.101.0"},{"o":37,"a":"AppVersion","t":8,"c":"80.0.103","v":"> 80.0.103"},{"o":38,"a":"AppVersion","t":9,"c":"80.0.103","v":">= 80.0.103"},{"o":39,"a":"AppVersion","t":9,"c":"80.0.101","v":">= 80.0.101"},{"o":40,"a":"AppVersion","t":9,"c":"73.0.0-beta.2","v":">= 73.0.0-beta.2"},{"o":41,"a":"AppVersion","t":8,"c":"72.0.0-beta.2","v":"> 72.0.0-beta.2"},{"o":42,"a":"AppVersion","t":8,"c":"72.0.0-beta.1","v":"> 72.0.0-beta.1"},{"o":43,"a":"AppVersion","t":8,"c":"72.0.0-beta","v":"> 72.0.0-beta"},{"o":44,"a":"AppVersion","t":8,"c":"72.0.0-alpha","v":"> 72.0.0-alpha"},{"o":45,"a":"AppVersion","t":8,"c":"72.0.0-1a","v":"> 72.0.0-1a"},{"o":46,"a":"AppVersion","t":8,"c":"72.0.0-10a","v":"> 72.0.0-10a"},{"o":47,"a":"AppVersion","t":8,"c":"72.0.0-2","v":"> 72.0.0-2"},{"o":48,"a":"AppVersion","t":8,"c":"72.0.0-1","v":"> 72.0.0-1"},{"o":49,"a":"AppVersion","t":9,"c":"71.0.0+anothermetadata","v":">= 71.0.0+anothermetadata"},{"o":50,"a":"AppVersion","t":9,"c":"71.0.0-patch3+anothermetadata","v":">= 71.0.0-patch3+anothermetadata"},{"o":51,"a":"AppVersion","t":9,"c":"71.0.0-patch2","v":">= 71.0.0-patch2"},{"o":52,"a":"AppVersion","t":9,"c":"71.0.0-patch1+metadata","v":">= 71.0.0-patch1+metadata"},{"o":53,"a":"AppVersion","t":9,"c":"60.73.0-beta.2","v":">= 60.73.0-beta.2"},{"o":54,"a":"AppVersion","t":8,"c":"60.72.0-beta.2","v":"> 60.72.0-beta.2"},{"o":55,"a":"AppVersion","t":8,"c":"60.72.0-beta.1","v":"> 60.72.0-beta.1"},{"o":56,"a":"AppVersion","t":8,"c":"60.72.0-beta","v":"> 60.72.0-beta"},{"o":57,"a":"AppVersion","t":8,"c":"60.72.0-alpha","v":"> 60.72.0-alpha"},{"o":58,"a":"AppVersion","t":8,"c":"60.72.0-1a","v":"> 60.72.0-1a"},{"o":59,"a":"AppVersion","t":8,"c":"60.72.0-10a","v":"> 60.72.0-10a"},{"o":60,"a":"AppVersion","t":8,"c":"60.72.0-2","v":"> 60.72.0-2"},{"o":61,"a":"AppVersion","t":8,"c":"60.72.0-1","v":"> 60.72.0-1"},{"o":62,"a":"AppVersion","t":9,"c":"60.71.0+anothermetadata","v":">= 60.71.0+anothermetadata"},{"o":63,"a":"AppVersion","t":9,"c":"60.71.0-patch3+anothermetadata","v":">= 60.71.0-patch3+anothermetadata"},{"o":64,"a":"AppVersion","t":9,"c":"60.71.0-patch2","v":">= 60.71.0-patch2"},{"o":65,"a":"AppVersion","t":9,"c":"60.71.0-patch1+metadata","v":">= 60.71.0-patch1+metadata"},{"o":66,"a":"AppVersion","t":9,"c":"50.60.73-beta.2","v":">= 50.60.73-beta.2"},{"o":67,"a":"AppVersion","t":8,"c":"50.60.72-beta.2","v":"> 50.60.72-beta.2"},{"o":68,"a":"AppVersion","t":8,"c":"50.60.72-beta.1","v":"> 50.60.72-beta.1"},{"o":69,"a":"AppVersion","t":8,"c":"50.60.72-beta","v":"> 50.60.72-beta"},{"o":70,"a":"AppVersion","t":8,"c":"50.60.72-alpha","v":"> 50.60.72-alpha"},{"o":71,"a":"AppVersion","t":8,"c":"50.60.72-1a","v":"> 50.60.72-1a"},{"o":72,"a":"AppVersion","t":8,"c":"50.60.72-10a","v":"> 50.60.72-10a"},{"o":73,"a":"AppVersion","t":8,"c":"50.60.72-2","v":"> 50.60.72-2"},{"o":74,"a":"AppVersion","t":8,"c":"50.60.72-1","v":"> 50.60.72-1"},{"o":75,"a":"AppVersion","t":9,"c":"50.60.71+anothermetadata","v":">= 50.60.71+anothermetadata"},{"o":76,"a":"AppVersion","t":9,"c":"50.60.71-patch3+anothermetadata","v":">= 50.60.71-patch3+anothermetadata"},{"o":77,"a":"AppVersion","t":9,"c":"50.60.71-patch2","v":">= 50.60.71-patch2"},{"o":78,"a":"AppVersion","t":9,"c":"50.60.71-patch1+metadata","v":">= 50.60.71-patch1+metadata"},{"o":79,"a":"AppVersion","t":9,"c":"40.0.0-patch","v":">= 40.0.0-patch"},{"o":80,"a":"AppVersion","t":9,"c":"30.0.0-alpha","v":">= 30.0.0-alpha"}]}} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/sample_semantic_2_v5.json b/src/ConfigCat.Client.Tests/data/sample_semantic_2_v5.json new file mode 100644 index 00000000..3f7df770 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/sample_semantic_2_v5.json @@ -0,0 +1,579 @@ +{ + + "f": { + "precedenceTests": { + "v": "DEFAULT-FROM-CC-APP", + "t": 1, + "p": [], + "r": [ + { + "o": 0, + "a": "AppVersion", + "t": 6, + "c": "1.9.1-2", + "v": "< 1.9.1-2" + }, + { + "o": 1, + "a": "AppVersion", + "t": 6, + "c": "1.9.1-10", + "v": "< 1.9.1-10" + }, + { + "o": 2, + "a": "AppVersion", + "t": 6, + "c": "1.9.1-10a", + "v": "< 1.9.1-10a" + }, + { + "o": 3, + "a": "AppVersion", + "t": 6, + "c": "1.9.1-1a", + "v": "< 1.9.1-1a" + }, + { + "o": 4, + "a": "AppVersion", + "t": 6, + "c": "1.9.1-alpha", + "v": "< 1.9.1-alpha" + }, + { + "o": 5, + "a": "AppVersion", + "t": 6, + "c": "1.9.99-alpha", + "v": "< 1.9.99-alpha" + }, + { + "o": 6, + "a": "AppVersion", + "t": 4, + "c": "1.9.99-alpha", + "v": "= 1.9.99-alpha" + }, + { + "o": 7, + "a": "AppVersion", + "t": 6, + "c": "1.9.99-beta", + "v": "< 1.9.99-beta" + }, + { + "o": 8, + "a": "AppVersion", + "t": 6, + "c": "1.9.99-rc", + "v": "< 1.9.99-rc" + }, + { + "o": 9, + "a": "AppVersion", + "t": 6, + "c": "1.9.99-rc.1", + "v": "< 1.9.99-rc.1" + }, + { + "o": 10, + "a": "AppVersion", + "t": 6, + "c": "1.9.99-rc.2", + "v": "< 1.9.99-rc.2" + }, + { + "o": 11, + "a": "AppVersion", + "t": 6, + "c": "1.9.99-rc.20", + "v": "< 1.9.99-rc.20" + }, + { + "o": 12, + "a": "AppVersion", + "t": 6, + "c": "1.9.99-rc.20a", + "v": "< 1.9.99-rc.20a" + }, + { + "o": 13, + "a": "AppVersion", + "t": 6, + "c": "1.9.99-rc.2a", + "v": "< 1.9.99-rc.2a" + }, + { + "o": 14, + "a": "AppVersion", + "t": 6, + "c": "1.9.99", + "v": "< 1.9.99" + }, + { + "o": 15, + "a": "AppVersion", + "t": 6, + "c": "1.9.100", + "v": "< 1.9.100" + }, + { + "o": 16, + "a": "AppVersion", + "t": 6, + "c": "1.10.0-alpha", + "v": "< 1.10.0-alpha" + }, + { + "o": 17, + "a": "AppVersion", + "t": 7, + "c": "1.10.0-alpha", + "v": "<= 1.10.0-alpha" + }, + { + "o": 18, + "a": "AppVersion", + "t": 6, + "c": "1.10.0", + "v": "< 1.10.0" + }, + { + "o": 19, + "a": "AppVersion", + "t": 7, + "c": "1.10.0", + "v": "<= 1.10.0" + }, + { + "o": 20, + "a": "AppVersion", + "t": 7, + "c": "1.10.1", + "v": "<= 1.10.1" + }, + { + "o": 21, + "a": "AppVersion", + "t": 7, + "c": "1.10.3", + "v": "<= 1.10.3" + }, + { + "o": 22, + "a": "AppVersion", + "t": 6, + "c": "2.0.0", + "v": "< 2.0.0" + }, + { + "o": 23, + "a": "AppVersion", + "t": 4, + "c": "2.0.0", + "v": "= 2.0.0" + }, + { + "o": 24, + "a": "AppVersion", + "t": 4, + "c": "3.0.0+build3", + "v": "= 3.0.0+build3" + }, + { + "o": 25, + "a": "AppVersion", + "t": 4, + "c": "4.0.0+001", + "v": "= 4.0.0+001" + }, + { + "o": 26, + "a": "AppVersion", + "t": 4, + "c": "5.0.0+20130313144700", + "v": "= 5.0.0+20130313144700" + }, + { + "o": 27, + "a": "AppVersion", + "t": 4, + "c": "6.0.0+exp.sha.5114f85", + "v": "= 6.0.0+exp.sha.5114f85" + }, + { + "o": 28, + "a": "AppVersion", + "t": 4, + "c": "7.0.0-patch", + "v": "= 7.0.0-patch" + }, + { + "o": 29, + "a": "AppVersion", + "t": 4, + "c": "8.0.0-patch+anothermetadata", + "v": "= 8.0.0-patch+anothermetadata" + }, + { + "o": 30, + "a": "AppVersion", + "t": 4, + "c": "9.0.0-patch+metadata", + "v": "= 9.0.0-patch+metadata" + }, + { + "o": 31, + "a": "AppVersion", + "t": 8, + "c": "103.0.0", + "v": "> 103.0.0" + }, + { + "o": 32, + "a": "AppVersion", + "t": 9, + "c": "103.0.0", + "v": ">= 103.0.0" + }, + { + "o": 33, + "a": "AppVersion", + "t": 9, + "c": "101.0.0", + "v": ">= 101.0.0" + }, + { + "o": 34, + "a": "AppVersion", + "t": 8, + "c": "90.103.0", + "v": "> 90.103.0" + }, + { + "o": 35, + "a": "AppVersion", + "t": 9, + "c": "90.103.0", + "v": ">= 90.103.0" + }, + { + "o": 36, + "a": "AppVersion", + "t": 9, + "c": "90.101.0", + "v": ">= 90.101.0" + }, + { + "o": 37, + "a": "AppVersion", + "t": 8, + "c": "80.0.103", + "v": "> 80.0.103" + }, + { + "o": 38, + "a": "AppVersion", + "t": 9, + "c": "80.0.103", + "v": ">= 80.0.103" + }, + { + "o": 39, + "a": "AppVersion", + "t": 9, + "c": "80.0.101", + "v": ">= 80.0.101" + }, + { + "o": 40, + "a": "AppVersion", + "t": 9, + "c": "73.0.0-beta.2", + "v": ">= 73.0.0-beta.2" + }, + { + "o": 41, + "a": "AppVersion", + "t": 8, + "c": "72.0.0-beta.2", + "v": "> 72.0.0-beta.2" + }, + { + "o": 42, + "a": "AppVersion", + "t": 8, + "c": "72.0.0-beta.1", + "v": "> 72.0.0-beta.1" + }, + { + "o": 43, + "a": "AppVersion", + "t": 8, + "c": "72.0.0-beta", + "v": "> 72.0.0-beta" + }, + { + "o": 44, + "a": "AppVersion", + "t": 8, + "c": "72.0.0-alpha", + "v": "> 72.0.0-alpha" + }, + { + "o": 45, + "a": "AppVersion", + "t": 8, + "c": "72.0.0-1a", + "v": "> 72.0.0-1a" + }, + { + "o": 46, + "a": "AppVersion", + "t": 8, + "c": "72.0.0-10a", + "v": "> 72.0.0-10a" + }, + { + "o": 47, + "a": "AppVersion", + "t": 8, + "c": "72.0.0-2", + "v": "> 72.0.0-2" + }, + { + "o": 48, + "a": "AppVersion", + "t": 8, + "c": "72.0.0-1", + "v": "> 72.0.0-1" + }, + { + "o": 49, + "a": "AppVersion", + "t": 9, + "c": "71.0.0+anothermetadata", + "v": ">= 71.0.0+anothermetadata" + }, + { + "o": 50, + "a": "AppVersion", + "t": 9, + "c": "71.0.0-patch3+anothermetadata", + "v": ">= 71.0.0-patch3+anothermetadata" + }, + { + "o": 51, + "a": "AppVersion", + "t": 9, + "c": "71.0.0-patch2", + "v": ">= 71.0.0-patch2" + }, + { + "o": 52, + "a": "AppVersion", + "t": 9, + "c": "71.0.0-patch1+metadata", + "v": ">= 71.0.0-patch1+metadata" + }, + { + "o": 53, + "a": "AppVersion", + "t": 9, + "c": "60.73.0-beta.2", + "v": ">= 60.73.0-beta.2" + }, + { + "o": 54, + "a": "AppVersion", + "t": 8, + "c": "60.72.0-beta.2", + "v": "> 60.72.0-beta.2" + }, + { + "o": 55, + "a": "AppVersion", + "t": 8, + "c": "60.72.0-beta.1", + "v": "> 60.72.0-beta.1" + }, + { + "o": 56, + "a": "AppVersion", + "t": 8, + "c": "60.72.0-beta", + "v": "> 60.72.0-beta" + }, + { + "o": 57, + "a": "AppVersion", + "t": 8, + "c": "60.72.0-alpha", + "v": "> 60.72.0-alpha" + }, + { + "o": 58, + "a": "AppVersion", + "t": 8, + "c": "60.72.0-1a", + "v": "> 60.72.0-1a" + }, + { + "o": 59, + "a": "AppVersion", + "t": 8, + "c": "60.72.0-10a", + "v": "> 60.72.0-10a" + }, + { + "o": 60, + "a": "AppVersion", + "t": 8, + "c": "60.72.0-2", + "v": "> 60.72.0-2" + }, + { + "o": 61, + "a": "AppVersion", + "t": 8, + "c": "60.72.0-1", + "v": "> 60.72.0-1" + }, + { + "o": 62, + "a": "AppVersion", + "t": 9, + "c": "60.71.0+anothermetadata", + "v": ">= 60.71.0+anothermetadata" + }, + { + "o": 63, + "a": "AppVersion", + "t": 9, + "c": "60.71.0-patch3+anothermetadata", + "v": ">= 60.71.0-patch3+anothermetadata" + }, + { + "o": 64, + "a": "AppVersion", + "t": 9, + "c": "60.71.0-patch2", + "v": ">= 60.71.0-patch2" + }, + { + "o": 65, + "a": "AppVersion", + "t": 9, + "c": "60.71.0-patch1+metadata", + "v": ">= 60.71.0-patch1+metadata" + }, + { + "o": 66, + "a": "AppVersion", + "t": 9, + "c": "50.60.73-beta.2", + "v": ">= 50.60.73-beta.2" + }, + { + "o": 67, + "a": "AppVersion", + "t": 8, + "c": "50.60.72-beta.2", + "v": "> 50.60.72-beta.2" + }, + { + "o": 68, + "a": "AppVersion", + "t": 8, + "c": "50.60.72-beta.1", + "v": "> 50.60.72-beta.1" + }, + { + "o": 69, + "a": "AppVersion", + "t": 8, + "c": "50.60.72-beta", + "v": "> 50.60.72-beta" + }, + { + "o": 70, + "a": "AppVersion", + "t": 8, + "c": "50.60.72-alpha", + "v": "> 50.60.72-alpha" + }, + { + "o": 71, + "a": "AppVersion", + "t": 8, + "c": "50.60.72-1a", + "v": "> 50.60.72-1a" + }, + { + "o": 72, + "a": "AppVersion", + "t": 8, + "c": "50.60.72-10a", + "v": "> 50.60.72-10a" + }, + { + "o": 73, + "a": "AppVersion", + "t": 8, + "c": "50.60.72-2", + "v": "> 50.60.72-2" + }, + { + "o": 74, + "a": "AppVersion", + "t": 8, + "c": "50.60.72-1", + "v": "> 50.60.72-1" + }, + { + "o": 75, + "a": "AppVersion", + "t": 9, + "c": "50.60.71+anothermetadata", + "v": ">= 50.60.71+anothermetadata" + }, + { + "o": 76, + "a": "AppVersion", + "t": 9, + "c": "50.60.71-patch3+anothermetadata", + "v": ">= 50.60.71-patch3+anothermetadata" + }, + { + "o": 77, + "a": "AppVersion", + "t": 9, + "c": "50.60.71-patch2", + "v": ">= 50.60.71-patch2" + }, + { + "o": 78, + "a": "AppVersion", + "t": 9, + "c": "50.60.71-patch1+metadata", + "v": ">= 50.60.71-patch1+metadata" + }, + { + "o": 79, + "a": "AppVersion", + "t": 9, + "c": "40.0.0-patch", + "v": ">= 40.0.0-patch" + }, + { + "o": 80, + "a": "AppVersion", + "t": 9, + "c": "30.0.0-alpha", + "v": ">= 30.0.0-alpha" + } + ] + } + } +} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/sample_semantic_v4.json b/src/ConfigCat.Client.Tests/data/sample_semantic_v4.json deleted file mode 100644 index e211b833..00000000 --- a/src/ConfigCat.Client.Tests/data/sample_semantic_v4.json +++ /dev/null @@ -1 +0,0 @@ -{"isOneOf":{"v":"Default","t":1,"p":[],"r":[{"o":0,"a":"Custom1","t":4,"c":"1.0.0, 2","v":"Is one of (1.0.0, 2)"},{"o":1,"a":"Custom1","t":4,"c":"1.0.0","v":"Is one of (1.0.0)"},{"o":2,"a":"Custom1","t":4,"c":" , 2.0.1, 2.0.2, ","v":"Is one of ( , 2.0.1, 2.0.2, )"},{"o":3,"a":"Custom1","t":4,"c":"3......","v":"Is one of (3......)"},{"o":4,"a":"Custom1","t":4,"c":"3....","v":"Is one of (3...)"},{"o":5,"a":"Custom1","t":4,"c":"3..0","v":"Is one of (3..0)"},{"o":6,"a":"Custom1","t":4,"c":"3.0","v":"Is one of (3.0)"},{"o":7,"a":"Custom1","t":4,"c":"3.0.","v":"Is one of (3.0.)"},{"o":8,"a":"Custom1","t":4,"c":"3.0.0","v":"Is one of (3.0.0)"}]},"isOneOfWithPercentage":{"v":"Default","t":1,"p":[{"o":0,"v":"20%","p":20},{"o":1,"v":"80%","p":80}],"r":[{"o":0,"a":"Custom1","t":4,"c":"1.0.0","v":"is one of (1.0.0)"}]},"isNotOneOf":{"v":"Default","t":1,"p":[],"r":[{"o":0,"a":"Custom1","t":5,"c":"1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, ","v":"Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )"},{"o":1,"a":"Custom1","t":5,"c":"1.0.0, 3.0.1","v":"Is not one of (1.0.0, 3.0.1)"}]},"isNotOneOfWithPercentage":{"v":"Default","t":1,"p":[{"o":0,"v":"20%","p":20},{"o":1,"v":"80%","p":80}],"r":[{"o":0,"a":"Custom1","t":5,"c":"1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, ","v":"Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )"},{"o":1,"a":"Custom1","t":5,"c":"1.0.0, 3.0.1","v":"Is not one of (1.0.0, 3.0.1)"}]},"lessThanWithPercentage":{"v":"Default","t":1,"p":[{"o":0,"v":"20%","p":20},{"o":1,"v":"80%","p":80}],"r":[{"o":0,"a":"Custom1","t":6,"c":" 1.0.0 ","v":"< 1.0.0"}]},"relations":{"v":"Default","t":1,"p":[],"r":[{"o":0,"a":"Custom1","t":6,"c":"1.0.0,","v":"<1.0.0,"},{"o":1,"a":"Custom1","t":6,"c":"1.0.0","v":"< 1.0.0"},{"o":2,"a":"Custom1","t":7,"c":"1.0.0","v":"<=1.0.0"},{"o":3,"a":"Custom1","t":8,"c":"2.0.0","v":">2.0.0"},{"o":4,"a":"Custom1","t":9,"c":"2.0.0","v":">=2.0.0"}]}} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/sample_semantic_v5.json b/src/ConfigCat.Client.Tests/data/sample_semantic_v5.json new file mode 100644 index 00000000..0cadba72 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/sample_semantic_v5.json @@ -0,0 +1,219 @@ +{ + "f": { + "isOneOf": { + "v": "Default", + "t": 1, + "p": [], + "r": [ + { + "o": 0, + "a": "Custom1", + "t": 4, + "c": "1.0.0, 2", + "v": "Is one of (1.0.0, 2)" + }, + { + "o": 1, + "a": "Custom1", + "t": 4, + "c": "1.0.0", + "v": "Is one of (1.0.0)" + }, + { + "o": 2, + "a": "Custom1", + "t": 4, + "c": " , 2.0.1, 2.0.2, ", + "v": "Is one of ( , 2.0.1, 2.0.2, )" + }, + { + "o": 3, + "a": "Custom1", + "t": 4, + "c": "3......", + "v": "Is one of (3......)" + }, + { + "o": 4, + "a": "Custom1", + "t": 4, + "c": "3....", + "v": "Is one of (3...)" + }, + { + "o": 5, + "a": "Custom1", + "t": 4, + "c": "3..0", + "v": "Is one of (3..0)" + }, + { + "o": 6, + "a": "Custom1", + "t": 4, + "c": "3.0", + "v": "Is one of (3.0)" + }, + { + "o": 7, + "a": "Custom1", + "t": 4, + "c": "3.0.", + "v": "Is one of (3.0.)" + }, + { + "o": 8, + "a": "Custom1", + "t": 4, + "c": "3.0.0", + "v": "Is one of (3.0.0)" + } + ] + }, + "isOneOfWithPercentage": { + "v": "Default", + "t": 1, + "p": [ + { + "o": 0, + "v": "20%", + "p": 20 + }, + { + "o": 1, + "v": "80%", + "p": 80 + } + ], + "r": [ + { + "o": 0, + "a": "Custom1", + "t": 4, + "c": "1.0.0", + "v": "is one of (1.0.0)" + } + ] + }, + "isNotOneOf": { + "v": "Default", + "t": 1, + "p": [], + "r": [ + { + "o": 0, + "a": "Custom1", + "t": 5, + "c": "1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, ", + "v": "Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )" + }, + { + "o": 1, + "a": "Custom1", + "t": 5, + "c": "1.0.0, 3.0.1", + "v": "Is not one of (1.0.0, 3.0.1)" + } + ] + }, + "isNotOneOfWithPercentage": { + "v": "Default", + "t": 1, + "p": [ + { + "o": 0, + "v": "20%", + "p": 20 + }, + { + "o": 1, + "v": "80%", + "p": 80 + } + ], + "r": [ + { + "o": 0, + "a": "Custom1", + "t": 5, + "c": "1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, ", + "v": "Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )" + }, + { + "o": 1, + "a": "Custom1", + "t": 5, + "c": "1.0.0, 3.0.1", + "v": "Is not one of (1.0.0, 3.0.1)" + } + ] + }, + "lessThanWithPercentage": { + "v": "Default", + "t": 1, + "p": [ + { + "o": 0, + "v": "20%", + "p": 20 + }, + { + "o": 1, + "v": "80%", + "p": 80 + } + ], + "r": [ + { + "o": 0, + "a": "Custom1", + "t": 6, + "c": " 1.0.0 ", + "v": "< 1.0.0" + } + ] + }, + "relations": { + "v": "Default", + "t": 1, + "p": [], + "r": [ + { + "o": 0, + "a": "Custom1", + "t": 6, + "c": "1.0.0,", + "v": "<1.0.0," + }, + { + "o": 1, + "a": "Custom1", + "t": 6, + "c": "1.0.0", + "v": "< 1.0.0" + }, + { + "o": 2, + "a": "Custom1", + "t": 7, + "c": "1.0.0", + "v": "<=1.0.0" + }, + { + "o": 3, + "a": "Custom1", + "t": 8, + "c": "2.0.0", + "v": ">2.0.0" + }, + { + "o": 4, + "a": "Custom1", + "t": 9, + "c": "2.0.0", + "v": ">=2.0.0" + } + ] + } + } +} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/sample_sensitive_v4.json b/src/ConfigCat.Client.Tests/data/sample_sensitive_v4.json deleted file mode 100644 index 52dfa7b9..00000000 --- a/src/ConfigCat.Client.Tests/data/sample_sensitive_v4.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "isNotOneOfSensitive": { - "v": "ToAll", - "t": 1, - "p": [], - "r": [ - { - "o": 0, - "a": "Identifier", - "t": 17, - "c": "68d93aa74a0aa1664f65ad6c0515f24769b15c84,8409e4e5d27a1465165012b03b2606f0e5b08250", - "v": "Kigyo" - }, - { - "o": 1, - "a": "Email", - "t": 17, - "c": "2e1c7263a639cf2719f585dfa0be3953c13dd36f,532df0aa59af3cf1d3d876316225e987e63bf8a6", - "v": "Angolna" - }, - { - "o": 2, - "a": "Country", - "t": 17, - "c": "707fe00aa123eb0be5010f1d3065c2b6d7934ca4,ff95dc990b9440c8ff18edd8592bf43915e510b9,e2ff49d5209adefb1d572ca4ca42701ac5b167ad", - "v": "Ireland" - } - ] - }, - "isOneOfSensitive": { - "v": "ToAll", - "t": 1, - "p": [], - "r": [ - { - "o": 0, - "a": "Email", - "t": 16, - "c": "532df0aa59af3cf1d3d876316225e987e63bf8a6", - "v": "Macska" - }, - { - "o": 1, - "a": "Identifier", - "t": 16, - "c": "cc1a672b80f85ec48aa620a588864285e2b04a45,68d93aa74a0aa1664f65ad6c0515f24769b15c84", - "v": "Allat" - }, - { - "o": 2, - "a": "Country", - "t": 16, - "c": "707fe00aa123eb0be5010f1d3065c2b6d7934ca4,ff95dc990b9440c8ff18edd8592bf43915e510b9,e2ff49d5209adefb1d572ca4ca42701ac5b167ad", - "v": "Britt" - } - ] - } -} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/sample_sensitive_v5.json b/src/ConfigCat.Client.Tests/data/sample_sensitive_v5.json new file mode 100644 index 00000000..70d92d9f --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/sample_sensitive_v5.json @@ -0,0 +1,63 @@ +{ + "f": + { + "isNotOneOfSensitive": { + "v": "ToAll", + "t": 1, + "p": [], + "r": [ + { + "o": 0, + "a": "Identifier", + "t": 17, + "c": "68d93aa74a0aa1664f65ad6c0515f24769b15c84,8409e4e5d27a1465165012b03b2606f0e5b08250", + "v": "Kigyo" + }, + { + "o": 1, + "a": "Email", + "t": 17, + "c": "2e1c7263a639cf2719f585dfa0be3953c13dd36f,532df0aa59af3cf1d3d876316225e987e63bf8a6", + "v": "Angolna" + }, + { + "o": 2, + "a": "Country", + "t": 17, + "c": + "707fe00aa123eb0be5010f1d3065c2b6d7934ca4,ff95dc990b9440c8ff18edd8592bf43915e510b9,e2ff49d5209adefb1d572ca4ca42701ac5b167ad", + "v": "Ireland" + } + ] + }, + "isOneOfSensitive": { + "v": "ToAll", + "t": 1, + "p": [], + "r": [ + { + "o": 0, + "a": "Email", + "t": 16, + "c": "532df0aa59af3cf1d3d876316225e987e63bf8a6", + "v": "Macska" + }, + { + "o": 1, + "a": "Identifier", + "t": 16, + "c": "cc1a672b80f85ec48aa620a588864285e2b04a45,68d93aa74a0aa1664f65ad6c0515f24769b15c84", + "v": "Allat" + }, + { + "o": 2, + "a": "Country", + "t": 16, + "c": + "707fe00aa123eb0be5010f1d3065c2b6d7934ca4,ff95dc990b9440c8ff18edd8592bf43915e510b9,e2ff49d5209adefb1d572ca4ca42701ac5b167ad", + "v": "Britt" + } + ] + } + } +} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/sample_v4.json b/src/ConfigCat.Client.Tests/data/sample_v4.json deleted file mode 100644 index 165169ed..00000000 --- a/src/ConfigCat.Client.Tests/data/sample_v4.json +++ /dev/null @@ -1,332 +0,0 @@ -{ - "stringDefaultCat": { - "v": "Cat", - "t": 1, - "p": [], - "r": [] - }, - "stringIsInDogDefaultCat": { - "v": "Cat", - "t": 1, - "p": [], - "r": [ - { - "o": 0, - "a": "Email", - "t": 0, - "c": "a@configcat.com, b@configcat.com", - "v": "Dog" - }, - { - "o": 1, - "a": "Custom1", - "t": 0, - "c": "admin", - "v": "Dog" - } - ] - }, - "stringIsNotInDogDefaultCat": { - "v": "Cat", - "t": 1, - "p": [], - "r": [ - { - "o": 0, - "a": "Email", - "t": 1, - "c": "a@configcat.com,b@configcat.com", - "v": "Dog" - } - ] - }, - "stringContainsDogDefaultCat": { - "v": "Cat", - "t": 1, - "p": [], - "r": [ - { - "o": 0, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": "Dog" - } - ] - }, - "stringNotContainsDogDefaultCat": { - "v": "Cat", - "t": 1, - "p": [], - "r": [ - { - "o": 0, - "a": "Email", - "t": 3, - "c": "@configcat.com", - "v": "Dog" - } - ] - }, - "string25Cat25Dog25Falcon25Horse": { - "v": "Chicken", - "t": 1, - "p": [ - { - "o": 0, - "v": "Cat", - "p": 25 - }, - { - "o": 1, - "v": "Dog", - "p": 25 - }, - { - "o": 2, - "v": "Falcon", - "p": 25 - }, - { - "o": 3, - "v": "Horse", - "p": 25 - } - ], - "r": [] - }, - "string75Cat0Dog25Falcon0Horse": { - "v": "Chicken", - "t": 1, - "p": [ - { - "o": 0, - "v": "Cat", - "p": 75 - }, - { - "o": 1, - "v": "Dog", - "p": 0 - }, - { - "o": 2, - "v": "Falcon", - "p": 25 - }, - { - "o": 3, - "v": "Horse", - "p": 0 - } - ], - "r": [] - }, - "string25Cat25Dog25Falcon25HorseAdvancedRules": { - "v": "Chicken", - "t": 1, - "p": [ - { - "o": 0, - "v": "Cat", - "p": 25 - }, - { - "o": 1, - "v": "Dog", - "p": 25 - }, - { - "o": 2, - "v": "Falcon", - "p": 25 - }, - { - "o": 3, - "v": "Horse", - "p": 25 - } - ], - "r": [ - { - "o": 0, - "a": "Country", - "t": 0, - "c": "Hungary, United Kingdom", - "v": "Dolphin" - }, - { - "o": 1, - "a": "Custom1", - "t": 2, - "c": "admi", - "v": "Lion" - }, - { - "o": 2, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": "Kitten" - } - ] - }, - "boolDefaultTrue": { - "v": true, - "t": 0, - "p": [], - "r": [] - }, - "boolDefaultFalse": { - "v": false, - "t": 0, - "p": [], - "r": [] - }, - "bool30TrueAdvancedRules": { - "v": true, - "t": 0, - "p": [ - { - "o": 0, - "v": true, - "p": 30 - }, - { - "o": 1, - "v": false, - "p": 70 - } - ], - "r": [ - { - "o": 0, - "a": "Email", - "t": 0, - "c": "a@configcat.com, b@configcat.com", - "v": false - }, - { - "o": 1, - "a": "Country", - "t": 2, - "c": "United", - "v": false - } - ] - }, - "integer25One25Two25Three25FourAdvancedRules": { - "v": -1, - "t": 2, - "p": [ - { - "o": 0, - "v": 1, - "p": 25 - }, - { - "o": 1, - "v": 2, - "p": 25 - }, - { - "o": 2, - "v": 3, - "p": 25 - }, - { - "o": 3, - "v": 4, - "p": 25 - } - ], - "r": [ - { - "o": 0, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": 5 - } - ] - }, - "integerDefaultOne": { - "v": 1, - "t": 2, - "p": [], - "r": [] - }, - "doubleDefaultPi": { - "v": 3.1415, - "t": 3, - "p": [], - "r": [] - }, - "double25Pi25E25Gr25Zero": { - "v": -1.0, - "t": 3, - "p": [ - { - "o": 0, - "v": 3.1415, - "p": 25 - }, - { - "o": 1, - "v": 2.7182, - "p": 25 - }, - { - "o": 2, - "v": 1.61803, - "p": 25 - }, - { - "o": 3, - "v": 0.0, - "p": 25 - } - ], - "r": [ - { - "o": 0, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": 5.561 - } - ] - }, - "keySampleText": { - "v": "Cat", - "t": 1, - "p": [ - { - "o": 0, - "v": "Falcon", - "p": 50 - }, - { - "o": 1, - "v": "Horse", - "p": 50 - } - ], - "r": [ - { - "o": 0, - "a": "Country", - "t": 0, - "c": "Hungary,Bahamas", - "v": "Dog" - }, - { - "o": 1, - "a": "SubscriptionType", - "t": 0, - "c": "unlimited", - "v": "Lion" - } - ] - } -} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/sample_v5.json b/src/ConfigCat.Client.Tests/data/sample_v5.json new file mode 100644 index 00000000..253e5320 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/sample_v5.json @@ -0,0 +1,334 @@ +{ + "f": { + "stringDefaultCat": { + "v": "Cat", + "t": 1, + "p": [], + "r": [] + }, + "stringIsInDogDefaultCat": { + "v": "Cat", + "t": 1, + "p": [], + "r": [ + { + "o": 0, + "a": "Email", + "t": 0, + "c": "a@configcat.com, b@configcat.com", + "v": "Dog" + }, + { + "o": 1, + "a": "Custom1", + "t": 0, + "c": "admin", + "v": "Dog" + } + ] + }, + "stringIsNotInDogDefaultCat": { + "v": "Cat", + "t": 1, + "p": [], + "r": [ + { + "o": 0, + "a": "Email", + "t": 1, + "c": "a@configcat.com,b@configcat.com", + "v": "Dog" + } + ] + }, + "stringContainsDogDefaultCat": { + "v": "Cat", + "t": 1, + "p": [], + "r": [ + { + "o": 0, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": "Dog" + } + ] + }, + "stringNotContainsDogDefaultCat": { + "v": "Cat", + "t": 1, + "p": [], + "r": [ + { + "o": 0, + "a": "Email", + "t": 3, + "c": "@configcat.com", + "v": "Dog" + } + ] + }, + "string25Cat25Dog25Falcon25Horse": { + "v": "Chicken", + "t": 1, + "p": [ + { + "o": 0, + "v": "Cat", + "p": 25 + }, + { + "o": 1, + "v": "Dog", + "p": 25 + }, + { + "o": 2, + "v": "Falcon", + "p": 25 + }, + { + "o": 3, + "v": "Horse", + "p": 25 + } + ], + "r": [] + }, + "string75Cat0Dog25Falcon0Horse": { + "v": "Chicken", + "t": 1, + "p": [ + { + "o": 0, + "v": "Cat", + "p": 75 + }, + { + "o": 1, + "v": "Dog", + "p": 0 + }, + { + "o": 2, + "v": "Falcon", + "p": 25 + }, + { + "o": 3, + "v": "Horse", + "p": 0 + } + ], + "r": [] + }, + "string25Cat25Dog25Falcon25HorseAdvancedRules": { + "v": "Chicken", + "t": 1, + "p": [ + { + "o": 0, + "v": "Cat", + "p": 25 + }, + { + "o": 1, + "v": "Dog", + "p": 25 + }, + { + "o": 2, + "v": "Falcon", + "p": 25 + }, + { + "o": 3, + "v": "Horse", + "p": 25 + } + ], + "r": [ + { + "o": 0, + "a": "Country", + "t": 0, + "c": "Hungary, United Kingdom", + "v": "Dolphin" + }, + { + "o": 1, + "a": "Custom1", + "t": 2, + "c": "admi", + "v": "Lion" + }, + { + "o": 2, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": "Kitten" + } + ] + }, + "boolDefaultTrue": { + "v": true, + "t": 0, + "p": [], + "r": [] + }, + "boolDefaultFalse": { + "v": false, + "t": 0, + "p": [], + "r": [] + }, + "bool30TrueAdvancedRules": { + "v": true, + "t": 0, + "p": [ + { + "o": 0, + "v": true, + "p": 30 + }, + { + "o": 1, + "v": false, + "p": 70 + } + ], + "r": [ + { + "o": 0, + "a": "Email", + "t": 0, + "c": "a@configcat.com, b@configcat.com", + "v": false + }, + { + "o": 1, + "a": "Country", + "t": 2, + "c": "United", + "v": false + } + ] + }, + "integer25One25Two25Three25FourAdvancedRules": { + "v": -1, + "t": 2, + "p": [ + { + "o": 0, + "v": 1, + "p": 25 + }, + { + "o": 1, + "v": 2, + "p": 25 + }, + { + "o": 2, + "v": 3, + "p": 25 + }, + { + "o": 3, + "v": 4, + "p": 25 + } + ], + "r": [ + { + "o": 0, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": 5 + } + ] + }, + "integerDefaultOne": { + "v": 1, + "t": 2, + "p": [], + "r": [] + }, + "doubleDefaultPi": { + "v": 3.1415, + "t": 3, + "p": [], + "r": [] + }, + "double25Pi25E25Gr25Zero": { + "v": -1.0, + "t": 3, + "p": [ + { + "o": 0, + "v": 3.1415, + "p": 25 + }, + { + "o": 1, + "v": 2.7182, + "p": 25 + }, + { + "o": 2, + "v": 1.61803, + "p": 25 + }, + { + "o": 3, + "v": 0.0, + "p": 25 + } + ], + "r": [ + { + "o": 0, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": 5.561 + } + ] + }, + "keySampleText": { + "v": "Cat", + "t": 1, + "p": [ + { + "o": 0, + "v": "Falcon", + "p": 50 + }, + { + "o": 1, + "v": "Horse", + "p": 50 + } + ], + "r": [ + { + "o": 0, + "a": "Country", + "t": 0, + "c": "Hungary,Bahamas", + "v": "Dog" + }, + { + "o": 1, + "a": "SubscriptionType", + "t": 0, + "c": "unlimited", + "v": "Lion" + } + ] + } + } +} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/sample_variationid_v4.json b/src/ConfigCat.Client.Tests/data/sample_variationid_v4.json deleted file mode 100644 index 244f1b6a..00000000 --- a/src/ConfigCat.Client.Tests/data/sample_variationid_v4.json +++ /dev/null @@ -1,142 +0,0 @@ -{ - "boolean": { - "v": false, - "i": "a0e56eda", - "t": 0, - "p": [ - { - "o": 0, - "v": true, - "p": 50, - "i": "67787ae4" - }, - { - "o": 1, - "v": false, - "p": 50, - "i": "a0e56eda" - } - ], - "r": [ - { - "o": 0, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": true, - "i": "67787ae4" - } - ] - }, - "text": { - "v": "c", - "t": 1, - "i": "3f05be89", - "p": [ - { - "o": 0, - "v": "a", - "p": 50, - "i": "30ba32b9" - }, - { - "o": 1, - "v": "b", - "p": 50, - "i": "cf19e913" - } - ], - "r": [ - { - "o": 0, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": "true", - "i": "9bdc6a1f" - }, - { - "o": 1, - "a": "Email", - "t": 2, - "c": "@test.com", - "v": "false", - "i": "65310deb" - } - ] - }, - "whole": { - "v": 999999, - "i": "cf2e9162", - "t": 2, - "p": [ - { - "o": 0, - "v": 0, - "p": 50, - "i": "ec14f6a9" - }, - { - "o": 1, - "v": -1, - "p": 50, - "i": "61a5a033" - } - ], - "r": [ - { - "o": 0, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": 1, - "i": "ab30533b" - } - ] - }, - "decimal": { - "v": 0.0, - "i": "63612d39", - "t": 3, - "p": [ - { - "o": 0, - "v": 1.0, - "p": 50, - "i": "d0dbc27f" - }, - { - "o": 1, - "v": 2.0, - "p": 50, - "i": "8155ad7b" - } - ], - "r": [ - { - "o": 0, - "a": "Email", - "t": 2, - "c": "@configcat.com", - "v": -2147483647.2147484, - "i": "8f9559cf" - }, - { - "o": 1, - "a": "Email", - "t": 0, - "c": "a@test.com", - "v": 0.12345678912345678, - "i": "d66c5781" - }, - { - "o": 2, - "a": "Email", - "t": 0, - "c": "b@test.com", - "v": 0.12345678912, - "i": "d66c5781" - } - ] - } -} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/sample_variationid_v5.json b/src/ConfigCat.Client.Tests/data/sample_variationid_v5.json new file mode 100644 index 00000000..c11b4278 --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/sample_variationid_v5.json @@ -0,0 +1,144 @@ +{ + "f": { + "boolean": { + "v": false, + "i": "a0e56eda", + "t": 0, + "p": [ + { + "o": 0, + "v": true, + "p": 50, + "i": "67787ae4" + }, + { + "o": 1, + "v": false, + "p": 50, + "i": "a0e56eda" + } + ], + "r": [ + { + "o": 0, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": true, + "i": "67787ae4" + } + ] + }, + "text": { + "v": "c", + "t": 1, + "i": "3f05be89", + "p": [ + { + "o": 0, + "v": "a", + "p": 50, + "i": "30ba32b9" + }, + { + "o": 1, + "v": "b", + "p": 50, + "i": "cf19e913" + } + ], + "r": [ + { + "o": 0, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": "true", + "i": "9bdc6a1f" + }, + { + "o": 1, + "a": "Email", + "t": 2, + "c": "@test.com", + "v": "false", + "i": "65310deb" + } + ] + }, + "whole": { + "v": 999999, + "i": "cf2e9162", + "t": 2, + "p": [ + { + "o": 0, + "v": 0, + "p": 50, + "i": "ec14f6a9" + }, + { + "o": 1, + "v": -1, + "p": 50, + "i": "61a5a033" + } + ], + "r": [ + { + "o": 0, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": 1, + "i": "ab30533b" + } + ] + }, + "decimal": { + "v": 0.0, + "i": "63612d39", + "t": 3, + "p": [ + { + "o": 0, + "v": 1.0, + "p": 50, + "i": "d0dbc27f" + }, + { + "o": 1, + "v": 2.0, + "p": 50, + "i": "8155ad7b" + } + ], + "r": [ + { + "o": 0, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": -2147483647.2147484, + "i": "8f9559cf" + }, + { + "o": 1, + "a": "Email", + "t": 0, + "c": "a@test.com", + "v": 0.12345678912345678, + "i": "d66c5781" + }, + { + "o": 2, + "a": "Email", + "t": 0, + "c": "b@test.com", + "v": 0.12345678912, + "i": "d66c5781" + } + ] + } + } +} \ No newline at end of file diff --git a/src/ConfigCatClient/Cache/CacheParameters.cs b/src/ConfigCatClient/Cache/CacheParameters.cs new file mode 100644 index 00000000..25831534 --- /dev/null +++ b/src/ConfigCatClient/Cache/CacheParameters.cs @@ -0,0 +1,9 @@ +namespace ConfigCat.Client.Cache +{ + internal class CacheParameters + { + public IConfigCache ConfigCache { get; set; } + + public string CacheKey { get; set; } + } +} diff --git a/src/ConfigCatClient/Cache/IConfigCache.cs b/src/ConfigCatClient/Cache/IConfigCache.cs index 69a4a7aa..e80a37cc 100644 --- a/src/ConfigCatClient/Cache/IConfigCache.cs +++ b/src/ConfigCatClient/Cache/IConfigCache.cs @@ -1,4 +1,7 @@ -namespace ConfigCat.Client +using System.Threading; +using System.Threading.Tasks; + +namespace ConfigCat.Client { /// /// Defines cache @@ -8,13 +11,14 @@ public interface IConfigCache /// /// Set a into cache /// + /// A string identifying the value. /// - void Set(ProjectConfig config); + Task SetAsync(string key, ProjectConfig config); /// /// Get a from cache /// /// - ProjectConfig Get(); + Task GetAsync(string key, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/ConfigCatClient/Cache/InMemoryConfigCache.cs b/src/ConfigCatClient/Cache/InMemoryConfigCache.cs index 5fb8e70d..1178201a 100644 --- a/src/ConfigCatClient/Cache/InMemoryConfigCache.cs +++ b/src/ConfigCatClient/Cache/InMemoryConfigCache.cs @@ -1,21 +1,23 @@ using System.Threading; +using System.Threading.Tasks; namespace ConfigCat.Client { internal class InMemoryConfigCache : IConfigCache { - private ProjectConfig config; + private ProjectConfig projectConfig; private readonly ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim(); /// - public void Set(ProjectConfig config) + public Task SetAsync(string key, ProjectConfig config) { this.lockSlim.EnterWriteLock(); try { - this.config = config; + this.projectConfig = config; + return Task.FromResult(0); } finally { @@ -24,13 +26,13 @@ public void Set(ProjectConfig config) } /// - public ProjectConfig Get() + public Task GetAsync(string key, CancellationToken cancellationToken = default) { this.lockSlim.EnterReadLock(); try { - return this.config; + return Task.FromResult(this.projectConfig); } finally { diff --git a/src/ConfigCatClient/ConfigCatClient.cs b/src/ConfigCatClient/ConfigCatClient.cs index 133f52a9..46b15933 100644 --- a/src/ConfigCatClient/ConfigCatClient.cs +++ b/src/ConfigCatClient/ConfigCatClient.cs @@ -6,6 +6,8 @@ using System.Net.Http; using System.Reflection; using System.Threading.Tasks; +using ConfigCat.Client.Cache; +using ConfigCat.Client.Security; namespace ConfigCat.Client { @@ -22,6 +24,8 @@ public class ConfigCatClient : IConfigCatClient private readonly IConfigDeserializer configDeserializer; + private readonly CacheParameters cacheParameters; + private static readonly string version = typeof(ConfigCatClient).GetTypeInfo().Assembly.GetCustomAttribute().InformationalVersion; /// @@ -41,8 +45,9 @@ public LogLevel LogLevel /// Create an instance of ConfigCatClient and setup AutoPoll mode /// /// SDK Key to access configuration + /// Default: Global. Set this parameter to be in sync with the Data Governance preference on the Dashboard: https://app.configcat.com/organization/data-governance (Only Organization Admins have access) /// When the is null or empty - public ConfigCatClient(string sdkKey) : this(new AutoPollConfiguration { SdkKey = sdkKey }) + public ConfigCatClient(string sdkKey, DataGovernance dataGovernance = DataGovernance.Global) : this(new AutoPollConfiguration { SdkKey = sdkKey, DataGovernance = dataGovernance }) { } @@ -55,16 +60,17 @@ public ConfigCatClient(string sdkKey) : this(new AutoPollConfiguration { SdkKey public ConfigCatClient(AutoPollConfiguration configuration) : this((ConfigurationBase)configuration) { - var configService = new AutoPollConfigService( - new HttpConfigFetcher(configuration.CreateUrl(), "a-" + version, configuration.Logger, configuration.HttpClientHandler), - configuration.ConfigCache ?? new InMemoryConfigCache(), + var autoPollService = new AutoPollConfigService( + new HttpConfigFetcher(configuration.CreateUri(), "a-" + version, configuration.Logger, configuration.HttpClientHandler, configuration.IsCustomBaseUrl), + this.cacheParameters, TimeSpan.FromSeconds(configuration.PollIntervalSeconds), TimeSpan.FromSeconds(configuration.MaxInitWaitTimeSeconds), - configuration.Logger); + configuration.Logger + ); - configService.OnConfigurationChanged += configuration.RaiseOnConfigurationChanged; + autoPollService.OnConfigurationChanged += configuration.RaiseOnConfigurationChanged; - this.configService = configService; + this.configService = autoPollService; } /// @@ -76,13 +82,13 @@ public ConfigCatClient(AutoPollConfiguration configuration) public ConfigCatClient(LazyLoadConfiguration configuration) : this((ConfigurationBase)configuration) { - var configService = new LazyLoadConfigService( - new HttpConfigFetcher(configuration.CreateUrl(), "l-" + version, configuration.Logger, configuration.HttpClientHandler), - configuration.ConfigCache ?? new InMemoryConfigCache(), + var lazyLoadService = new LazyLoadConfigService( + new HttpConfigFetcher(configuration.CreateUri(), "l-" + version, configuration.Logger, configuration.HttpClientHandler, configuration.IsCustomBaseUrl), + this.cacheParameters, configuration.Logger, TimeSpan.FromSeconds(configuration.CacheTimeToLiveSeconds)); - this.configService = configService; + this.configService = lazyLoadService; } /// @@ -95,8 +101,8 @@ public ConfigCatClient(ManualPollConfiguration configuration) : this((ConfigurationBase)configuration) { var configService = new ManualPollConfigService( - new HttpConfigFetcher(configuration.CreateUrl(), "m-" + version, configuration.Logger, configuration.HttpClientHandler), - configuration.ConfigCache ?? new InMemoryConfigCache(), + new HttpConfigFetcher(configuration.CreateUri(), "m-" + version, configuration.Logger, configuration.HttpClientHandler, configuration.IsCustomBaseUrl), + this.cacheParameters, configuration.Logger); this.configService = configService; @@ -114,6 +120,11 @@ private ConfigCatClient(ConfigurationBase configuration) this.log = configuration.Logger; this.configDeserializer = new ConfigDeserializer(this.log); this.configEvaluator = new RolloutEvaluator(this.log, this.configDeserializer); + this.cacheParameters = new CacheParameters + { + ConfigCache = configuration.ConfigCache ?? new InMemoryConfigCache(), + CacheKey = GetCacheKey(configuration) + }; } /// @@ -316,5 +327,11 @@ public static ConfigCatClientBuilder Create(string sdkKey) return ConfigCatClientBuilder.Initialize(sdkKey); } + private static string GetCacheKey(ConfigurationBase configuration) + { + var key = $"dotnet_{ConfigurationBase.ConfigFileName}_{configuration.SdkKey}"; + + return HashUtils.HashString(key); + } } } \ No newline at end of file diff --git a/src/ConfigCatClient/ConfigCatClient.csproj b/src/ConfigCatClient/ConfigCatClient.csproj index c38298e7..2137457d 100644 --- a/src/ConfigCatClient/ConfigCatClient.csproj +++ b/src/ConfigCatClient/ConfigCatClient.csproj @@ -1,7 +1,7 @@  - net45;netstandard1.3;netstandard2.0 + net45;netstandard1.3;netstandard2.0;netstandard2.1 ConfigCat.Client ConfigCat.Client true diff --git a/src/ConfigCatClient/ConfigCatClientBuilder.cs b/src/ConfigCatClient/ConfigCatClientBuilder.cs index 1ee0e725..08edd563 100644 --- a/src/ConfigCatClient/ConfigCatClientBuilder.cs +++ b/src/ConfigCatClient/ConfigCatClientBuilder.cs @@ -7,6 +7,7 @@ public class ConfigCatClientBuilder { internal string SdkKey { get; private set; } internal ILogger Logger { get; private set; } = new ConsoleLogger(); + internal DataGovernance DataGovernance { get; private set; } = DataGovernance.Global; /// /// Create a instance with @@ -47,7 +48,7 @@ public LazyLoadConfigurationBuilder WithLazyLoad() /// /// Set Logger instance /// - /// Implementation of ILogger + /// Implementation of /// public ConfigCatClientBuilder WithLogger(ILogger logger) { @@ -55,5 +56,17 @@ public ConfigCatClientBuilder WithLogger(ILogger logger) return this; } + + /// + /// Set + /// + /// Describes the location of your feature flag and setting data within the ConfigCat CDN. + /// + public ConfigCatClientBuilder WithDataGovernance(DataGovernance dataGovernance) + { + this.DataGovernance = dataGovernance; + + return this; + } } } \ No newline at end of file diff --git a/src/ConfigCatClient/ConfigDeserializer.cs b/src/ConfigCatClient/ConfigDeserializer.cs index 623ac84e..7ad35170 100644 --- a/src/ConfigCatClient/ConfigDeserializer.cs +++ b/src/ConfigCatClient/ConfigDeserializer.cs @@ -18,11 +18,16 @@ public bool TryDeserialize(ProjectConfig projectConfig, out IDictionary>(projectConfig.JsonString); + var settingsWithPreferences = JsonConvert.DeserializeObject(projectConfig.JsonString); + + settings = settingsWithPreferences.Settings; + return true; } } diff --git a/src/ConfigCatClient/ConfigService/AutoPollConfigService.cs b/src/ConfigCatClient/ConfigService/AutoPollConfigService.cs index 008c3ffa..57c8a607 100644 --- a/src/ConfigCatClient/ConfigService/AutoPollConfigService.cs +++ b/src/ConfigCatClient/ConfigService/AutoPollConfigService.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using ConfigCat.Client.Cache; namespace ConfigCat.Client.ConfigService { @@ -10,14 +11,16 @@ internal sealed class AutoPollConfigService : ConfigServiceBase, IConfigService private readonly Timer timer; + private ManualResetEventSlim initializedEventSlim = new ManualResetEventSlim(false); + public event OnConfigurationChangedEventHandler OnConfigurationChanged; internal AutoPollConfigService( IConfigFetcher configFetcher, - IConfigCache configCache, + CacheParameters cacheParameters, TimeSpan pollingInterval, TimeSpan maxInitWaitTime, - ILogger logger) : this(configFetcher, configCache, pollingInterval, maxInitWaitTime, logger, true) + ILogger logger) : this(configFetcher, cacheParameters, pollingInterval, maxInitWaitTime, logger, true) { } @@ -25,18 +28,19 @@ internal AutoPollConfigService( /// For test purpose only /// /// - /// + /// /// /// /// /// internal AutoPollConfigService( IConfigFetcher configFetcher, - IConfigCache configCache, + CacheParameters cacheParameters, TimeSpan pollingInterval, TimeSpan maxInitWaitTime, ILogger logger, - bool startTimer) : base(configFetcher, configCache, logger) + bool startTimer + ) : base(configFetcher, cacheParameters, logger) { if (startTimer) { @@ -45,24 +49,31 @@ internal AutoPollConfigService( this.maxInitWaitExpire = DateTime.UtcNow.Add(maxInitWaitTime); } - + protected override void Dispose(bool disposing) { this.timer?.Dispose(); - base.Dispose(disposing); + base.Dispose(disposing); } - public Task GetConfigAsync() + public async Task GetConfigAsync() { - var d = this.maxInitWaitExpire - DateTime.UtcNow; + var delay = this.maxInitWaitExpire - DateTime.UtcNow; + + var cacheConfig = await this.configCache.GetAsync(base.cacheKey).ConfigureAwait(false); - if (d > TimeSpan.Zero) + if (delay > TimeSpan.Zero && cacheConfig.Equals(ProjectConfig.Empty)) { - Task.Run(() => RefreshLogic("init")).Wait(d); + if (!initializedEventSlim.Wait(delay)) + { + await RefreshLogicAsync("init"); + } + + cacheConfig = await this.configCache.GetAsync(base.cacheKey).ConfigureAwait(false); } - return Task.FromResult(this.configCache.Get()); + return cacheConfig; } public async Task RefreshConfigAsync() @@ -74,17 +85,19 @@ private async Task RefreshLogicAsync(object sender) { this.log.Debug($"RefreshLogic start [{sender}]"); - var latestConfig = this.configCache.Get(); + var latestConfig = await this.configCache.GetAsync(base.cacheKey).ConfigureAwait(false); - var newConfig = await this.configFetcher.Fetch(latestConfig); + var newConfig = await this.configFetcher.Fetch(latestConfig).ConfigureAwait(false); if (!latestConfig.Equals(newConfig) && !newConfig.Equals(ProjectConfig.Empty)) { this.log.Debug("config changed"); - this.configCache.Set(newConfig); + await this.configCache.SetAsync(base.cacheKey, newConfig).ConfigureAwait(false); OnConfigurationChanged?.Invoke(this, OnConfigurationChangedEventArgs.Empty); + + initializedEventSlim.Set(); } } diff --git a/src/ConfigCatClient/ConfigService/ConfigServiceBase.cs b/src/ConfigCatClient/ConfigService/ConfigServiceBase.cs index d4d9a938..298acedf 100644 --- a/src/ConfigCatClient/ConfigService/ConfigServiceBase.cs +++ b/src/ConfigCatClient/ConfigService/ConfigServiceBase.cs @@ -1,4 +1,5 @@ using System; +using ConfigCat.Client.Cache; namespace ConfigCat.Client.ConfigService { @@ -12,11 +13,15 @@ internal abstract class ConfigServiceBase : IDisposable protected readonly ILogger log; - protected ConfigServiceBase(IConfigFetcher configFetcher, IConfigCache configCache, ILogger log) + protected readonly string cacheKey; + + protected ConfigServiceBase(IConfigFetcher configFetcher, CacheParameters cacheParameters, ILogger log) { this.configFetcher = configFetcher; - this.configCache = configCache; + this.configCache = cacheParameters.ConfigCache; + + this.cacheKey = cacheParameters.CacheKey; this.log = log; } diff --git a/src/ConfigCatClient/ConfigService/LazyLoadConfigService.cs b/src/ConfigCatClient/ConfigService/LazyLoadConfigService.cs index 146b4d5f..70e9fb30 100644 --- a/src/ConfigCatClient/ConfigService/LazyLoadConfigService.cs +++ b/src/ConfigCatClient/ConfigService/LazyLoadConfigService.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using ConfigCat.Client.Cache; namespace ConfigCat.Client.ConfigService { @@ -7,15 +8,15 @@ internal sealed class LazyLoadConfigService : ConfigServiceBase, IConfigService { private readonly TimeSpan cacheTimeToLive; - internal LazyLoadConfigService(IConfigFetcher configFetcher, IConfigCache configCache, ILogger logger, TimeSpan cacheTimeToLive) - : base(configFetcher, configCache, logger) + internal LazyLoadConfigService(IConfigFetcher configFetcher, CacheParameters cacheParameters, ILogger logger, TimeSpan cacheTimeToLive) + : base(configFetcher, cacheParameters, logger) { this.cacheTimeToLive = cacheTimeToLive; } public async Task GetConfigAsync() { - var config = this.configCache.Get(); + var config = await this.configCache.GetAsync(base.cacheKey).ConfigureAwait(false); if (config.TimeStamp < DateTime.UtcNow.Add(-this.cacheTimeToLive)) { @@ -29,7 +30,7 @@ public async Task GetConfigAsync() public async Task RefreshConfigAsync() { - var config = this.configCache.Get(); + var config = await this.configCache.GetAsync(base.cacheKey).ConfigureAwait(false); await RefreshConfigLogic(config).ConfigureAwait(false); } @@ -38,7 +39,7 @@ private async Task RefreshConfigLogic(ProjectConfig config) { config = await this.configFetcher.Fetch(config).ConfigureAwait(false); - this.configCache.Set(config); + await this.configCache.SetAsync(base.cacheKey, config).ConfigureAwait(false); return config; } diff --git a/src/ConfigCatClient/ConfigService/ManualPollConfigService.cs b/src/ConfigCatClient/ConfigService/ManualPollConfigService.cs index 675d9d21..41c7b141 100644 --- a/src/ConfigCatClient/ConfigService/ManualPollConfigService.cs +++ b/src/ConfigCatClient/ConfigService/ManualPollConfigService.cs @@ -1,26 +1,25 @@ using System.Threading.Tasks; +using ConfigCat.Client.Cache; namespace ConfigCat.Client.ConfigService { internal sealed class ManualPollConfigService : ConfigServiceBase, IConfigService { - internal ManualPollConfigService(IConfigFetcher configFetcher, IConfigCache configCache, ILogger logger) - : base(configFetcher, configCache, logger) { } + internal ManualPollConfigService(IConfigFetcher configFetcher, CacheParameters cacheParameters, ILogger logger) + : base(configFetcher, cacheParameters, logger) { } - public Task GetConfigAsync() + public async Task GetConfigAsync() { - var config = this.configCache.Get(); - - return Task.FromResult(config); + return await this.configCache.GetAsync(base.cacheKey).ConfigureAwait(false); } public async Task RefreshConfigAsync() { - var config = this.configCache.Get(); + var config = await this.configCache.GetAsync(base.cacheKey).ConfigureAwait(false); config = await this.configFetcher.Fetch(config).ConfigureAwait(false); - this.configCache.Set(config); + await this.configCache.SetAsync(base.cacheKey, config).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/src/ConfigCatClient/Configuration/ConfigurationBase.cs b/src/ConfigCatClient/Configuration/ConfigurationBase.cs index d76e70e5..4a6fbcc6 100644 --- a/src/ConfigCatClient/Configuration/ConfigurationBase.cs +++ b/src/ConfigCatClient/Configuration/ConfigurationBase.cs @@ -1,5 +1,6 @@ using System; using System.Net.Http; +using System.Runtime.CompilerServices; namespace ConfigCat.Client { @@ -8,7 +9,9 @@ namespace ConfigCat.Client /// public abstract class ConfigurationBase { - private ILogger _logger; + private ILogger logger; + + private Uri baseUrl = new Uri(BaseUrlGlobal); /// /// Logger instance @@ -17,11 +20,11 @@ public ILogger Logger { get { - return this._logger ?? new LoggerWrapper(new ConsoleLogger()); + return this.logger ?? new LoggerWrapper(new ConsoleLogger()); } set { - this._logger = new LoggerWrapper(value ?? throw new ArgumentNullException(nameof(Logger))); + this.logger = new LoggerWrapper(value ?? throw new ArgumentNullException(nameof(Logger))); } } @@ -43,7 +46,21 @@ public ILogger Logger /// /// You can set a BaseUrl if you want to use a proxy server between your application and ConfigCat /// - public Uri BaseUrl { get; set; } = new Uri("https://cdn.configcat.com"); + public Uri BaseUrl + { + get => baseUrl; + set + { + this.IsCustomBaseUrl = true; + baseUrl = value; + } + } + + /// + /// Default: Global. Set this parameter to be in sync with the Data Governance preference on the Dashboard: + /// https://app.configcat.com/organization/data-governance (Only Organization Admins have access) + /// + public DataGovernance DataGovernance { get; set; } = DataGovernance.Global; internal virtual void Validate() { @@ -58,9 +75,32 @@ internal virtual void Validate() } } - internal Uri CreateUrl() + internal Uri CreateUri() { - return new Uri(BaseUrl, "configuration-files/" + this.SdkKey + "/config_v4.json"); + var baseUri = BaseUrl; + + if (!IsCustomBaseUrl) + { + switch (DataGovernance) + { + case DataGovernance.EuOnly: + baseUri = new Uri(BaseUrlEu); + break; + default: + baseUri = new Uri(BaseUrlGlobal); + break; + } + } + + return new Uri(baseUri, "configuration-files/" + this.SdkKey + "/" + ConfigFileName); } + + internal const string ConfigFileName = "config_v5.json"; + + internal const string BaseUrlGlobal = "https://cdn-global.configcat.com"; + + internal const string BaseUrlEu = "https://cdn-eu.configcat.com"; + + internal bool IsCustomBaseUrl { get; private set; } } } \ No newline at end of file diff --git a/src/ConfigCatClient/Configuration/ConfigurationBuilderBase.cs b/src/ConfigCatClient/Configuration/ConfigurationBuilderBase.cs index eb9b866b..00532091 100644 --- a/src/ConfigCatClient/Configuration/ConfigurationBuilderBase.cs +++ b/src/ConfigCatClient/Configuration/ConfigurationBuilderBase.cs @@ -13,7 +13,8 @@ internal ConfigurationBuilderBase(ConfigCatClientBuilder clientBuilder) this.configuration = new T { SdkKey = clientBuilder.SdkKey, - Logger = clientBuilder.Logger + Logger = clientBuilder.Logger, + DataGovernance = clientBuilder.DataGovernance }; } } diff --git a/src/ConfigCatClient/Configuration/DataGovernance.cs b/src/ConfigCatClient/Configuration/DataGovernance.cs new file mode 100644 index 00000000..61dcfad2 --- /dev/null +++ b/src/ConfigCatClient/Configuration/DataGovernance.cs @@ -0,0 +1,17 @@ +namespace ConfigCat.Client +{ + /// + /// Control the location of the config.json files containing your feature flags and settings within the ConfigCat CDN. + /// + public enum DataGovernance : byte + { + /// + /// Select this if your feature flags are published to all global CDN nodes. + /// + Global = 0, + /// + /// Select this if your feature flags are published to CDN nodes only in the EU. + /// + EuOnly = 1 + } +} diff --git a/src/ConfigCatClient/Evaluate/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluate/RolloutEvaluator.cs index 6deda560..f2c82d51 100644 --- a/src/ConfigCatClient/Evaluate/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluate/RolloutEvaluator.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Security.Cryptography; using System.Text; +using ConfigCat.Client.Security; namespace ConfigCat.Client.Evaluate { @@ -129,7 +130,7 @@ private bool TryEvaluateVariations(ICollection rolloutPer { var hashCandidate = key + user.Identifier; - var hashValue = HashString(hashCandidate).Substring(0, 7); + var hashValue = HashUtils.HashString(hashCandidate).Substring(0, 7); var hashScale = int.Parse(hashValue, NumberStyles.HexNumber) % 100; @@ -272,7 +273,7 @@ private static bool TryEvaluateRules(ICollection rules, User use if (rule.ComparisonValue .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(t => t.Trim()) - .Contains(HashString(comparisonAttributeValue))) + .Contains(HashUtils.HashString(comparisonAttributeValue))) { logger.Log(l + "match"); @@ -286,7 +287,7 @@ private static bool TryEvaluateRules(ICollection rules, User use if (!rule.ComparisonValue .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(t => t.Trim()) - .Contains(HashString(comparisonAttributeValue))) + .Contains(HashUtils.HashString(comparisonAttributeValue))) { logger.Log(l + "match"); @@ -427,22 +428,5 @@ private static bool EvaluateSemVer(string s1, string s2, ComparatorEnum comparat return false; } - - private static string HashString(string s) - { - using (var hash = SHA1.Create()) - { - var hashedBytes = hash.ComputeHash(Encoding.UTF8.GetBytes(s)); - - var result = new StringBuilder(); - - foreach (byte t in hashedBytes) - { - result.Append(t.ToString("x2")); - } - - return result.ToString(); - } - } } } \ No newline at end of file diff --git a/src/ConfigCatClient/Evaluate/Setting.cs b/src/ConfigCatClient/Evaluate/Setting.cs index f5a93cc5..fae28e65 100644 --- a/src/ConfigCatClient/Evaluate/Setting.cs +++ b/src/ConfigCatClient/Evaluate/Setting.cs @@ -3,6 +3,24 @@ namespace ConfigCat.Client.Evaluate { + internal class SettingsWithPreferences + { + [JsonProperty(PropertyName = "f")] + public Dictionary Settings { get; set; } + + [JsonProperty(PropertyName = "p")] + public Preferences Preferences { get; set; } + } + + internal class Preferences + { + [JsonProperty(PropertyName = "u")] + public string Url { get; set; } + + [JsonProperty(PropertyName = "r")] + public RedirectMode RedirectMode { get; set; } + } + internal class Setting { [JsonProperty(PropertyName = "v")] @@ -106,4 +124,11 @@ internal enum ComparatorEnum : byte SensitiveNotOneOf = 17 } + + internal enum RedirectMode : byte + { + No = 0, + Should = 1, + Force = 2 + } } \ No newline at end of file diff --git a/src/ConfigCatClient/HttpConfigFetcher.cs b/src/ConfigCatClient/HttpConfigFetcher.cs index 0d65fb4c..e0b95526 100644 --- a/src/ConfigCatClient/HttpConfigFetcher.cs +++ b/src/ConfigCatClient/HttpConfigFetcher.cs @@ -1,7 +1,10 @@ using System; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; +using ConfigCat.Client.Evaluate; +using Newtonsoft.Json; namespace ConfigCat.Client { @@ -15,11 +18,13 @@ internal sealed class HttpConfigFetcher : IConfigFetcher, IDisposable private readonly HttpClientHandler httpClientHandler; + private readonly bool isCustomUri; + private HttpClient httpClient; - private readonly Uri requestUri; + private Uri requestUri; - public HttpConfigFetcher(Uri requestUri, string productVersion, ILogger logger, HttpClientHandler httpClientHandler) + public HttpConfigFetcher(Uri requestUri, string productVersion, ILogger logger, HttpClientHandler httpClientHandler, bool isCustomUri) { this.requestUri = requestUri; @@ -29,37 +34,31 @@ public HttpConfigFetcher(Uri requestUri, string productVersion, ILogger logger, this.httpClientHandler = httpClientHandler; + this.isCustomUri = isCustomUri; + ReInitializeHttpClient(); } public async Task Fetch(ProjectConfig lastConfig) { - ProjectConfig newConfig = ProjectConfig.Empty; - - var request = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = this.requestUri - }; + var newConfig = lastConfig; try { - if (lastConfig.HttpETag != null) - { - request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(lastConfig.HttpETag)); - } - - newConfig = lastConfig; + var fetchResult = await FetchRequest(lastConfig, this.requestUri); - var response = await this.httpClient.SendAsync(request).ConfigureAwait(false); + var response = fetchResult.Item1; if (response.IsSuccessStatusCode) { - newConfig.HttpETag = response.Headers.ETag.Tag; + newConfig.HttpETag = response.Headers.ETag?.Tag; - newConfig.JsonString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + newConfig.JsonString = fetchResult.Item2; + } + else if (response.StatusCode == HttpStatusCode.NotModified) + { } - else if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + else if (response.StatusCode == HttpStatusCode.NotFound) { this.log.Error("Double-check your SDK Key at https://app.configcat.com/sdkkey"); } @@ -80,26 +79,116 @@ public async Task Fetch(ProjectConfig lastConfig) return newConfig; } - private void ReInitializeHttpClient() + private async Task> FetchRequest(ProjectConfig lastConfig, Uri requestUri, sbyte maxExecutionCount = 3) { - lock (this.lck) + var request = new HttpRequestMessage { - if (this.httpClientHandler == null) + Method = HttpMethod.Get, + RequestUri = requestUri + }; + + if (lastConfig.HttpETag != null) + { + request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(lastConfig.HttpETag)); + } + + var response = await this.httpClient.SendAsync(request).ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + var body = JsonConvert.DeserializeObject(responseBody); + + if (body?.Preferences != null) { - this.httpClient = new HttpClient(new HttpClientHandler + var newBaseUrl = body.Preferences.Url; + + if (newBaseUrl == null || requestUri.Host == new Uri(newBaseUrl).Host) { - AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate - }); + return Tuple.Create(response, responseBody); + } + + Evaluate.RedirectMode redirect = body.Preferences.RedirectMode; + + if (isCustomUri && redirect != RedirectMode.Force) + { + return Tuple.Create(response, responseBody); + } + + UpdateRequestUri(new Uri(newBaseUrl)); + + if (redirect == RedirectMode.No) + { + return Tuple.Create(response, responseBody); + } + + if (redirect == RedirectMode.Should) + { + this.log.Warning("Your dataGovernance parameter at ConfigCatClient initialization is not in sync " + + "with your preferences on the ConfigCat Dashboard: " + + "https://app.configcat.com/organization/data-governance. " + + "Only Organization Admins can access this preference."); + } + + if (maxExecutionCount <= 1) + { + log.Error("Redirect loop during config.json fetch. Please contact support@configcat.com."); + return Tuple.Create(response, responseBody); + } + + return await this.FetchRequest( + lastConfig, + ReplaceUri(request.RequestUri, new Uri(newBaseUrl)), + --maxExecutionCount); } else { - this.httpClient = new HttpClient(this.httpClientHandler, false); + return Tuple.Create(response, responseBody); } + } + + return Tuple.Create(response, null); + } + + private void UpdateRequestUri(Uri newUri) + { + lock (this.lck) + { + this.requestUri = ReplaceUri(this.requestUri, newUri); + } + } + + private static Uri ReplaceUri(Uri oldUri, Uri newUri) + { + return new Uri(newUri, oldUri.AbsolutePath); + } - this.httpClient.Timeout = TimeSpan.FromSeconds(30); + private void ReInitializeHttpClient() + { + lock (this.lck) + { + ReInitializeHttpClientLogic(); + } + } - this.httpClient.DefaultRequestHeaders.Add("X-ConfigCat-UserAgent", new ProductInfoHeaderValue("ConfigCat-Dotnet", productVersion).ToString()); + private void ReInitializeHttpClientLogic() + { + if (this.httpClientHandler == null) + { + this.httpClient = new HttpClient(new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate + }); } + else + { + this.httpClient = new HttpClient(this.httpClientHandler, false); + } + + this.httpClient.Timeout = TimeSpan.FromSeconds(30); + + this.httpClient.DefaultRequestHeaders.Add("X-ConfigCat-UserAgent", new ProductInfoHeaderValue("ConfigCat-Dotnet", productVersion).ToString()); } public void Dispose() diff --git a/src/ConfigCatClient/Security/HashUtils.cs b/src/ConfigCatClient/Security/HashUtils.cs new file mode 100644 index 00000000..ee0c5395 --- /dev/null +++ b/src/ConfigCatClient/Security/HashUtils.cs @@ -0,0 +1,25 @@ +using System.Security.Cryptography; +using System.Text; + +namespace ConfigCat.Client.Security +{ + internal class HashUtils + { + public static string HashString(string s) + { + using (var hash = SHA1.Create()) + { + var hashedBytes = hash.ComputeHash(Encoding.UTF8.GetBytes(s)); + + var result = new StringBuilder(); + + foreach (var t in hashedBytes) + { + result.Append(t.ToString("x2")); + } + + return result.ToString(); + } + } + } +}