using GB5Shared.DTO.Framework.AutoNumber;
using GB5Shared.DTO.Framework.CommonConfig;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.Enums.Client;
using GB5Shared.GenerateAutoNumber;
using GB5Shared.QueryExecutor;
using Microsoft.Extensions.Options;
using Moq;
using SwBLL.ClientDatabase;
using SwBLL.Integration;
using SwBLL.Provisioning;
using SwBLL.RolloutCampaign;
using SwBLL.UpgradePackage;
using SwDAL.CustomCode.RolloutCampaign;
using SwDAL.DTO.ClientDatabase;
using SwDAL.DTO.RolloutCampaign;
using SwDAL.DTO.UpgradePackage;
using SwDAL.Enums;
using Xunit;
namespace SwTests;
///
/// Covers Entitlement-aware gating (tracker §40) inside RolloutCampaignBLL.CreateCampaignAsync —
/// Gate A (subscription/support/deployment-type eligibility) and Gate B (feature/module
/// licensing for MetadataOrStandardData packages). All dependencies are mocked; no real
/// Gb5system/EntitlementDb database or live Entitlement host is touched.
///
public class RolloutCampaignBLLTests
{
private static readonly LoginDTO Login = new() { ClientId = 1, UserId = -1 };
private static (RolloutCampaignBLL Svc, Mock Dal, Mock Pkg,
Mock ClientDb, Mock Ent)
BuildService()
{
var dal = new Mock();
var pkg = new Mock();
var clientDb = new Mock();
var ent = new Mock();
var provisioning = new Mock();
var queryExecutor = new Mock();
queryExecutor
.Setup(q => q.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, false, It.IsAny()))
.ReturnsAsync(new List { 1 });
var dbConfig = new Mock>();
dbConfig.Setup(o => o.Value).Returns(new Gb5SystemDTO { DataBaseType = 0 });
var autoNumber = new AutoNumber(queryExecutor.Object, dbConfig.Object);
var svc = new RolloutCampaignBLL(dal.Object, provisioning.Object, pkg.Object, clientDb.Object, ent.Object, autoNumber);
return (svc, dal, pkg, clientDb, ent);
}
private static UpgradePackageDTO SchemaPackage(int id = 1) => new()
{
PackageId = id, PackageContentType = (byte)PackageContentType.Schema, RequiredFeatureCode = null
};
private static UpgradePackageDTO MetadataPackage(int id, string? requiredFeatureCode) => new()
{
PackageId = id, PackageContentType = (byte)PackageContentType.MetadataOrStandardData, RequiredFeatureCode = requiredFeatureCode
};
private static ClientDatabaseDTO ClientDb(int clientDbId, int tenantId) => new()
{
ClientDbId = clientDbId, TenantId = tenantId
};
private static EntitlementSnapshotDTO Snapshot(
int clientId, bool active = true, bool inSupport = true, byte deploymentType = (byte)ClientDeploymentType.SaaS,
params string[] enabledFeatureCodes) => new()
{
ClientId = clientId,
SubscriptionStatus = active ? (byte)1 : (byte)2, // 1=Active 2=Expired (EntitlementDAL.Enums.SubscriptionStatusEnum)
SupportValidTill = inSupport ? DateTime.UtcNow.AddDays(30) : DateTime.UtcNow.AddDays(-100),
GracePeriodDays = 0,
DeploymentType = deploymentType,
EnabledFeatureCodes = enabledFeatureCodes.ToList()
};
private void SetupResolvedTarget(Mock dal, int clientDbId)
=> dal.Setup(d => d.ResolveTargetsAllForDbModel(9, Login, default)).ReturnsAsync(new[] { clientDbId });
[Fact]
public async Task CreateCampaignAsync_SchemaPackage_ActiveInSupportSaaSClient_IsIncluded()
{
var (svc, dal, pkg, clientDb, ent) = BuildService();
pkg.Setup(p => p.GetById(1, Login, default)).ReturnsAsync(SchemaPackage());
SetupResolvedTarget(dal, 100);
clientDb.Setup(c => c.GetById(100, Login, default)).ReturnsAsync(ClientDb(100, tenantId: 55));
ent.Setup(e => e.GetSnapshotAsync(55, Login, default)).ReturnsAsync(Snapshot(55));
var dto = new RolloutCampaignDTO { PackageId = 1, DbModelId = 9, TargetScope = 0 };
var id = await svc.CreateCampaignAsync(dto, Login, default);
// AutoNumber's mocked query result makes the allocated id itself 0 in this test setup
// (same "AUTOID seeding" formula quirk documented elsewhere in this engagement) — the
// real assertion is that campaign creation succeeded at all, not the specific id value.
dal.Verify(d => d.InsertCampaign(It.IsAny(), Login, default), Times.Once);
_ = id;
dal.Verify(d => d.InsertTarget(It.Is(t => t.ClientDbId == 100), Login, default), Times.Once);
}
[Fact]
public async Task CreateCampaignAsync_ExpiredSubscription_ExcludedFromEvenAutoScope()
{
var (svc, dal, pkg, clientDb, ent) = BuildService();
pkg.Setup(p => p.GetById(1, Login, default)).ReturnsAsync(SchemaPackage());
SetupResolvedTarget(dal, 100);
clientDb.Setup(c => c.GetById(100, Login, default)).ReturnsAsync(ClientDb(100, tenantId: 55));
ent.Setup(e => e.GetSnapshotAsync(55, Login, default)).ReturnsAsync(Snapshot(55, active: false));
var dto = new RolloutCampaignDTO { PackageId = 1, DbModelId = 9, TargetScope = 0 };
await Assert.ThrowsAsync(() => svc.CreateCampaignAsync(dto, Login, default));
dal.Verify(d => d.InsertTarget(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task CreateCampaignAsync_SupportLapsedPastGrace_Excluded()
{
var (svc, dal, pkg, clientDb, ent) = BuildService();
pkg.Setup(p => p.GetById(1, Login, default)).ReturnsAsync(SchemaPackage());
SetupResolvedTarget(dal, 100);
clientDb.Setup(c => c.GetById(100, Login, default)).ReturnsAsync(ClientDb(100, tenantId: 55));
ent.Setup(e => e.GetSnapshotAsync(55, Login, default)).ReturnsAsync(Snapshot(55, inSupport: false));
var dto = new RolloutCampaignDTO { PackageId = 1, DbModelId = 9, TargetScope = 0 };
await Assert.ThrowsAsync(() => svc.CreateCampaignAsync(dto, Login, default));
}
[Fact]
public async Task CreateCampaignAsync_OnPremClient_ExcludedFromAllForDbModel_ButIncludedViaSelectedList()
{
var (svc, dal, pkg, clientDb, ent) = BuildService();
pkg.Setup(p => p.GetById(1, Login, default)).ReturnsAsync(SchemaPackage());
clientDb.Setup(c => c.GetById(100, Login, default)).ReturnsAsync(ClientDb(100, tenantId: 55));
ent.Setup(e => e.GetSnapshotAsync(55, Login, default)).ReturnsAsync(Snapshot(55, deploymentType: (byte)ClientDeploymentType.OnPrem));
SetupResolvedTarget(dal, 100);
var autoScopeDto = new RolloutCampaignDTO { PackageId = 1, DbModelId = 9, TargetScope = 0 };
await Assert.ThrowsAsync(() => svc.CreateCampaignAsync(autoScopeDto, Login, default));
var selectedListDto = new RolloutCampaignDTO { PackageId = 1, DbModelId = 9, TargetScope = 2, SelectedClientDbIds = new List { 100 } };
var id = await svc.CreateCampaignAsync(selectedListDto, Login, default);
// AutoNumber's mocked query result makes the allocated id itself 0 in this test setup
// (same "AUTOID seeding" formula quirk documented elsewhere in this engagement) — the
// real assertion is that campaign creation succeeded at all, not the specific id value.
dal.Verify(d => d.InsertCampaign(It.IsAny(), Login, default), Times.Once);
_ = id;
dal.Verify(d => d.InsertTarget(It.Is(t => t.ClientDbId == 100), Login, default), Times.Once);
}
[Fact]
public async Task CreateCampaignAsync_MetadataPackage_ClientMissingRequiredFeature_Excluded()
{
var (svc, dal, pkg, clientDb, ent) = BuildService();
pkg.Setup(p => p.GetById(2, Login, default)).ReturnsAsync(MetadataPackage(2, "MOD.PRODUCTION"));
SetupResolvedTarget(dal, 100);
clientDb.Setup(c => c.GetById(100, Login, default)).ReturnsAsync(ClientDb(100, tenantId: 55));
ent.Setup(e => e.GetSnapshotAsync(55, Login, default)).ReturnsAsync(Snapshot(55, enabledFeatureCodes: "MOD.INVENTORY"));
var dto = new RolloutCampaignDTO { PackageId = 2, DbModelId = 9, TargetScope = 0 };
await Assert.ThrowsAsync(() => svc.CreateCampaignAsync(dto, Login, default));
}
[Fact]
public async Task CreateCampaignAsync_MetadataPackage_ClientHasRequiredFeature_Included()
{
var (svc, dal, pkg, clientDb, ent) = BuildService();
pkg.Setup(p => p.GetById(2, Login, default)).ReturnsAsync(MetadataPackage(2, "MOD.PRODUCTION"));
SetupResolvedTarget(dal, 100);
clientDb.Setup(c => c.GetById(100, Login, default)).ReturnsAsync(ClientDb(100, tenantId: 55));
ent.Setup(e => e.GetSnapshotAsync(55, Login, default)).ReturnsAsync(Snapshot(55, enabledFeatureCodes: "MOD.PRODUCTION"));
var dto = new RolloutCampaignDTO { PackageId = 2, DbModelId = 9, TargetScope = 0 };
var id = await svc.CreateCampaignAsync(dto, Login, default);
// AutoNumber's mocked query result makes the allocated id itself 0 in this test setup
// (same "AUTOID seeding" formula quirk documented elsewhere in this engagement) — the
// real assertion is that campaign creation succeeded at all, not the specific id value.
dal.Verify(d => d.InsertCampaign(It.IsAny(), Login, default), Times.Once);
_ = id;
}
[Fact]
public async Task CreateCampaignAsync_MetadataPackage_NoRequiredFeatureCode_SkipsGateBRegardlessOfFeatures()
{
var (svc, dal, pkg, clientDb, ent) = BuildService();
pkg.Setup(p => p.GetById(2, Login, default)).ReturnsAsync(MetadataPackage(2, requiredFeatureCode: null));
SetupResolvedTarget(dal, 100);
clientDb.Setup(c => c.GetById(100, Login, default)).ReturnsAsync(ClientDb(100, tenantId: 55));
ent.Setup(e => e.GetSnapshotAsync(55, Login, default)).ReturnsAsync(Snapshot(55)); // no enabled features at all
var dto = new RolloutCampaignDTO { PackageId = 2, DbModelId = 9, TargetScope = 0 };
var id = await svc.CreateCampaignAsync(dto, Login, default);
// AutoNumber's mocked query result makes the allocated id itself 0 in this test setup
// (same "AUTOID seeding" formula quirk documented elsewhere in this engagement) — the
// real assertion is that campaign creation succeeded at all, not the specific id value.
dal.Verify(d => d.InsertCampaign(It.IsAny(), Login, default), Times.Once);
_ = id;
}
[Fact]
public async Task CreateCampaignAsync_SchemaPackage_ClientMissingAnyFeature_StillIncluded_SchemaNeverGated()
{
var (svc, dal, pkg, clientDb, ent) = BuildService();
pkg.Setup(p => p.GetById(1, Login, default)).ReturnsAsync(SchemaPackage());
SetupResolvedTarget(dal, 100);
clientDb.Setup(c => c.GetById(100, Login, default)).ReturnsAsync(ClientDb(100, tenantId: 55));
ent.Setup(e => e.GetSnapshotAsync(55, Login, default)).ReturnsAsync(Snapshot(55)); // zero enabled features
var dto = new RolloutCampaignDTO { PackageId = 1, DbModelId = 9, TargetScope = 0 };
var id = await svc.CreateCampaignAsync(dto, Login, default);
// AutoNumber's mocked query result makes the allocated id itself 0 in this test setup
// (same "AUTOID seeding" formula quirk documented elsewhere in this engagement) — the
// real assertion is that campaign creation succeeded at all, not the specific id value.
dal.Verify(d => d.InsertCampaign(It.IsAny(), Login, default), Times.Once);
_ = id;
}
[Fact]
public async Task CreateCampaignAsync_NoEntitlementSnapshot_ExcludedNotIncludedByDefault()
{
var (svc, dal, pkg, clientDb, ent) = BuildService();
pkg.Setup(p => p.GetById(1, Login, default)).ReturnsAsync(SchemaPackage());
SetupResolvedTarget(dal, 100);
clientDb.Setup(c => c.GetById(100, Login, default)).ReturnsAsync(ClientDb(100, tenantId: 55));
ent.Setup(e => e.GetSnapshotAsync(55, Login, default)).ReturnsAsync((EntitlementSnapshotDTO?)null);
var dto = new RolloutCampaignDTO { PackageId = 1, DbModelId = 9, TargetScope = 0 };
await Assert.ThrowsAsync(() => svc.CreateCampaignAsync(dto, Login, default));
}
}