6b5fe6aa82
Server-054/055/056, Contracts-020/021/022, Tests-036/038/039, IntegrationTests-030/031/032 (+033 deferred to live rig), Client.Dotnet-026/028/029 (+027 won't-fix), Client.Go-030..034, Client.Python-032..036, Client.Rust-033..038. Key fix: SessionEventDistributor orphaned a subscriber that registered after the pump completed but before disposal (Server-056) -> register paths now complete late registrants under _lifecycleLock; regression test added. The racy dashboard-mirror gRPC test made deterministic (Tests-039). Verified green locally: gateway Tests targeted classes (GatewaySession, SessionEventDistributor, GatewayOptionsValidator, ProtobufContractRoundTrip, GatewaySessionDashboardMirror) + dotnet/go/python/rust client suites.
484 lines
19 KiB
C#
484 lines
19 KiB
C#
using Microsoft.Extensions.Options;
|
|
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Tests.Configuration;
|
|
|
|
public sealed class GatewayOptionsValidatorTests
|
|
{
|
|
// Constructs the minimal valid GatewayOptions by relying on each sub-option's
|
|
// design-default values; those defaults are validated separately in GatewayOptionsTests.
|
|
private static GatewayOptions ValidOptions() => new();
|
|
|
|
// Returns enabled LDAP options that pass all checks except Port.
|
|
// The class defaults already satisfy the blank-field checks; we only
|
|
// override Enabled (must be true to exercise the port check) and Port.
|
|
private static LdapOptions LdapOptionsWithPort(int port) => new()
|
|
{
|
|
Enabled = true,
|
|
Port = port,
|
|
};
|
|
|
|
private static GatewayOptions CloneWithLdap(GatewayOptions source, LdapOptions ldap)
|
|
=> new()
|
|
{
|
|
Authentication = source.Authentication,
|
|
Ldap = ldap,
|
|
Worker = source.Worker,
|
|
Sessions = source.Sessions,
|
|
Events = source.Events,
|
|
Dashboard = source.Dashboard,
|
|
Protocol = source.Protocol,
|
|
Alarms = source.Alarms,
|
|
Tls = source.Tls,
|
|
};
|
|
|
|
private static GatewayOptions CloneWithTls(GatewayOptions source, TlsOptions tls)
|
|
=> new()
|
|
{
|
|
Authentication = source.Authentication,
|
|
Ldap = source.Ldap,
|
|
Worker = source.Worker,
|
|
Sessions = source.Sessions,
|
|
Events = source.Events,
|
|
Dashboard = source.Dashboard,
|
|
Protocol = source.Protocol,
|
|
Alarms = source.Alarms,
|
|
Tls = tls,
|
|
};
|
|
|
|
[Fact]
|
|
public void Validate_Succeeds_WithDefaultTlsOptions()
|
|
{
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, ValidOptions());
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Fails_WhenTlsValidityYearsOutOfRange()
|
|
{
|
|
GatewayOptions withBadTls = CloneWithTls(ValidOptions(), new TlsOptions { ValidityYears = 0 });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, withBadTls);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:ValidityYears"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Fails_WhenTlsValidityYearsTooLarge()
|
|
{
|
|
GatewayOptions withBadTls = CloneWithTls(ValidOptions(), new TlsOptions { ValidityYears = 101 });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, withBadTls);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:ValidityYears"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Fails_WhenAdditionalDnsNameBlank()
|
|
{
|
|
GatewayOptions options = CloneWithTls(ValidOptions(), new TlsOptions { AdditionalDnsNames = [" "] });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:AdditionalDnsNames"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Fails_WhenSelfSignedCertPathBlank()
|
|
{
|
|
GatewayOptions options = CloneWithTls(ValidOptions(), new TlsOptions { SelfSignedCertPath = " " });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:SelfSignedCertPath must not be blank."));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Fails_WhenLdapPortIsZero()
|
|
{
|
|
GatewayOptions options = CloneWithLdap(ValidOptions(), LdapOptionsWithPort(0));
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(
|
|
result.Failures!,
|
|
f => f.Contains("MxGateway:Ldap:Port must be between 1 and 65535 (was 0)"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Fails_WhenLdapPortExceedsMaximum()
|
|
{
|
|
GatewayOptions options = CloneWithLdap(ValidOptions(), LdapOptionsWithPort(70000));
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(
|
|
result.Failures!,
|
|
f => f.Contains("MxGateway:Ldap:Port must be between 1 and 65535 (was 70000)"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Succeeds_WhenLdapEnabledWithValidPort()
|
|
{
|
|
GatewayOptions options = CloneWithLdap(ValidOptions(), LdapOptionsWithPort(389));
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// AlarmFallbackOptions validation
|
|
// -------------------------------------------------------------------------
|
|
|
|
private static AlarmsOptions EnabledAlarmsWithFallback(AlarmFallbackOptions fallback) => new()
|
|
{
|
|
Enabled = true,
|
|
DefaultArea = "Galaxy",
|
|
Fallback = fallback,
|
|
};
|
|
|
|
private static GatewayOptions CloneWithAlarms(GatewayOptions source, AlarmsOptions alarms)
|
|
=> new()
|
|
{
|
|
Authentication = source.Authentication,
|
|
Ldap = source.Ldap,
|
|
Worker = source.Worker,
|
|
Sessions = source.Sessions,
|
|
Events = source.Events,
|
|
Dashboard = source.Dashboard,
|
|
Protocol = source.Protocol,
|
|
Alarms = alarms,
|
|
Tls = source.Tls,
|
|
};
|
|
|
|
[Fact]
|
|
public void Validate_Succeeds_WhenAlarmsDisabled_FallbackNotValidated()
|
|
{
|
|
// Even an invalid Mode is acceptable when Enabled = false.
|
|
GatewayOptions options = CloneWithAlarms(
|
|
ValidOptions(),
|
|
new AlarmsOptions
|
|
{
|
|
Enabled = false,
|
|
Fallback = new AlarmFallbackOptions { Mode = "InvalidMode" },
|
|
});
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Succeeds_WhenAlarmsEnabled_DefaultAutoConfig()
|
|
{
|
|
// Default AlarmFallbackOptions (Mode="Auto") must pass validation when alarms are enabled.
|
|
GatewayOptions options = CloneWithAlarms(
|
|
ValidOptions(),
|
|
EnabledAlarmsWithFallback(new AlarmFallbackOptions()));
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Auto")]
|
|
[InlineData("ForceAlarmManager")]
|
|
[InlineData("ForceSubtag")]
|
|
[InlineData("auto")]
|
|
[InlineData("FORCESUBTAG")]
|
|
public void Validate_Succeeds_WhenAlarmsEnabled_RecognisedMode(string mode)
|
|
{
|
|
AlarmsOptions alarms = EnabledAlarmsWithFallback(new AlarmFallbackOptions { Mode = mode });
|
|
GatewayOptions options = CloneWithAlarms(ValidOptions(), alarms);
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Fails_WhenAlarmsEnabled_InvalidMode()
|
|
{
|
|
GatewayOptions options = CloneWithAlarms(
|
|
ValidOptions(),
|
|
EnabledAlarmsWithFallback(new AlarmFallbackOptions { Mode = "InvalidMode" }));
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Alarms:Fallback") && f.Contains("Mode"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Fails_WhenForceSubtag_NoGalaxyRepository_NoIncludes()
|
|
{
|
|
// ForceSubtag without galaxy repository and without IncludeAttributes must fail.
|
|
GatewayOptions options = CloneWithAlarms(
|
|
ValidOptions(),
|
|
EnabledAlarmsWithFallback(new AlarmFallbackOptions
|
|
{
|
|
Mode = "ForceSubtag",
|
|
Discovery = new AlarmDiscoveryOptions
|
|
{
|
|
UseGalaxyRepository = false,
|
|
IncludeAttributes = [],
|
|
},
|
|
}));
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(
|
|
result.Failures!,
|
|
f => f.Contains("ForceSubtag") && f.Contains("Discovery"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Succeeds_WhenForceSubtag_NoGalaxyRepository_WithIncludes()
|
|
{
|
|
// ForceSubtag without galaxy repository is allowed when IncludeAttributes is non-empty.
|
|
GatewayOptions options = CloneWithAlarms(
|
|
ValidOptions(),
|
|
EnabledAlarmsWithFallback(new AlarmFallbackOptions
|
|
{
|
|
Mode = "ForceSubtag",
|
|
Discovery = new AlarmDiscoveryOptions
|
|
{
|
|
UseGalaxyRepository = false,
|
|
IncludeAttributes = ["attr1"],
|
|
},
|
|
}));
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Succeeds_WhenForceSubtag_WithGalaxyRepository()
|
|
{
|
|
// ForceSubtag + UseGalaxyRepository=true (default) must pass even without IncludeAttributes.
|
|
GatewayOptions options = CloneWithAlarms(
|
|
ValidOptions(),
|
|
EnabledAlarmsWithFallback(new AlarmFallbackOptions
|
|
{
|
|
Mode = "ForceSubtag",
|
|
Discovery = new AlarmDiscoveryOptions { UseGalaxyRepository = true },
|
|
}));
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, nameof(AlarmFallbackOptions.ConsecutiveFailureThreshold))]
|
|
[InlineData(-1, nameof(AlarmFallbackOptions.ConsecutiveFailureThreshold))]
|
|
public void Validate_Fails_WhenConsecutiveFailureThresholdBelowOne(int value, string keyPart)
|
|
{
|
|
GatewayOptions options = CloneWithAlarms(
|
|
ValidOptions(),
|
|
EnabledAlarmsWithFallback(new AlarmFallbackOptions { ConsecutiveFailureThreshold = value }));
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(result.Failures!, f => f.Contains(keyPart));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, nameof(AlarmFallbackOptions.FailbackProbeIntervalSeconds))]
|
|
[InlineData(-5, nameof(AlarmFallbackOptions.FailbackProbeIntervalSeconds))]
|
|
public void Validate_Fails_WhenFailbackProbeIntervalSecondsBelowOne(int value, string keyPart)
|
|
{
|
|
GatewayOptions options = CloneWithAlarms(
|
|
ValidOptions(),
|
|
EnabledAlarmsWithFallback(new AlarmFallbackOptions { FailbackProbeIntervalSeconds = value }));
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(result.Failures!, f => f.Contains(keyPart));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, nameof(AlarmFallbackOptions.FailbackStableProbes))]
|
|
[InlineData(-1, nameof(AlarmFallbackOptions.FailbackStableProbes))]
|
|
public void Validate_Fails_WhenFailbackStableProbesBelowOne(int value, string keyPart)
|
|
{
|
|
GatewayOptions options = CloneWithAlarms(
|
|
ValidOptions(),
|
|
EnabledAlarmsWithFallback(new AlarmFallbackOptions { FailbackStableProbes = value }));
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(result.Failures!, f => f.Contains(keyPart));
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// AllowMultipleEventSubscribers / MaxEventSubscribersPerSession validation
|
|
// -------------------------------------------------------------------------
|
|
|
|
private static GatewayOptions CloneWithSessions(GatewayOptions source, SessionOptions sessions)
|
|
=> new()
|
|
{
|
|
Authentication = source.Authentication,
|
|
Ldap = source.Ldap,
|
|
Worker = source.Worker,
|
|
Sessions = sessions,
|
|
Events = source.Events,
|
|
Dashboard = source.Dashboard,
|
|
Protocol = source.Protocol,
|
|
Alarms = source.Alarms,
|
|
Tls = source.Tls,
|
|
};
|
|
|
|
[Fact]
|
|
public void Validate_Succeeds_WhenAllowMultipleEventSubscribersIsTrue()
|
|
{
|
|
// AllowMultipleEventSubscribers=true must now validate cleanly (no longer rejected).
|
|
GatewayOptions options = CloneWithSessions(
|
|
ValidOptions(),
|
|
new SessionOptions { AllowMultipleEventSubscribers = true });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(-1)]
|
|
public void Validate_Fails_WhenMaxEventSubscribersPerSessionBelowOne(int value)
|
|
{
|
|
GatewayOptions options = CloneWithSessions(
|
|
ValidOptions(),
|
|
new SessionOptions { MaxEventSubscribersPerSession = value });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(
|
|
result.Failures!,
|
|
f => f.Contains("MxGateway:Sessions:MaxEventSubscribersPerSession"));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(8)]
|
|
[InlineData(32)]
|
|
public void Validate_Succeeds_WhenMaxEventSubscribersPerSessionIsPositive(int value)
|
|
{
|
|
GatewayOptions options = CloneWithSessions(
|
|
ValidOptions(),
|
|
new SessionOptions { MaxEventSubscribersPerSession = value });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Succeeds_WithDefaultSessionOptions()
|
|
{
|
|
// Default SessionOptions (AllowMultipleEventSubscribers=false, MaxEventSubscribersPerSession=8)
|
|
// must validate cleanly.
|
|
GatewayOptions options = CloneWithSessions(ValidOptions(), new SessionOptions());
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// WorkerReadyWaitTimeoutMs validation
|
|
// -------------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void Validate_Fails_WhenWorkerReadyWaitTimeoutMsIsNegative()
|
|
{
|
|
GatewayOptions options = CloneWithSessions(
|
|
ValidOptions(),
|
|
new SessionOptions { WorkerReadyWaitTimeoutMs = -1 });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(
|
|
result.Failures!,
|
|
f => f.Contains("MxGateway:Sessions:WorkerReadyWaitTimeoutMs"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Succeeds_WhenWorkerReadyWaitTimeoutMsIsZero()
|
|
{
|
|
GatewayOptions options = CloneWithSessions(
|
|
ValidOptions(),
|
|
new SessionOptions { WorkerReadyWaitTimeoutMs = 0 });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Succeeds_WhenWorkerReadyWaitTimeoutMsIsPositive()
|
|
{
|
|
GatewayOptions options = CloneWithSessions(
|
|
ValidOptions(),
|
|
new SessionOptions { WorkerReadyWaitTimeoutMs = 5000 });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Fails_WhenDetachGraceSecondsIsNegative()
|
|
{
|
|
GatewayOptions options = CloneWithSessions(
|
|
ValidOptions(),
|
|
new SessionOptions { DetachGraceSeconds = -1 });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(
|
|
result.Failures!,
|
|
f => f.Contains("MxGateway:Sessions:DetachGraceSeconds"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Succeeds_WhenDetachGraceSecondsIsZero()
|
|
{
|
|
GatewayOptions options = CloneWithSessions(
|
|
ValidOptions(),
|
|
new SessionOptions { DetachGraceSeconds = 0 });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// ReplayBufferCapacity / ReplayRetentionSeconds validation
|
|
// -------------------------------------------------------------------------
|
|
|
|
private static GatewayOptions CloneWithEvents(GatewayOptions source, EventOptions events)
|
|
=> new()
|
|
{
|
|
Authentication = source.Authentication,
|
|
Ldap = source.Ldap,
|
|
Worker = source.Worker,
|
|
Sessions = source.Sessions,
|
|
Events = events,
|
|
Dashboard = source.Dashboard,
|
|
Protocol = source.Protocol,
|
|
Alarms = source.Alarms,
|
|
Tls = source.Tls,
|
|
};
|
|
|
|
[Fact]
|
|
public void Validate_Fails_WhenReplayBufferCapacityIsNegative()
|
|
{
|
|
GatewayOptions options = CloneWithEvents(
|
|
ValidOptions(),
|
|
new EventOptions { ReplayBufferCapacity = -1 });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(
|
|
result.Failures!,
|
|
f => f.Contains("MxGateway:Events:ReplayBufferCapacity"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Succeeds_WhenReplayBufferCapacityIsZero()
|
|
{
|
|
GatewayOptions options = CloneWithEvents(
|
|
ValidOptions(),
|
|
new EventOptions { ReplayBufferCapacity = 0 });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Fails_WhenReplayRetentionSecondsIsNegative()
|
|
{
|
|
GatewayOptions options = CloneWithEvents(
|
|
ValidOptions(),
|
|
new EventOptions { ReplayRetentionSeconds = -1 });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Failed);
|
|
Assert.Contains(
|
|
result.Failures!,
|
|
f => f.Contains("MxGateway:Events:ReplayRetentionSeconds"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Succeeds_WhenReplayRetentionSecondsIsZero()
|
|
{
|
|
GatewayOptions options = CloneWithEvents(
|
|
ValidOptions(),
|
|
new EventOptions { ReplayRetentionSeconds = 0 });
|
|
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
|
Assert.True(result.Succeeded);
|
|
}
|
|
}
|