using System; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.GOP; using FrameworkDAL.CustomCode.GOP; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.GOP; using GB5Shared.Deployment; using GB5Shared.EventLogPublish; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; namespace FrameworkTests; /// /// GOP had zero test coverage anywhere in the repo before this (confirmed by search) — worth /// closing here specifically for the Approval gate, since Metadata Promotion's whole /// Ingest->Approval->Import flow depends on ApproveGopExecutionStep/RejectGopExecutionStep /// enforcing the PendingApproval precondition correctly. Mirrors SwTests' /// ChangeRequestBLLExecutionRoleTests guard-clause pattern (illegal-transition tests, not /// happy-path-only). GopQueueBLL's constructor also takes IGopFlowDAL/IValidation/IGB5Environment, /// none of which ApproveGopExecutionStep/RejectGopExecutionStep touch — passed as /// Mock.Of<T>()/null defaults accordingly. /// public class GopQueueBLLApprovalGuardTests { private static LoginDTO Login() => new() { ClientId = -1, UserId = 7 }; private static GopQueueBLL BllWith(Mock queueDal) => new( Mock.Of(), queueDal.Object, Mock.Of(), Mock.Of(), new EventLogPublish(new Dapr.Client.DaprClientBuilder().Build(), NullLogger.Instance)); private static GopExecutionHeaderDTO HeaderWithStatus(string status) => new() { ExecutionId = 55, ClientId = -1, Status = status, CurrentNodeId = 3, CurrentNodeCode = "approval-step", FlowId = 1, FlowCode = "TestFlow", SnapshotVersion = "1" }; [Fact] public async Task Approve_ExecutionNotFound_Throws() { var queueDal = new Mock(); queueDal.Setup(d => d.GetExecutionById(-1, 55, It.IsAny())) .ReturnsAsync((GopExecutionHeaderDTO?)null); var bll = BllWith(queueDal); await Assert.ThrowsAsync( () => bll.ApproveGopExecutionStep(55, null, Login())); } [Fact] public async Task Approve_NotPendingApproval_ThrowsAndNeverResumes() { var queueDal = new Mock(MockBehavior.Strict); queueDal.Setup(d => d.GetExecutionById(-1, 55, It.IsAny())) .ReturnsAsync(HeaderWithStatus("Running")); // not PendingApproval var bll = BllWith(queueDal); await Assert.ThrowsAsync( () => bll.ApproveGopExecutionStep(55, null, Login())); // Strict mock with no ResumeExecutionAfterApproval setup — reaching it would have // thrown a MockException instead, proving Approve never resumes a non-pending execution. } [Fact] public async Task Approve_PendingApproval_ResumesExecution() { var queueDal = new Mock(); queueDal.Setup(d => d.GetExecutionById(-1, 55, It.IsAny())) .ReturnsAsync(HeaderWithStatus("PendingApproval")); queueDal.Setup(d => d.ResumeExecutionAfterApproval(-1, 55, It.IsAny())) .ReturnsAsync(true); var bll = BllWith(queueDal); var result = await bll.ApproveGopExecutionStep(55, "looks good", Login()); Assert.Contains("55", result); queueDal.Verify(d => d.ResumeExecutionAfterApproval(-1, 55, It.IsAny()), Times.Once); } [Fact] public async Task Approve_ResumeRacesAndFails_Throws() { // Another request already moved it out of PendingApproval between the read and the // resume attempt — ResumeExecutionAfterApproval's own compare-and-swap returns false. var queueDal = new Mock(); queueDal.Setup(d => d.GetExecutionById(-1, 55, It.IsAny())) .ReturnsAsync(HeaderWithStatus("PendingApproval")); queueDal.Setup(d => d.ResumeExecutionAfterApproval(-1, 55, It.IsAny())) .ReturnsAsync(false); var bll = BllWith(queueDal); await Assert.ThrowsAsync( () => bll.ApproveGopExecutionStep(55, null, Login())); } [Fact] public async Task Reject_EmptyReason_ThrowsWithoutTouchingDal() { var queueDal = new Mock(MockBehavior.Strict); var bll = BllWith(queueDal); await Assert.ThrowsAsync( () => bll.RejectGopExecutionStep(55, "", Login())); } [Fact] public async Task Reject_NotPendingApproval_ThrowsAndNeverDeadLetters() { var queueDal = new Mock(MockBehavior.Strict); queueDal.Setup(d => d.GetExecutionById(-1, 55, It.IsAny())) .ReturnsAsync(HeaderWithStatus("Executed")); // terminal, not PendingApproval var bll = BllWith(queueDal); await Assert.ThrowsAsync( () => bll.RejectGopExecutionStep(55, "no longer needed", Login())); } [Fact] public async Task Reject_PendingApproval_DeadLettersWithReason() { var queueDal = new Mock(); queueDal.Setup(d => d.GetExecutionById(-1, 55, It.IsAny())) .ReturnsAsync(HeaderWithStatus("PendingApproval")); queueDal.Setup(d => d.UpdateExecutionStatus(-1, 55, "Failed", It.IsAny(), null, It.IsAny())) .Returns(Task.CompletedTask); queueDal.Setup(d => d.InsertDeadLetter(It.IsAny(), It.IsAny())) .ReturnsAsync(1); var bll = BllWith(queueDal); var result = await bll.RejectGopExecutionStep(55, "not promotable — references client-only data", Login()); Assert.Contains("55", result); queueDal.Verify(d => d.InsertDeadLetter( It.Is(dl => dl.ErrorMessage.Contains("not promotable")), It.IsAny()), Times.Once); } }