using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.GOP; using FrameworkBLL.Promotion; using FrameworkSL.Controllers.Promotion; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.GOP; using GB5Shared.DTO.Promotion; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Quartz; using Xunit; namespace FrameworkTests; /// /// PromotionExchangeIngestQuartzJob is the Tier 2 (Exchange) destination-side poller — these /// tests cover its per-item isolation (one bad package/bundle never stops the rest of the batch) /// and that it replicates the exact same OpenPackageAsync -> SubmitExecution("PromotionIngest") /// sequence ImportPromotionPackageService already runs for a manually uploaded Tier 1 file. /// public class PromotionExchangeIngestQuartzJobTests { private static Mock FakeContext() { var ctx = new Mock(); ctx.Setup(c => c.CancellationToken).Returns(CancellationToken.None); return ctx; } private static PromotionExchangeIngestQuartzJob NewJob( Mock exchange, Mock promotion, Mock gop, LoginDTO? systemLogin = null) { var systemContext = new Mock(); systemContext.Setup(c => c.GetSystemLogin()).Returns(systemLogin ?? new LoginDTO { ClientId = -1, DatabaseName = "GB5DEMO" }); return new PromotionExchangeIngestQuartzJob( exchange.Object, promotion.Object, gop.Object, systemContext.Object, NullLogger.Instance); } [Fact] public async Task Execute_NoPendingPackages_NeverCallsDownloadOrSubmit() { var exchange = new Mock(); exchange.Setup(e => e.ListPendingAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); var promotion = new Mock(); var gop = new Mock(); await NewJob(exchange, promotion, gop).Execute(FakeContext().Object); exchange.Verify(e => e.DownloadAsync(It.IsAny(), It.IsAny()), Times.Never); gop.Verify(g => g.SubmitExecution(It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Execute_OnePendingPackage_DownloadsOpensAndSubmitsOnePerBundle() { var exchange = new Mock(); exchange.Setup(e => e.ListPendingAsync(It.IsAny())) .ReturnsAsync(new List { new() { PackageId = 5, OriginEnvironmentCode = "ORIGIN1" } }); exchange.Setup(e => e.DownloadAsync(5, It.IsAny())) .ReturnsAsync(new byte[] { 1, 2, 3 }); var promotion = new Mock(); promotion.Setup(p => p.OpenPackageAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new PromotionPackageManifestDTO { Items = new List { new() { EntityTypeCode = "MENU", OriginEntityId = 1 }, new() { EntityTypeCode = "REPORTVIEW", OriginEntityId = 2 } } }); var gop = new Mock(); gop.Setup(g => g.SubmitExecution(It.IsAny(), It.IsAny())) .ReturnsAsync(new GopSubmitResponseDTO { ExecutionId = 1 }); var systemLogin = new LoginDTO { ClientId = -1, DatabaseName = "GB5DEMO" }; await NewJob(exchange, promotion, gop, systemLogin).Execute(FakeContext().Object); gop.Verify(g => g.SubmitExecution( It.Is(r => r.SourceCode == "PromotionIngest"), systemLogin), Times.Exactly(2)); } [Fact] public async Task Execute_DownloadReturnsNull_SkipsThatPackageWithoutThrowing() { var exchange = new Mock(); exchange.Setup(e => e.ListPendingAsync(It.IsAny())) .ReturnsAsync(new List { new() { PackageId = 5 } }); exchange.Setup(e => e.DownloadAsync(5, It.IsAny())) .ReturnsAsync((byte[]?)null); var promotion = new Mock(); var gop = new Mock(); await NewJob(exchange, promotion, gop).Execute(FakeContext().Object); promotion.Verify(p => p.OpenPackageAsync(It.IsAny(), It.IsAny()), Times.Never); gop.Verify(g => g.SubmitExecution(It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Execute_OnePackageThrowsWhileOpening_OtherPackageStillProcessed() { var exchange = new Mock(); exchange.Setup(e => e.ListPendingAsync(It.IsAny())) .ReturnsAsync(new List { new() { PackageId = 1 }, new() { PackageId = 2 } }); exchange.Setup(e => e.DownloadAsync(1, It.IsAny())).ReturnsAsync(new byte[] { 9 }); exchange.Setup(e => e.DownloadAsync(2, It.IsAny())).ReturnsAsync(new byte[] { 8 }); var promotion = new Mock(); promotion.Setup(p => p.OpenPackageAsync(It.Is(b => b[0] == 9), It.IsAny())) .ThrowsAsync(new InvalidOperationException("corrupt package")); promotion.Setup(p => p.OpenPackageAsync(It.Is(b => b[0] == 8), It.IsAny())) .ReturnsAsync(new PromotionPackageManifestDTO { Items = new List { new() { EntityTypeCode = "MENU", OriginEntityId = 3 } } }); var gop = new Mock(); gop.Setup(g => g.SubmitExecution(It.IsAny(), It.IsAny())) .ReturnsAsync(new GopSubmitResponseDTO { ExecutionId = 1 }); // Must not throw out of Execute even though package 1 fails mid-processing. await NewJob(exchange, promotion, gop).Execute(FakeContext().Object); gop.Verify(g => g.SubmitExecution(It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task Execute_OneBundleSubmitFails_OtherBundleInSamePackageStillSubmitted() { var exchange = new Mock(); exchange.Setup(e => e.ListPendingAsync(It.IsAny())) .ReturnsAsync(new List { new() { PackageId = 1 } }); exchange.Setup(e => e.DownloadAsync(1, It.IsAny())).ReturnsAsync(new byte[] { 1 }); var promotion = new Mock(); promotion.Setup(p => p.OpenPackageAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new PromotionPackageManifestDTO { Items = new List { new() { EntityTypeCode = "MENU", OriginEntityId = 1 }, new() { EntityTypeCode = "REPORTVIEW", OriginEntityId = 2 } } }); var gop = new Mock(); gop.Setup(g => g.SubmitExecution(It.Is(r => r.PayloadJson.Contains("\"MENU\"")), It.IsAny())) .ThrowsAsync(new InvalidOperationException("GOP unavailable")); gop.Setup(g => g.SubmitExecution(It.Is(r => r.PayloadJson.Contains("REPORTVIEW")), It.IsAny())) .ReturnsAsync(new GopSubmitResponseDTO { ExecutionId = 2 }); await NewJob(exchange, promotion, gop).Execute(FakeContext().Object); gop.Verify(g => g.SubmitExecution(It.IsAny(), It.IsAny()), Times.Exactly(2)); } }