feat(mesh): ConfigSource/ConfigServe options + validator (Phase 3 dark switch)
ConfigSource:Mode = Direct (default, read central SQL) | FetchAndCache (fetch the artifact from central over gRPC, read LocalDb). The validator fails host start on a FetchAndCache shape that cannot fetch — no endpoints, a non-http(s) endpoint, no shared key, or a non-positive timeout — because each otherwise surfaces as a silent absence (a deploy that never applies). ConfigServe (the central serve surface) takes a plain Configure: a 0 port is a valid "disabled". Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
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) =>
|
||||
new ConfigSourceOptionsValidator().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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user