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