Skip to content

Commit 1db617a

Browse files
committed
feat: Add comprehensive test suite for HttpClient resilience library
1 parent b15326a commit 1db617a

9 files changed

Lines changed: 2155 additions & 0 deletions
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
using FluentAssertions;
2+
using Xunit;
3+
4+
namespace Reliable.HttpClient.Tests;
5+
6+
public class BuilderIntegrationTests
7+
{
8+
[Fact]
9+
public void FullConfigurationWorkflow_WithAllBuilders_WorksCorrectly()
10+
{
11+
// Arrange & Act
12+
HttpClientOptions options = new HttpClientOptionsBuilder()
13+
.WithBaseUrl("https://api.example.com/v1")
14+
.WithTimeout(TimeSpan.FromSeconds(45))
15+
.WithUserAgent("IntegrationTestClient/1.0")
16+
.WithHeader("Authorization", "Bearer integration-token")
17+
.WithHeader("X-API-Version", "v1")
18+
.WithHeaders(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
19+
{
20+
{ "X-Client-Id", "test-client" },
21+
{ "Accept", "application/json" },
22+
})
23+
.WithRetry(retry => retry
24+
.WithMaxRetries(7)
25+
.WithBaseDelay(TimeSpan.FromMilliseconds(500))
26+
.WithMaxDelay(TimeSpan.FromSeconds(30))
27+
.WithJitter(0.4))
28+
.WithCircuitBreaker(cb => cb
29+
.WithFailureThreshold(8)
30+
.WithOpenDuration(TimeSpan.FromMinutes(2)))
31+
.Build();
32+
33+
// Assert
34+
// Main options
35+
options.BaseUrl.Should().Be("https://api.example.com/v1");
36+
options.TimeoutSeconds.Should().Be(45);
37+
options.UserAgent.Should().Be("IntegrationTestClient/1.0");
38+
39+
// Headers
40+
options.DefaultHeaders.Should().HaveCount(4);
41+
options.DefaultHeaders["Authorization"].Should().Be("Bearer integration-token");
42+
options.DefaultHeaders["X-API-Version"].Should().Be("v1");
43+
options.DefaultHeaders["X-Client-Id"].Should().Be("test-client");
44+
options.DefaultHeaders["Accept"].Should().Be("application/json");
45+
46+
// Retry configuration
47+
options.Retry.MaxRetries.Should().Be(7);
48+
options.Retry.BaseDelay.Should().Be(TimeSpan.FromMilliseconds(500));
49+
options.Retry.MaxDelay.Should().Be(TimeSpan.FromSeconds(30));
50+
options.Retry.JitterFactor.Should().Be(0.4);
51+
52+
// Circuit breaker configuration
53+
options.CircuitBreaker.Enabled.Should().BeTrue();
54+
options.CircuitBreaker.FailuresBeforeOpen.Should().Be(8);
55+
options.CircuitBreaker.OpenDuration.Should().Be(TimeSpan.FromMinutes(2));
56+
}
57+
58+
[Fact]
59+
public void BuilderChaining_WithHeaderManipulation_WorksCorrectly()
60+
{
61+
// Arrange & Act
62+
HttpClientOptions options = new HttpClientOptionsBuilder()
63+
.WithHeader("Initial-Header", "initial-value")
64+
.WithHeaders(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
65+
{
66+
{ "Header1", "value1" },
67+
{ "Header2", "value2" },
68+
{ "Header3", "value3" },
69+
})
70+
.WithHeader("Override-Header", "original")
71+
.WithHeader("Override-Header", "overridden") // Should override
72+
.WithoutHeader("Header2") // Should remove
73+
.WithHeader("Final-Header", "final-value")
74+
.Build();
75+
76+
// Assert
77+
options.DefaultHeaders.Should().HaveCount(5);
78+
options.DefaultHeaders["Initial-Header"].Should().Be("initial-value");
79+
options.DefaultHeaders["Header1"].Should().Be("value1");
80+
options.DefaultHeaders["Header3"].Should().Be("value3");
81+
options.DefaultHeaders["Override-Header"].Should().Be("overridden");
82+
options.DefaultHeaders["Final-Header"].Should().Be("final-value");
83+
options.DefaultHeaders.Should().NotContainKey("Header2");
84+
}
85+
86+
[Fact]
87+
public void BuilderChaining_WithCircuitBreakerToggling_WorksCorrectly()
88+
{
89+
// Arrange & Act
90+
HttpClientOptions options = new HttpClientOptionsBuilder()
91+
.WithCircuitBreaker(cb => cb
92+
.WithFailureThreshold(3)
93+
.WithOpenDuration(TimeSpan.FromSeconds(30)))
94+
.WithoutCircuitBreaker() // Disable after configuration
95+
.Build();
96+
97+
// Assert
98+
options.CircuitBreaker.Enabled.Should().BeFalse();
99+
// Configuration should still be preserved
100+
options.CircuitBreaker.FailuresBeforeOpen.Should().Be(3);
101+
options.CircuitBreaker.OpenDuration.Should().Be(TimeSpan.FromSeconds(30));
102+
}
103+
104+
[Fact]
105+
public void BuilderChaining_WithMultipleRetryConfigurations_LastConfigurationWins()
106+
{
107+
// Arrange & Act
108+
HttpClientOptions options = new HttpClientOptionsBuilder()
109+
.WithRetry(retry => retry
110+
.WithMaxRetries(3)
111+
.WithBaseDelay(TimeSpan.FromSeconds(1)))
112+
.WithRetry(retry => retry
113+
.WithMaxRetries(5) // Should override previous
114+
.WithJitter(0.6)) // Should add to previous configuration
115+
.Build();
116+
117+
// Assert
118+
options.Retry.MaxRetries.Should().Be(5);
119+
options.Retry.BaseDelay.Should().Be(TimeSpan.FromSeconds(1)); // Should be preserved
120+
options.Retry.JitterFactor.Should().Be(0.6);
121+
}
122+
123+
[Fact]
124+
public void ImplicitConversion_WorksInDifferentContexts()
125+
{
126+
// Arrange
127+
var builder = new HttpClientOptionsBuilder()
128+
.WithBaseUrl("https://test.com")
129+
.WithTimeout(TimeSpan.FromSeconds(30));
130+
131+
// Act & Assert - Implicit conversion in method parameter
132+
ValidateOptions(builder); // Should work without explicit conversion
133+
134+
// Act & Assert - Implicit conversion in assignment
135+
HttpClientOptions options = builder;
136+
options.BaseUrl.Should().Be("https://test.com");
137+
options.TimeoutSeconds.Should().Be(30);
138+
}
139+
140+
[Fact]
141+
public void BuilderReuse_AccumulatesChanges()
142+
{
143+
// Arrange
144+
HttpClientOptionsBuilder builder1 = new HttpClientOptionsBuilder()
145+
.WithBaseUrl("https://api.example.com")
146+
.WithRetry(retry => retry.WithMaxRetries(3));
147+
148+
HttpClientOptionsBuilder builder2 = new HttpClientOptionsBuilder()
149+
.WithBaseUrl("https://api.example.com")
150+
.WithTimeout(TimeSpan.FromSeconds(60))
151+
.WithRetry(retry => retry.WithMaxRetries(5));
152+
153+
// Act
154+
HttpClientOptions options1 = builder1.Build();
155+
HttpClientOptions options2 = builder2.Build();
156+
157+
// Assert
158+
options1.Should().NotBeSameAs(options2);
159+
options1.TimeoutSeconds.Should().Be(30); // default
160+
options1.Retry.MaxRetries.Should().Be(3);
161+
162+
options2.TimeoutSeconds.Should().Be(60); // modified
163+
options2.Retry.MaxRetries.Should().Be(5); // modified
164+
165+
// Both should have the same base URL
166+
options1.BaseUrl.Should().Be("https://api.example.com");
167+
options2.BaseUrl.Should().Be("https://api.example.com");
168+
}
169+
170+
[Fact]
171+
public void ComplexWorkflow_WithValidationErrors_ThrowsAtBuild()
172+
{
173+
// Arrange
174+
HttpClientOptionsBuilder builder = new HttpClientOptionsBuilder()
175+
.WithBaseUrl("https://api.example.com")
176+
.WithRetry(retry => retry
177+
.WithMaxRetries(5)
178+
.WithBaseDelay(TimeSpan.FromSeconds(10))
179+
.WithMaxDelay(TimeSpan.FromSeconds(5))); // Invalid: base > max
180+
181+
// Act & Assert
182+
builder.Invoking(b => b.Build())
183+
.Should().Throw<ArgumentException>()
184+
.WithParameterName(nameof(RetryOptions.BaseDelay));
185+
}
186+
187+
[Fact]
188+
public void MinimalConfiguration_WithOnlyRequiredSettings_WorksCorrectly()
189+
{
190+
// Arrange & Act
191+
HttpClientOptions options = new HttpClientOptionsBuilder()
192+
.WithBaseUrl("https://minimal.example.com")
193+
.Build();
194+
195+
// Assert
196+
options.BaseUrl.Should().Be("https://minimal.example.com");
197+
// All other values should be defaults
198+
options.TimeoutSeconds.Should().Be(30);
199+
options.UserAgent.Should().Be("Reliable.HttpClient/1.1.0");
200+
options.DefaultHeaders.Should().BeEmpty();
201+
options.Retry.MaxRetries.Should().Be(3);
202+
options.CircuitBreaker.Enabled.Should().BeTrue();
203+
options.CircuitBreaker.FailuresBeforeOpen.Should().Be(5);
204+
}
205+
206+
[Fact]
207+
public void HeadersClearingWorkflow_WorksCorrectly()
208+
{
209+
// Arrange & Act
210+
HttpClientOptions options = new HttpClientOptionsBuilder()
211+
.WithHeaders(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
212+
{
213+
{ "Header1", "value1" },
214+
{ "Header2", "value2" },
215+
{ "Header3", "value3" },
216+
})
217+
.WithoutHeaders() // Clear all headers
218+
.WithHeader("NewHeader", "new-value") // Add after clearing
219+
.Build();
220+
221+
// Assert
222+
options.DefaultHeaders.Should().HaveCount(1);
223+
options.DefaultHeaders["NewHeader"].Should().Be("new-value");
224+
options.DefaultHeaders.Should().NotContainKey("Header1");
225+
options.DefaultHeaders.Should().NotContainKey("Header2");
226+
options.DefaultHeaders.Should().NotContainKey("Header3");
227+
}
228+
229+
private static void ValidateOptions(HttpClientOptions options)
230+
{
231+
options.Should().NotBeNull();
232+
options.BaseUrl.Should().Be("https://test.com");
233+
options.TimeoutSeconds.Should().Be(30);
234+
}
235+
}

0 commit comments

Comments
 (0)