Files
lmxopcua/tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests/AkkaClusterOptionsRoleFallbackTests.cs
T

128 lines
5.3 KiB
C#

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests;
/// <summary>
/// Gitea #532 — <c>AkkaClusterOptions.Roles</c> must actually apply the <c>OTOPCUA_ROLES</c> fallback
/// its own XML doc has always described.
/// </summary>
/// <remarks>
/// <para>
/// These go through <c>AddOtOpcUaCluster</c> + a real service provider rather than calling
/// <c>RoleParser.ResolveEffectiveRoles</c> directly, because the defect was never in the parsing —
/// it was that nothing <em>called</em> it on the way to the bound options. A unit test of the
/// helper alone would have passed against the broken tree.
/// </para>
/// <para>
/// The consequence being pinned: <c>BuildClusterOptions</c> feeds <c>Roles</c> straight to Akka as
/// the node's gossiped member roles. Empty there means a node that joins, comes Up, serves OPC UA
/// — and is invisible to <c>RoleLeader</c>, to <c>MembersWithRole("driver")</c>, to every
/// role-scoped singleton, and to <c>ConfigPublishCoordinator</c>'s expected-ack set. The field
/// symptom was "no driver-role nodes are Up" with nothing wrong in the node's own logs
/// (<c>docs/deployments/wonder-app-vd03-makino-z-34184.md</c>).
/// </para>
/// </remarks>
[Collection(RoleEnvironmentCollection.Name)]
public class AkkaClusterOptionsRoleFallbackTests
{
private static AkkaClusterOptions Bind(params (string Key, string Value)[] settings)
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(settings.ToDictionary(s => s.Key, s => (string?)s.Value))
.Build();
var services = new ServiceCollection();
services.AddLogging();
services.AddOtOpcUaCluster(configuration);
return services.BuildServiceProvider().GetRequiredService<IOptions<AkkaClusterOptions>>().Value;
}
[Fact]
public void Roles_fall_back_to_the_environment_when_Cluster_Roles_is_empty()
{
using (new EnvVarScope(RoleParser.RolesEnvVar, "admin,driver"))
{
Bind().Roles.ShouldBe(new[] { "admin", "driver" });
}
}
[Fact]
public void Akka_member_roles_carry_the_fallback()
{
// The end that matters. This is the value handed to Akka.Cluster.Hosting, so an empty array here
// is a role-less member — the exact production shape #532 describes.
using (new EnvVarScope(RoleParser.RolesEnvVar, "driver"))
{
ServiceCollectionExtensions.BuildClusterOptions(Bind()).Roles.ShouldBe(new[] { "driver" });
}
}
[Fact]
public void Configured_roles_win_over_the_environment()
{
// Negative control for the fallback: without this, a PostConfigure that unconditionally
// overwrote Roles from the environment would pass the test above and silently ignore every
// deployment's explicit Cluster:Roles.
using (new EnvVarScope(RoleParser.RolesEnvVar, "admin,driver,cluster-SITE-B"))
{
Bind(("Cluster:Roles:0", "driver"), ("Cluster:Roles:1", "cluster-SITE-A")).Roles
.ShouldBe(new[] { "driver", "cluster-SITE-A" });
}
}
[Fact]
public void No_roles_from_either_source_stays_empty()
{
using (new EnvVarScope(RoleParser.RolesEnvVar, null))
{
Bind().Roles.ShouldBeEmpty();
}
}
[Fact]
public void The_cluster_role_from_the_environment_reaches_the_ClusterId_derivation()
{
// The over-grant fix in ConfigDbApiKeyStore reads its ClusterId off these options, and a null
// ClusterId applies no cluster filter at all — so an env-only node failed OPEN before #532.
using (new EnvVarScope(RoleParser.RolesEnvVar, "driver,cluster-SITE-A"))
{
RoleParser.ClusterIdFromRoles(Bind().Roles).ShouldBe("site-a");
}
}
/// <summary>
/// R3-04 (structural identity, choice 3): <c>AkkaClusterOptions.Roles</c> is not a SECOND
/// derivation of the role rule — it is the registered <see cref="EffectiveRoles"/> instance's
/// <see cref="EffectiveRoles.Gossiped"/> list, verbatim. A mixed-case configured role proves
/// both the normalization (plain lowered, cluster suffix preserved) and the identity: the two
/// views can never disagree because there is only one derivation.
/// </summary>
[Fact]
public void Bound_options_Roles_IS_the_registered_EffectiveRoles_Gossiped_list()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Cluster:Roles:0"] = "Driver",
["Cluster:Roles:1"] = "Cluster-SITE-A",
})
.Build();
var services = new ServiceCollection();
services.AddLogging();
services.AddOtOpcUaCluster(configuration);
var provider = services.BuildServiceProvider();
var bound = provider.GetRequiredService<IOptions<AkkaClusterOptions>>().Value;
var effectiveRoles = provider.GetRequiredService<EffectiveRoles>();
bound.Roles.ShouldBe(new[] { "driver", "cluster-SITE-A" });
bound.Roles.ShouldBe(effectiveRoles.Gossiped);
}
}