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);
}
}