using System.Data.Common; using Dapr.Client; using EntitlementBLL.Exceptions; using EntitlementBLL.Implementations; using EntitlementDAL.DTOs; using EntitlementDAL.Interfaces; 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 new BetaClientBLL.EnrolAsync/OptOutAsync — specifically the reactivate-vs-insert /// branch driven by UX_MENTITLEMENTBETACLIENT (CLIENTID, BETAPROGRAMNAME) NOT being filtered by /// STATUS: a client re-enrolling after a prior opt-out must reactivate the existing (soft-deleted) /// row, never insert a duplicate that would violate the unique key. /// public class BetaClientBLLTests { private static (BetaClientBLL Svc, Mock BetaClientDal) BuildService() { var betaClientDal = new Mock(); betaClientDal .Setup(d => d.SaveAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); betaClientDal .Setup(d => d.ReactivateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); betaClientDal .Setup(d => d.DeleteAsync(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 { 301 }); 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 eventLog = new EventLogPublish(new DaprClientBuilder().Build(), NullLogger.Instance); var svc = new BetaClientBLL(betaClientDal.Object, autoNumber, queryExecutor.Object, eventLog); return (svc, betaClientDal); } private static LoginDTO TestLogin() => new() { ClientId = 42, UserId = -1 }; [Fact] public async Task Test_EnrolAsync_NoPriorRow_InsertsNewEnrolment() { var (svc, betaClientDal) = BuildService(); betaClientDal.Setup(d => d.GetByClientAndProgramAsync(42, "EARLY_ACCESS", It.IsAny(), It.IsAny())) .ReturnsAsync((BetaClientDTO?)null); var validFrom = DateTime.UtcNow; var validUntil = validFrom.AddMonths(6); await svc.EnrolAsync(42, "EARLY_ACCESS", validFrom, validUntil, TestLogin(), CancellationToken.None); betaClientDal.Verify(d => d.SaveAsync( It.Is(b => b.ClientId == 42 && b.BetaProgramName == "EARLY_ACCESS" && b.ValidUntil == validUntil), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); betaClientDal.Verify(d => d.ReactivateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Test_EnrolAsync_PriorOptedOutRow_ReactivatesInsteadOfInserting() { var (svc, betaClientDal) = BuildService(); betaClientDal.Setup(d => d.GetByClientAndProgramAsync(42, "EARLY_ACCESS", It.IsAny(), It.IsAny())) .ReturnsAsync(new BetaClientDTO { BetaClientId = 55, ClientId = 42, BetaProgramName = "EARLY_ACCESS", Status = 0 }); var validFrom = DateTime.UtcNow; var validUntil = validFrom.AddMonths(6); await svc.EnrolAsync(42, "EARLY_ACCESS", validFrom, validUntil, TestLogin(), CancellationToken.None); betaClientDal.Verify(d => d.ReactivateAsync(55, validFrom, validUntil, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); betaClientDal.Verify(d => d.SaveAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Test_OptOutAsync_ExistingEnrolment_SoftDeletes() { var (svc, betaClientDal) = BuildService(); betaClientDal.Setup(d => d.GetByClientAndProgramAsync(42, "EARLY_ACCESS", It.IsAny(), It.IsAny())) .ReturnsAsync(new BetaClientDTO { BetaClientId = 55, ClientId = 42, BetaProgramName = "EARLY_ACCESS", Status = 1 }); await svc.OptOutAsync(42, "EARLY_ACCESS", TestLogin(), CancellationToken.None); betaClientDal.Verify(d => d.DeleteAsync(55, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task Test_OptOutAsync_NoExistingEnrolment_Throws() { var (svc, betaClientDal) = BuildService(); betaClientDal.Setup(d => d.GetByClientAndProgramAsync(42, "NEVER_ENROLLED", It.IsAny(), It.IsAny())) .ReturnsAsync((BetaClientDTO?)null); await Assert.ThrowsAsync( () => svc.OptOutAsync(42, "NEVER_ENROLLED", TestLogin(), CancellationToken.None)); betaClientDal.Verify(d => d.DeleteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } }