Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.3.0] - 2026-08-15

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

### 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
Expand Down Expand Up @@ -105,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

2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
</PropertyGroup>

<PropertyGroup>
<PackageBaseVersion>1.2.2</PackageBaseVersion>
<PackageBaseVersion>1.3.0</PackageBaseVersion>
<MinVerTagPrefix></MinVerTagPrefix>
</PropertyGroup>

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion samples/NoaaClientSample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/NoaaClient/NoaaClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public sealed class NoaaClientOptions
/// </summary>
public Uri? ServerUrl { get; set; }

/// <summary>
/// Use a proxy
/// </summary>
public bool UseProxy { get; set; } = false;

/// <summary>
/// Gets the required server URL. Throws <see cref="ArgumentNullException"/> if not set.
/// </summary>
Expand Down
15 changes: 13 additions & 2 deletions src/NoaaClient/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": ""
}
Comment thread
alex1ozr marked this conversation as resolved.
}
```

- **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
Expand Down
45 changes: 38 additions & 7 deletions src/NoaaClient/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,21 +16,50 @@ public static class ServiceCollectionExtensions
/// <summary>
/// Adds NOAA clients to the service collection.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
/// <param name="configuration">The application configuration used to read the <see cref="NoaaClientOptions"/> section.</param>
/// <remarks>
/// Registers <see cref="Ace.IAceClient"/> for backward compatibility only; prefer <see cref="Rtsw.IRtswClient"/>.
/// <para>
/// When <see cref="NoaaClientOptions.UseProxy"/> is enabled in configuration, client requests are routed
/// through the proxy configured in the <c>Proxy</c> section (see <see cref="ProxyOptions"/>).
/// </para>
/// </remarks>
public static IServiceCollection AddNoaaClients(this IServiceCollection services)
public static IServiceCollection AddNoaaClients(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddOptions<NoaaClientOptions>()
.BindConfiguration(NoaaClientOptions.OptionKey);
.Bind(configuration.GetSection(NoaaClientOptions.OptionKey));

services.AddOptions<ProxyOptions>()
.Bind(configuration.GetSection(ProxyOptions.OptionKey));

var useProxy = configuration
.GetSection(NoaaClientOptions.OptionKey)
.GetValue<bool>(nameof(NoaaClientOptions.UseProxy));

#pragma warning disable CS0618 // ACE client registration pending removal in issue #3.
services.AddHttpClient<IAceClient, AceClient>();
services.AddNoaaHttpClient<IAceClient, AceClient>(useProxy);
#pragma warning restore CS0618
services.AddHttpClient<IKpIndexClient, KpIndexClient>();
services.AddHttpClient<IRtswClient, RtswClient>();
services.AddHttpClient<IWsaEnlilClient, WsaEnlilClient>();
services.AddNoaaHttpClient<IKpIndexClient, KpIndexClient>(useProxy);
services.AddNoaaHttpClient<IRtswClient, RtswClient>(useProxy);
services.AddNoaaHttpClient<IWsaEnlilClient, WsaEnlilClient>(useProxy);

return services;
}

private static void AddNoaaHttpClient<TClient, TImplementation>(
this IServiceCollection services,
bool useProxy)
where TClient : class
where TImplementation : class, TClient
{
var builder = services.AddHttpClient<TClient, TImplementation>();

if (useProxy)
{
builder.ConfigurePrimaryHttpProxyMessageHandler();
}
}
}
142 changes: 142 additions & 0 deletions tests/UnitTests/NoaaClient/ServiceCollectionExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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;

/// <summary>
/// Tests for <see cref="ServiceCollectionExtensions"/> proxy configuration.
/// </summary>
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>();
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<WebProxy>();
var credentials = webProxy.Credentials.ShouldBeOfType<NetworkCredential>();
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<InvalidOperationException>(
() => 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.AddNoaaClients(configuration);

using var provider = services.BuildServiceProvider();

var handlerFactory = provider.GetRequiredService<IHttpMessageHandlerFactory>();
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<string, string?>
{
[$"{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();
}
}
Loading