using System.Data.Common; using EntitlementBLL.Common; using EntitlementBLL.Implementations; using EntitlementBLL.Interfaces; using EntitlementBLL.Options; using EntitlementDAL.DTOs; using EntitlementDAL.Interfaces; using GB5Shared.DTO.Framework.CommonConfig; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace EntitlementTests; /// /// ECDSA bundle sign/verify round-trip — all repos mocked, no real DB/Redis. Uses the /// dev-fallback ephemeral key path (EcdsaKeyOptions left empty) since no real key is committed. /// public class EntitlementBundleTests { private static (EntitlementBundleService svc, Mock bundleDal) BuildService() { var entitlementService = new Mock(); entitlementService .Setup(s => s.GetResolvedBundleAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new EntitlementBundleDto { TenantId = 42, Subscriptions = new List { 1 }, Entitlements = new List { new() { Feature = "MOD.INVENTORY", Enabled = true } }, Licenses = new Dictionary(), BetaPrograms = new List(), ValidUntil = DateTime.UtcNow.AddHours(1), IssuedAt = DateTime.UtcNow }); var bundleDal = new Mock(); bundleDal.Setup(b => b.ExpirePriorCurrentAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); bundleDal.Setup(b => b.SaveAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); // Real AutoNumber wired to a mocked IQueryExecutor — AutoNumber.GetNumberAsync is not // virtual, so it can't be mocked directly; instead mock the QueryAsync call it makes // internally to simulate the MAUTONUMBER increment round-trip. var queryExecutor = new Mock(); queryExecutor .Setup(q => q.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { 100 }); 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 }); // Empty EcdsaKeyOptions — exercises the dev-fallback ephemeral key path deliberately, // matching how this module runs before a real key is configured. var keyOptions = Microsoft.Extensions.Options.Options.Create(new EcdsaKeyOptions { KeyId = "test-key-1" }); var svc = new EntitlementBundleService( entitlementService.Object, bundleDal.Object, autoNumber, queryExecutor.Object, loginFactory.Object, keyOptions, NullLogger.Instance); return (svc, bundleDal); } [Fact] public async Task Test_BundleSignVerify_RoundTrip() { var (svc, _) = BuildService(); var signed = await svc.IssueSignedBundleAsync(42, CancellationToken.None); Assert.NotNull(signed); Assert.NotEmpty(signed.Sig); Assert.Equal("test-key-1", signed.Kid); Assert.True(svc.VerifyBundle(signed)); } [Fact] public async Task Test_BundleExpired_VerifyFails() { var (svc, _) = BuildService(); var signed = await svc.IssueSignedBundleAsync(42, CancellationToken.None); // Simulate an expired bundle by rewriting ValidUntil into the past — VerifyBundle must // reject on expiry alone, before even re-checking the signature. signed.Payload.ValidUntil = DateTime.UtcNow.AddDays(-1); Assert.False(svc.VerifyBundle(signed)); } }