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