|
| 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