using System; using System.Collections.Generic; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.Promotion; using FrameworkDAL.CustomCode.Promotion; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Promotion; using GB5Shared.Promotion; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; namespace FrameworkTests; /// /// PromotionBLL is the single implementation behind both the synchronous "Export Package" HTTP /// endpoint and PromotionExportNodeExecutor (the GOP node kept for a possible future automated /// trigger) — this covers BuildExportBundleAsync's validation-blocking behavior directly, plus /// BuildPackageAsync/OpenPackageAsync's round trip through a mocked (not real-crypto) /// IPromotionPackageCrypto, since the crypto itself is covered separately in /// PromotionPackageCryptoTests. /// public class PromotionBLLTests { private static LoginDTO Login() => new() { ClientId = -1, UserId = 7 }; private static IServiceProvider ServiceProviderWith(string entityTypeCode, IPromotableEntity entity) { var services = new ServiceCollection(); services.AddKeyedSingleton(entityTypeCode, entity); return services.BuildServiceProvider(); } [Fact] public async Task BuildExportBundleAsync_UnknownEntityType_ThrowsBeforeExporting() { var sp = new ServiceCollection().BuildServiceProvider(); // nothing registered for "MENU" var bll = new PromotionBLL(sp, Mock.Of(), Mock.Of(), NullLogger.Instance); await Assert.ThrowsAsync( () => bll.BuildExportBundleAsync("MENU", 42, Login(), CancellationToken.None)); } [Fact] public async Task BuildExportBundleAsync_ValidationBlocked_ThrowsWithReason() { var entity = new Mock(); entity.Setup(e => e.ExportForPromotionAsync(42, It.IsAny(), It.IsAny())) .ReturnsAsync(new PromotionBundleDTO { EntityTypeCode = "MENU", OriginEntityId = 42 }); entity.Setup(e => e.ValidateForPromotionAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(PromotionValidationResultDTO.Block("references a client-only addon column")); var sp = ServiceProviderWith("MENU", entity.Object); var bll = new PromotionBLL(sp, Mock.Of(), Mock.Of(), NullLogger.Instance); var ex = await Assert.ThrowsAsync( () => bll.BuildExportBundleAsync("MENU", 42, Login(), CancellationToken.None)); Assert.Contains("addon", ex.Message); } [Fact] public async Task BuildExportBundleAsync_Promotable_ReturnsTheBundle() { var entity = new Mock(); entity.Setup(e => e.ExportForPromotionAsync(42, It.IsAny(), It.IsAny())) .ReturnsAsync(new PromotionBundleDTO { EntityTypeCode = "MENU", OriginEntityId = 42 }); entity.Setup(e => e.ValidateForPromotionAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(PromotionValidationResultDTO.Ok()); var sp = ServiceProviderWith("MENU", entity.Object); var bll = new PromotionBLL(sp, Mock.Of(), Mock.Of(), NullLogger.Instance); var bundle = await bll.BuildExportBundleAsync("MENU", 42, Login(), CancellationToken.None); Assert.Equal("MENU", bundle.EntityTypeCode); Assert.Equal(42, bundle.OriginEntityId); } [Fact] public async Task BuildPackageAsync_NoItems_Throws() { var bll = new PromotionBLL( new ServiceCollection().BuildServiceProvider(), Mock.Of(), Mock.Of(), NullLogger.Instance); await Assert.ThrowsAsync(() => bll.BuildPackageAsync(new List(), "IMPSYS-PARTNERX", Login(), CancellationToken.None)); } [Fact] public async Task BuildPackageAsync_ThenOpenPackageAsync_RoundTripsTheManifest() { // A fake "crypto" that just passes bytes through unchanged, so this test isolates // PromotionBLL's own manifest-wrapping/serialization logic from real cryptography // (covered separately in PromotionPackageCryptoTests). var crypto = new Mock(); byte[]? capturedPlaintext = null; crypto.Setup(c => c.EncryptPackageAsync(It.IsAny(), "IMPSYS-PARTNERX", It.IsAny(), It.IsAny())) .Callback((pt, _, _, _) => capturedPlaintext = pt) .ReturnsAsync(() => capturedPlaintext!); crypto.Setup(c => c.DecryptPackageAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(() => capturedPlaintext!); var bll = new PromotionBLL( new ServiceCollection().BuildServiceProvider(), crypto.Object, Mock.Of(), NullLogger.Instance); var items = new List { new() { EntityTypeCode = "MENU", OriginEntityId = 42, OriginEnvironmentCode = "CLIENT-ACME" }, new() { EntityTypeCode = "PORTLETTYPE", OriginEntityId = 7, OriginEnvironmentCode = "CLIENT-ACME" } }; var packageBytes = await bll.BuildPackageAsync(items, "IMPSYS-PARTNERX", Login(), CancellationToken.None); var manifest = await bll.OpenPackageAsync(packageBytes, CancellationToken.None); Assert.Equal(2, manifest.Items.Count); Assert.Equal("MENU", manifest.Items[0].EntityTypeCode); Assert.Equal(42, manifest.Items[0].OriginEntityId); Assert.Equal("PORTLETTYPE", manifest.Items[1].EntityTypeCode); Assert.Equal("CLIENT-ACME", manifest.OriginEnvironmentCode); } [Fact] public async Task GetSelectListPromotableEntityType_ReturnsDalResultAsJson() { var entityTypeDal = new Mock(); entityTypeDal.Setup(d => d.GetSelectList(It.IsAny(), It.IsAny())) .ReturnsAsync(new[] { new FrameworkDAL.DTO.Promotion.PromotableEntityTypeDTO { EntityTypeCode = "MENU", EntityTypeName = "Menu" } }); var bll = new PromotionBLL( new ServiceCollection().BuildServiceProvider(), Mock.Of(), entityTypeDal.Object, NullLogger.Instance); var json = await bll.GetSelectListPromotableEntityType(Login(), CancellationToken.None); Assert.Contains("MENU", json); Assert.Contains("Menu", json); } }