From d0304f47c8c855c98d6ca6ac48965d0a954aec4a Mon Sep 17 00:00:00 2001 From: Ermilov Aleksei Date: Fri, 14 Aug 2026 15:20:15 +0500 Subject: [PATCH 1/4] Add proxy support to NOAA clients via UseProxy option Introduce NoaaClientOptions.UseProxy; when enabled, NOAA client requests are routed through the proxy from the Proxy section using AuroraScienceHub.Framework.Http.Proxy. The registration now reads configuration, so AddNoaaClients(IConfiguration) is required. Update the sample and package README, and add unit tests covering disabled proxy, proxy address, credentials, and the missing-address fast-fail case. Co-Authored-By: Claude --- CHANGELOG.md | 5 + samples/NoaaClientSample/Program.cs | 2 +- src/NoaaClient/NoaaClientOptions.cs | 5 + src/NoaaClient/README.md | 15 +- src/NoaaClient/ServiceCollectionExtensions.cs | 42 ++++- .../ServiceCollectionExtensionsTests.cs | 143 ++++++++++++++++++ 6 files changed, 203 insertions(+), 9 deletions(-) create mode 100644 tests/UnitTests/NoaaClient/ServiceCollectionExtensionsTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index e4ae874..3053ddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +#### NoaaClient Package +- New `NoaaClientOptions.UseProxy` configuration option to route NOAA client requests through a proxy (configured in the `Proxy` section) via `AuroraScienceHub.Framework.Http.Proxy` + ## [1.2.2] - 2026-08-11 ### Fixed diff --git a/samples/NoaaClientSample/Program.cs b/samples/NoaaClientSample/Program.cs index 86629ae..ba69a49 100644 --- a/samples/NoaaClientSample/Program.cs +++ b/samples/NoaaClientSample/Program.cs @@ -14,7 +14,7 @@ // Setup DI and configuration var builder = Host.CreateApplicationBuilder(args); builder.Configuration.AddJsonFile("appsettings.json", optional: false); -builder.Services.AddNoaaClients(); +builder.Services.AddNoaaClients(builder.Configuration); var host = builder.Build(); // Get NOAA clients from DI diff --git a/src/NoaaClient/NoaaClientOptions.cs b/src/NoaaClient/NoaaClientOptions.cs index c30e1f4..7ee384d 100644 --- a/src/NoaaClient/NoaaClientOptions.cs +++ b/src/NoaaClient/NoaaClientOptions.cs @@ -17,6 +17,11 @@ public sealed class NoaaClientOptions /// public Uri? ServerUrl { get; set; } + /// + /// Use a proxy + /// + public bool UseProxy { get; set; } = false; + /// /// Gets the required server URL. Throws if not set. /// diff --git a/src/NoaaClient/README.md b/src/NoaaClient/README.md index 1789b96..6f5bb33 100644 --- a/src/NoaaClient/README.md +++ b/src/NoaaClient/README.md @@ -17,15 +17,26 @@ dotnet add package AuroraScienceHub.Integrations.NoaaClient ```json { "Noaa": { - "ServerUrl": "https://services.swpc.noaa.gov" + "ServerUrl": "https://services.swpc.noaa.gov", + "UseProxy": false + }, + "Proxy": { + "Address": "http://proxy.example.com:8080", + "UserName": "", + "Password": "" } } ``` +- **Noaa:ServerUrl** — NOAA SWPC base URL (required). +- **Noaa:UseProxy** — when `true`, client requests are routed through the proxy configured in the `Proxy` section. Default: `false`. +- **Proxy:Address** — proxy server URI (required when `UseProxy` is enabled; startup fails with `InvalidOperationException` if missing). +- **Proxy:UserName** / **Proxy:Password** — optional proxy credentials. When omitted, default credentials are used. + Register clients in DI: ```csharp -builder.Services.AddNoaaClients(); +builder.Services.AddNoaaClients(builder.Configuration); ``` ## RTSW Client diff --git a/src/NoaaClient/ServiceCollectionExtensions.cs b/src/NoaaClient/ServiceCollectionExtensions.cs index bab307a..553eb2d 100644 --- a/src/NoaaClient/ServiceCollectionExtensions.cs +++ b/src/NoaaClient/ServiceCollectionExtensions.cs @@ -1,7 +1,9 @@ +using AuroraScienceHub.Framework.Http.Proxy; using AuroraScienceHub.Integrations.NoaaClient.Ace; -using AuroraScienceHub.Integrations.NoaaClient.WsaEnlil; using AuroraScienceHub.Integrations.NoaaClient.KpIndex; using AuroraScienceHub.Integrations.NoaaClient.Rtsw; +using AuroraScienceHub.Integrations.NoaaClient.WsaEnlil; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace AuroraScienceHub.Integrations.NoaaClient; @@ -14,21 +16,49 @@ public static class ServiceCollectionExtensions /// /// Adds NOAA clients to the service collection. /// + /// The to add services to. + /// The application configuration used to read the section. /// /// Registers for backward compatibility only; prefer . + /// + /// When is enabled in configuration, client requests are routed + /// through the proxy configured in the Proxy section (see ). + /// /// - public static IServiceCollection AddNoaaClients(this IServiceCollection services) + public static IServiceCollection AddNoaaClients( + this IServiceCollection services, + IConfiguration configuration) { services.AddOptions() .BindConfiguration(NoaaClientOptions.OptionKey); + services.AddProxyOptions(); + + var useProxy = configuration + .GetSection(NoaaClientOptions.OptionKey) + .GetValue(nameof(NoaaClientOptions.UseProxy)); + #pragma warning disable CS0618 // ACE client registration pending removal in issue #3. - services.AddHttpClient(); + services.AddNoaaHttpClient(useProxy); #pragma warning restore CS0618 - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); + services.AddNoaaHttpClient(useProxy); + services.AddNoaaHttpClient(useProxy); + services.AddNoaaHttpClient(useProxy); return services; } + + private static void AddNoaaHttpClient( + this IServiceCollection services, + bool useProxy) + where TClient : class + where TImplementation : class, TClient + { + var builder = services.AddHttpClient(); + + if (useProxy) + { + builder.ConfigurePrimaryHttpProxyMessageHandler(); + } + } } diff --git a/tests/UnitTests/NoaaClient/ServiceCollectionExtensionsTests.cs b/tests/UnitTests/NoaaClient/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..6cb85a5 --- /dev/null +++ b/tests/UnitTests/NoaaClient/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,143 @@ +using System.Net; +using System.Reflection; +using AuroraScienceHub.Integrations.NoaaClient; +using AuroraScienceHub.Integrations.NoaaClient.Rtsw; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; +using Shouldly; + +namespace AuroraScienceHub.Integrations.UnitTests.NoaaClient; + +/// +/// Tests for proxy configuration. +/// +public sealed class ServiceCollectionExtensionsTests +{ + private static readonly Uri ProxyAddress = new("http://proxy.example.com:8080"); + + [Fact(DisplayName = "Noaa clients do not use a custom proxy when UseProxy is not enabled")] + public void AddNoaaClients_WhenUseProxyDisabled_ConfiguresClientWithoutProxy() + { + // Arrange / Act + var (_, proxy) = CreatePrimaryHandlerProxy(CreateConfiguration(useProxy: false), typeof(IRtswClient)); + + // Assert + proxy.ShouldBeNull(); + } + + [Fact(DisplayName = "Noaa clients route through the configured proxy when UseProxy is enabled")] + public void AddNoaaClients_WhenUseProxyEnabled_ConfiguresProxyAddress() + { + // Arrange / Act + var (useProxy, proxy) = CreatePrimaryHandlerProxy( + CreateConfiguration(useProxy: true, address: ProxyAddress), + typeof(IRtswClient)); + + // Assert + useProxy.ShouldBeTrue(); + var webProxy = proxy.ShouldBeOfType(); + webProxy.Address.ShouldBe(ProxyAddress); + } + + [Fact(DisplayName = "Noaa clients apply proxy credentials when configured")] + public void AddNoaaClients_WhenProxyCredentialsConfigured_AppliesCredentials() + { + // Arrange / Act + var (_, proxy) = CreatePrimaryHandlerProxy( + CreateConfiguration(useProxy: true, address: ProxyAddress, userName: "user", password: "pass"), + typeof(IRtswClient)); + + // Assert + var webProxy = proxy.ShouldBeOfType(); + var credentials = webProxy.Credentials.ShouldBeOfType(); + credentials.UserName.ShouldBe("user"); + credentials.Password.ShouldBe("pass"); + webProxy.UseDefaultCredentials.ShouldBeFalse(); + } + + [Fact(DisplayName = "Noaa clients fail fast when UseProxy is enabled but the proxy address is missing")] + public void AddNoaaClients_WhenUseProxyEnabledWithoutAddress_ThrowsInvalidOperationException() + { + // Arrange / Act + var exception = Should.Throw( + () => CreatePrimaryHandlerProxy(CreateConfiguration(useProxy: true), typeof(IRtswClient))); + + // Assert + exception.Message.ShouldContain("Proxy"); + } + + private static (bool UseProxy, IWebProxy? Proxy) CreatePrimaryHandlerProxy( + IConfiguration configuration, + Type clientType) + { + var services = new ServiceCollection(); + services.AddSingleton(configuration); + services.AddNoaaClients(configuration); + + using var provider = services.BuildServiceProvider(); + + var handlerFactory = provider.GetRequiredService(); + var handler = handlerFactory.CreateHandler(clientType.Name); + var primaryHandler = UnwrapPrimaryHandler(handler); + + return primaryHandler switch + { + HttpClientHandler httpClientHandler => (httpClientHandler.UseProxy, httpClientHandler.Proxy), + SocketsHttpHandler socketsHandler => (socketsHandler.UseProxy, socketsHandler.Proxy), + _ => throw new InvalidOperationException( + $"Unexpected primary handler type {primaryHandler.GetType().Name}."), + }; + } + + private static HttpMessageHandler UnwrapPrimaryHandler(HttpMessageHandler handler) + { + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + while (true) + { + if (handler is HttpClientHandler or SocketsHttpHandler) + { + return handler; + } + + var innerHandlerProperty = handler.GetType().GetProperty("InnerHandler", flags) + ?? throw new InvalidOperationException( + $"Cannot unwrap message handler of type {handler.GetType().Name}."); + + handler = (HttpMessageHandler)innerHandlerProperty.GetValue(handler)!; + } + } + + private static IConfiguration CreateConfiguration( + bool useProxy, + Uri? address = null, + string? userName = null, + string? password = null) + { + var data = new Dictionary + { + [$"{NoaaClientOptions.OptionKey}:ServerUrl"] = "https://noaa.test", + [$"{NoaaClientOptions.OptionKey}:UseProxy"] = useProxy.ToString(), + }; + + if (address is not null) + { + data["Proxy:Address"] = address.ToString(); + } + + if (userName is not null) + { + data["Proxy:UserName"] = userName; + } + + if (password is not null) + { + data["Proxy:Password"] = password; + } + + return new ConfigurationBuilder() + .AddInMemoryCollection(data) + .Build(); + } +} From 78c63330f1f614c919f1573692599d6f063a7259 Mon Sep 17 00:00:00 2001 From: Ermilov Aleksei Date: Fri, 14 Aug 2026 15:32:04 +0500 Subject: [PATCH 2/4] Bind NoaaClient and Proxy options from the provided configuration BindConfiguration reads IConfiguration from the DI container at resolve time, which would break when a consumer passes configuration to AddNoaaClients without registering it. Bind both NoaaClientOptions and ProxyOptions directly from the passed instance instead, making the method self-contained. The test helper no longer registers IConfiguration, exercising exactly that scenario. Co-Authored-By: Claude --- src/NoaaClient/ServiceCollectionExtensions.cs | 5 +++-- .../UnitTests/NoaaClient/ServiceCollectionExtensionsTests.cs | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NoaaClient/ServiceCollectionExtensions.cs b/src/NoaaClient/ServiceCollectionExtensions.cs index 553eb2d..5b92a30 100644 --- a/src/NoaaClient/ServiceCollectionExtensions.cs +++ b/src/NoaaClient/ServiceCollectionExtensions.cs @@ -30,9 +30,10 @@ public static IServiceCollection AddNoaaClients( IConfiguration configuration) { services.AddOptions() - .BindConfiguration(NoaaClientOptions.OptionKey); + .Bind(configuration.GetSection(NoaaClientOptions.OptionKey)); - services.AddProxyOptions(); + services.AddOptions() + .Bind(configuration.GetSection(ProxyOptions.OptionKey)); var useProxy = configuration .GetSection(NoaaClientOptions.OptionKey) diff --git a/tests/UnitTests/NoaaClient/ServiceCollectionExtensionsTests.cs b/tests/UnitTests/NoaaClient/ServiceCollectionExtensionsTests.cs index 6cb85a5..6f2ceaf 100644 --- a/tests/UnitTests/NoaaClient/ServiceCollectionExtensionsTests.cs +++ b/tests/UnitTests/NoaaClient/ServiceCollectionExtensionsTests.cs @@ -72,7 +72,6 @@ private static (bool UseProxy, IWebProxy? Proxy) CreatePrimaryHandlerProxy( Type clientType) { var services = new ServiceCollection(); - services.AddSingleton(configuration); services.AddNoaaClients(configuration); using var provider = services.BuildServiceProvider(); From 71dfb2357f2007c2713f9ed08636a715fa541931 Mon Sep 17 00:00:00 2001 From: Ermilov Aleksei Date: Fri, 14 Aug 2026 15:33:30 +0500 Subject: [PATCH 3/4] Document breaking AddNoaaClients signature change Add a Changed entry to the Unreleased changelog calling out that AddNoaaClients now requires IConfiguration, and update the main README quick example to pass builder.Configuration. Co-Authored-By: Claude --- CHANGELOG.md | 5 +++++ README.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3053ddb..dcfd0e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### NoaaClient Package - New `NoaaClientOptions.UseProxy` configuration option to route NOAA client requests through a proxy (configured in the `Proxy` section) via `AuroraScienceHub.Framework.Http.Proxy` +### Changed + +#### NoaaClient Package +- **Breaking:** `AddNoaaClients()` now requires an `IConfiguration` argument (`AddNoaaClients(configuration)`); `NoaaClientOptions` and `ProxyOptions` are bound directly from the provided configuration + ## [1.2.2] - 2026-08-11 ### Fixed diff --git a/README.md b/README.md index bd748d5..5210ae8 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ dotnet add package AuroraScienceHub.Integrations.NoaaClient ```csharp // Register services -builder.Services.AddNoaaClients(); +builder.Services.AddNoaaClients(builder.Configuration); // Inject and use clients public class SpaceWeatherService From 12237894019962d2503f898a50ad74c65dd022e5 Mon Sep 17 00:00:00 2001 From: Ermilov Aleksei Date: Sat, 15 Aug 2026 08:03:04 +0500 Subject: [PATCH 4/4] Bump NoaaClient version to 1.3.0 Set PackageBaseVersion to 1.3.0 and turn the Unreleased changelog section into the 1.3.0 release (proxy support + breaking AddNoaaClients(IConfiguration) signature change), fixing the missing 1.2.2 compare link. Co-Authored-By: Claude --- CHANGELOG.md | 6 +++++- Directory.Build.props | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcfd0e5..8ea9e85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.3.0] - 2026-08-15 + ### Added #### NoaaClient Package @@ -115,8 +117,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Embedded debug symbols in NuGet packages - GitHub Actions CI/CD pipeline for build and test +[1.3.0]: https://github.com/Aurora-Science-Hub/Integrations/compare/1.2.2...1.3.0 +[1.2.2]: https://github.com/Aurora-Science-Hub/Integrations/compare/1.1.1...1.2.2 [1.1.1]: https://github.com/Aurora-Science-Hub/Integrations/compare/1.1.0...1.1.1 [1.1.0]: https://github.com/Aurora-Science-Hub/Integrations/compare/1.0.0...1.1.0 [1.0.0]: https://github.com/Aurora-Science-Hub/Integrations/releases/tag/1.0.0 -[Unreleased]: https://github.com/Aurora-Science-Hub/Integrations/compare/1.1.1...HEAD +[Unreleased]: https://github.com/Aurora-Science-Hub/Integrations/compare/1.3.0...HEAD diff --git a/Directory.Build.props b/Directory.Build.props index a2419b6..3a98c7b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -29,7 +29,7 @@ - 1.2.2 + 1.3.0