using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using NSubstitute;
using ScadaLink.Commons.Interfaces.Repositories;
namespace ScadaLink.ExternalSystemGateway.Tests;
///
/// ExternalSystemGateway-013: configuration options must actually influence the
/// registered HTTP client — an operator setting them must not be silently ignored.
///
public class ServiceWiringTests
{
[Fact]
public void MaxConcurrentConnectionsPerSystem_IsAppliedToTheNamedHttpClientPrimaryHandler()
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary
{
["ScadaLink:ExternalSystemGateway:MaxConcurrentConnectionsPerSystem"] = "4",
})
.Build();
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton(config);
services.AddSingleton(Substitute.For());
services.AddExternalSystemGateway();
using var provider = services.BuildServiceProvider();
// Resolve the per-system named client's message-handler chain and walk to the
// primary handler — the option must be reflected in MaxConnectionsPerServer.
var handlerFactory = provider.GetRequiredService();
var handler = handlerFactory.CreateHandler("ExternalSystem_AnySystem");
var primary = FindPrimaryHandler(handler);
var sockets = Assert.IsType(primary);
Assert.Equal(4, sockets.MaxConnectionsPerServer);
}
[Fact]
public void MaxConcurrentConnectionsPerSystem_IsNotAppliedToNonGatewayHttpClients()
{
// ExternalSystemGateway-016: the gateway's connection cap must be scoped to
// its own per-system clients ("ExternalSystem_{name}"). It must NOT leak onto
// unrelated HttpClient consumers in the same host process (e.g. the
// Notification Service's OAuth2 token client) — that would silently throttle
// and override the primary-handler configuration of another component.
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary
{
["ScadaLink:ExternalSystemGateway:MaxConcurrentConnectionsPerSystem"] = "4",
})
.Build();
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton(config);
services.AddSingleton(Substitute.For());
services.AddExternalSystemGateway();
// A client owned by a different component, registered the way the
// Notification Service registers its OAuth2 token client — a plain
// AddHttpClient with no custom primary handler. Its primary handler must
// remain the framework default (uncapped), not the gateway's SocketsHttpHandler.
services.AddHttpClient("NotificationService_OAuth2");
using var provider = services.BuildServiceProvider();
var handlerFactory = provider.GetRequiredService();
// The gateway's own client must still get the gateway cap.
var gatewayPrimary = FindPrimaryHandler(handlerFactory.CreateHandler("ExternalSystem_AnySystem"));
Assert.Equal(4, Assert.IsType(gatewayPrimary).MaxConnectionsPerServer);
// The unrelated component's client must NOT inherit the gateway's connection
// cap. With ConfigureHttpClientDefaults the primary handler is a
// SocketsHttpHandler capped at the gateway's value (the leak); with a scoped
// registration it is the framework default whose MaxConnectionsPerServer is
// int.MaxValue.
var otherPrimary = FindPrimaryHandler(handlerFactory.CreateHandler("NotificationService_OAuth2"));
if (otherPrimary is SocketsHttpHandler otherSockets)
{
Assert.NotEqual(4, otherSockets.MaxConnectionsPerServer);
}
}
private static HttpMessageHandler FindPrimaryHandler(HttpMessageHandler handler)
{
var current = handler;
while (current is DelegatingHandler delegating && delegating.InnerHandler != null)
{
current = delegating.InnerHandler;
}
return current;
}
}