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 _));
}
}