using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.GOP; using FrameworkBLL.GOP.Worker; using FrameworkDAL.CustomCode.GOP; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.GOP; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; namespace FrameworkTests; /// /// Regression coverage for a real bug found live on 2026-08-21 verifying Metadata Promotion's /// first-ever GOP execution on GB5DEMO: GopExecutionPipeline.ExecuteAsync takes an explicit /// `clientId` (the execution's OWN tenant, correctly used by GetExecutionById/UpdateExecutionStatus /// etc.) alongside a `loginDTO` used purely for DB connection purposes. GopFlowDAL.GetSnapshotSteps/ /// GetSnapshotStepEdges instead filtered by loginDTO.ClientId — fine for a normal per-tenant flow /// where the two happen to match, but wrong for exactly Promotion's own design: a background /// worker's LoginDTO carries the real business tenant (e.g. GB5DEMO's -1399999868, built from /// MSERVERCONFIG), while the queued execution itself belongs to the framework-level ClientId=-1 /// tenant. The mismatch made GetSnapshotSteps return zero rows, which the pipeline's own "no steps /// found" branch silently treated as a completed Success — an execution that never touched a single /// node, approval gate included, reporting as if it had worked perfectly. /// public class GopExecutionPipelineTenantIdTests { private const int ExecutionTenantId = -1; // the execution's own ClientId (e.g. Promotion's framework-level tenant) private const int WorkerConnectionTenantId = -1399999868; // the worker's LoginDTO.ClientId (GB5DEMO's real business tenant) private static GopExecutionHeaderDTO FreshHeader() => new() { ClientId = ExecutionTenantId, ExecutionId = 1033, FlowId = 42, FlowCode = "PromotionIngest", SnapshotVersion = "v1", Status = "Queued", SourcePayloadJson = "{}", CurrentNodeId = null }; private static GopFlowSnapshotStepDTO ApprovalRootStep() => new() { ClientId = ExecutionTenantId, NodeId = 1, NodeCode = "approve", NodeType = "Approval", AssignedRole = "PROMOTIONREVIEWER" }; [Fact] public async Task ExecuteAsync_WhenExecutionTenantDiffersFromLoginTenant_StillLoadsAndTraversesSteps() { var queueDal = new Mock(); var flowDal = new Mock(); var hubNotifier = new Mock(); var header = FreshHeader(); queueDal.Setup(q => q.GetExecutionById(ExecutionTenantId, header.ExecutionId, It.IsAny())) .ReturnsAsync(header); // The regression: GetSnapshotSteps must be called with the EXECUTION's tenant // (ExecutionTenantId), not the connecting LoginDTO's ClientId (WorkerConnectionTenantId). // Wired to return the real step ONLY for the correct tenant, and empty for the wrong one — // so if the bug reappears, the pipeline would immediately go down the "no steps found" path. flowDal.Setup(f => f.GetSnapshotSteps(ExecutionTenantId, header.FlowId, header.SnapshotVersion, It.IsAny())) .ReturnsAsync(new List { ApprovalRootStep() }); flowDal.Setup(f => f.GetSnapshotSteps(WorkerConnectionTenantId, header.FlowId, header.SnapshotVersion, It.IsAny())) .ReturnsAsync(new List()); // what the bug used to return flowDal.Setup(f => f.GetSnapshotStepEdges(ExecutionTenantId, header.FlowId, header.SnapshotVersion, It.IsAny(), It.IsAny())) .ReturnsAsync(new List()); var sp = new ServiceCollection().BuildServiceProvider(); var pipeline = new GopExecutionPipeline( queueDal.Object, flowDal.Object, sp, hubNotifier.Object, NullLogger.Instance); // loginDTO.ClientId deliberately differs from the execution's own tenant — exactly the // real GopWorkerService shape (workerLogin.ClientId comes from MSERVERCONFIG, not from the // queued execution row). var workerLogin = new LoginDTO { ClientId = WorkerConnectionTenantId, DatabaseName = "GB5DEMO" }; await pipeline.ExecuteAsync(header.ExecutionId, ExecutionTenantId, workerLogin, CancellationToken.None); // Proves real traversal reached the Approval gate (only possible if GetSnapshotSteps // actually returned the step) rather than short-circuiting via the "no steps found" branch. queueDal.Verify(q => q.UpdateExecutionApprovalPending( ExecutionTenantId, header.ExecutionId, 1, "approve", It.IsAny(), "PROMOTIONREVIEWER", null, It.IsAny()), Times.Once); // Never silently marked Success/Failed via the empty-steps branch. queueDal.Verify(q => q.UpdateExecutionStatus( It.IsAny(), header.ExecutionId, "Success", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } }