using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using ZB.MOM.WW.Secrets.Replicator.Grpc.DependencyInjection; namespace ZB.MOM.WW.Secrets.Replicator.Grpc.Tests; /// /// Registration-time validation for the hub server. A misconfigured hub must fail at startup with /// a message naming the exact configuration key, not serve unauthenticated at runtime. /// public sealed class SecretsGrpcHubServerOptionsTests { [Theory] [InlineData("")] [InlineData(" ")] [InlineData("\t\n")] public void Validate_throws_when_the_bearer_token_is_unset(string token) { var options = new SecretsGrpcHubServerOptions { BearerToken = token }; InvalidOperationException error = Assert.Throws(() => options.Validate()); Assert.Contains("Secrets:GrpcHub:BearerToken", error.Message, StringComparison.Ordinal); } [Fact] public void Validate_names_the_section_path_the_hub_was_registered_under() { var options = new SecretsGrpcHubServerOptions { BearerToken = string.Empty }; options.SectionPath = "Central:Secrets:Hub"; InvalidOperationException error = Assert.Throws(() => options.Validate()); Assert.Contains("Central:Secrets:Hub:BearerToken", error.Message, StringComparison.Ordinal); } [Fact] public void Validate_accepts_a_configured_token() { var options = new SecretsGrpcHubServerOptions { BearerToken = "a-configured-hub-token" }; options.Validate(); } [Fact] public void Validate_never_echoes_the_configured_token() { // A token that is present but only whitespace-padded is still a real credential; the // failure message must never carry it. const string token = " "; var options = new SecretsGrpcHubServerOptions { BearerToken = token }; InvalidOperationException error = Assert.Throws(() => options.Validate()); Assert.DoesNotContain($"'{token}'", error.Message, StringComparison.Ordinal); Assert.DoesNotContain("was ", error.Message, StringComparison.Ordinal); } [Fact] public void Bearer_token_defaults_to_empty_so_an_unconfigured_hub_cannot_serve() { var options = new SecretsGrpcHubServerOptions(); Assert.Equal(string.Empty, options.BearerToken); Assert.Throws(() => options.Validate()); } [Fact] public void Section_path_defaults_to_the_documented_convention() { var options = new SecretsGrpcHubServerOptions(); Assert.Equal("Secrets:GrpcHub", options.SectionPath); } [Fact] public void Max_names_per_request_defaults_above_the_followers_own_fetch_chunk() { // The follower chunks at 500; the cap sits above that so a compliant sweep never meets it, // and meeting it therefore means something. var options = new SecretsGrpcHubServerOptions(); Assert.Equal(1000, options.MaxNamesPerRequest); } [Theory] [InlineData(0)] [InlineData(-1)] [InlineData(int.MinValue)] public void Validate_rejects_a_nonpositive_request_cap(int cap) { var options = new SecretsGrpcHubServerOptions { BearerToken = "a-configured-hub-token", MaxNamesPerRequest = cap, }; InvalidOperationException error = Assert.Throws(() => options.Validate()); Assert.Contains("Secrets:GrpcHub:MaxNamesPerRequest", error.Message, StringComparison.Ordinal); } [Fact] public void Validate_accepts_a_cap_of_one() { var options = new SecretsGrpcHubServerOptions { BearerToken = "a-configured-hub-token", MaxNamesPerRequest = 1, }; options.Validate(); } [Fact] public void Validate_names_the_section_path_on_the_request_cap_too() { var options = new SecretsGrpcHubServerOptions { BearerToken = "a-configured-hub-token", MaxNamesPerRequest = 0, }; options.SectionPath = "Central:Secrets:Hub"; InvalidOperationException error = Assert.Throws(() => options.Validate()); Assert.Contains("Central:Secrets:Hub:MaxNamesPerRequest", error.Message, StringComparison.Ordinal); } } /// /// The registration extension itself, driven through a real and a /// real . What is pinned here is the coherence of what a host ends up /// holding: the token it configured, bound from the section it named, reported under that same /// section path — and the refusal of a second registration, which cannot deliver that coherence. /// public sealed class SecretsGrpcHubServerExtensionsTests { private const string Token = "a-configured-hub-token"; [Fact] public void Registration_binds_the_token_and_stamps_the_default_section_path() { var services = new ServiceCollection(); services.AddZbSecretsGrpcHub(Configuration(("Secrets:GrpcHub:BearerToken", Token))); SecretsGrpcHubServerOptions options = Resolve(services); Assert.Equal(Token, options.BearerToken); Assert.Equal("Secrets:GrpcHub", options.SectionPath); Assert.Equal(1000, options.MaxNamesPerRequest); } [Fact] public void Registration_binds_the_request_cap_when_configured() { var services = new ServiceCollection(); services.AddZbSecretsGrpcHub(Configuration( ("Secrets:GrpcHub:BearerToken", Token), ("Secrets:GrpcHub:MaxNamesPerRequest", "250"))); Assert.Equal(250, Resolve(services).MaxNamesPerRequest); } [Fact] public void Registration_under_a_custom_section_path_binds_and_stamps_that_path() { var services = new ServiceCollection(); services.AddZbSecretsGrpcHub( Configuration(("Central:Secrets:Hub:BearerToken", Token)), "Central:Secrets:Hub"); SecretsGrpcHubServerOptions options = Resolve(services); Assert.Equal(Token, options.BearerToken); Assert.Equal("Central:Secrets:Hub", options.SectionPath); } [Fact] public void Registration_under_a_custom_section_path_names_that_path_when_the_token_is_missing() { // End to end: the operator is told the key THIS host reads, not the library's default. var services = new ServiceCollection(); InvalidOperationException error = Assert.Throws( () => services.AddZbSecretsGrpcHub( Configuration(("Secrets:GrpcHub:BearerToken", Token)), "Central:Secrets:Hub")); Assert.Contains("Central:Secrets:Hub:BearerToken", error.Message, StringComparison.Ordinal); } [Fact] public void Registration_without_a_token_throws_naming_the_exact_key() { var services = new ServiceCollection(); InvalidOperationException error = Assert.Throws( () => services.AddZbSecretsGrpcHub(Configuration())); Assert.Contains("Secrets:GrpcHub:BearerToken", error.Message, StringComparison.Ordinal); } [Fact] public void Registration_with_a_nonpositive_request_cap_throws() { var services = new ServiceCollection(); InvalidOperationException error = Assert.Throws( () => services.AddZbSecretsGrpcHub(Configuration( ("Secrets:GrpcHub:BearerToken", Token), ("Secrets:GrpcHub:MaxNamesPerRequest", "0")))); Assert.Contains("Secrets:GrpcHub:MaxNamesPerRequest", error.Message, StringComparison.Ordinal); } [Fact] public void Registering_the_hub_twice_throws() { var services = new ServiceCollection(); IConfiguration configuration = Configuration(("Secrets:GrpcHub:BearerToken", Token)); services.AddZbSecretsGrpcHub(configuration); InvalidOperationException error = Assert.Throws( () => services.AddZbSecretsGrpcHub(configuration)); Assert.Contains("once", error.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public void Registering_the_hub_twice_under_different_sections_throws_rather_than_merging() { // This is the case the guard exists for: binding MERGES (the second section's absent // BearerToken leaves the first one's standing) while the SectionPath stamp is last-one-wins, // so the surviving options would report a section that did not supply the token they hold. var services = new ServiceCollection(); IConfiguration configuration = Configuration( ("Secrets:GrpcHub:BearerToken", Token), ("Central:Secrets:Hub:MaxNamesPerRequest", "7")); services.AddZbSecretsGrpcHub(configuration); Assert.Throws( () => services.AddZbSecretsGrpcHub(configuration, "Central:Secrets:Hub")); SecretsGrpcHubServerOptions options = Resolve(services); Assert.Equal("Secrets:GrpcHub", options.SectionPath); Assert.Equal(Token, options.BearerToken); Assert.Equal(1000, options.MaxNamesPerRequest); } private static IConfiguration Configuration(params (string Key, string Value)[] entries) => new ConfigurationBuilder() .AddInMemoryCollection(entries.Select(e => new KeyValuePair(e.Key, e.Value))) .Build(); private static SecretsGrpcHubServerOptions Resolve(IServiceCollection services) { using ServiceProvider provider = services.BuildServiceProvider(); return provider.GetRequiredService>().Value; } }