using EntitlementBLL.Common; using EntitlementBLL.Exceptions; using EntitlementBLL.Implementations; using EntitlementBLL.Interfaces; using EntitlementDAL.DTOs; using EntitlementDAL.Interfaces; using GB5Shared.DTO.Framework.Login; using Moq; using Xunit; namespace EntitlementTests; /// /// Covers ClientEntitlementSnapshotBLL — the combined subscription/support/deployment-type/ /// enabled-feature read that SqlWorkbench's rollout/provisioning Gate A/Gate B (tracker §40) /// calls over HTTP via SwBLL.Integration.EntitlementSubscriptionClient. /// public class ClientEntitlementSnapshotBLLTests { private static (ClientEntitlementSnapshotBLL Svc, Mock Sub, Mock Plan, Mock ClientDal) BuildService() { var sub = new Mock(); var plan = new Mock(); var clientDal = new Mock(); 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 ClientEntitlementSnapshotBLL(sub.Object, plan.Object, clientDal.Object, loginFactory.Object); return (svc, sub, plan, clientDal); } [Fact] public async Task GetSnapshotAsync_ZeroClientId_Throws() { var (svc, _, _, _) = BuildService(); await Assert.ThrowsAsync(() => svc.GetSnapshotAsync(0, default)); } [Fact] public async Task GetSnapshotAsync_RealNegativeClientId_DoesNotThrow() { // Real GB5 ClientIds are large negative AutoNumber-allocated values, never small // positive ones — live-verified 2026-09-08 that a "clientId <= 0" guard silently // rejected every genuine client, aborting Gate A/B rollout-campaign resolution entirely. var (svc, sub, plan, clientDal) = BuildService(); sub.Setup(s => s.GetAsync(-999999001, default)).ThrowsAsync(new EntitlementNotFoundException("No subscription found.")); clientDal.Setup(d => d.GetClientDeploymentTypeAsync(-999999001, It.IsAny(), default)).ReturnsAsync((byte?)null); var result = await svc.GetSnapshotAsync(-999999001, default); Assert.Equal(-999999001, result.ClientId); Assert.Equal((byte)0, result.SubscriptionStatus); } [Fact] public async Task GetSnapshotAsync_NoSubscription_ReturnsPendingStatusWithNoFeatures() { var (svc, sub, plan, clientDal) = BuildService(); sub.Setup(s => s.GetAsync(7, default)).ThrowsAsync(new EntitlementNotFoundException("No subscription found for ClientId 7.")); clientDal.Setup(d => d.GetClientDeploymentTypeAsync(7, It.IsAny(), default)).ReturnsAsync((byte?)null); var result = await svc.GetSnapshotAsync(7, default); Assert.Equal(7, result.ClientId); Assert.Equal((byte)0, result.SubscriptionStatus); Assert.Empty(result.EnabledFeatureCodes); plan.Verify(p => p.GetPlanFeaturesAsync(It.IsAny(), It.IsAny(), default), Times.Never); } [Fact] public async Task GetSnapshotAsync_ActiveSubscription_ReturnsEnabledFeatureCodesAndDeploymentType() { var (svc, sub, plan, clientDal) = BuildService(); sub.Setup(s => s.GetAsync(7, default)).ReturnsAsync(new SubscriptionDto { ClientId = 7, PlanId = 3, SubscriptionStatus = 1, SupportValidTill = DateTime.UtcNow.AddDays(10), GracePeriodDays = 5 }); plan.Setup(p => p.GetPlanFeaturesAsync(3, It.IsAny(), default)).ReturnsAsync(new List { new() { FeatureCode = "MOD.PRODUCTION", IsEnabled = 1 }, new() { FeatureCode = "MOD.DISABLED", IsEnabled = 0 } }); clientDal.Setup(d => d.GetClientDeploymentTypeAsync(7, It.IsAny(), default)).ReturnsAsync((byte?)1); var result = await svc.GetSnapshotAsync(7, default); Assert.Equal((byte)1, result.SubscriptionStatus); Assert.Equal((byte)1, result.DeploymentType); Assert.Single(result.EnabledFeatureCodes); Assert.Contains("MOD.PRODUCTION", result.EnabledFeatureCodes); } }