03de3dcc14
The bundle importer writes external systems, methods and database connections through the raw DbContext, bypassing the ManagementActor handlers that invalidate ExternalDefinitionLookupCache (#65), so the importing node served pre-import definitions for up to DefinitionCacheTtl. New Commons seam IExternalDefinitionCacheInvalidator, implemented by the cache and registered as the same singleton; BundleImporter takes it optionally and invalidates once after every committed import (never on rollback; never throws). Peer-central-node invalidation (the other half of #83) is NOT done here: the issue leaves open whether the 10 s TTL bound is acceptable for the peer. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
122 lines
5.5 KiB
C#
122 lines
5.5 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Http;
|
|
using NSubstitute;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests;
|
|
|
|
/// <summary>
|
|
/// Configuration options must actually influence the
|
|
/// registered HTTP client — an operator setting them must not be silently ignored.
|
|
/// </summary>
|
|
public class ServiceWiringTests
|
|
{
|
|
/// <summary>
|
|
/// Gitea #83: the Commons invalidation seam resolves to the SAME lookup-cache singleton the
|
|
/// gateway reads through, so a bundle import that invalidates it actually clears the cache.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DefinitionCacheInvalidator_IsTheLookupCacheSingleton()
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddLogging();
|
|
services.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build());
|
|
services.AddSingleton(Substitute.For<IExternalSystemRepository>());
|
|
services.AddExternalSystemGateway();
|
|
|
|
using var provider = services.BuildServiceProvider();
|
|
|
|
Assert.Same(
|
|
provider.GetRequiredService<ExternalDefinitionLookupCache>(),
|
|
provider.GetRequiredService<IExternalDefinitionCacheInvalidator>());
|
|
}
|
|
|
|
/// <summary>Verifies that the per-system connection cap is applied to the primary handler of the named HTTP client.</summary>
|
|
[Fact]
|
|
public void MaxConcurrentConnectionsPerSystem_IsAppliedToTheNamedHttpClientPrimaryHandler()
|
|
{
|
|
var config = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["ScadaBridge:ExternalSystemGateway:MaxConcurrentConnectionsPerSystem"] = "4",
|
|
})
|
|
.Build();
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddLogging();
|
|
services.AddSingleton<IConfiguration>(config);
|
|
services.AddSingleton(Substitute.For<IExternalSystemRepository>());
|
|
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<IHttpMessageHandlerFactory>();
|
|
var handler = handlerFactory.CreateHandler("ExternalSystem_AnySystem");
|
|
|
|
var primary = FindPrimaryHandler(handler);
|
|
var sockets = Assert.IsType<SocketsHttpHandler>(primary);
|
|
Assert.Equal(4, sockets.MaxConnectionsPerServer);
|
|
}
|
|
|
|
/// <summary>Verifies that the per-system connection cap is not applied to HTTP clients that do not belong to the gateway.</summary>
|
|
[Fact]
|
|
public void MaxConcurrentConnectionsPerSystem_IsNotAppliedToNonGatewayHttpClients()
|
|
{
|
|
// 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<string, string?>
|
|
{
|
|
["ScadaBridge:ExternalSystemGateway:MaxConcurrentConnectionsPerSystem"] = "4",
|
|
})
|
|
.Build();
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddLogging();
|
|
services.AddSingleton<IConfiguration>(config);
|
|
services.AddSingleton(Substitute.For<IExternalSystemRepository>());
|
|
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<IHttpMessageHandlerFactory>();
|
|
|
|
// The gateway's own client must still get the gateway cap.
|
|
var gatewayPrimary = FindPrimaryHandler(handlerFactory.CreateHandler("ExternalSystem_AnySystem"));
|
|
Assert.Equal(4, Assert.IsType<SocketsHttpHandler>(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;
|
|
}
|
|
}
|