diff --git a/appveyor.yml b/appveyor.yml index 7bd8cca8..6520eb5a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -33,4 +33,4 @@ notifications: to: - developer@configcat.com on_build_success: false - on_build_failure: false + on_build_failure: true 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/ConfigCacheTests.cs b/src/ConfigCat.Client.Tests/ConfigCacheTests.cs index 48145887..6caf4a9a 100644 --- a/src/ConfigCat.Client.Tests/ConfigCacheTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCacheTests.cs @@ -5,6 +5,7 @@ namespace ConfigCat.Client.Tests { + [TestCategory(TestCategories.Integration)] [TestClass] public class ConfigCacheTests { 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 c7a23b2f..fe6cf6a8 100644 --- a/src/ConfigCat.Client.Tests/ConfigServiceTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigServiceTests.cs @@ -20,7 +20,7 @@ public class ConfigServiceTests ProjectConfig cachedPc = new ProjectConfig("CACHED", DateTime.UtcNow.Add(-defaultExpire), "67890"); ProjectConfig fetchedPc = new ProjectConfig("FETCHED", DateTime.UtcNow, "12345"); - + [TestInitialize] public void TestInitialize() { @@ -290,7 +290,7 @@ public async Task AutoPollConfigService_RefreshConfigAsync_ConfigChanged_ShouldR Assert.AreEqual(1, eventChanged); } - + [TestMethod] public void AutoPollConfigService_Dispose_ShouldStopTimer() { @@ -310,7 +310,7 @@ public void AutoPollConfigService_Dispose_ShouldStopTimer() var service = new AutoPollConfigService( fetcherMock.Object, - new CacheParameters { ConfigCache = cacheMock.Object, CacheKey = ""}, + new CacheParameters { ConfigCache = cacheMock.Object, CacheKey = "" }, TimeSpan.FromSeconds(0.2d), TimeSpan.Zero, loggerMock.Object, @@ -489,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 4b6eff54..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 { diff --git a/src/ConfigCat.Client.Tests/DataGovernanceTests.cs b/src/ConfigCat.Client.Tests/DataGovernanceTests.cs index 2247b9bd..fb444e83 100644 --- a/src/ConfigCat.Client.Tests/DataGovernanceTests.cs +++ b/src/ConfigCat.Client.Tests/DataGovernanceTests.cs @@ -1,28 +1,44 @@ 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 { - Uri GlobalUri = new Uri(ConfigurationBase.BaseUrlGlobal); + 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"); - [TestMethod] - public async Task WithDefaultDataGovernanceSetting_ShouldUseGlobalCdnEveryRequests() + [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" + SdkKey = "DEMO", + DataGovernance = dataGovernance }; + byte requestCount = 0; + var requests = new SortedList(); + var handlerMock = new Mock(MockBehavior.Strict); handlerMock .Protected() @@ -31,7 +47,10 @@ public async Task WithDefaultDataGovernanceSetting_ShouldUseGlobalCdnEveryReques ItExpr.IsAny(), ItExpr.IsAny() ) - + .Callback((message, _) => + { + requests.Add(requestCount++, message); + }) .ReturnsAsync(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, @@ -39,7 +58,12 @@ public async Task WithDefaultDataGovernanceSetting_ShouldUseGlobalCdnEveryReques }) .Verifiable(); - IConfigFetcher fetcher = new HttpConfigFetcher(configuration.CreateUri(), "DEMO", Mock.Of(), handlerMock.Object, configuration.IsCustomBaseUrl); + IConfigFetcher fetcher = new HttpConfigFetcher( + configuration.CreateUri(), + "DEMO", + Mock.Of(), + handlerMock.Object, + configuration.IsCustomBaseUrl); // Act @@ -48,7 +72,339 @@ public async Task WithDefaultDataGovernanceSetting_ShouldUseGlobalCdnEveryReques // Assert handlerMock.VerifyAll(); - // TODO invoke count + URL check + 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 344de528..f249e949 100644 --- a/src/ConfigCat.Client.Tests/HttpConfigFetcherTests.cs +++ b/src/ConfigCat.Client.Tests/HttpConfigFetcherTests.cs @@ -44,6 +44,26 @@ public void HttpConfigFetcher_WithCustomHttpClientHandler_HandlersDisposeShouldN Assert.IsFalse(myHandler.Disposed); } + [TestMethod] + 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() { @@ -63,7 +83,5 @@ public async Task HttpConfigFetcher_ThrowAnException_ShouldReturnPassedConfig() Assert.AreEqual(lastConfig, actual); } - - // TODO race condition tests for Fetch()!!! } } 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/ConfigCatClient/ConfigCatClient.cs b/src/ConfigCatClient/ConfigCatClient.cs index 00171942..46b15933 100644 --- a/src/ConfigCatClient/ConfigCatClient.cs +++ b/src/ConfigCatClient/ConfigCatClient.cs @@ -45,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 }) { } diff --git a/src/ConfigCatClient/Configuration/ConfigurationBase.cs b/src/ConfigCatClient/Configuration/ConfigurationBase.cs index 6b578460..4a6fbcc6 100644 --- a/src/ConfigCatClient/Configuration/ConfigurationBase.cs +++ b/src/ConfigCatClient/Configuration/ConfigurationBase.cs @@ -81,7 +81,15 @@ internal Uri CreateUri() if (!IsCustomBaseUrl) { - baseUri = DataGovernance == DataGovernance.Global ? new Uri(BaseUrlGlobal) : new Uri(BaseUrlEu); + 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); diff --git a/src/ConfigCatClient/Configuration/DataGovernance.cs b/src/ConfigCatClient/Configuration/DataGovernance.cs index 62aa47f9..61dcfad2 100644 --- a/src/ConfigCatClient/Configuration/DataGovernance.cs +++ b/src/ConfigCatClient/Configuration/DataGovernance.cs @@ -8,10 +8,10 @@ public enum DataGovernance : byte /// /// Select this if your feature flags are published to all global CDN nodes. /// - Global = 1, + Global = 0, /// /// Select this if your feature flags are published to CDN nodes only in the EU. /// - EuOnly = 2 + EuOnly = 1 } } diff --git a/src/ConfigCatClient/Evaluate/Setting.cs b/src/ConfigCatClient/Evaluate/Setting.cs index 4b2995c0..fae28e65 100644 --- a/src/ConfigCatClient/Evaluate/Setting.cs +++ b/src/ConfigCatClient/Evaluate/Setting.cs @@ -18,7 +18,7 @@ internal class Preferences public string Url { get; set; } [JsonProperty(PropertyName = "r")] - public byte RedirectMode { get; set; } + public RedirectMode RedirectMode { get; set; } } internal class Setting @@ -124,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 4af668d4..e0b95526 100644 --- a/src/ConfigCatClient/HttpConfigFetcher.cs +++ b/src/ConfigCatClient/HttpConfigFetcher.cs @@ -2,26 +2,15 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; -using System.Text; -using System.Threading; using System.Threading.Tasks; using ConfigCat.Client.Evaluate; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace ConfigCat.Client { internal sealed class HttpConfigFetcher : IConfigFetcher, IDisposable { - private enum RedirectMode : byte - { - NoRedirect = 0, - ShouldRedirect = 1, - ForceRedirect = 2 - } - private readonly object lck = new object(); - private readonly object flck = new object(); private readonly string productVersion; @@ -52,25 +41,17 @@ public HttpConfigFetcher(Uri requestUri, string productVersion, ILogger logger, public async Task Fetch(ProjectConfig lastConfig) { - var newConfig = ProjectConfig.Empty; + var newConfig = lastConfig; try { - newConfig = lastConfig; - - var request = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri(this.requestUri.ToString()) - }; - - var fetchResult = await FetchRequest(lastConfig, request); + var fetchResult = await FetchRequest(lastConfig, this.requestUri); var response = fetchResult.Item1; if (response.IsSuccessStatusCode) { - newConfig.HttpETag = response.Headers.ETag.Tag; + newConfig.HttpETag = response.Headers.ETag?.Tag; newConfig.JsonString = fetchResult.Item2; } @@ -98,8 +79,14 @@ public async Task Fetch(ProjectConfig lastConfig) return newConfig; } - private async Task> FetchRequest(ProjectConfig lastConfig, HttpRequestMessage request, byte maxExecutionCount = 2) + private async Task> FetchRequest(ProjectConfig lastConfig, Uri requestUri, sbyte maxExecutionCount = 3) { + var request = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = requestUri + }; + if (lastConfig.HttpETag != null) { request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(lastConfig.HttpETag)); @@ -122,21 +109,21 @@ private async Task> FetchRequest(ProjectConfi return Tuple.Create(response, responseBody); } - byte redirect = body.Preferences.RedirectMode; + Evaluate.RedirectMode redirect = body.Preferences.RedirectMode; - if (isCustomUri && redirect != (byte)RedirectMode.ForceRedirect) + if (isCustomUri && redirect != RedirectMode.Force) { return Tuple.Create(response, responseBody); } UpdateRequestUri(new Uri(newBaseUrl)); - if (redirect == (byte)RedirectMode.NoRedirect) + if (redirect == RedirectMode.No) { return Tuple.Create(response, responseBody); } - if (redirect == (byte)RedirectMode.ShouldRedirect) + if (redirect == RedirectMode.Should) { this.log.Warning("Your dataGovernance parameter at ConfigCatClient initialization is not in sync " + "with your preferences on the ConfigCat Dashboard: " + @@ -144,7 +131,7 @@ private async Task> FetchRequest(ProjectConfi "Only Organization Admins can access this preference."); } - if (maxExecutionCount <= 0) + if (maxExecutionCount <= 1) { log.Error("Redirect loop during config.json fetch. Please contact support@configcat.com."); return Tuple.Create(response, responseBody); @@ -152,13 +139,13 @@ private async Task> FetchRequest(ProjectConfi return await this.FetchRequest( lastConfig, - new HttpRequestMessage - { - RequestUri = ReplaceUri(request.RequestUri, new Uri(newBaseUrl)), - Method = HttpMethod.Get - }, + ReplaceUri(request.RequestUri, new Uri(newBaseUrl)), --maxExecutionCount); } + else + { + return Tuple.Create(response, responseBody); + } } return Tuple.Create(response, null);