Skip to content

Commit cb6fe56

Browse files
[FSSDK-12148] test addition
1 parent ab3efea commit cb6fe56

3 files changed

Lines changed: 234 additions & 6 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
* Copyright 2026, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#if !NET35 && !NET40
18+
using System;
19+
using System.Collections.Generic;
20+
using System.Net;
21+
using System.Net.Http;
22+
using System.Threading;
23+
using System.Threading.Tasks;
24+
using Moq;
25+
using NUnit.Framework;
26+
using OptimizelySDK.Event;
27+
using OptimizelySDK.Event.Dispatcher;
28+
using OptimizelySDK.Logger;
29+
30+
namespace OptimizelySDK.Tests.EventTests
31+
{
32+
[TestFixture]
33+
public class HttpClientEventDispatcher45Test
34+
{
35+
[SetUp]
36+
public void Setup()
37+
{
38+
_mockLogger = new Mock<ILogger>();
39+
_mockLogger.Setup(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()));
40+
_requestTimestamps = new List<DateTime>();
41+
}
42+
43+
private Mock<ILogger> _mockLogger;
44+
private List<DateTime> _requestTimestamps;
45+
46+
[Test]
47+
public void DispatchEvent_Success_SingleAttempt()
48+
{
49+
var handler = new MockHttpMessageHandler(HttpStatusCode.OK);
50+
var httpClient = new HttpClient(handler);
51+
var dispatcher = new HttpClientEventDispatcher45(httpClient)
52+
{
53+
Logger = _mockLogger.Object,
54+
};
55+
var logEvent = CreateLogEvent();
56+
57+
dispatcher.DispatchEvent(logEvent);
58+
Thread.Sleep(500); // Wait for async dispatch
59+
60+
Assert.AreEqual(1, handler.RequestCount);
61+
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, It.IsAny<string>()), Times.Never);
62+
}
63+
64+
[Test]
65+
public void DispatchEvent_ServerError500_RetriesThreeTimes()
66+
{
67+
var handler = new MockHttpMessageHandler(HttpStatusCode.InternalServerError);
68+
var httpClient = new HttpClient(handler);
69+
var dispatcher = new HttpClientEventDispatcher45(httpClient)
70+
{
71+
Logger = _mockLogger.Object,
72+
};
73+
var logEvent = CreateLogEvent();
74+
75+
dispatcher.DispatchEvent(logEvent);
76+
Thread.Sleep(1500);
77+
78+
Assert.AreEqual(3, handler.RequestCount);
79+
_mockLogger.Verify(
80+
l => l.Log(LogLevel.ERROR, It.Is<string>(s => s.Contains("3 attempt(s)"))),
81+
Times.Once);
82+
}
83+
84+
[Test]
85+
public void DispatchEvent_ClientError400_NoRetry()
86+
{
87+
var handler = new MockHttpMessageHandler(HttpStatusCode.BadRequest);
88+
var httpClient = new HttpClient(handler);
89+
var dispatcher = new HttpClientEventDispatcher45(httpClient)
90+
{
91+
Logger = _mockLogger.Object,
92+
};
93+
var logEvent = CreateLogEvent();
94+
95+
dispatcher.DispatchEvent(logEvent);
96+
Thread.Sleep(500);
97+
98+
Assert.AreEqual(1, handler.RequestCount);
99+
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, It.IsAny<string>()), Times.Once);
100+
}
101+
102+
[Test]
103+
public void DispatchEvent_SucceedsOnSecondAttempt_StopsRetrying()
104+
{
105+
var handler = new MockHttpMessageHandler(new[]
106+
{
107+
HttpStatusCode.InternalServerError,
108+
HttpStatusCode.OK,
109+
});
110+
var httpClient = new HttpClient(handler);
111+
var dispatcher = new HttpClientEventDispatcher45(httpClient)
112+
{
113+
Logger = _mockLogger.Object,
114+
};
115+
var logEvent = CreateLogEvent();
116+
117+
dispatcher.DispatchEvent(logEvent);
118+
Thread.Sleep(1000);
119+
120+
Assert.AreEqual(2, handler.RequestCount);
121+
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, It.IsAny<string>()), Times.Never);
122+
}
123+
124+
[Test]
125+
public void DispatchEvent_ExponentialBackoff_VerifyTiming()
126+
{
127+
var handler =
128+
new MockHttpMessageHandler(HttpStatusCode.InternalServerError, _requestTimestamps);
129+
var httpClient = new HttpClient(handler);
130+
var dispatcher = new HttpClientEventDispatcher45(httpClient)
131+
{
132+
Logger = _mockLogger.Object,
133+
};
134+
var logEvent = CreateLogEvent();
135+
136+
dispatcher.DispatchEvent(logEvent);
137+
Thread.Sleep(1500); // Wait for all retries
138+
139+
Assert.AreEqual(3, _requestTimestamps.Count);
140+
141+
// First retry after ~200ms
142+
var firstDelay = (_requestTimestamps[1] - _requestTimestamps[0]).TotalMilliseconds;
143+
Assert.That(firstDelay, Is.GreaterThanOrEqualTo(180).And.LessThan(350),
144+
$"First retry delay was {firstDelay}ms, expected ~200ms");
145+
146+
// Second retry after ~400ms
147+
var secondDelay = (_requestTimestamps[2] - _requestTimestamps[1]).TotalMilliseconds;
148+
Assert.That(secondDelay, Is.GreaterThanOrEqualTo(380).And.LessThan(550),
149+
$"Second retry delay was {secondDelay}ms, expected ~400ms");
150+
}
151+
152+
private static LogEvent CreateLogEvent()
153+
{
154+
return new LogEvent(
155+
"https://logx.optimizely.com/v1/events",
156+
new Dictionary<string, object>
157+
{
158+
{ "accountId", "12345" },
159+
{ "visitors", new object[] { } },
160+
},
161+
"POST",
162+
new Dictionary<string, string>());
163+
}
164+
165+
/// <summary>
166+
/// Mock HTTP message handler for testing.
167+
/// </summary>
168+
private class MockHttpMessageHandler : HttpMessageHandler
169+
{
170+
private readonly HttpStatusCode[] _statusCodes;
171+
private readonly List<DateTime> _timestamps;
172+
private int _currentIndex;
173+
174+
public MockHttpMessageHandler(HttpStatusCode statusCode,
175+
List<DateTime> timestamps = null
176+
)
177+
: this(new[] { statusCode }, timestamps) { }
178+
179+
public MockHttpMessageHandler(HttpStatusCode[] statusCodes,
180+
List<DateTime> timestamps = null
181+
)
182+
{
183+
_statusCodes = statusCodes;
184+
_timestamps = timestamps;
185+
_currentIndex = 0;
186+
}
187+
188+
public int RequestCount { get; private set; }
189+
190+
protected override Task<HttpResponseMessage> SendAsync(
191+
HttpRequestMessage request,
192+
CancellationToken cancellationToken
193+
)
194+
{
195+
RequestCount++;
196+
_timestamps?.Add(DateTime.Now);
197+
198+
var statusCode = _currentIndex < _statusCodes.Length ?
199+
_statusCodes[_currentIndex] :
200+
_statusCodes[_statusCodes.Length - 1];
201+
202+
_currentIndex++;
203+
204+
var response = new HttpResponseMessage(statusCode)
205+
{
206+
Content = new StringContent("{}"),
207+
};
208+
209+
return Task.FromResult(response);
210+
}
211+
}
212+
}
213+
}
214+
#endif

OptimizelySDK.Tests/OptimizelySDK.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
<Compile Include="EventTests\DefaultEventDispatcherTest.cs"/>
102102
<Compile Include="EventTests\EventBuilderTest.cs"/>
103103
<Compile Include="EventTests\ForwardingEventProcessorTest.cs"/>
104+
<Compile Include="EventTests\HttpClientEventDispatcher45Test.cs"/>
104105
<Compile Include="EventTests\LogEventTest.cs"/>
105106
<Compile Include="EventTests\TestEventDispatcher.cs"/>
106107
<Compile Include="EventTests\TestForwardingEventDispatcher.cs"/>

OptimizelySDK/Event/Dispatcher/HttpClientEventDispatcher45.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,29 @@ namespace OptimizelySDK.Event.Dispatcher
2828
public class HttpClientEventDispatcher45 : IEventDispatcher
2929
{
3030
/// <summary>
31-
/// HTTP client object.
31+
/// Default shared HTTP client instance for all dispatchers.
3232
/// </summary>
33-
private static readonly HttpClient Client;
33+
private static readonly HttpClient DefaultClient = new HttpClient();
3434

3535
/// <summary>
36-
/// Constructor for initializing static members.
36+
/// HTTP client instance used by this dispatcher.
3737
/// </summary>
38-
static HttpClientEventDispatcher45()
38+
private readonly HttpClient _client;
39+
40+
/// <summary>
41+
/// Default constructor using the shared static HttpClient.
42+
/// </summary>
43+
public HttpClientEventDispatcher45() : this(null)
44+
{
45+
}
46+
47+
/// <summary>
48+
/// Constructor allowing injection of a custom HttpClient for testing.
49+
/// </summary>
50+
/// <param name="httpClient">Custom HttpClient instance, or null to use the default shared instance.</param>
51+
internal HttpClientEventDispatcher45(HttpClient httpClient)
3952
{
40-
Client = new HttpClient();
53+
_client = httpClient ?? DefaultClient;
4154
}
4255

4356
public ILogger Logger { get; set; } = new DefaultLogger();
@@ -85,7 +98,7 @@ private async Task DispatchEventAsync(LogEvent logEvent)
8598
}
8699
}
87100

88-
response = await Client.SendAsync(request).ConfigureAwait(false);
101+
response = await _client.SendAsync(request).ConfigureAwait(false);
89102
response.EnsureSuccessStatusCode();
90103

91104
// Success - exit the retry loop

0 commit comments

Comments
 (0)