using EntitlementBLL.Common; using EntitlementBLL.Implementations; using EntitlementBLL.Options; using EntitlementDAL.DTOs; using EntitlementDAL.Enums; using EntitlementDAL.Interfaces; using GB5Shared.DTO.Framework.CommonConfig; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace EntitlementTests; /// /// Covers the Thread 1 reconciliation pass: MPROVISIONINGJOB-backed idempotency and the /// richer ProvisioningStatusDto contract. Only the idempotency short-circuit and the /// pure-read status path are exercised here — both avoid needing the full /// AutoNumber/transaction machinery that the happy-path provisioning flow requires. /// public class ProvisioningJobTests { private static (ProvisioningService svc, Mock jobDal, Mock subDal) BuildService() { var subDal = new Mock(); var planDal = new Mock(); var grantDal = new Mock(); var licenseDal = new Mock(); var auditDal = new Mock(); var jobDal = new Mock(); var queryExecutor = new Mock(); 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 svc = new ProvisioningService( subDal.Object, planDal.Object, grantDal.Object, licenseDal.Object, auditDal.Object, jobDal.Object, autoNumber, queryExecutor.Object, loginFactory.Object); return (svc, jobDal, subDal); } [Fact] public async Task Test_Provision_DuplicateEngagementRef_ReturnsAlreadyExisted_NoNewJobRow() { var (svc, jobDal, subDal) = BuildService(); const string engagementRef = "IDMS-ENG-001"; var existingJob = new ProvisioningJobDTO { ProvisioningJobId = 1, ClientId = 42, IdmsEngagementRef = engagementRef, JobStatus = (byte)ProvisioningJobStatusEnum.Completed }; jobDal.Setup(j => j.GetByEngagementRefAsync(engagementRef, It.IsAny(), It.IsAny())) .ReturnsAsync(existingJob); subDal.Setup(s => s.GetByClientAsync(42, It.IsAny(), It.IsAny())) .ReturnsAsync(new SubscriptionDTO { SubscriptionId = 99, ClientId = 42, SubscriptionStatus = (byte)SubscriptionStatusEnum.Active }); var result = await svc.ProvisionSubscriptionAsync(new ProvisionSubscriptionRequest { ClientId = 42, PlanId = 1, IdmsEngagementRef = engagementRef }, CancellationToken.None); Assert.True(result.AlreadyExisted); Assert.Equal(99, result.SubscriptionId); Assert.Equal(42, result.ClientId); jobDal.Verify(j => j.SaveAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Test_GetStatus_Completed_Reflects100PercentAndNoRetry() { var (svc, jobDal, _) = BuildService(); jobDal.Setup(j => j.GetByClientAsync(42, It.IsAny(), It.IsAny())) .ReturnsAsync(new ProvisioningJobDTO { ProvisioningJobId = 1, ClientId = 42, JobStatus = (byte)ProvisioningJobStatusEnum.Completed, CurrentStep = "", CompletedSteps = "[\"SUBSCRIPTION_CREATED\",\"ENTITLEMENT_SEEDED\"]" }); var status = await svc.GetStatusAsync(42, CancellationToken.None); Assert.Equal("Completed", status.Status); Assert.Equal(100, status.ProgressPct); Assert.False(status.CanRetry); Assert.Equal(2, status.CompletedSteps.Length); } [Fact] public async Task Test_GetStatus_NoJobForClient_ReturnsNotFound() { var (svc, jobDal, _) = BuildService(); jobDal.Setup(j => j.GetByClientAsync(99, It.IsAny(), It.IsAny())) .ReturnsAsync((ProvisioningJobDTO?)null); var status = await svc.GetStatusAsync(99, CancellationToken.None); Assert.Equal("not_found", status.Status); Assert.Equal(99, status.ClientId); } [Fact] public async Task Test_GetStatus_Failed_CanRetryIsTrue() { var (svc, jobDal, _) = BuildService(); jobDal.Setup(j => j.GetByClientAsync(7, It.IsAny(), It.IsAny())) .ReturnsAsync(new ProvisioningJobDTO { ProvisioningJobId = 2, ClientId = 7, JobStatus = (byte)ProvisioningJobStatusEnum.Failed, CurrentStep = "SUBSCRIPTION_CREATE", CompletedSteps = "[]", LastError = "boom" }); var status = await svc.GetStatusAsync(7, CancellationToken.None); Assert.Equal("Failed", status.Status); Assert.True(status.CanRetry); Assert.Equal("boom", status.ErrorMessage); Assert.Equal(0, status.ProgressPct); } /// Regression test for tracker §37 Decision 3's fix — before it, a job onboarded via /// ClientOnboardingOrchestratorBLL's 3-step sequence reported 100% the moment it reached only /// its 2nd step (DB_PROVISIONING_CR_SUBMITTED), because GetStatusAsync's old TotalSteps=2 /// denominator was sized for the OTHER (QuickStart) pipeline's own, shorter step sequence. /// A CLIENT_IDENTITY_CREATED entry is what distinguishes which pipeline's sequence applies. [Fact] public async Task Test_GetStatus_OnboardingOrchestratorPipeline_TwoOfThreeSteps_Reports67NotPrematurely100() { var (svc, jobDal, _) = BuildService(); jobDal.Setup(j => j.GetByClientAsync(9, It.IsAny(), It.IsAny())) .ReturnsAsync(new ProvisioningJobDTO { ProvisioningJobId = 3, ClientId = 9, JobStatus = (byte)ProvisioningJobStatusEnum.Running, CurrentStep = "DB_PROVISIONING_CR_SUBMITTED", CompletedSteps = "[\"CLIENT_IDENTITY_CREATED\",\"DB_PROVISIONING_CR_SUBMITTED\"]", }); var status = await svc.GetStatusAsync(9, CancellationToken.None); Assert.Equal("Running", status.Status); Assert.Equal(67, status.ProgressPct); // 2 of 3 core steps — NOT 100 } /// The QuickStart pipeline's own, shorter (2-step) sequence must still compute /// correctly and reach a genuine 100% once both its steps are recorded — proves the fix /// didn't regress the pipeline TotalSteps=2 was originally sized for. [Fact] public async Task Test_GetStatus_QuickStartPipeline_BothStepsRecorded_Reports100() { var (svc, jobDal, _) = BuildService(); jobDal.Setup(j => j.GetByClientAsync(11, It.IsAny(), It.IsAny())) .ReturnsAsync(new ProvisioningJobDTO { ProvisioningJobId = 4, ClientId = 11, JobStatus = (byte)ProvisioningJobStatusEnum.Running, CurrentStep = "ENTITLEMENT_SEED", CompletedSteps = "[\"SUBSCRIPTION_CREATED\",\"ENTITLEMENT_SEEDED\"]", }); var status = await svc.GetStatusAsync(11, CancellationToken.None); Assert.Equal(100, status.ProgressPct); } }