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