128 lines
5.3 KiB
C#
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);
|
|
}
|
|
}
|