using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using ZB.MOM.WW.Auth.Abstractions.Ldap; using ZB.MOM.WW.Auth.AspNetCore; using ZB.MOM.WW.Auth.Ldap; namespace ZB.MOM.WW.Auth.AspNetCore.Tests; public class ServiceCollectionExtensionsTests { private const string LdapSection = "Auth:Ldap"; private const string LdapServer = "ldap.example.com"; private static IConfiguration BuildConfiguration() => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { [$"{LdapSection}:Server"] = LdapServer, [$"{LdapSection}:SearchBase"] = "dc=example,dc=com", [$"{LdapSection}:ServiceAccountDn"] = "cn=svc,dc=example,dc=com", [$"{LdapSection}:Transport"] = nameof(LdapTransport.Ldaps), }) .Build(); [Fact] public void AddZbLdapAuth_ResolvesLdapAuthService() { IConfiguration config = BuildConfiguration(); var services = new ServiceCollection(); services.AddZbLdapAuth(config, LdapSection); using ServiceProvider provider = services.BuildServiceProvider(); var service = provider.GetRequiredService(); Assert.NotNull(service); } [Fact] public void AddZbLdapAuth_ILdapAuthService_IsSingleton() { IConfiguration config = BuildConfiguration(); var services = new ServiceCollection(); services.AddZbLdapAuth(config, LdapSection); using ServiceProvider provider = services.BuildServiceProvider(); var first = provider.GetRequiredService(); var second = provider.GetRequiredService(); Assert.Same(first, second); } [Fact] public void AddZbLdapAuth_BindsOptionsFromSection() { IConfiguration config = BuildConfiguration(); var services = new ServiceCollection(); services.AddZbLdapAuth(config, LdapSection); using ServiceProvider provider = services.BuildServiceProvider(); var options = provider.GetRequiredService>(); Assert.Equal(LdapServer, options.Value.Server); Assert.Equal("dc=example,dc=com", options.Value.SearchBase); Assert.Equal(LdapTransport.Ldaps, options.Value.Transport); } [Fact] public void AddZbLdapAuth_RegistersOptionsValidator() { IConfiguration config = BuildConfiguration(); var services = new ServiceCollection(); services.AddZbLdapAuth(config, LdapSection); using ServiceProvider provider = services.BuildServiceProvider(); var validators = provider.GetServices>().ToList(); Assert.Contains(validators, v => v is LdapOptionsValidator); } // --- Auth-001: ValidateOnStart must run options validation at host startup, not first login --- private static IConfiguration BuildInsecureConfiguration() => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { [$"{LdapSection}:Server"] = LdapServer, [$"{LdapSection}:SearchBase"] = "dc=example,dc=com", [$"{LdapSection}:ServiceAccountDn"] = "cn=svc,dc=example,dc=com", // Plaintext transport without AllowInsecure: the validator must reject this. [$"{LdapSection}:Transport"] = nameof(LdapTransport.None), [$"{LdapSection}:AllowInsecure"] = "false", }) .Build(); [Fact] public async Task AddZbLdapAuth_StartingHost_FailsForInsecureConfig() { // The misconfiguration must surface at host start, not deferred until the first login // (i.e. the first ILdapAuthService resolution). ValidateOnStart wires the host's // start-time options validation, so StartAsync must throw OptionsValidationException. IConfiguration config = BuildInsecureConfiguration(); using IHost host = new HostBuilder() .ConfigureServices(services => services.AddZbLdapAuth(config, LdapSection)) .Build(); OptionsValidationException ex = await Assert.ThrowsAsync(() => host.StartAsync()); Assert.Contains(nameof(LdapOptions.Transport), string.Join(" ", ex.Failures)); } [Fact] public async Task AddZbLdapAuth_StartingHost_SucceedsForSecureConfig() { // A valid (secure) config must start cleanly — proving ValidateOnStart does not reject // well-formed options. IConfiguration config = BuildConfiguration(); using IHost host = new HostBuilder() .ConfigureServices(services => services.AddZbLdapAuth(config, LdapSection)) .Build(); await host.StartAsync(); await host.StopAsync(); } }