Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Authenticated Datafile Support #222

Merged
merged 11 commits into from
Jun 22, 2020
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# Optimizely C# SDK Changelog
## [Unreleased]

### New Features
- Adds support for DatafileAuthToken in HttpProjectConfigManager and in it's Builder class.
- Added Gzip format for datafile download

## 3.4.1
April 29th, 2020
Expand Down
108 changes: 106 additions & 2 deletions OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019, Optimizely
* Copyright 2019-2020, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -21,19 +21,27 @@
using OptimizelySDK.Tests.NotificationTests;
using System;
using System.Diagnostics;
using System.Net.Http;

namespace OptimizelySDK.Tests.DatafileManagement_Tests
{
[TestFixture]
public class HttpProjectConfigManagerTest
{
private Mock<ILogger> LoggerMock;
private Mock<HttpProjectConfigManager.HttpClient> HttpClientMock;
private Mock<TestNotificationCallbacks> NotificationCallbackMock = new Mock<TestNotificationCallbacks>();

[SetUp]
public void Setup()
{
LoggerMock = new Mock<ILogger>();
HttpClientMock = new Mock<HttpProjectConfigManager.HttpClient> { CallBase = true };
HttpClientMock.Reset();
var field = typeof(HttpProjectConfigManager).GetField("Client",
System.Reflection.BindingFlags.Static |
System.Reflection.BindingFlags.NonPublic);
field.SetValue(field, HttpClientMock.Object);
LoggerMock.Setup(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()));
NotificationCallbackMock.Setup(nc => nc.TestConfigUpdateCallback());

Expand All @@ -57,7 +65,7 @@ public void TestHttpConfigManagerRetreiveProjectConfigByURL()
[Test]
public void TestHttpClientHandler()
{
var httpConfigHandler = HttpProjectConfigManager.GetHttpClientHandler();
var httpConfigHandler = HttpProjectConfigManager.HttpClient.GetHttpClientHandler();
Assert.IsTrue(httpConfigHandler.AutomaticDecompression == (System.Net.DecompressionMethods.Deflate | System.Net.DecompressionMethods.GZip));
}

Expand Down Expand Up @@ -300,6 +308,102 @@ public void TestDefaultValuesWhenNotProvided()

LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, $"No polling interval provided, using default period {TimeSpan.FromMinutes(5).TotalMilliseconds}ms"));
LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, $"No Blocking timeout provided, using default blocking timeout {TimeSpan.FromSeconds(15).TotalMilliseconds}ms"));
}

[Test]
public void TestAuthUrlWhenTokenProvided()
{
var t = MockSendAsync();

var httpManager = new HttpProjectConfigManager.Builder()
.WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z")
.WithLogger(LoggerMock.Object)
.WithAuthToken("datafile1")
.WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50))
.Build(true);

// it's to wait if SendAsync is not triggered.
t.Wait(2000);

HttpClientMock.Verify(_ => _.SendAsync(
It.Is<System.Net.Http.HttpRequestMessage>(requestMessage =>
requestMessage.RequestUri.ToString() == "https://config.optimizely.com/datafiles/auth/QBw9gFM8oTn7ogY9ANCC1z.json"
)));
}

[Test]
public void TestDefaultUrlWhenTokenNotProvided()
{
var t = MockSendAsync();

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);
HttpClientMock.Verify(_ => _.SendAsync(
It.Is<System.Net.Http.HttpRequestMessage>(requestMessage =>
requestMessage.RequestUri.ToString() == "https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json"
)));
}

[Test]
public void TestAuthenticationHeaderWhenTokenProvided()
{
var t = MockSendAsync();

var httpManager = new HttpProjectConfigManager.Builder()
.WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z")
.WithLogger(LoggerMock.Object)
.WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50))
.WithAuthToken("datafile1")
.Build(true);

// it's to wait if SendAsync is not triggered.
t.Wait(2000);

HttpClientMock.Verify(_ => _.SendAsync(
It.Is<System.Net.Http.HttpRequestMessage>(requestMessage =>
requestMessage.Headers.Authorization.ToString() == "Bearer datafile1"
)));
}

[Test]
public void TestFormatUrlHigherPriorityThanDefaultUrl()
{

var t = MockSendAsync();
var httpManager = new HttpProjectConfigManager.Builder()
.WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z")
.WithLogger(LoggerMock.Object)
.WithFormat("http://customformat/{0}.json")
.WithAuthToken("datafile1")
.WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50))
.Build(true);
// it's to wait if SendAsync is not triggered.
t.Wait(2000);
HttpClientMock.Verify(_ => _.SendAsync(
It.Is<System.Net.Http.HttpRequestMessage>(requestMessage =>
requestMessage.RequestUri.ToString() == "http://customformat/QBw9gFM8oTn7ogY9ANCC1z.json"
)));

}

public System.Threading.Tasks.Task MockSendAsync()
{
var t = new System.Threading.Tasks.TaskCompletionSource<bool>();

HttpClientMock.Setup(_ => _.SendAsync(It.IsAny<System.Net.Http.HttpRequestMessage>()))
.Returns(System.Threading.Tasks.Task.FromResult<HttpResponseMessage>(new HttpResponseMessage { StatusCode = System.Net.HttpStatusCode.OK, Content = new StringContent(string.Empty) }))
.Callback(()
=> {
t.SetResult(true);
});

return t.Task;
}

#endregion
Expand Down
90 changes: 68 additions & 22 deletions OptimizelySDK/Config/HttpProjectConfigManager.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019, Optimizely
* Copyright 2019-2020, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -28,34 +28,64 @@ public class HttpProjectConfigManager : PollingProjectConfigManager
{
private string Url;
private string LastModifiedSince = string.Empty;

private string AuthenticatedDatafileToken = string.Empty;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call it datafileAuthToken

private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, bool autoUpdate, ILogger logger, IErrorHandler errorHandler)
: base(period, blockingTimeout, autoUpdate, logger, errorHandler)
{
Url = url;
}

private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, bool autoUpdate, ILogger logger, IErrorHandler errorHandler, string authDatafileToken)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

datafileAuthToken

: this(period, url, blockingTimeout, autoUpdate, logger, errorHandler)
{
AuthenticatedDatafileToken = authDatafileToken;
}

public Task OnReady()
{
return CompletableConfigManager.Task;
}

#if !NET40 && !NET35
private static System.Net.Http.HttpClient Client;
static HttpProjectConfigManager()
// HttpClient wrapper class which can be used to mock HttpClient for unit testing.
public class HttpClient
{
Client = new System.Net.Http.HttpClient(GetHttpClientHandler());
}
private System.Net.Http.HttpClient Client;

public static System.Net.Http.HttpClientHandler GetHttpClientHandler()
{
var handler = new System.Net.Http.HttpClientHandler() {
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
};
public HttpClient()
{
Client = new System.Net.Http.HttpClient(GetHttpClientHandler());
}

return handler;
public HttpClient(System.Net.Http.HttpClient httpClient) : this()
{
if (httpClient != null) {
Client = httpClient;
}
}

public static System.Net.Http.HttpClientHandler GetHttpClientHandler()
{
var handler = new System.Net.Http.HttpClientHandler() {
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
};

return handler;
}

public virtual Task<System.Net.Http.HttpResponseMessage> SendAsync(System.Net.Http.HttpRequestMessage httpRequestMessage)
{
return Client.SendAsync(httpRequestMessage);
}
}

private static HttpClient Client;

static HttpProjectConfigManager()
{
Client = new HttpClient();
}

private string GetRemoteDatafileResponse()
{
var request = new System.Net.Http.HttpRequestMessage {
Expand All @@ -67,6 +97,10 @@ private string GetRemoteDatafileResponse()
if (!string.IsNullOrEmpty(LastModifiedSince))
request.Headers.Add("If-Modified-Since", LastModifiedSince);

if (!string.IsNullOrEmpty(AuthenticatedDatafileToken)) {
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", AuthenticatedDatafileToken);
}

var httpResponse = Client.SendAsync(request);
httpResponse.Wait();

Expand Down Expand Up @@ -136,11 +170,12 @@ public class Builder
private readonly TimeSpan DEFAULT_PERIOD = TimeSpan.FromMinutes(5);
private readonly TimeSpan DEFAULT_BLOCKINGOUT_PERIOD = TimeSpan.FromSeconds(15);
private readonly string DEFAULT_FORMAT = "https://cdn.optimizely.com/datafiles/{0}.json";

private readonly string DEFAULT_AUTHENTICATED_DATAFILE_FORMAT = "https://config.optimizely.com/datafiles/auth/{0}.json";
private string Datafile;
private string AuthenticatedDatafileToken;
private string SdkKey;
private string Url;
private string Format;
private string Format;
private ILogger Logger;
private IErrorHandler ErrorHandler;
private TimeSpan Period;
Expand Down Expand Up @@ -174,6 +209,14 @@ public Builder WithSdkKey(string sdkKey)

return this;
}
#if !NET40 && !NET35
public Builder WithAuthToken(string authToken)
{
this.AuthenticatedDatafileToken = authToken;

return this;
}
#endif

public Builder WithUrl(string url)
{
Expand Down Expand Up @@ -260,15 +303,18 @@ public HttpProjectConfigManager Build(bool defer)
ErrorHandler = new DefaultErrorHandler();

if (string.IsNullOrEmpty(Format)) {
Format = DEFAULT_FORMAT;
}

if (string.IsNullOrEmpty(Url) && string.IsNullOrEmpty(SdkKey))
{
ErrorHandler.HandleError(new Exception("SdkKey cannot be null"));
if (string.IsNullOrEmpty(AuthenticatedDatafileToken)) {
Format = DEFAULT_FORMAT;
} else {
Format = DEFAULT_AUTHENTICATED_DATAFILE_FORMAT;
}
}
else if (!string.IsNullOrEmpty(SdkKey))
{

if (string.IsNullOrEmpty(Url)) {
if (string.IsNullOrEmpty(SdkKey)) {
ErrorHandler.HandleError(new Exception("SdkKey cannot be null"));
}
Url = string.Format(Format, SdkKey);
}

Expand All @@ -290,7 +336,7 @@ public HttpProjectConfigManager Build(bool defer)
}


configManager = new HttpProjectConfigManager(Period, Url, BlockingTimeoutSpan, AutoUpdate, Logger, ErrorHandler);
configManager = new HttpProjectConfigManager(Period, Url, BlockingTimeoutSpan, AutoUpdate, Logger, ErrorHandler, AuthenticatedDatafileToken);

if (Datafile != null)
{
Expand Down
32 changes: 32 additions & 0 deletions OptimizelySDK/OptimizelyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,39 @@ public static Optimizely NewDefaultInstance(string sdkKey)
{
return NewDefaultInstance(sdkKey, null);
}
#if !NET40 && !NET35
public static Optimizely NewDefaultInstance(string sdkKey, string fallback, string datafileAuthToken)
{
var logger = OptimizelyLogger ?? new DefaultLogger();
var errorHandler = new DefaultErrorHandler();
var eventDispatcher = new DefaultEventDispatcher(logger);
var builder = new HttpProjectConfigManager.Builder();
var notificationCenter = new NotificationCenter();

var configManager = builder
.WithSdkKey(sdkKey)
.WithDatafile(fallback)
.WithLogger(logger)
.WithErrorHandler(errorHandler)
.WithAuthToken(datafileAuthToken)
.WithNotificationCenter(notificationCenter)
.Build(true);

EventProcessor eventProcessor = null;

#if !NETSTANDARD1_6 && !NET35
eventProcessor = new BatchEventProcessor.Builder()
.WithLogger(logger)
.WithMaxBatchSize(MaxEventBatchSize)
.WithFlushInterval(MaxEventFlushInterval)
.WithEventDispatcher(eventDispatcher)
.WithNotificationCenter(notificationCenter)
.Build();
#endif

return NewDefaultInstance(configManager, notificationCenter, eventDispatcher, errorHandler, logger, eventProcessor: eventProcessor);
}
#endif
public static Optimizely NewDefaultInstance(string sdkKey, string fallback)
{
var logger = OptimizelyLogger ?? new DefaultLogger();
Expand Down