255 lines
8.6 KiB
C#
255 lines
8.6 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests;
|
|
|
|
/// <summary>
|
|
/// <see cref="EffectiveRoles"/> is the single role authority (archreview R3-04, 01/S-12 = 03/C5,
|
|
/// 01/C-9): one factory, one precedence (<c>Cluster:Roles</c> non-empty wins, else
|
|
/// <c>OTOPCUA_ROLES</c>, else empty), two-layer normalization (plain roles lower-cased, a cluster
|
|
/// role's ClusterId suffix preserved verbatim), and validation on BOTH sources.
|
|
/// </summary>
|
|
[Collection(RoleEnvironmentCollection.Name)]
|
|
public sealed class EffectiveRolesTests
|
|
{
|
|
// ---- Precedence matrix -------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void Configured_only_resolves_from_Configured_source()
|
|
{
|
|
var roles = EffectiveRoles.Resolve(["admin", "driver"], null);
|
|
|
|
roles.Source.ShouldBe(RoleSource.Configured);
|
|
roles.Gossiped.ShouldBe(new[] { "admin", "driver" });
|
|
}
|
|
|
|
[Fact]
|
|
public void Environment_only_resolves_from_Environment_source()
|
|
{
|
|
var roles = EffectiveRoles.Resolve(null, "admin,driver");
|
|
|
|
roles.Source.ShouldBe(RoleSource.Environment);
|
|
roles.Gossiped.ShouldBe(new[] { "admin", "driver" });
|
|
}
|
|
|
|
[Fact]
|
|
public void Both_agree_resolves_from_Configured_source()
|
|
{
|
|
var roles = EffectiveRoles.Resolve(["driver"], "driver");
|
|
|
|
roles.Source.ShouldBe(RoleSource.Configured);
|
|
roles.Gossiped.ShouldBe(new[] { "driver" });
|
|
}
|
|
|
|
[Fact]
|
|
public void Both_differ_Configured_wins_outright()
|
|
{
|
|
var roles = EffectiveRoles.Resolve(["admin"], "driver,cluster-SITE-A");
|
|
|
|
roles.Source.ShouldBe(RoleSource.Configured);
|
|
roles.Gossiped.ShouldBe(new[] { "admin" });
|
|
}
|
|
|
|
[Fact]
|
|
public void Neither_source_resolves_empty_with_None_source()
|
|
{
|
|
var roles = EffectiveRoles.Resolve(null, null);
|
|
|
|
roles.Source.ShouldBe(RoleSource.None);
|
|
roles.Gossiped.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Empty_configured_array_falls_back_to_the_environment()
|
|
{
|
|
var roles = EffectiveRoles.Resolve([], "driver");
|
|
|
|
roles.Source.ShouldBe(RoleSource.Environment);
|
|
roles.Gossiped.ShouldBe(new[] { "driver" });
|
|
}
|
|
|
|
// ---- Gossiped normalization: two-layer (choice 2) ----------------------------------------
|
|
|
|
[Fact]
|
|
public void Configured_plain_role_is_trimmed_and_lower_cased_in_Gossiped()
|
|
{
|
|
var roles = EffectiveRoles.Resolve([" Driver "], null);
|
|
|
|
roles.Gossiped.ShouldBe(new[] { "driver" });
|
|
}
|
|
|
|
/// <summary>
|
|
/// THE falsification control for choice 2: a cluster role's ClusterId suffix must survive
|
|
/// UNCHANGED — lower-casing it would rename the Akka role of a running deployment
|
|
/// (<c>cluster-SITE-A</c> vs <c>cluster-site-a</c> are different gossiped roles).
|
|
/// </summary>
|
|
[Fact]
|
|
public void Configured_cluster_role_suffix_case_is_preserved_in_Gossiped()
|
|
{
|
|
var roles = EffectiveRoles.Resolve(["Cluster-SITE-A"], null);
|
|
|
|
// The "cluster-" prefix normalizes (it is a plain marker), the "SITE-A" suffix does not.
|
|
roles.Gossiped.ShouldBe(new[] { "cluster-SITE-A" });
|
|
}
|
|
|
|
[Fact]
|
|
public void Configured_cluster_role_with_already_lowercase_prefix_keeps_suffix_verbatim()
|
|
{
|
|
var roles = EffectiveRoles.Resolve(["cluster-Site-A"], null);
|
|
|
|
roles.Gossiped.ShouldBe(new[] { "cluster-Site-A" });
|
|
}
|
|
|
|
[Fact]
|
|
public void Environment_cluster_role_suffix_IS_lowercased_unlike_Configured()
|
|
{
|
|
// Unchanged from RoleParser.Parse's existing (documented) behaviour: the two sources
|
|
// normalize differently on purpose (RoleParser.ClusterIdFromRole's remarks).
|
|
var roles = EffectiveRoles.Resolve(null, "driver,cluster-SITE-A");
|
|
|
|
roles.Gossiped.ShouldBe(new[] { "driver", "cluster-site-a" });
|
|
}
|
|
|
|
// ---- Has*/HasAdmin/HasDriver case-insensitivity ------------------------------------------
|
|
|
|
[Theory]
|
|
[InlineData("driver")]
|
|
[InlineData("Driver")]
|
|
[InlineData("DRIVER")]
|
|
[InlineData(" driver ")]
|
|
public void HasDriver_true_for_every_casing(string configuredRole)
|
|
{
|
|
EffectiveRoles.Resolve([configuredRole], null).HasDriver.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void HasAdmin_false_when_admin_is_not_present()
|
|
{
|
|
EffectiveRoles.Resolve(["driver"], null).HasAdmin.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Has_is_case_insensitive_for_an_arbitrary_role()
|
|
{
|
|
var roles = EffectiveRoles.Resolve(["Admin", "Driver"], null);
|
|
|
|
roles.Has("admin").ShouldBeTrue();
|
|
roles.Has("ADMIN").ShouldBeTrue();
|
|
roles.Has("driver").ShouldBeTrue();
|
|
roles.Has("dev").ShouldBeFalse();
|
|
}
|
|
|
|
// ---- Unknown / invalid role from EITHER source throws, naming the key --------------------
|
|
|
|
[Fact]
|
|
public void Unknown_role_from_the_configured_source_throws_naming_the_key()
|
|
{
|
|
var ex = Should.Throw<ArgumentException>(() => EffectiveRoles.Resolve(["Drivr"], null));
|
|
|
|
ex.Message.ShouldContain("Cluster:Roles");
|
|
ex.Message.ShouldContain("Drivr");
|
|
}
|
|
|
|
[Fact]
|
|
public void Unknown_role_from_the_environment_source_throws()
|
|
{
|
|
// The env path already validated via RoleParser.Parse — this pins that it still does.
|
|
Should.Throw<ArgumentException>(() => EffectiveRoles.Resolve(null, "admin,master"));
|
|
}
|
|
|
|
// ---- ClusterRole / ClusterId ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void ClusterRole_and_ClusterId_are_null_for_a_legacy_node()
|
|
{
|
|
var roles = EffectiveRoles.Resolve(["admin", "driver"], null);
|
|
|
|
roles.ClusterRole.ShouldBeNull();
|
|
roles.ClusterId.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void ClusterRole_and_ClusterId_resolve_for_a_split_topology_node()
|
|
{
|
|
var roles = EffectiveRoles.Resolve(["driver", "cluster-SITE-A"], null);
|
|
|
|
roles.ClusterRole.ShouldBe("cluster-SITE-A");
|
|
roles.ClusterId.ShouldBe("SITE-A");
|
|
}
|
|
|
|
// ---- Resolve(IConfiguration) overload ------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void Resolve_from_configuration_reads_Cluster_Roles()
|
|
{
|
|
var configuration = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["Cluster:Roles:0"] = "driver",
|
|
["Cluster:Roles:1"] = "cluster-SITE-A",
|
|
})
|
|
.Build();
|
|
|
|
var roles = EffectiveRoles.Resolve(configuration);
|
|
|
|
roles.Source.ShouldBe(RoleSource.Configured);
|
|
roles.Gossiped.ShouldBe(new[] { "driver", "cluster-SITE-A" });
|
|
}
|
|
|
|
[Fact]
|
|
public void Resolve_from_configuration_falls_back_to_the_environment()
|
|
{
|
|
using (new EnvVarScope(RoleParser.RolesEnvVar, "admin,driver"))
|
|
{
|
|
var configuration = new ConfigurationBuilder().Build();
|
|
|
|
var roles = EffectiveRoles.Resolve(configuration);
|
|
|
|
roles.Source.ShouldBe(RoleSource.Environment);
|
|
roles.Gossiped.ShouldBe(new[] { "admin", "driver" });
|
|
}
|
|
}
|
|
|
|
// ---- DisagreementWarning (choice 5, T6) ----------------------------------------------------
|
|
|
|
[Fact]
|
|
public void Both_sources_set_and_agreeing_has_no_warning()
|
|
{
|
|
var roles = EffectiveRoles.Resolve(["driver"], "driver");
|
|
|
|
roles.DisagreementWarning.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Only_one_source_set_has_no_warning()
|
|
{
|
|
EffectiveRoles.Resolve(["driver"], null).DisagreementWarning.ShouldBeNull();
|
|
EffectiveRoles.Resolve(null, "driver").DisagreementWarning.ShouldBeNull();
|
|
EffectiveRoles.Resolve(null, null).DisagreementWarning.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Both_sources_set_and_disagreeing_produces_a_warning_naming_both_keys()
|
|
{
|
|
var roles = EffectiveRoles.Resolve(["admin"], "driver,cluster-SITE-A");
|
|
|
|
roles.DisagreementWarning.ShouldNotBeNull();
|
|
roles.DisagreementWarning.ShouldContain("Cluster:Roles");
|
|
roles.DisagreementWarning.ShouldContain(RoleParser.RolesEnvVar);
|
|
// Configured still wins outright — the warning is observability, not a behaviour change.
|
|
roles.Gossiped.ShouldBe(new[] { "admin" });
|
|
roles.Source.ShouldBe(RoleSource.Configured);
|
|
}
|
|
|
|
[Fact]
|
|
public void Both_sources_set_but_only_differing_by_case_do_not_warn()
|
|
{
|
|
// Same normalized set spelled differently in each source is NOT the "Cluster:Roles wins
|
|
// unintentionally" shape the warning exists for.
|
|
var roles = EffectiveRoles.Resolve(["Driver"], "driver");
|
|
|
|
roles.DisagreementWarning.ShouldBeNull();
|
|
}
|
|
}
|