using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.GOP; using FrameworkBLL.Promotion; using GB5Shared.Deployment; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.GOP; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; namespace FrameworkTests; /// /// PromotionFlowProvisioningBLL authors the real "PromotionIngest" GOP flow (Ingest -> Approval -> /// Import) via IGopFlowBLL's design-time API rather than hand-inserted snapshot rows, so /// GopPublishService's own DAG/Approval-role validation actually runs. Covers idempotency (the /// common re-run case) and the happy-path shape of what gets authored (3 steps, 2 edges, the /// Approval step's required AssignedRole, and that PublishGopFlow/SaveGopSourceBinding both use the /// FlowId that SaveGopFlow assigned). /// public class PromotionFlowProvisioningBLLTests { private static LoginDTO Login() => new() { ClientId = -1, UserId = 7 }; private static IGB5Environment DevEnvironment() => Mock.Of(e => e.GopEnvironmentCode == "DEV"); [Fact] public async Task AlreadyProvisioned_IsANoOp() { var gopFlowBLL = new Mock(MockBehavior.Strict); gopFlowBLL.Setup(b => b.GetAllSourceBindings(It.IsAny())) .ReturnsAsync(new List { new() { SourceCode = "PromotionIngest", IsActive = true, FlowId = 501, SnapshotVersion = "v1" } }); var bll = new PromotionFlowProvisioningBLL( gopFlowBLL.Object, DevEnvironment(), NullLogger.Instance); var result = await bll.EnsureIngestFlowProvisionedAsync(Login(), CancellationToken.None); Assert.Contains("501", result); // Strict mock with only GetAllSourceBindings set up — reaching GetGopFlowList/SaveGopFlow/etc. // would have thrown a MockException instead, proving this really is a no-op. } [Fact] public async Task NotYetProvisioned_CreatesFlowStepsEdgesPublishesAndBinds() { var gopFlowBLL = new Mock(); gopFlowBLL.Setup(b => b.GetAllSourceBindings(It.IsAny())) .ReturnsAsync(new List()); gopFlowBLL.Setup(b => b.GetGopFlowList(It.IsAny(), It.IsAny())) .ReturnsAsync(new List()); gopFlowBLL.Setup(b => b.SaveGopFlow(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((dto, _, _) => dto.FlowId = 501) .ReturnsAsync("Saved 501"); gopFlowBLL.Setup(b => b.GetGopFlowGraph(501, It.IsAny(), It.IsAny())) .ReturnsAsync(new GopFlowGraphDTO()); // empty — no steps yet var stepIdCounter = 1000; gopFlowBLL.Setup(b => b.SaveGopFlowStep(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((dto, _, _) => dto.FlowStepId = ++stepIdCounter) .ReturnsAsync("Saved step"); var savedEdges = new List(); gopFlowBLL.Setup(b => b.SaveGopFlowStepEdge(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((dto, _, _) => savedEdges.Add(dto)) .ReturnsAsync("Saved edge"); gopFlowBLL.Setup(b => b.PublishGopFlow(501, "v1", It.IsAny(), It.IsAny())) .ReturnsAsync(GopPublishResultDTO.Ok(9001, "v1")); GopSourceBindingDTO? capturedBinding = null; gopFlowBLL.Setup(b => b.SaveGopSourceBinding(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((dto, _, _) => capturedBinding = dto) .ReturnsAsync("Saved binding"); var bll = new PromotionFlowProvisioningBLL( gopFlowBLL.Object, DevEnvironment(), NullLogger.Instance); var result = await bll.EnsureIngestFlowProvisionedAsync(Login(), CancellationToken.None); Assert.Contains("501", result); Assert.Contains("v1", result); gopFlowBLL.Verify(b => b.SaveGopFlowStep( It.Is(s => s.NodeType == "PromotionIngest" && s.FlowId == 501), It.IsAny(), It.IsAny()), Times.Once); gopFlowBLL.Verify(b => b.SaveGopFlowStep( It.Is(s => s.NodeType == "Approval" && !string.IsNullOrWhiteSpace(s.AssignedRole)), It.IsAny(), It.IsAny()), Times.Once); gopFlowBLL.Verify(b => b.SaveGopFlowStep( It.Is(s => s.NodeType == "PromotionImport"), It.IsAny(), It.IsAny()), Times.Once); Assert.Equal(2, savedEdges.Count); Assert.All(savedEdges, e => Assert.Equal("OnSuccess", e.EdgeCondition)); Assert.NotNull(capturedBinding); Assert.Equal("PromotionIngest", capturedBinding!.SourceCode); Assert.Equal(501, capturedBinding.FlowId); Assert.Equal("v1", capturedBinding.SnapshotVersion); } [Fact] public async Task ExistingFlowWithNoSteps_ReusesFlowIdRatherThanCreatingANewFlow() { var gopFlowBLL = new Mock(); gopFlowBLL.Setup(b => b.GetAllSourceBindings(It.IsAny())) .ReturnsAsync(new List()); gopFlowBLL.Setup(b => b.GetGopFlowList(It.IsAny(), It.IsAny())) .ReturnsAsync(new List { new() { FlowId = 777, FlowCode = "PromotionIngest" } }); gopFlowBLL.Setup(b => b.GetGopFlowGraph(777, It.IsAny(), It.IsAny())) .ReturnsAsync(new GopFlowGraphDTO()); var stepIdCounter = 2000; gopFlowBLL.Setup(b => b.SaveGopFlowStep(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((dto, _, _) => dto.FlowStepId = ++stepIdCounter) .ReturnsAsync("Saved step"); gopFlowBLL.Setup(b => b.SaveGopFlowStepEdge(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("Saved edge"); gopFlowBLL.Setup(b => b.PublishGopFlow(777, "v1", It.IsAny(), It.IsAny())) .ReturnsAsync(GopPublishResultDTO.Ok(9002, "v1")); gopFlowBLL.Setup(b => b.SaveGopSourceBinding(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("Saved binding"); var bll = new PromotionFlowProvisioningBLL( gopFlowBLL.Object, DevEnvironment(), NullLogger.Instance); var result = await bll.EnsureIngestFlowProvisionedAsync(Login(), CancellationToken.None); Assert.Contains("777", result); gopFlowBLL.Verify(b => b.SaveGopFlow(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task PublishFailure_ThrowsWithTheValidationErrors() { var gopFlowBLL = new Mock(); gopFlowBLL.Setup(b => b.GetAllSourceBindings(It.IsAny())) .ReturnsAsync(new List()); gopFlowBLL.Setup(b => b.GetGopFlowList(It.IsAny(), It.IsAny())) .ReturnsAsync(new List()); gopFlowBLL.Setup(b => b.SaveGopFlow(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((dto, _, _) => dto.FlowId = 501) .ReturnsAsync("Saved"); gopFlowBLL.Setup(b => b.GetGopFlowGraph(501, It.IsAny(), It.IsAny())) .ReturnsAsync(new GopFlowGraphDTO()); gopFlowBLL.Setup(b => b.SaveGopFlowStep(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("Saved step"); gopFlowBLL.Setup(b => b.SaveGopFlowStepEdge(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("Saved edge"); gopFlowBLL.Setup(b => b.PublishGopFlow(501, "v1", It.IsAny(), It.IsAny())) .ReturnsAsync(GopPublishResultDTO.Failure(new List { "Approval step 'approve' requires an AssignedRole" })); var bll = new PromotionFlowProvisioningBLL( gopFlowBLL.Object, DevEnvironment(), NullLogger.Instance); var ex = await Assert.ThrowsAsync( () => bll.EnsureIngestFlowProvisionedAsync(Login(), CancellationToken.None)); Assert.Contains("AssignedRole", ex.Message); gopFlowBLL.Verify(b => b.SaveGopSourceBinding( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } }