Skip to content

Commit f1c903c

Browse files
feat: Add Authenticated Datafile Support (#222)
* adf implementation * fixed unit tests * injecting httpclient * fixed unit tests * fixed unit tests * added callback * added callback * revised names * added in changelog * renamed variable * renamed to DatafileAccessToken
1 parent fade4e5 commit f1c903c

File tree

4 files changed

+211
-24
lines changed

4 files changed

+211
-24
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
# Optimizely C# SDK Changelog
2+
## [Unreleased]
3+
4+
### New Features
5+
- Adds support for DatafileAuthToken in HttpProjectConfigManager and in it's Builder class.
6+
- Added Gzip format for datafile download
27

38
## 3.4.1
49
April 29th, 2020

OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019, Optimizely
2+
* Copyright 2019-2020, Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,19 +21,27 @@
2121
using OptimizelySDK.Tests.NotificationTests;
2222
using System;
2323
using System.Diagnostics;
24+
using System.Net.Http;
2425

2526
namespace OptimizelySDK.Tests.DatafileManagement_Tests
2627
{
2728
[TestFixture]
2829
public class HttpProjectConfigManagerTest
2930
{
3031
private Mock<ILogger> LoggerMock;
32+
private Mock<HttpProjectConfigManager.HttpClient> HttpClientMock;
3133
private Mock<TestNotificationCallbacks> NotificationCallbackMock = new Mock<TestNotificationCallbacks>();
3234

3335
[SetUp]
3436
public void Setup()
3537
{
3638
LoggerMock = new Mock<ILogger>();
39+
HttpClientMock = new Mock<HttpProjectConfigManager.HttpClient> { CallBase = true };
40+
HttpClientMock.Reset();
41+
var field = typeof(HttpProjectConfigManager).GetField("Client",
42+
System.Reflection.BindingFlags.Static |
43+
System.Reflection.BindingFlags.NonPublic);
44+
field.SetValue(field, HttpClientMock.Object);
3745
LoggerMock.Setup(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()));
3846
NotificationCallbackMock.Setup(nc => nc.TestConfigUpdateCallback());
3947

@@ -57,7 +65,7 @@ public void TestHttpConfigManagerRetreiveProjectConfigByURL()
5765
[Test]
5866
public void TestHttpClientHandler()
5967
{
60-
var httpConfigHandler = HttpProjectConfigManager.GetHttpClientHandler();
68+
var httpConfigHandler = HttpProjectConfigManager.HttpClient.GetHttpClientHandler();
6169
Assert.IsTrue(httpConfigHandler.AutomaticDecompression == (System.Net.DecompressionMethods.Deflate | System.Net.DecompressionMethods.GZip));
6270
}
6371

@@ -300,6 +308,102 @@ public void TestDefaultValuesWhenNotProvided()
300308

301309
LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, $"No polling interval provided, using default period {TimeSpan.FromMinutes(5).TotalMilliseconds}ms"));
302310
LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, $"No Blocking timeout provided, using default blocking timeout {TimeSpan.FromSeconds(15).TotalMilliseconds}ms"));
311+
}
312+
313+
[Test]
314+
public void TestAuthUrlWhenTokenProvided()
315+
{
316+
var t = MockSendAsync();
317+
318+
var httpManager = new HttpProjectConfigManager.Builder()
319+
.WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z")
320+
.WithLogger(LoggerMock.Object)
321+
.WithAccessToken("datafile1")
322+
.WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50))
323+
.Build(true);
324+
325+
// it's to wait if SendAsync is not triggered.
326+
t.Wait(2000);
327+
328+
HttpClientMock.Verify(_ => _.SendAsync(
329+
It.Is<System.Net.Http.HttpRequestMessage>(requestMessage =>
330+
requestMessage.RequestUri.ToString() == "https://config.optimizely.com/datafiles/auth/QBw9gFM8oTn7ogY9ANCC1z.json"
331+
)));
332+
}
333+
334+
[Test]
335+
public void TestDefaultUrlWhenTokenNotProvided()
336+
{
337+
var t = MockSendAsync();
338+
339+
var httpManager = new HttpProjectConfigManager.Builder()
340+
.WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z")
341+
.WithLogger(LoggerMock.Object)
342+
.WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50))
343+
.Build(true);
344+
345+
// it's to wait if SendAsync is not triggered.
346+
t.Wait(2000);
347+
HttpClientMock.Verify(_ => _.SendAsync(
348+
It.Is<System.Net.Http.HttpRequestMessage>(requestMessage =>
349+
requestMessage.RequestUri.ToString() == "https://cdn.optimizely.com/datafiles/QBw9gFM8oTn7ogY9ANCC1z.json"
350+
)));
351+
}
352+
353+
[Test]
354+
public void TestAuthenticationHeaderWhenTokenProvided()
355+
{
356+
var t = MockSendAsync();
357+
358+
var httpManager = new HttpProjectConfigManager.Builder()
359+
.WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z")
360+
.WithLogger(LoggerMock.Object)
361+
.WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50))
362+
.WithAccessToken("datafile1")
363+
.Build(true);
364+
365+
// it's to wait if SendAsync is not triggered.
366+
t.Wait(2000);
367+
368+
HttpClientMock.Verify(_ => _.SendAsync(
369+
It.Is<System.Net.Http.HttpRequestMessage>(requestMessage =>
370+
requestMessage.Headers.Authorization.ToString() == "Bearer datafile1"
371+
)));
372+
}
373+
374+
[Test]
375+
public void TestFormatUrlHigherPriorityThanDefaultUrl()
376+
{
377+
378+
var t = MockSendAsync();
379+
var httpManager = new HttpProjectConfigManager.Builder()
380+
.WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z")
381+
.WithLogger(LoggerMock.Object)
382+
.WithFormat("http://customformat/{0}.json")
383+
.WithAccessToken("datafile1")
384+
.WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(50))
385+
.Build(true);
386+
// it's to wait if SendAsync is not triggered.
387+
t.Wait(2000);
388+
HttpClientMock.Verify(_ => _.SendAsync(
389+
It.Is<System.Net.Http.HttpRequestMessage>(requestMessage =>
390+
requestMessage.RequestUri.ToString() == "http://customformat/QBw9gFM8oTn7ogY9ANCC1z.json"
391+
)));
392+
393+
}
394+
395+
public System.Threading.Tasks.Task MockSendAsync()
396+
{
397+
var t = new System.Threading.Tasks.TaskCompletionSource<bool>();
398+
399+
HttpClientMock.Setup(_ => _.SendAsync(It.IsAny<System.Net.Http.HttpRequestMessage>()))
400+
.Returns(System.Threading.Tasks.Task.FromResult<HttpResponseMessage>(new HttpResponseMessage { StatusCode = System.Net.HttpStatusCode.OK, Content = new StringContent(string.Empty) }))
401+
.Callback(()
402+
=> {
403+
t.SetResult(true);
404+
});
405+
406+
return t.Task;
303407
}
304408

305409
#endregion

OptimizelySDK/Config/HttpProjectConfigManager.cs

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019, Optimizely
2+
* Copyright 2019-2020, Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,34 +28,64 @@ public class HttpProjectConfigManager : PollingProjectConfigManager
2828
{
2929
private string Url;
3030
private string LastModifiedSince = string.Empty;
31-
31+
private string DatafileAccessToken = string.Empty;
3232
private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, bool autoUpdate, ILogger logger, IErrorHandler errorHandler)
3333
: base(period, blockingTimeout, autoUpdate, logger, errorHandler)
3434
{
3535
Url = url;
3636
}
3737

38+
private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, bool autoUpdate, ILogger logger, IErrorHandler errorHandler, string datafileAccessToken)
39+
: this(period, url, blockingTimeout, autoUpdate, logger, errorHandler)
40+
{
41+
DatafileAccessToken = datafileAccessToken;
42+
}
43+
3844
public Task OnReady()
3945
{
4046
return CompletableConfigManager.Task;
4147
}
4248

4349
#if !NET40 && !NET35
44-
private static System.Net.Http.HttpClient Client;
45-
static HttpProjectConfigManager()
50+
// HttpClient wrapper class which can be used to mock HttpClient for unit testing.
51+
public class HttpClient
4652
{
47-
Client = new System.Net.Http.HttpClient(GetHttpClientHandler());
48-
}
53+
private System.Net.Http.HttpClient Client;
4954

50-
public static System.Net.Http.HttpClientHandler GetHttpClientHandler()
51-
{
52-
var handler = new System.Net.Http.HttpClientHandler() {
53-
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
54-
};
55+
public HttpClient()
56+
{
57+
Client = new System.Net.Http.HttpClient(GetHttpClientHandler());
58+
}
5559

56-
return handler;
60+
public HttpClient(System.Net.Http.HttpClient httpClient) : this()
61+
{
62+
if (httpClient != null) {
63+
Client = httpClient;
64+
}
65+
}
66+
67+
public static System.Net.Http.HttpClientHandler GetHttpClientHandler()
68+
{
69+
var handler = new System.Net.Http.HttpClientHandler() {
70+
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
71+
};
72+
73+
return handler;
74+
}
75+
76+
public virtual Task<System.Net.Http.HttpResponseMessage> SendAsync(System.Net.Http.HttpRequestMessage httpRequestMessage)
77+
{
78+
return Client.SendAsync(httpRequestMessage);
79+
}
5780
}
5881

82+
private static HttpClient Client;
83+
84+
static HttpProjectConfigManager()
85+
{
86+
Client = new HttpClient();
87+
}
88+
5989
private string GetRemoteDatafileResponse()
6090
{
6191
var request = new System.Net.Http.HttpRequestMessage {
@@ -67,6 +97,10 @@ private string GetRemoteDatafileResponse()
6797
if (!string.IsNullOrEmpty(LastModifiedSince))
6898
request.Headers.Add("If-Modified-Since", LastModifiedSince);
6999

100+
if (!string.IsNullOrEmpty(DatafileAccessToken)) {
101+
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", DatafileAccessToken);
102+
}
103+
70104
var httpResponse = Client.SendAsync(request);
71105
httpResponse.Wait();
72106

@@ -136,11 +170,12 @@ public class Builder
136170
private readonly TimeSpan DEFAULT_PERIOD = TimeSpan.FromMinutes(5);
137171
private readonly TimeSpan DEFAULT_BLOCKINGOUT_PERIOD = TimeSpan.FromSeconds(15);
138172
private readonly string DEFAULT_FORMAT = "https://cdn.optimizely.com/datafiles/{0}.json";
139-
173+
private readonly string DEFAULT_AUTHENTICATED_DATAFILE_FORMAT = "https://config.optimizely.com/datafiles/auth/{0}.json";
140174
private string Datafile;
175+
private string DatafileAccessToken;
141176
private string SdkKey;
142177
private string Url;
143-
private string Format;
178+
private string Format;
144179
private ILogger Logger;
145180
private IErrorHandler ErrorHandler;
146181
private TimeSpan Period;
@@ -174,6 +209,14 @@ public Builder WithSdkKey(string sdkKey)
174209

175210
return this;
176211
}
212+
#if !NET40 && !NET35
213+
public Builder WithAccessToken(string accessToken)
214+
{
215+
this.DatafileAccessToken = accessToken;
216+
217+
return this;
218+
}
219+
#endif
177220

178221
public Builder WithUrl(string url)
179222
{
@@ -260,15 +303,18 @@ public HttpProjectConfigManager Build(bool defer)
260303
ErrorHandler = new DefaultErrorHandler();
261304

262305
if (string.IsNullOrEmpty(Format)) {
263-
Format = DEFAULT_FORMAT;
264-
}
265306

266-
if (string.IsNullOrEmpty(Url) && string.IsNullOrEmpty(SdkKey))
267-
{
268-
ErrorHandler.HandleError(new Exception("SdkKey cannot be null"));
307+
if (string.IsNullOrEmpty(DatafileAccessToken)) {
308+
Format = DEFAULT_FORMAT;
309+
} else {
310+
Format = DEFAULT_AUTHENTICATED_DATAFILE_FORMAT;
311+
}
269312
}
270-
else if (!string.IsNullOrEmpty(SdkKey))
271-
{
313+
314+
if (string.IsNullOrEmpty(Url)) {
315+
if (string.IsNullOrEmpty(SdkKey)) {
316+
ErrorHandler.HandleError(new Exception("SdkKey cannot be null"));
317+
}
272318
Url = string.Format(Format, SdkKey);
273319
}
274320

@@ -290,7 +336,7 @@ public HttpProjectConfigManager Build(bool defer)
290336
}
291337

292338

293-
configManager = new HttpProjectConfigManager(Period, Url, BlockingTimeoutSpan, AutoUpdate, Logger, ErrorHandler);
339+
configManager = new HttpProjectConfigManager(Period, Url, BlockingTimeoutSpan, AutoUpdate, Logger, ErrorHandler, DatafileAccessToken);
294340

295341
if (Datafile != null)
296342
{

OptimizelySDK/OptimizelyFactory.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,39 @@ public static Optimizely NewDefaultInstance(string sdkKey)
112112
{
113113
return NewDefaultInstance(sdkKey, null);
114114
}
115+
#if !NET40 && !NET35
116+
public static Optimizely NewDefaultInstance(string sdkKey, string fallback, string datafileAuthToken)
117+
{
118+
var logger = OptimizelyLogger ?? new DefaultLogger();
119+
var errorHandler = new DefaultErrorHandler();
120+
var eventDispatcher = new DefaultEventDispatcher(logger);
121+
var builder = new HttpProjectConfigManager.Builder();
122+
var notificationCenter = new NotificationCenter();
115123

124+
var configManager = builder
125+
.WithSdkKey(sdkKey)
126+
.WithDatafile(fallback)
127+
.WithLogger(logger)
128+
.WithErrorHandler(errorHandler)
129+
.WithAccessToken(datafileAuthToken)
130+
.WithNotificationCenter(notificationCenter)
131+
.Build(true);
132+
133+
EventProcessor eventProcessor = null;
134+
135+
#if !NETSTANDARD1_6 && !NET35
136+
eventProcessor = new BatchEventProcessor.Builder()
137+
.WithLogger(logger)
138+
.WithMaxBatchSize(MaxEventBatchSize)
139+
.WithFlushInterval(MaxEventFlushInterval)
140+
.WithEventDispatcher(eventDispatcher)
141+
.WithNotificationCenter(notificationCenter)
142+
.Build();
143+
#endif
144+
145+
return NewDefaultInstance(configManager, notificationCenter, eventDispatcher, errorHandler, logger, eventProcessor: eventProcessor);
146+
}
147+
#endif
116148
public static Optimizely NewDefaultInstance(string sdkKey, string fallback)
117149
{
118150
var logger = OptimizelyLogger ?? new DefaultLogger();

0 commit comments

Comments
 (0)