using System.Data.Common; using Dapr.Client; using EntitlementBLL.Auth; using EntitlementBLL.Legal; using EntitlementBLL.Options; using EntitlementDAL.DTOs; using EntitlementDAL.Enums; using EntitlementDAL.Interfaces; using GB5Shared.DirectAction; using GB5Shared.DTO.DirectAction; using GB5Shared.ActionProcessor; using GB5Shared.DTO.Framework.CommonConfig; using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace EntitlementTests; /// /// Covers the Thread 8 capability-grant claim wiring: MENTITLEMENTCLIENTUSERROLE grants /// (TECH_ADMIN/COMMERCIAL_ADMIN) are resolved once at login and carried on the JWT's Capabilities /// claim, so an endpoint (e.g. RequestChangePlan) can check capability membership with no DB /// round-trip. Covers both the claim serialization itself (ClientAccessTokenClaims.ToClaims) and /// ClientAuthBLL.LoginAsync actually populating it from IClientUserRoleDAL. /// public class ClientCapabilityTests { [Fact] public void Test_ToClaims_WithCapabilities_ProducesCommaJoinedClaim() { var claims = new ClientAccessTokenClaims { ClientUserId = 1, ClientId = 42, Role = ClientRoleCodes.ClientAdmin, Capabilities = new List { EntitlementClientCapabilityCodes.TechAdmin, EntitlementClientCapabilityCodes.CommercialAdmin } }; var capabilitiesClaim = claims.ToClaims().Single(c => c.Type == ClientJwtClaimTypes.Capabilities); Assert.Equal("TECH_ADMIN,COMMERCIAL_ADMIN", capabilitiesClaim.Value); } [Fact] public void Test_ToClaims_NoCapabilities_ProducesEmptyClaim() { var claims = new ClientAccessTokenClaims { ClientUserId = 1, ClientId = 42, Role = ClientRoleCodes.ClientUser }; var capabilitiesClaim = claims.ToClaims().Single(c => c.Type == ClientJwtClaimTypes.Capabilities); Assert.Equal(string.Empty, capabilitiesClaim.Value); } private static (ClientAuthBLL Svc, Mock ClientUserDal, Mock ClientUserRoleDal, Mock JwtService) BuildService() { var clientUserDal = new Mock(); var clientUserRoleDal = new Mock(); var refreshTokenDal = new Mock(); var jwtService = new Mock(); jwtService.Setup(j => j.IssueTokenPairAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new ClientTokenPair { AccessToken = "access-token", AccessTokenExpiresOn = DateTime.UtcNow.AddMinutes(15), RefreshToken = "raw-refresh-token", RefreshTokenHash = "HASH-raw-refresh-token", RefreshTokenExpiresOn = DateTime.UtcNow.AddDays(30) }); clientUserDal.Setup(d => d.UpdateFailedLoginCountAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); clientUserDal.Setup(d => d.UpdateLastLoginOnAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); refreshTokenDal.Setup(d => d.SaveAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); var queryExecutor = new Mock(); queryExecutor .Setup(q => q.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { 101 }); queryExecutor.Setup(q => q.BeginTransactionAsync(It.IsAny())).ReturnsAsync((DbTransaction)null!); queryExecutor.Setup(q => q.CommitAsync(It.IsAny())).Returns(Task.CompletedTask); queryExecutor.Setup(q => q.RollbackAsync(It.IsAny())).Returns(Task.CompletedTask); var dbConfig = new Mock>(); dbConfig.Setup(o => o.Value).Returns(new Gb5SystemDTO { DataBaseType = 0 }); var autoNumber = new AutoNumber(queryExecutor.Object, dbConfig.Object); var lockoutOptions = Options.Create(new ClientAccountLockoutOptions()); var passwordResetOptions = Options.Create(new ClientPasswordResetOptions()); var tokenService = new Mock(); var directActionTokenDal = new Mock(); var eventActionRunDal = new Mock(); var actionOutboxDal = new Mock(); var eventLog = new EventLogPublish(new DaprClientBuilder().Build(), NullLogger.Instance); var svc = new ClientAuthBLL( clientUserDal.Object, clientUserRoleDal.Object, refreshTokenDal.Object, jwtService.Object, autoNumber, queryExecutor.Object, lockoutOptions, tokenService.Object, directActionTokenDal.Object, eventActionRunDal.Object, actionOutboxDal.Object, passwordResetOptions, eventLog, new Mock().Object, new Mock().Object, NullLogger.Instance); return (svc, clientUserDal, clientUserRoleDal, jwtService); } [Fact] public async Task Test_LoginAsync_ResolvesCapabilities_AndIncludesThemOnIssuedClaims() { var (svc, clientUserDal, clientUserRoleDal, jwtService) = BuildService(); const string password = "Correct-Horse-Battery-Staple"; clientUserDal.Setup(d => d.GetByClientAndEmailAsync(42, "admin@example.com", It.IsAny(), It.IsAny())) .ReturnsAsync(new ClientUserDTO { ClientUserId = 1, ClientId = 42, Role = (byte)ClientUserRoleEnum.ClientAdmin, Status = (byte)ClientUserStatusEnum.Active, PasswordHash = GB5Shared.EncryptionHelper.PasswordHasher.Hash(password) }); clientUserRoleDal.Setup(d => d.GetActiveRoleCodesAsync(1, It.IsAny(), It.IsAny())) .ReturnsAsync(new List { EntitlementClientCapabilityCodes.CommercialAdmin }); var result = await svc.LoginAsync(42, "admin@example.com", password, new LoginDTO { ClientId = 42, UserId = -1 }, CancellationToken.None); Assert.Equal(ClientLoginResultStatus.Success, result.Status); jwtService.Verify(j => j.IssueTokenPairAsync( It.Is(c => c.Capabilities.Count == 1 && c.Capabilities[0] == EntitlementClientCapabilityCodes.CommercialAdmin), It.IsAny()), Times.Once); } }