using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; using Microsoft.Extensions.Logging.Abstractions; using Moq; using SwBLL.ChangeRequest; using SwBLL.ClientDatabase; using SwBLL.DbServer; using SwBLL.DdlScript; using SwBLL.Provisioning; using SwBLL.UserResourceRole; using SwDAL.CustomCode.ChangeRequest; using SwDAL.DTO.ChangeRequest; using SwDAL.DTO.ClientDatabase; using SwDAL.DTO.DbServer; using SwDAL.DTO.DdlScript; using SwDAL.Enums; using Xunit; namespace SwTests; /// /// Covers QueueExecutionAsync/ProcessQueuedExecutionsAsync (tracker §37) — the background /// alternative to Execute() built because §36.13's own live proof found the inline /// foreach-over-scripts loop can run for several minutes on a large batch and is tied to the /// calling HTTP connection's own CancellationToken. These tests mock IChangeRequestDAL directly /// so none of them touch a real database. /// public class ChangeRequestBLLAsyncExecutionTests { private static (ChangeRequestBLL Svc, Mock Dal, Mock ClientDatabaseBLL) BuildService() { var dal = new Mock(); var clientDatabaseBLL = new Mock(); var dbServerBLL = new Mock(); var targetDbExecutor = new Mock(); var ddlScriptBLL = new Mock(); var provisioner = new Mock(); dal.Setup(d => d.UpdateExecutionStatusAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); dal.Setup(d => d.AppendTimeline(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); dal.Setup(d => d.UpdateStatus(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); dal.Setup(d => d.GetLinkedApprovedDdlScripts(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { new() { DdlScriptId = 1, SqlScript = "SELECT 1", Sequence = 1 } }); clientDatabaseBLL.Setup(c => c.GetById(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new ClientDatabaseDTO { ClientDbId = 5, ClientDbCode = "acme01", DatabaseName = "acme01", DbServerId = 1, DatabaseRole = 0, TenantId = 1 }); dbServerBLL.Setup(d => d.GetByIdWithCredentials(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new DbServerDTO { DbServerId = 1, DbType = 0, HostName = "sql01" }); targetDbExecutor.Setup(t => t.BuildConnectionStringForRoleAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("conn-str"); targetDbExecutor.Setup(t => t.ExecuteScriptAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(0); ddlScriptBLL.Setup(d => d.LogExecutionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("logged"); var eventLogPublish = new EventLogPublish(null!, NullLogger.Instance); var svc = new ChangeRequestBLL( dal.Object, clientDatabaseBLL.Object, dbServerBLL.Object, targetDbExecutor.Object, ddlScriptBLL.Object, provisioner.Object, new Mock().Object, eventLogPublish, NullLogger.Instance); return (svc, dal, clientDatabaseBLL); } private static ChangeRequestDTO ApprovedDdlCr(int id = 100) => new() { ChangeRequestId = id, ClientDatabaseId = 5, CrStatus = ChangeRequestStatus.Approved, Category = ChangeRequestCategory.SchemaChange, QueryType = ChangeRequestQueryType.DDL }; private static LoginDTO TestLogin() => new() { ClientId = 1, UserId = -1 }; [Fact] public async Task Test_QueueExecutionAsync_NotFound_Throws() { var (svc, dal, _) = BuildService(); dal.Setup(d => d.GetById(999, It.IsAny(), It.IsAny())) .ReturnsAsync((ChangeRequestDTO?)null); await Assert.ThrowsAsync( () => svc.QueueExecutionAsync(999, TestLogin(), CancellationToken.None)); dal.Verify(d => d.UpdateExecutionStatusAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Test_QueueExecutionAsync_NotApproved_Throws_NoExecutionStatusChange() { var (svc, dal, _) = BuildService(); var cr = ApprovedDdlCr(); cr.CrStatus = ChangeRequestStatus.Draft; dal.Setup(d => d.GetById(cr.ChangeRequestId, It.IsAny(), It.IsAny())).ReturnsAsync(cr); await Assert.ThrowsAsync( () => svc.QueueExecutionAsync(cr.ChangeRequestId, TestLogin(), CancellationToken.None)); dal.Verify(d => d.UpdateExecutionStatusAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Test_QueueExecutionAsync_Approved_SetsQueuedAndAppendsTimeline() { var (svc, dal, _) = BuildService(); var cr = ApprovedDdlCr(); dal.Setup(d => d.GetById(cr.ChangeRequestId, It.IsAny(), It.IsAny())).ReturnsAsync(cr); var result = await svc.QueueExecutionAsync(cr.ChangeRequestId, TestLogin(), CancellationToken.None); Assert.Contains("queued", result, StringComparison.OrdinalIgnoreCase); // queuedById is stamped with the queuing user's own id (TestLogin().UserId == -1) so // ExecuteCoreAsync can later resolve the correct acting user for a background-worker // execution — see ChangeRequestQB.UPDATE_EXECUTION_STATUS's own doc comment. dal.Verify(d => d.UpdateExecutionStatusAsync(cr.ChangeRequestId, (byte)ChangeRequestExecutionStatus.Queued, It.IsAny(), It.IsAny(), -1), Times.Once); dal.Verify(d => d.AppendTimeline(cr.ChangeRequestId, CrTimelineEventType.Queued, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task Test_ProcessQueuedExecutionsAsync_NoQueuedRows_ReturnsZero_NoWork() { var (svc, dal, _) = BuildService(); dal.Setup(d => d.GetQueuedForExecutionAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(Enumerable.Empty()); var processed = await svc.ProcessQueuedExecutionsAsync(TestLogin(), CancellationToken.None); Assert.Equal(0, processed); dal.Verify(d => d.UpdateStatus(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Test_ProcessQueuedExecutionsAsync_OneQueuedRow_RunsIt_MarksDone() { var (svc, dal, _) = BuildService(); var cr = ApprovedDdlCr(); dal.Setup(d => d.GetQueuedForExecutionAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new List { cr }); var processed = await svc.ProcessQueuedExecutionsAsync(TestLogin(), CancellationToken.None); Assert.Equal(1, processed); // Running set first, Done set last — both real transitions, not skipped. dal.Verify(d => d.UpdateExecutionStatusAsync(cr.ChangeRequestId, (byte)ChangeRequestExecutionStatus.Running, It.IsAny(), It.IsAny()), Times.Once); dal.Verify(d => d.UpdateExecutionStatusAsync(cr.ChangeRequestId, (byte)ChangeRequestExecutionStatus.Done, It.IsAny(), It.IsAny()), Times.Once); dal.Verify(d => d.UpdateExecutionStatusAsync(cr.ChangeRequestId, (byte)ChangeRequestExecutionStatus.Failed, It.IsAny(), It.IsAny()), Times.Never); // The real CR-status transition Execute() itself performs — proves ExecuteCoreAsync ran. dal.Verify(d => d.UpdateStatus(cr.ChangeRequestId, (byte)ChangeRequestStatus.Executed, (byte)ChangeRequestStatus.Approved, It.IsAny(), null, It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task Test_ProcessQueuedExecutionsAsync_OneRowFails_MarksFailed_DoesNotThrow_ContinuesBatch() { var (svc, dal, _) = BuildService(); var failing = ApprovedDdlCr(id: 100); var succeeds = ApprovedDdlCr(id: 101); dal.Setup(d => d.GetQueuedForExecutionAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new List { failing, succeeds }); // clientDatabaseBLL isn't parameterized per-CR in BuildService(), so force the failure via // GetLinkedApprovedDdlScripts throwing only for the first CR's id. dal.Setup(d => d.GetLinkedApprovedDdlScripts(failing.ChangeRequestId, It.IsAny(), It.IsAny())) .ThrowsAsync(new InvalidOperationException("boom")); var processed = await svc.ProcessQueuedExecutionsAsync(TestLogin(), CancellationToken.None); Assert.Equal(1, processed); // only the second CR counts as processed dal.Verify(d => d.UpdateExecutionStatusAsync(failing.ChangeRequestId, (byte)ChangeRequestExecutionStatus.Failed, It.IsAny(), It.IsAny()), Times.Once); dal.Verify(d => d.UpdateExecutionStatusAsync(succeeds.ChangeRequestId, (byte)ChangeRequestExecutionStatus.Done, It.IsAny(), It.IsAny()), Times.Once); } }