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.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 individual-user first-login gate (tracker §50.3/§51.7): /// ClientAuthBLL.LoginAsync computing PendingAgreementVersionIds (never blocking the login /// itself, even on a lookup failure) and AcceptPendingAgreementsAsync recording acceptance /// against the caller's own identity. /// public class ClientAuthEulaGateTests { private static (ClientAuthBLL Svc, Mock ClientUserDal, Mock AgreementConsentProvider, Mock ClientProvisioningDal) BuildService() { var clientUserDal = new Mock(); var clientUserRoleDal = new Mock(); clientUserRoleDal .Setup(d => d.GetActiveRoleCodesAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(Enumerable.Empty()); 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 agreementConsentProvider = new Mock(); var clientProvisioningDal = new Mock(); 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, agreementConsentProvider.Object, clientProvisioningDal.Object, NullLogger.Instance); return (svc, clientUserDal, agreementConsentProvider, clientProvisioningDal); } private static ClientUserDTO ActiveAdmin(string password) => new() { ClientUserId = 1, ClientId = 42, Role = (byte)ClientUserRoleEnum.ClientAdmin, Status = (byte)ClientUserStatusEnum.Active, PasswordHash = GB5Shared.EncryptionHelper.PasswordHasher.Hash(password) }; private static ClientUserDTO ActiveUser(string password) => new() { ClientUserId = 2, ClientId = 42, Role = (byte)ClientUserRoleEnum.ClientUser, Status = (byte)ClientUserStatusEnum.Active, PasswordHash = GB5Shared.EncryptionHelper.PasswordHasher.Hash(password) }; [Fact] public async Task Test_LoginAsync_NothingPending_ReturnsEmptyArray() { var (svc, clientUserDal, agreementConsentProvider, _) = BuildService(); const string password = "Correct-Horse-Battery-Staple"; clientUserDal.Setup(d => d.GetByClientAndEmailAsync(42, "admin@example.com", It.IsAny(), It.IsAny())) .ReturnsAsync(ActiveAdmin(password)); agreementConsentProvider .Setup(a => a.GetPendingIndividualAgreementVersionIdsAsync(AgreementSubjectType.ClientAdmin, 1, It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(Array.Empty()); var result = await svc.LoginAsync(42, "admin@example.com", password, new LoginDTO { ClientId = 42, UserId = -1 }, CancellationToken.None); Assert.Equal(ClientLoginResultStatus.Success, result.Status); Assert.Empty(result.PendingAgreementVersionIds); } [Fact] public async Task Test_LoginAsync_ResolvesRealJurisdictionCode_PassesItToPendingLookup() { var (svc, clientUserDal, agreementConsentProvider, clientProvisioningDal) = BuildService(); const string password = "Correct-Horse-Battery-Staple"; clientUserDal.Setup(d => d.GetByClientAndEmailAsync(42, "admin@example.com", It.IsAny(), It.IsAny())) .ReturnsAsync(ActiveAdmin(password)); clientProvisioningDal .Setup(d => d.GetClientJurisdictionCodeAsync(42, It.IsAny(), It.IsAny())) .ReturnsAsync("UAE"); agreementConsentProvider .Setup(a => a.GetPendingIndividualAgreementVersionIdsAsync(AgreementSubjectType.ClientAdmin, 1, "UAE", It.IsAny(), It.IsAny())) .ReturnsAsync(Array.Empty()); var result = await svc.LoginAsync(42, "admin@example.com", password, new LoginDTO { ClientId = 42, UserId = -1 }, CancellationToken.None); Assert.Equal(ClientLoginResultStatus.Success, result.Status); agreementConsentProvider.Verify(a => a.GetPendingIndividualAgreementVersionIdsAsync( AgreementSubjectType.ClientAdmin, 1, "UAE", It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task Test_LoginAsync_NoJurisdictionCaptured_FallsBackToUniversalEmptyString() { var (svc, clientUserDal, agreementConsentProvider, clientProvisioningDal) = BuildService(); const string password = "Correct-Horse-Battery-Staple"; clientUserDal.Setup(d => d.GetByClientAndEmailAsync(42, "admin@example.com", It.IsAny(), It.IsAny())) .ReturnsAsync(ActiveAdmin(password)); clientProvisioningDal .Setup(d => d.GetClientJurisdictionCodeAsync(42, It.IsAny(), It.IsAny())) .ReturnsAsync((string?)null); agreementConsentProvider .Setup(a => a.GetPendingIndividualAgreementVersionIdsAsync(AgreementSubjectType.ClientAdmin, 1, string.Empty, It.IsAny(), It.IsAny())) .ReturnsAsync(Array.Empty()); var result = await svc.LoginAsync(42, "admin@example.com", password, new LoginDTO { ClientId = 42, UserId = -1 }, CancellationToken.None); Assert.Equal(ClientLoginResultStatus.Success, result.Status); agreementConsentProvider.Verify(a => a.GetPendingIndividualAgreementVersionIdsAsync( AgreementSubjectType.ClientAdmin, 1, string.Empty, It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task Test_LoginAsync_ClientAdmin_PendingEula_ReturnsVersionIds_AndStillIssuesToken() { var (svc, clientUserDal, agreementConsentProvider, _) = BuildService(); const string password = "Correct-Horse-Battery-Staple"; clientUserDal.Setup(d => d.GetByClientAndEmailAsync(42, "admin@example.com", It.IsAny(), It.IsAny())) .ReturnsAsync(ActiveAdmin(password)); agreementConsentProvider .Setup(a => a.GetPendingIndividualAgreementVersionIdsAsync(AgreementSubjectType.ClientAdmin, 1, It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new[] { -1370000301 }); var result = await svc.LoginAsync(42, "admin@example.com", password, new LoginDTO { ClientId = 42, UserId = -1 }, CancellationToken.None); Assert.Equal(ClientLoginResultStatus.Success, result.Status); Assert.NotNull(result.TokenPair); Assert.Single(result.PendingAgreementVersionIds); Assert.Equal(-1370000301, result.PendingAgreementVersionIds[0]); } [Fact] public async Task Test_LoginAsync_EndUserRole_ChecksEndUserSubjectType_NotClientAdmin() { var (svc, clientUserDal, agreementConsentProvider, _) = BuildService(); const string password = "Correct-Horse-Battery-Staple"; clientUserDal.Setup(d => d.GetByClientAndEmailAsync(42, "user@example.com", It.IsAny(), It.IsAny())) .ReturnsAsync(ActiveUser(password)); agreementConsentProvider .Setup(a => a.GetPendingIndividualAgreementVersionIdsAsync(AgreementSubjectType.EndUser, 2, It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new[] { -1370000301 }); var result = await svc.LoginAsync(42, "user@example.com", password, new LoginDTO { ClientId = 42, UserId = -1 }, CancellationToken.None); Assert.Equal(ClientLoginResultStatus.Success, result.Status); Assert.Single(result.PendingAgreementVersionIds); agreementConsentProvider.Verify(a => a.GetPendingIndividualAgreementVersionIdsAsync( AgreementSubjectType.ClientAdmin, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Test_LoginAsync_PendingLookupThrows_LoginStillSucceeds_WithEmptyPendingList() { var (svc, clientUserDal, agreementConsentProvider, _) = BuildService(); const string password = "Correct-Horse-Battery-Staple"; clientUserDal.Setup(d => d.GetByClientAndEmailAsync(42, "admin@example.com", It.IsAny(), It.IsAny())) .ReturnsAsync(ActiveAdmin(password)); agreementConsentProvider .Setup(a => a.GetPendingIndividualAgreementVersionIdsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new InvalidOperationException("DB unavailable")); var result = await svc.LoginAsync(42, "admin@example.com", password, new LoginDTO { ClientId = 42, UserId = -1 }, CancellationToken.None); // The whole point of this gate: a failure to compute pending agreements is never a // reason to fail the login itself — a real token is still issued. Assert.Equal(ClientLoginResultStatus.Success, result.Status); Assert.NotNull(result.TokenPair); Assert.Empty(result.PendingAgreementVersionIds); } [Fact] public async Task Test_AcceptPendingAgreementsAsync_NoVersionIds_Throws() { var (svc, _, _, _) = BuildService(); await Assert.ThrowsAsync(() => svc.AcceptPendingAgreementsAsync(1, ClientRoleCodes.ClientAdmin, Array.Empty(), "1.2.3.4", "UA", new LoginDTO { ClientId = 42, UserId = -1 }, CancellationToken.None)); } [Fact] public async Task Test_AcceptPendingAgreementsAsync_ClientAdmin_RecordsAsClientAdminSubjectType() { var (svc, _, agreementConsentProvider, _) = BuildService(); agreementConsentProvider .Setup(a => a.RecordAcceptanceAsync( AgreementSubjectType.ClientAdmin, 1, It.IsAny(), "1.2.3.4", "UA", AgreementAcceptanceMethod.Clickwrap, It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); await svc.AcceptPendingAgreementsAsync(1, ClientRoleCodes.ClientAdmin, new[] { -1370000301 }, "1.2.3.4", "UA", new LoginDTO { ClientId = 42, UserId = -1 }, CancellationToken.None); agreementConsentProvider.Verify(a => a.RecordAcceptanceAsync( AgreementSubjectType.ClientAdmin, 1, It.Is(ids => ids.Length == 1 && ids[0] == -1370000301), "1.2.3.4", "UA", AgreementAcceptanceMethod.Clickwrap, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task Test_AcceptPendingAgreementsAsync_ClientUser_RecordsAsEndUserSubjectType() { var (svc, _, agreementConsentProvider, _) = BuildService(); agreementConsentProvider .Setup(a => a.RecordAcceptanceAsync( AgreementSubjectType.EndUser, 2, It.IsAny(), null, null, AgreementAcceptanceMethod.Clickwrap, It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); await svc.AcceptPendingAgreementsAsync(2, ClientRoleCodes.ClientUser, new[] { -1370000301 }, null, null, new LoginDTO { ClientId = 42, UserId = -1 }, CancellationToken.None); agreementConsentProvider.Verify(a => a.RecordAcceptanceAsync( AgreementSubjectType.EndUser, 2, It.IsAny(), null, null, AgreementAcceptanceMethod.Clickwrap, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } }