using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using NSubstitute; using ZB.MOM.WW.Auth.Abstractions.Ldap; using ZB.MOM.WW.Auth.Abstractions.Roles; using ZB.MOM.WW.ScadaBridge.CentralUI.Auth; using ZB.MOM.WW.ScadaBridge.Security; using ZB.MOM.WW.ScadaBridge.Security.Auth; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Auth; /// /// POST /auth/token (review 2026-10-21 05/U1): the minted token carries everything /// ManagementAuthenticator needs (username, roles, site scope), the response advertises /// management_bearer + expires_in, and a login-disabled deployment refuses the exchange. /// public class AuthTokenEndpointTests { private const string Key = "this-is-a-test-signing-key-for-hmac-sha256-must-be-long-enough"; private static HttpContext Context(bool disableLogin, RoleMappingResult mapping, out JwtTokenService jwt) { var ldap = Substitute.For(); ldap.AuthenticateAsync("alice", "pw", Arg.Any()) .Returns(LdapAuthResult.Success("alice", "Alice A", ["SiteDeployers"])); var mapper = Substitute.For>(); mapper.MapAsync(Arg.Any>(), Arg.Any()) .Returns(new GroupRoleMapping(mapping.Roles, Scope: mapping)); var options = Options.Create(new SecurityOptions { JwtSigningKey = Key, JwtExpiryMinutes = 15 }); jwt = new JwtTokenService(options, NullLogger.Instance); var services = new ServiceCollection().AddLogging(); services.AddSingleton(Options.Create(new AuthDisableLoginOptions { DisableLogin = disableLogin })); services.AddSingleton>(options); services.AddSingleton(TimeProvider.System); services.AddSingleton(); services.AddSingleton(ldap); services.AddSingleton(mapper); services.AddSingleton(jwt); var context = new DefaultHttpContext { RequestServices = services.BuildServiceProvider() }; context.Request.Method = "POST"; context.Request.ContentType = "application/x-www-form-urlencoded"; context.Request.Form = new FormCollection(new() { ["username"] = "alice", ["password"] = "pw", }); context.Response.Body = new MemoryStream(); return context; } private static async Task<(int Status, JsonElement Body)> Execute(HttpContext context) { var result = await AuthEndpoints.HandleTokenAsync(context); await result.ExecuteAsync(context); context.Response.Body.Position = 0; using var doc = await JsonDocument.ParseAsync(context.Response.Body); return (context.Response.StatusCode, doc.RootElement.Clone()); } /// The token round-trips the user's roles and site scope and advertises bearer support. /// A task that represents the asynchronous operation. [Fact] public async Task Token_CarriesRolesAndScope_AndAdvertisesBearer() { var context = Context(false, new RoleMappingResult([Roles.Deployer], ["4", "9"], false), out var jwt); var (status, body) = await Execute(context); Assert.Equal(200, status); Assert.True(body.GetProperty("management_bearer").GetBoolean()); Assert.Equal(15 * 60, body.GetProperty("expires_in").GetInt32()); var principal = jwt.ValidateToken(body.GetProperty("access_token").GetString()!); Assert.NotNull(principal); Assert.Equal("alice", principal!.FindFirst(JwtTokenService.UsernameClaimType)!.Value); Assert.Equal("Alice A", principal.FindFirst(JwtTokenService.DisplayNameClaimType)!.Value); Assert.Equal([Roles.Deployer], principal.FindAll(JwtTokenService.RoleClaimType).Select(c => c.Value)); Assert.Equal(["4", "9"], principal.FindAll(JwtTokenService.SiteIdClaimType).Select(c => c.Value)); } /// A system-wide grant carries no site-id claims (empty == system-wide downstream). /// A task that represents the asynchronous operation. [Fact] public async Task Token_SystemWide_HasNoSiteClaims() { var context = Context(false, new RoleMappingResult([Roles.Deployer], [], true), out var jwt); var (_, body) = await Execute(context); var principal = jwt.ValidateToken(body.GetProperty("access_token").GetString()!); Assert.Empty(principal!.FindAll(JwtTokenService.SiteIdClaimType)); } /// A login-disabled deployment refuses the exchange so the CLI falls back to Basic. /// A task that represents the asynchronous operation. [Fact] public async Task Token_WhenLoginDisabled_Is404() { var context = Context(true, new RoleMappingResult([Roles.Deployer], [], true), out _); var (status, body) = await Execute(context); Assert.Equal(404, status); Assert.False(body.TryGetProperty("access_token", out _)); } }