diff --git a/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs b/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs index 08cb1049..4d218706 100644 --- a/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs +++ b/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021, Optimizely + * Copyright 2019-2021, 2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,9 @@ */ using System; -using System.Diagnostics; +using System.Collections.Generic; using System.Net; using System.Net.Http; -using System.Threading; using System.Threading.Tasks; using Moq; using NUnit.Framework; @@ -50,16 +49,16 @@ public void Setup() } [Test] - public void TestHttpConfigManagerRetreiveProjectConfigByURL() + public void TestHttpConfigManagerRetrieveProjectConfigByURL() { var t = MockSendAsync(TestData.Datafile); - var httpManager = new HttpProjectConfigManager.Builder(). - WithUrl("https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json"). - WithLogger(LoggerMock.Object). - WithPollingInterval(TimeSpan.FromMilliseconds(1000)). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)). - WithStartByDefault(). - Build(); + var httpManager = new HttpProjectConfigManager.Builder() + .WithUrl("https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json") + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) + .WithStartByDefault() + .Build(); // This method waits until SendAsync is not triggered. // Time is given here to avoid hanging-up in any worst case. @@ -76,15 +75,15 @@ public void TestHttpConfigManagerRetreiveProjectConfigByURL() [Test] public void TestHttpConfigManagerWithInvalidStatus() { - var t = MockSendAsync(statusCode: HttpStatusCode.Forbidden); + MockSendAsync(statusCode: HttpStatusCode.Forbidden); - var httpManager = new HttpProjectConfigManager.Builder(). - WithUrl("https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json"). - WithLogger(LoggerMock.Object). - WithPollingInterval(TimeSpan.FromMilliseconds(1000)). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)). - WithStartByDefault(). - Build(); + var httpManager = new HttpProjectConfigManager.Builder() + .WithUrl("https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json") + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) + .WithStartByDefault() + .Build(); LoggerMock.Verify( _ => _.Log(LogLevel.ERROR, @@ -93,6 +92,60 @@ public void TestHttpConfigManagerWithInvalidStatus() httpManager.Dispose(); } + [Test] + public void TestSettingIfModifiedSinceInRequestHeader() + { + var t = MockSendAsync( + datafile: string.Empty, + statusCode: HttpStatusCode.NotModified, + responseContentHeaders: new Dictionary + { + { "Last-Modified", new DateTime(2050, 10, 10).ToString("R") }, + } + ); + + var httpManager = new HttpProjectConfigManager.Builder() + .WithDatafile(string.Empty) + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(2000)) + .WithStartByDefault() + .Build(defer: true); + httpManager.LastModifiedSince = new DateTime(2020, 4, 4).ToString("R"); + t.Wait(3000); + + LoggerMock.Verify( + _ => _.Log(LogLevel.DEBUG, "Set If-Modified-Since in request header."), + Times.AtLeastOnce); + + httpManager.Dispose(); + } + + [Test] + public void TestSettingLastModifiedFromResponseHeader() + { + MockSendAsync( + statusCode: HttpStatusCode.OK, + responseContentHeaders: new Dictionary + { + { "Last-Modified", new DateTime(2050, 10, 10).ToString("R") }, + } + ); + var httpManager = new HttpProjectConfigManager.Builder() + .WithUrl("https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json") + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) + .WithStartByDefault() + .Build(); + + LoggerMock.Verify( + _ => _.Log(LogLevel.DEBUG, "Set LastModifiedSince from response header."), + Times.AtLeastOnce); + + httpManager.Dispose(); + } + [Test] public void TestHttpClientHandler() { @@ -102,18 +155,18 @@ public void TestHttpClientHandler() } [Test] - public void TestHttpConfigManagerRetreiveProjectConfigGivenEmptyFormatUseDefaultFormat() + public void TestHttpConfigManagerRetrieveProjectConfigGivenEmptyFormatUseDefaultFormat() { var t = MockSendAsync(TestData.Datafile); - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithFormat(""). - WithLogger(LoggerMock.Object). - WithPollingInterval(TimeSpan.FromMilliseconds(1000)). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)). - WithStartByDefault(). - Build(); + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithFormat("") + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) + .WithStartByDefault() + .Build(); // This "Wait" notifies When SendAsync is triggered. // Time is given here to avoid hanging-up in any worst case. @@ -127,17 +180,17 @@ public void TestHttpConfigManagerRetreiveProjectConfigGivenEmptyFormatUseDefault } [Test] - public void TestHttpConfigManagerRetreiveProjectConfigBySDKKey() + public void TestHttpConfigManagerRetrieveProjectConfigBySDKKey() { var t = MockSendAsync(TestData.Datafile); - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithLogger(LoggerMock.Object). - WithPollingInterval(TimeSpan.FromMilliseconds(1000)). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)). - WithStartByDefault(). - Build(); + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) + .WithStartByDefault() + .Build(); t.Wait(1000); HttpClientMock.Verify(_ => _.SendAsync( @@ -150,17 +203,17 @@ public void TestHttpConfigManagerRetreiveProjectConfigBySDKKey() } [Test] - public void TestHttpConfigManagerRetreiveProjectConfigByFormat() + public void TestHttpConfigManagerRetrieveProjectConfigByFormat() { var t = MockSendAsync(TestData.Datafile); - var httpManager = new HttpProjectConfigManager.Builder().WithSdkKey("10192104166"). - WithFormat("https://cdn.optimizely.com/json/{0}.json"). - WithLogger(LoggerMock.Object). - WithPollingInterval(TimeSpan.FromMilliseconds(1000)). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)). - WithStartByDefault(). - Build(true); + var httpManager = new HttpProjectConfigManager.Builder().WithSdkKey("10192104166") + .WithFormat("https://cdn.optimizely.com/json/{0}.json") + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) + .WithStartByDefault() + .Build(true); t.Wait(1000); HttpClientMock.Verify(_ => _.SendAsync( @@ -181,13 +234,13 @@ public void TestHttpProjectConfigManagerDoesntRaiseExceptionForDefaultErrorHandl { var t = MockSendAsync(TestData.Datafile); - var httpManager = new HttpProjectConfigManager.Builder().WithSdkKey("10192104166"). - WithFormat("https://cdn.optimizely.com/json/{0}.json"). - WithLogger(LoggerMock.Object). - WithPollingInterval(TimeSpan.FromMilliseconds(1000)). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)). - WithStartByDefault(). - Build(true); + var httpManager = new HttpProjectConfigManager.Builder().WithSdkKey("10192104166") + .WithFormat("https://cdn.optimizely.com/json/{0}.json") + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) + .WithStartByDefault() + .Build(true); t.Wait(1000); HttpClientMock.Verify(_ => _.SendAsync( @@ -212,13 +265,13 @@ public void TestOnReadyPromiseResolvedImmediatelyWhenDatafileIsProvided() var httpManager = new HttpProjectConfigManager.Builder() // Revision - 15 - .WithSdkKey("10192104166"). - WithDatafile(TestData.Datafile). - WithLogger(LoggerMock.Object). - WithPollingInterval(TimeSpan.FromMilliseconds(1000)). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)). - WithStartByDefault(). - Build(); + .WithSdkKey("10192104166") + .WithDatafile(TestData.Datafile) + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) + .WithStartByDefault() + .Build(); // OnReady waits until is resolved, need to add time in case of deadlock. httpManager.OnReady().Wait(10000); @@ -240,13 +293,13 @@ public void TestOnReadyPromiseWaitsForProjectConfigRetrievalWhenDatafileIsNotPro var t = MockSendAsync(TestData.SimpleABExperimentsDatafile, TimeSpan.FromMilliseconds(1000)); - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithLogger(LoggerMock.Object). - WithPollingInterval(TimeSpan.FromSeconds(2)). - WithBlockingTimeoutPeriod(TimeSpan.FromSeconds(1)). - WithStartByDefault(true). - Build(); + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromSeconds(2)) + .WithBlockingTimeoutPeriod(TimeSpan.FromSeconds(1)) + .WithStartByDefault(true) + .Build(); t.Wait(); // OnReady waits until is resolved, need to add time in case of deadlock. @@ -260,15 +313,14 @@ public void TestHttpConfigManagerDoesNotWaitForTheConfigWhenDeferIsTrue() { var t = MockSendAsync(TestData.Datafile, TimeSpan.FromMilliseconds(150)); - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithLogger(LoggerMock.Object). - WithPollingInterval(TimeSpan.FromSeconds(2)) + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromSeconds(2)) // negligible timeout - . - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50)). - WithStartByDefault(). - Build(false); + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50)) + .WithStartByDefault() + .Build(false); // When blocking timeout is 0 and defer is false and getconfig immediately called // should return null @@ -289,15 +341,15 @@ public void TestHttpConfigManagerDoesNotWaitForTheConfigWhenDeferIsTrue() [Test] public void TestHttpConfigManagerSendConfigUpdateNotificationWhenProjectConfigGetsUpdated() { - var t = MockSendAsync(TestData.Datafile); + MockSendAsync(TestData.Datafile); - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithLogger(LoggerMock.Object). - WithPollingInterval(TimeSpan.FromMilliseconds(1000)). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(1000)). - WithStartByDefault(false). - Build(true); + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(1000)) + .WithStartByDefault(false) + .Build(true); httpManager.NotifyOnProjectConfigUpdate += NotificationCallbackMock.Object.TestConfigUpdateCallback; @@ -312,15 +364,15 @@ public void TestHttpConfigManagerSendConfigUpdateNotificationWhenProjectConfigGe [Test] public void TestHttpConfigManagerDoesNotSendConfigUpdateNotificationWhenDatafileIsProvided() { - var t = MockSendAsync(TestData.Datafile, TimeSpan.FromMilliseconds(100)); + MockSendAsync(TestData.Datafile, TimeSpan.FromMilliseconds(100)); - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithDatafile(TestData.Datafile). - WithLogger(LoggerMock.Object). - WithPollingInterval(TimeSpan.FromMilliseconds(1000)). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)). - Build(); + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithDatafile(TestData.Datafile) + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) + .Build(); httpManager.NotifyOnProjectConfigUpdate += @@ -335,17 +387,17 @@ public void TestHttpConfigManagerDoesNotSendConfigUpdateNotificationWhenDatafile [Test] public void TestDefaultBlockingTimeoutWhileProvidingZero() { - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithDatafile(TestData.Datafile). - WithLogger(LoggerMock.Object). - WithPollingInterval(TimeSpan.FromMilliseconds(1000)). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(0)). - WithStartByDefault(true). - Build(true); - - var fieldInfo = httpManager.GetType(). - GetField("BlockingTimeout", + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithDatafile(TestData.Datafile) + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(0)) + .WithStartByDefault(true) + .Build(true); + + var fieldInfo = httpManager.GetType() + .GetField("BlockingTimeout", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); var expectedBlockingTimeout = (TimeSpan)fieldInfo.GetValue(httpManager); @@ -359,14 +411,14 @@ public void TestDefaultBlockingTimeoutWhileProvidingZero() [Test] public void TestDefaultPeriodWhileProvidingZero() { - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithDatafile(TestData.Datafile). - WithLogger(LoggerMock.Object). - WithPollingInterval(TimeSpan.FromMilliseconds(0)). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(1000)). - WithStartByDefault(true). - Build(true); + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithDatafile(TestData.Datafile) + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(0)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(1000)) + .WithStartByDefault(true) + .Build(true); var fieldInfo = typeof(PollingProjectConfigManager).GetField("PollingInterval", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); @@ -381,14 +433,14 @@ public void TestDefaultPeriodWhileProvidingZero() [Test] public void TestDefaultPeriodWhileProvidingNegative() { - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithDatafile(TestData.Datafile). - WithLogger(LoggerMock.Object). - WithPollingInterval(TimeSpan.FromMilliseconds(-1)). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(1000)). - WithStartByDefault(true). - Build(true); + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithDatafile(TestData.Datafile) + .WithLogger(LoggerMock.Object) + .WithPollingInterval(TimeSpan.FromMilliseconds(-1)) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(1000)) + .WithStartByDefault(true) + .Build(true); var fieldInfo = typeof(PollingProjectConfigManager).GetField("PollingInterval", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); @@ -403,11 +455,11 @@ public void TestDefaultPeriodWhileProvidingNegative() [Test] public void TestDefaultPeriodWhileNotProvidingValue() { - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithDatafile(TestData.Datafile). - WithLogger(LoggerMock.Object). - Build(true); + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithDatafile(TestData.Datafile) + .WithLogger(LoggerMock.Object) + .Build(true); var fieldInfo = typeof(PollingProjectConfigManager).GetField("PollingInterval", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); @@ -422,14 +474,14 @@ public void TestDefaultPeriodWhileNotProvidingValue() [Test] public void TestDefaultBlockingTimeoutWhileNotProvidingValue() { - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithDatafile(TestData.Datafile). - WithLogger(LoggerMock.Object). - Build(true); - - var fieldInfo = httpManager.GetType(). - GetField("BlockingTimeout", + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithDatafile(TestData.Datafile) + .WithLogger(LoggerMock.Object) + .Build(true); + + var fieldInfo = httpManager.GetType() + .GetField("BlockingTimeout", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); var expectedBlockingTimeout = (TimeSpan)fieldInfo.GetValue(httpManager); @@ -443,11 +495,11 @@ public void TestDefaultBlockingTimeoutWhileNotProvidingValue() [Test] public void TestDefaultValuesWhenNotProvided() { - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithDatafile(TestData.Datafile). - WithLogger(LoggerMock.Object). - Build(true); + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithDatafile(TestData.Datafile) + .WithLogger(LoggerMock.Object) + .Build(true); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, $"No polling interval provided, using default period {TimeSpan.FromMinutes(5).TotalMilliseconds}ms")); @@ -461,12 +513,12 @@ public void TestAuthUrlWhenTokenProvided() { var t = MockSendAsync(); - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithLogger(LoggerMock.Object). - WithAccessToken("datafile1"). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50)). - Build(true); + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithLogger(LoggerMock.Object) + .WithAccessToken("datafile1") + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50)) + .Build(true); // it's to wait if SendAsync is not triggered. t.Wait(2000); @@ -484,11 +536,11 @@ public void TestDefaultUrlWhenTokenNotProvided() { var t = MockSendAsync(); - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithLogger(LoggerMock.Object). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50)). - Build(true); + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithLogger(LoggerMock.Object) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50)) + .Build(true); // it's to wait if SendAsync is not triggered. t.Wait(2000); @@ -505,12 +557,12 @@ public void TestAuthenticationHeaderWhenTokenProvided() { var t = MockSendAsync(); - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithLogger(LoggerMock.Object). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50)). - WithAccessToken("datafile1"). - Build(true); + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithLogger(LoggerMock.Object) + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50)) + .WithAccessToken("datafile1") + .Build(true); // it's to wait if SendAsync is not triggered. t.Wait(2000); @@ -526,13 +578,13 @@ public void TestAuthenticationHeaderWhenTokenProvided() public void TestFormatUrlHigherPriorityThanDefaultUrl() { var t = MockSendAsync(); - var httpManager = new HttpProjectConfigManager.Builder(). - WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z"). - WithLogger(LoggerMock.Object). - WithFormat("http://customformat/{0}.json"). - WithAccessToken("datafile1"). - WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50)). - Build(true); + var httpManager = new HttpProjectConfigManager.Builder() + .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") + .WithLogger(LoggerMock.Object) + .WithFormat("http://customformat/{0}.json") + .WithAccessToken("datafile1") + .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50)) + .Build(true); // it's to wait if SendAsync is not triggered. t.Wait(2000); HttpClientMock.Verify(_ => _.SendAsync( @@ -543,12 +595,13 @@ public void TestFormatUrlHigherPriorityThanDefaultUrl() httpManager.Dispose(); } - public Task MockSendAsync(string datafile = null, TimeSpan? delay = null, - HttpStatusCode statusCode = HttpStatusCode.OK + private Task MockSendAsync(string datafile = null, TimeSpan? delay = null, + HttpStatusCode statusCode = HttpStatusCode.OK, + Dictionary responseContentHeaders = null ) { return TestHttpProjectConfigManagerUtil.MockSendAsync(HttpClientMock, datafile, delay, - statusCode); + statusCode, responseContentHeaders); } #endregion diff --git a/OptimizelySDK.Tests/Utils/TestHttpProjectConfigManagerUtil.cs b/OptimizelySDK.Tests/Utils/TestHttpProjectConfigManagerUtil.cs index c4a0609e..4cbfb199 100644 --- a/OptimizelySDK.Tests/Utils/TestHttpProjectConfigManagerUtil.cs +++ b/OptimizelySDK.Tests/Utils/TestHttpProjectConfigManagerUtil.cs @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020, Optimizely + * Copyright 2019-2020, 2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ using System; +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -28,15 +29,16 @@ namespace OptimizelySDK.Tests.Utils /// public static class TestHttpProjectConfigManagerUtil { - public static Task MockSendAsync(Mock HttpClientMock, + public static Task MockSendAsync(Mock httpClientMock, string datafile = null, TimeSpan? delay = null, - HttpStatusCode statusCode = HttpStatusCode.OK + HttpStatusCode statusCode = HttpStatusCode.OK, + Dictionary responseContentHeaders = null ) { var t = new TaskCompletionSource(); - HttpClientMock.Setup(_ => _.SendAsync(It.IsAny())). - Returns(() => + httpClientMock.Setup(_ => _.SendAsync(It.IsAny())) + .Returns(() => { if (delay != null) { @@ -44,13 +46,23 @@ public static Task MockSendAsync(Mock HttpC Task.Delay(delay.Value).Wait(); } - return Task.FromResult(new HttpResponseMessage + var responseMessage = new HttpResponseMessage { StatusCode = statusCode, Content = new StringContent(datafile ?? string.Empty), - }); - }). - Callback(() + }; + + if (responseContentHeaders != null) + { + foreach (var header in responseContentHeaders) + { + responseMessage.Content.Headers.Add(header.Key, header.Value); + } + } + + return Task.FromResult(responseMessage); + }) + .Callback(() => { t.SetResult(true); @@ -69,7 +81,10 @@ public static void SetClientFieldValue(object value) var field = type.GetField("Client", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic); - field.SetValue(new object(), value); + if (field != null) + { + field.SetValue(new object(), value); + } } } } diff --git a/OptimizelySDK/Config/HttpProjectConfigManager.cs b/OptimizelySDK/Config/HttpProjectConfigManager.cs index 4e8bdd8c..6a73e2ec 100644 --- a/OptimizelySDK/Config/HttpProjectConfigManager.cs +++ b/OptimizelySDK/Config/HttpProjectConfigManager.cs @@ -19,8 +19,7 @@ #endif using System; -using System.Collections.Generic; -using System.Linq; +using System.Net; using System.Threading.Tasks; using OptimizelySDK.ErrorHandler; using OptimizelySDK.Logger; @@ -31,7 +30,7 @@ namespace OptimizelySDK.Config public class HttpProjectConfigManager : PollingProjectConfigManager { private string Url; - private string LastModifiedSince = string.Empty; + internal string LastModifiedSince = string.Empty; private string DatafileAccessToken = string.Empty; private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, @@ -120,6 +119,7 @@ private string GetRemoteDatafileResponse() if (!string.IsNullOrEmpty(LastModifiedSince)) { request.Headers.Add("If-Modified-Since", LastModifiedSince); + Logger.Log(LogLevel.DEBUG, $"Set If-Modified-Since in request header."); } if (!string.IsNullOrEmpty(DatafileAccessToken)) @@ -134,21 +134,23 @@ private string GetRemoteDatafileResponse() // Return from here if datafile is not modified. var result = httpResponse.Result; - if (!result.IsSuccessStatusCode) + + if (result.StatusCode == HttpStatusCode.NotModified) { - Logger.Log(LogLevel.ERROR, $"Error fetching datafile \"{result.StatusCode}\""); return null; } - // Update Last-Modified header if provided. - if (result.Headers.TryGetValues("Last-Modified", out var values)) + if (!result.IsSuccessStatusCode) { - LastModifiedSince = values.First(); + Logger.Log(LogLevel.ERROR, $"Error fetching datafile \"{result.StatusCode}\""); + return null; } - if (result.StatusCode == System.Net.HttpStatusCode.NotModified) + // Update Last-Modified header if provided. + if (result.Content.Headers.LastModified.HasValue) { - return null; + LastModifiedSince = result.Content.Headers.LastModified.ToString(); + Logger.Log(LogLevel.DEBUG, $"Set LastModifiedSince from response header."); } var content = result.Content.ReadAsStringAsync(); @@ -426,12 +428,12 @@ public HttpProjectConfigManager Build(bool defer) configManager.NotifyOnProjectConfigUpdate += () => { #if USE_ODP - NotificationCenterRegistry.GetNotificationCenter(SdkKey). - SendNotifications( + NotificationCenterRegistry.GetNotificationCenter(SdkKey) + .SendNotifications( NotificationCenter.NotificationType.OptimizelyConfigUpdate); #endif - NotificationCenter?.SendNotifications(NotificationCenter.NotificationType. - OptimizelyConfigUpdate); + NotificationCenter?.SendNotifications(NotificationCenter.NotificationType + .OptimizelyConfigUpdate); };