using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests;
///
/// A FetchAndCache 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.
///
public class ConfigSourceOptionsValidatorTests
{
private static ValidateOptionsResult Validate(ConfigSourceOptions o, params string[] roles)
{
var pairs = new Dictionary();
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();
}
}