using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests;
///
/// Gitea #532 — AkkaClusterOptions.Roles must actually apply the OTOPCUA_ROLES fallback
/// its own XML doc has always described.
///
///
///
/// These go through AddOtOpcUaCluster + a real service provider rather than calling
/// RoleParser.ResolveEffectiveRoles directly, because the defect was never in the parsing —
/// it was that nothing called it on the way to the bound options. A unit test of the
/// helper alone would have passed against the broken tree.
///
///
/// The consequence being pinned: BuildClusterOptions feeds Roles 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 RoleLeader, to MembersWithRole("driver"), to every
/// role-scoped singleton, and to ConfigPublishCoordinator's expected-ack set. The field
/// symptom was "no driver-role nodes are Up" with nothing wrong in the node's own logs
/// (docs/deployments/wonder-app-vd03-makino-z-34184.md).
///
///
[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>().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");
}
}
}