d630a7e267
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
169 lines
5.2 KiB
C#
169 lines
5.2 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Options;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests;
|
|
|
|
/// <summary>
|
|
/// A <c>FetchAndCache</c> node with no endpoint or no key does not error — it simply never
|
|
/// applies a deployment, and the deploy times out naming a node that is otherwise healthy. These
|
|
/// tests make that misconfiguration a loud host-start failure instead.
|
|
/// </summary>
|
|
public class ConfigSourceOptionsValidatorTests
|
|
{
|
|
private static ValidateOptionsResult Validate(ConfigSourceOptions o, params string[] roles)
|
|
{
|
|
var pairs = new Dictionary<string, string?>();
|
|
for (var i = 0; i < roles.Length; i++)
|
|
{
|
|
pairs[$"Cluster:Roles:{i}"] = roles[i];
|
|
}
|
|
|
|
var configuration = new ConfigurationBuilder().AddInMemoryCollection(pairs).Build();
|
|
|
|
return new ConfigSourceOptionsValidator(configuration).Validate(ConfigSourceOptions.SectionName, o);
|
|
}
|
|
|
|
private static ConfigSourceOptions ValidFetch() => new()
|
|
{
|
|
Mode = ConfigSourceOptions.ModeFetchAndCache,
|
|
CentralFetchEndpoints = ["http://central-1:4055", "http://central-2:4055"],
|
|
ApiKey = "shared-key",
|
|
FetchTimeoutSeconds = 30,
|
|
};
|
|
|
|
[Fact]
|
|
public void Default_options_are_valid()
|
|
{
|
|
// The default is Direct with everything empty — the pre-Phase-3 behaviour. A node that never
|
|
// sets this section must validate, which is the premise of the dark switch.
|
|
Validate(new ConfigSourceOptions()).Succeeded.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Unknown_mode_fails()
|
|
{
|
|
var result = Validate(new ConfigSourceOptions { Mode = "cache" });
|
|
|
|
result.Failed.ShouldBeTrue();
|
|
result.FailureMessage.ShouldContain("cache");
|
|
}
|
|
|
|
[Fact]
|
|
public void Mode_matching_is_case_insensitive()
|
|
{
|
|
Validate(new ConfigSourceOptions
|
|
{
|
|
Mode = "fetchandcache",
|
|
CentralFetchEndpoints = ["http://central-1:4055"],
|
|
ApiKey = "k",
|
|
}).Succeeded.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void FetchAndCache_with_no_endpoints_fails()
|
|
{
|
|
var o = ValidFetch();
|
|
o.CentralFetchEndpoints = [];
|
|
|
|
var result = Validate(o);
|
|
|
|
result.Failed.ShouldBeTrue();
|
|
result.FailureMessage.ShouldContain(nameof(ConfigSourceOptions.CentralFetchEndpoints));
|
|
}
|
|
|
|
[Fact]
|
|
public void FetchAndCache_with_a_non_http_endpoint_fails()
|
|
{
|
|
var o = ValidFetch();
|
|
o.CentralFetchEndpoints = ["central-1:4055"];
|
|
|
|
var result = Validate(o);
|
|
|
|
result.Failed.ShouldBeTrue();
|
|
result.FailureMessage.ShouldContain("central-1:4055");
|
|
}
|
|
|
|
[Fact]
|
|
public void FetchAndCache_with_an_empty_key_fails()
|
|
{
|
|
var o = ValidFetch();
|
|
o.ApiKey = "";
|
|
|
|
var result = Validate(o);
|
|
|
|
result.Failed.ShouldBeTrue();
|
|
result.FailureMessage.ShouldContain(nameof(ConfigSourceOptions.ApiKey));
|
|
}
|
|
|
|
[Fact]
|
|
public void FetchAndCache_with_a_non_positive_timeout_fails()
|
|
{
|
|
var o = ValidFetch();
|
|
o.FetchTimeoutSeconds = 0;
|
|
|
|
var result = Validate(o);
|
|
|
|
result.Failed.ShouldBeTrue();
|
|
result.FailureMessage.ShouldContain(nameof(ConfigSourceOptions.FetchTimeoutSeconds));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_fully_populated_FetchAndCache_config_is_valid()
|
|
{
|
|
Validate(ValidFetch()).Succeeded.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Direct_ignores_the_empty_fetch_surface()
|
|
{
|
|
// Under Direct the endpoints/key are never read, so their emptiness must not fail validation —
|
|
// otherwise every admin-only and every Direct node would be forced to carry fetch config.
|
|
Validate(new ConfigSourceOptions
|
|
{
|
|
Mode = ConfigSourceOptions.ModeDirect,
|
|
CentralFetchEndpoints = [],
|
|
ApiKey = "",
|
|
FetchTimeoutSeconds = 0,
|
|
}).Succeeded.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Driver_only_node_on_Direct_fails()
|
|
{
|
|
// A driver-only node has no central ConfigDb connection (Phase 4) — Direct silently produces a
|
|
// node that can never read its configuration.
|
|
var result = Validate(
|
|
new ConfigSourceOptions { Mode = ConfigSourceOptions.ModeDirect },
|
|
"driver");
|
|
|
|
result.Failed.ShouldBeTrue();
|
|
result.FailureMessage.ShouldContain(ConfigSourceOptions.ModeFetchAndCache);
|
|
}
|
|
|
|
[Fact]
|
|
public void Fused_admin_and_driver_node_on_Direct_is_valid()
|
|
{
|
|
// A fused node holds the ConfigDb connection via its admin role, so Direct is still legitimate.
|
|
Validate(
|
|
new ConfigSourceOptions { Mode = ConfigSourceOptions.ModeDirect },
|
|
"admin", "driver").Succeeded.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Driver_only_node_on_FetchAndCache_is_valid()
|
|
{
|
|
Validate(ValidFetch(), "driver").Succeeded.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Admin_only_node_on_Direct_is_valid()
|
|
{
|
|
// An admin-only node has no driver role at all, so the driver-only rule does not apply to it.
|
|
Validate(
|
|
new ConfigSourceOptions { Mode = ConfigSourceOptions.ModeDirect },
|
|
"admin").Succeeded.ShouldBeTrue();
|
|
}
|
|
}
|