using System.Data.Common; using Dapr.Client; using EntitlementBLL.Common; using EntitlementBLL.Exceptions; using EntitlementBLL.Implementations; using EntitlementBLL.Interfaces; using EntitlementDAL.DTOs; using EntitlementDAL.Enums; 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 lightweight, Entitlement-owned request/approval mechanism (Thread 8 ยง1.6): /// submitting a request never mutates anything (ChangePlan/Renew/BetaEnrol/BetaOptOut's real /// mutation only happens once a GOODBOOKS_ADMIN approves it), and ApproveChangeRequestAsync /// dispatches to the correct existing mutation method based on RequestType. /// public class ChangeRequestBLLTests { private static (ChangeRequestBLL Svc, Mock ChangeRequestDal, Mock SubscriptionDal, Mock SubscriptionService, Mock BetaClientBLL) BuildService() { var changeRequestDal = new Mock(); var subscriptionDal = new Mock(); var subscriptionService = new Mock(); var betaClientBLL = new Mock(); changeRequestDal .Setup(d => d.SaveAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); changeRequestDal .Setup(d => d.UpdateReviewAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); subscriptionService .Setup(s => s.ChangePlanAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); subscriptionService .Setup(s => s.RenewAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); betaClientBLL .Setup(b => b.EnrolAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("ok"); betaClientBLL .Setup(b => b.OptOutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("ok"); var queryExecutor = new Mock(); queryExecutor .Setup(q => q.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { 401 }); 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 loginFactory = new Mock(); loginFactory.Setup(f => f.Create(It.IsAny(), It.IsAny())) .Returns((int clientId, int userId) => new LoginDTO { ClientId = clientId, UserId = userId }); var eventLog = new EventLogPublish(new DaprClientBuilder().Build(), NullLogger.Instance); var svc = new ChangeRequestBLL( changeRequestDal.Object, subscriptionDal.Object, subscriptionService.Object, betaClientBLL.Object, autoNumber, queryExecutor.Object, loginFactory.Object, eventLog); return (svc, changeRequestDal, subscriptionDal, subscriptionService, betaClientBLL); } [Fact] public async Task Test_SubmitChangePlanRequestAsync_SubscriptionNotFound_Throws() { var (svc, _, subscriptionDal, _, _) = BuildService(); subscriptionDal.Setup(d => d.GetByIdAsync(999, It.IsAny(), It.IsAny())) .ReturnsAsync((SubscriptionDTO?)null); await Assert.ThrowsAsync( () => svc.SubmitChangePlanRequestAsync(42, 999, 5, 1, CancellationToken.None)); } [Fact] public async Task Test_SubmitChangePlanRequestAsync_SubscriptionBelongsToDifferentClient_Throws() { var (svc, _, subscriptionDal, _, _) = BuildService(); subscriptionDal.Setup(d => d.GetByIdAsync(10, It.IsAny(), It.IsAny())) .ReturnsAsync(new SubscriptionDTO { SubscriptionId = 10, ClientId = 999 }); await Assert.ThrowsAsync( () => svc.SubmitChangePlanRequestAsync(42, 10, 5, 1, CancellationToken.None)); } [Fact] public async Task Test_SubmitChangePlanRequestAsync_Valid_InsertsPendingRequest_NoMutationApplied() { var (svc, changeRequestDal, subscriptionDal, subscriptionService, _) = BuildService(); subscriptionDal.Setup(d => d.GetByIdAsync(10, It.IsAny(), It.IsAny())) .ReturnsAsync(new SubscriptionDTO { SubscriptionId = 10, ClientId = 42 }); var changeRequestId = await svc.SubmitChangePlanRequestAsync(42, 10, 5, 1, CancellationToken.None); Assert.True(changeRequestId > 0); changeRequestDal.Verify(d => d.SaveAsync( It.Is(r => r.RequestType == (byte)ChangeRequestTypeEnum.ChangePlan && r.RequestStatus == (byte)ChangeRequestStatusEnum.Pending && r.SubscriptionId == 10 && r.ProposedPlanId == 5), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); subscriptionService.Verify(s => s.ChangePlanAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Test_SubmitBetaOptOutRequestAsync_EmptyProgramName_Throws() { var (svc, _, _, _, _) = BuildService(); await Assert.ThrowsAsync( () => svc.SubmitBetaOptOutRequestAsync(42, "", 1, CancellationToken.None)); } [Fact] public async Task Test_ApproveChangeRequestAsync_NotPending_Throws() { var (svc, changeRequestDal, _, _, _) = BuildService(); changeRequestDal.Setup(d => d.GetByIdAsync(1, It.IsAny(), It.IsAny())) .ReturnsAsync(new ChangeRequestDTO { ChangeRequestId = 1, RequestStatus = (byte)ChangeRequestStatusEnum.Approved }); await Assert.ThrowsAsync( () => svc.ApproveChangeRequestAsync(1, 9, null, CancellationToken.None)); } [Fact] public async Task Test_ApproveChangeRequestAsync_ChangePlanType_DispatchesToSubscriptionService_AndMarksApproved() { var (svc, changeRequestDal, _, subscriptionService, _) = BuildService(); changeRequestDal.Setup(d => d.GetByIdAsync(1, It.IsAny(), It.IsAny())) .ReturnsAsync(new ChangeRequestDTO { ChangeRequestId = 1, ClientId = 42, RequestType = (byte)ChangeRequestTypeEnum.ChangePlan, RequestStatus = (byte)ChangeRequestStatusEnum.Pending, SubscriptionId = 10, ProposedPlanId = 5 }); var result = await svc.ApproveChangeRequestAsync(1, 9, "looks good", CancellationToken.None); Assert.NotNull(result); subscriptionService.Verify(s => s.ChangePlanAsync(10, 5, 9, It.IsAny()), Times.Once); changeRequestDal.Verify(d => d.UpdateReviewAsync(1, (byte)ChangeRequestStatusEnum.Approved, 9, "looks good", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task Test_ApproveChangeRequestAsync_RenewType_DispatchesToSubscriptionServiceRenewAsync() { var (svc, changeRequestDal, _, subscriptionService, _) = BuildService(); var proposedValidTill = DateTime.UtcNow.AddYears(1); changeRequestDal.Setup(d => d.GetByIdAsync(2, It.IsAny(), It.IsAny())) .ReturnsAsync(new ChangeRequestDTO { ChangeRequestId = 2, ClientId = 42, RequestType = (byte)ChangeRequestTypeEnum.Renew, RequestStatus = (byte)ChangeRequestStatusEnum.Pending, SubscriptionId = 10, ProposedValidTill = proposedValidTill }); await svc.ApproveChangeRequestAsync(2, 9, null, CancellationToken.None); subscriptionService.Verify(s => s.RenewAsync(10, proposedValidTill, 9, It.IsAny()), Times.Once); } [Fact] public async Task Test_ApproveChangeRequestAsync_BetaEnrolType_DispatchesToBetaClientBLLEnrolAsync() { var (svc, changeRequestDal, _, _, betaClientBLL) = BuildService(); var proposedValidTill = DateTime.UtcNow.AddMonths(6); changeRequestDal.Setup(d => d.GetByIdAsync(3, It.IsAny(), It.IsAny())) .ReturnsAsync(new ChangeRequestDTO { ChangeRequestId = 3, ClientId = 42, RequestType = (byte)ChangeRequestTypeEnum.BetaEnrol, RequestStatus = (byte)ChangeRequestStatusEnum.Pending, BetaProgramName = "EARLY_ACCESS", ProposedValidTill = proposedValidTill }); await svc.ApproveChangeRequestAsync(3, 9, null, CancellationToken.None); betaClientBLL.Verify(b => b.EnrolAsync(42, "EARLY_ACCESS", It.IsAny(), proposedValidTill, It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task Test_ApproveChangeRequestAsync_BetaOptOutType_DispatchesToBetaClientBLLOptOutAsync() { var (svc, changeRequestDal, _, _, betaClientBLL) = BuildService(); changeRequestDal.Setup(d => d.GetByIdAsync(4, It.IsAny(), It.IsAny())) .ReturnsAsync(new ChangeRequestDTO { ChangeRequestId = 4, ClientId = 42, RequestType = (byte)ChangeRequestTypeEnum.BetaOptOut, RequestStatus = (byte)ChangeRequestStatusEnum.Pending, BetaProgramName = "EARLY_ACCESS" }); await svc.ApproveChangeRequestAsync(4, 9, null, CancellationToken.None); betaClientBLL.Verify(b => b.OptOutAsync(42, "EARLY_ACCESS", It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task Test_RejectChangeRequestAsync_Valid_MarksRejected_NoMutationApplied() { var (svc, changeRequestDal, _, subscriptionService, betaClientBLL) = BuildService(); changeRequestDal.Setup(d => d.GetByIdAsync(1, It.IsAny(), It.IsAny())) .ReturnsAsync(new ChangeRequestDTO { ChangeRequestId = 1, ClientId = 42, RequestType = (byte)ChangeRequestTypeEnum.ChangePlan, RequestStatus = (byte)ChangeRequestStatusEnum.Pending, SubscriptionId = 10, ProposedPlanId = 5 }); var result = await svc.RejectChangeRequestAsync(1, 9, "not now", CancellationToken.None); Assert.NotNull(result); changeRequestDal.Verify(d => d.UpdateReviewAsync(1, (byte)ChangeRequestStatusEnum.Rejected, 9, "not now", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); subscriptionService.Verify(s => s.ChangePlanAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); betaClientBLL.Verify(b => b.EnrolAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } }