diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a4ebaba..06cc6da 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -88,8 +88,8 @@ jobs: "Services and exposures line coverage: $coveredLines/$($lines.Count) ($percentage)" | Write-Output - if ($lineRate -le 0.90) { - throw "Services and exposures line coverage must be greater than 90%; actual: $percentage." + if ($lineRate -le 0.95) { + throw "Services and exposures line coverage must be greater than 95%; actual: $percentage." } - name: Upload test results @@ -207,4 +207,4 @@ jobs: - name: Deploy coverage report id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 diff --git a/src/cCoder.Workflow.Engine.Tests/FlowCommunicationProcessingServiceTests.ConnectWorkflowRequestAsync.cs b/src/cCoder.Workflow.Engine.Tests/FlowCommunicationProcessingServiceTests.ConnectWorkflowRequestAsync.cs new file mode 100644 index 0000000..b3fabdb --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/FlowCommunicationProcessingServiceTests.ConnectWorkflowRequestAsync.cs @@ -0,0 +1,103 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Activities.Models; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class FlowCommunicationProcessingServiceTests +{ + [Fact] + public async Task ShouldConnectWorkflowRequestAsync() + { + // Given + WorkflowRequest request = CreateWorkflowRequest(); + + workflowHubConnectionBrokerMock + .Setup(expression: broker => broker.ConnectAsync( + url: $"{request.Api}Hubs/Workflow")) + .Returns(value: Task.CompletedTask); + + workflowHubConnectionBrokerMock + .Setup(expression: broker => broker.SendAsync( + level: "info", + message: It.IsAny(), + instanceId: request.InstanceId.ToString())) + .Returns(value: Task.CompletedTask); + + var service = CreateService(); + + // When + await service.ConnectWorkflowRequestAsync(workflowRequest: request); + + // Then + workflowHubConnectionBrokerMock.VerifyAll(); + loggingBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldLogWarningWhenWorkflowConnectionFailsAsync() + { + // Given + WorkflowRequest request = CreateWorkflowRequest(); + + workflowHubConnectionBrokerMock + .Setup(expression: broker => broker.ConnectAsync( + url: $"{request.Api}Hubs/Workflow")) + .Throws(exception: new InvalidOperationException()); + + workflowHubConnectionBrokerMock + .Setup(expression: broker => broker.DisconnectAsync()) + .Returns(value: ValueTask.CompletedTask); + + var service = CreateService(); + + // When + await service.ConnectWorkflowRequestAsync(workflowRequest: request); + + // Then + workflowHubConnectionBrokerMock.VerifyAll(); + loggingBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldDisconnectAndLogWhenWorkflowSendFailsAsync() + { + // Given + WorkflowRequest request = CreateWorkflowRequest(); + + workflowHubConnectionBrokerMock + .Setup(expression: broker => broker.ConnectAsync( + url: $"{request.Api}Hubs/Workflow")) + .Returns(value: Task.CompletedTask); + + workflowHubConnectionBrokerMock + .Setup(expression: broker => broker.SendAsync( + level: "info", + message: It.IsAny(), + instanceId: request.InstanceId.ToString())) + .Throws(exception: new InvalidOperationException("send failed")); + + workflowHubConnectionBrokerMock + .Setup(expression: broker => broker.DisconnectAsync()) + .Returns(value: ValueTask.CompletedTask); + + var service = CreateService(); + + // When + await service.ConnectWorkflowRequestAsync(workflowRequest: request); + + // Then + workflowHubConnectionBrokerMock.VerifyAll(); + + loggingBrokerMock.Verify( + expression: broker => broker.LogError( + message: "{Message}", + args: It.Is(match: arguments => + arguments.Single() as string == "send failed")), + times: Times.Once()); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/FlowCommunicationProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Engine.Tests/FlowCommunicationProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..25ef0cb --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/FlowCommunicationProcessingServiceTests.Exceptions.cs @@ -0,0 +1,50 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Activities.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class FlowCommunicationProcessingServiceTests +{ + [Theory] + [MemberData( + nameof(WorkflowRequestOrchestrationServiceTests.ExceptionMappings), + MemberType = typeof(WorkflowRequestOrchestrationServiceTests))] + public async Task ShouldMapLogWorkflowRequestAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + const string message = "message"; + WorkflowRequest request = CreateWorkflowRequest(); + + loggingBrokerMock + .Setup(expression: broker => broker.LogError( + message: "{Message}", + args: It.IsAny())) + .Throws(exception: exception); + + var service = CreateService(); + + // When + Func action = async () => await service + .LogWorkflowRequestAsync( + workflowRequest: request, + level: WorkflowLogLevel.Error, + message: message); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/FlowCommunicationProcessingServiceTests.LogWorkflowRequestAsync.cs b/src/cCoder.Workflow.Engine.Tests/FlowCommunicationProcessingServiceTests.LogWorkflowRequestAsync.cs new file mode 100644 index 0000000..b053931 --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/FlowCommunicationProcessingServiceTests.LogWorkflowRequestAsync.cs @@ -0,0 +1,106 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Activities.Models; +using cCoder.Workflow.Engine.Models.Exceptions; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class FlowCommunicationProcessingServiceTests +{ + [Fact] + public async Task ShouldLogWorkflowRequestAsync() + { + // Given + const string message = "message"; + WorkflowRequest request = CreateWorkflowRequest(); + var service = CreateService(); + + // When + await service.LogWorkflowRequestAsync( + workflowRequest: request, + level: WorkflowLogLevel.Info, + message: message); + + await service.LogWorkflowRequestAsync( + workflowRequest: request, + level: WorkflowLogLevel.Error, + message: message); + + await service.LogWorkflowRequestAsync( + workflowRequest: request, + level: WorkflowLogLevel.Fatal, + message: message); + + // Then + loggingBrokerMock.Verify( + expression: broker => broker.LogError( + message: "{Message}", + args: It.Is(match: arguments => + arguments.Single() as string == message)), + times: Times.Exactly(callCount: 2)); + } + + [Fact] + public async Task ShouldTruncateLongWorkflowRequestLogAsync() + { + // Given + string message = new(c: 'a', count: 4001); + string loggedMessage = null; + WorkflowRequest request = CreateWorkflowRequest(); + + loggingBrokerMock + .Setup(expression: broker => broker.LogError( + message: "{Message}", + args: It.IsAny())) + .Callback(action: (_, arguments) => + loggedMessage = arguments.Single() as string); + + var service = CreateService(); + + // When + await service.LogWorkflowRequestAsync( + workflowRequest: request, + level: WorkflowLogLevel.Error, + message: message); + + // Then + loggedMessage.Length + .Should() + .BeLessThan(expected: message.Length); + + loggedMessage + .Should() + .Contain(expected: "characters cut"); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task ShouldRejectInvalidWorkflowRequestAsync(string api) + { + // Given + WorkflowRequest request = CreateWorkflowRequest(); + request.Api = api; + var service = CreateService(); + + // When + Func action = async () => await service + .LogWorkflowRequestAsync( + workflowRequest: request, + level: WorkflowLogLevel.Info, + message: "message"); + + // Then + await action + .Should() + .ThrowAsync(); + + loggingBrokerMock.VerifyNoOtherCalls(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/FlowCommunicationProcessingServiceTests.cs b/src/cCoder.Workflow.Engine.Tests/FlowCommunicationProcessingServiceTests.cs new file mode 100644 index 0000000..ea90762 --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/FlowCommunicationProcessingServiceTests.cs @@ -0,0 +1,33 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Activities.Models; +using cCoder.Workflow.Engine.Brokers; +using cCoder.Workflow.Engine.Brokers.Loggings; +using cCoder.Workflow.Engine.Services.Processings; +using Moq; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class FlowCommunicationProcessingServiceTests +{ + private readonly Mock loggingBrokerMock = new(); + + private readonly Mock + workflowHubConnectionBrokerMock = + new(behavior: MockBehavior.Strict); + + private FlowCommunicationProcessingService CreateService() => + new( + logger: loggingBrokerMock.Object, + workflowHubConnectionBroker: + workflowHubConnectionBrokerMock.Object); + + private static WorkflowRequest CreateWorkflowRequest() => + new( + api: "https://localhost/", + token: "token", + flowId: Guid.NewGuid(), + instanceId: Guid.NewGuid()); +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/FlowExecutionOrchestrationAdapterTests.ExecuteAsync.cs b/src/cCoder.Workflow.Engine.Tests/FlowExecutionOrchestrationAdapterTests.ExecuteAsync.cs new file mode 100644 index 0000000..36a46e6 --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/FlowExecutionOrchestrationAdapterTests.ExecuteAsync.cs @@ -0,0 +1,42 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Activities.Models; +using cCoder.Workflow.Engine.Exposures; +using cCoder.Workflow.Engine.Services.Orchestrations; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class FlowExecutionOrchestrationAdapterTests +{ + [Fact] + public async Task ShouldExecuteAsync() + { + // Given + WorkflowRequest request = new( + api: "https://localhost/", + token: "token", + flowId: Guid.NewGuid(), + instanceId: Guid.NewGuid()); + + Mock serviceMock = + new(behavior: MockBehavior.Strict); + + serviceMock + .Setup(expression: service => service + .ExecuteWorkflowRequestAsync(workflowRequest: request)) + .Returns(value: ValueTask.CompletedTask); + + FlowExecutionOrchestrationAdapter adapter = + new(workflowRequestOrchestrationService: serviceMock.Object); + + // When + await adapter.ExecuteAsync(request: request); + + // Then + serviceMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/FlowInstanceProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Engine.Tests/FlowInstanceProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..e57e9d0 --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/FlowInstanceProcessingServiceTests.Exceptions.cs @@ -0,0 +1,47 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Engine.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class FlowInstanceProcessingServiceTests +{ + [Theory] + [MemberData( + nameof(WorkflowRequestOrchestrationServiceTests.ExceptionMappings), + MemberType = typeof(WorkflowRequestOrchestrationServiceTests))] + public async Task ShouldMapExecuteFlowExecutionAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + FlowExecution execution = CreateFlowExecution(); + + workflowHttpClientBrokerMock + .Setup(expression: broker => broker.GetStringAsync( + apiRoot: execution.Request.Api, + authToken: execution.Request.AuthToken, + requestUri: It.IsAny())) + .Throws(exception: exception); + + var service = CreateService(); + + // When + Func action = async () => await service + .ExecuteFlowExecutionAsync(flowExecution: execution); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/FlowInstanceProcessingServiceTests.ExecuteFlowExecutionAsync.cs b/src/cCoder.Workflow.Engine.Tests/FlowInstanceProcessingServiceTests.ExecuteFlowExecutionAsync.cs new file mode 100644 index 0000000..a290cca --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/FlowInstanceProcessingServiceTests.ExecuteFlowExecutionAsync.cs @@ -0,0 +1,335 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using cCoder.Workflow.Activities; +using cCoder.Workflow.Activities.Activities; +using cCoder.Workflow.Activities.Activities.Api; +using cCoder.Workflow.Activities.Models; +using cCoder.Workflow.Engine.Dependencies; +using cCoder.Workflow.Engine.Extensions; +using cCoder.Workflow.Engine.Models; +using cCoder.Workflow.Engine.Models.Exceptions; +using FluentAssertions; +using Moq; +using Newtonsoft.Json; +using Xunit; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class FlowInstanceProcessingServiceTests +{ + [Fact] + public async Task ShouldExecuteFlowExecutionAsync() + { + // Given + FlowExecution execution = CreateFlowExecution(); + + Start start = new() { Ref = "start" }; + InfoActivity info = new() { Ref = "info", Message = "done" }; + ApiGet apiGet = new() { Ref = "api", Result = "ignored" }; + + Flow flow = new() + { + Name = "Flow", + Activities = [start, info, apiGet], + Links = + [ + new Link + { + Source = start.Ref, + Destination = info.Ref, + Expression = "destination.Message = source.Ref" + } + ] + }; + + WorkflowContext workflowContext = new() + { + Flow = flow, + ExecutionState = "Complete" + }; + + FlowInstanceData instanceData = new() + { + Id = execution.Request.InstanceId, + FlowDefinitionId = execution.Request.FlowId, + Name = "Instance", + Caller = "caller", + ContextString = JsonConvert.SerializeObject( + value: workflowContext, + settings: ObjectExtensions.GetJsonSettings()), + + FlowDefinition = new() + { + Id = execution.Request.FlowId, + AppId = 7 + } + }; + + string rawInstance = JsonConvert.SerializeObject( + value: instanceData, + settings: ObjectExtensions.GetJsonSettings()); + + workflowHttpClientBrokerMock + .Setup(expression: broker => broker.GetStringAsync( + apiRoot: execution.Request.Api, + authToken: execution.Request.AuthToken, + requestUri: It.IsAny())) + .Returns(value: ValueTask.FromResult(result: rawInstance)); + + workflowContextBrokerMock + .Setup(expression: broker => broker.CreateWorkflowExecutionContext( + flowExecution: execution)) + .Returns(valueFunction: () => new WorkflowExecutionContext( + flowExecution: execution)); + + workflowContextBrokerMock + .Setup(expression: broker => broker + .ExecuteWorkflowExecutionContextAsync( + workflowExecutionContext: It.IsAny(), + apiRoot: execution.Request.Api, + authToken: execution.Request.AuthToken)) + .Callback( + action: (context, _, _) => + context.ExecutionState = workflowContext.ExecutionState) + .Returns(value: Task.CompletedTask); + + var service = CreateService(); + + // When + FlowExecution actual = await service.ExecuteFlowExecutionAsync( + flowExecution: execution); + + // Then + actual + .Should() + .BeSameAs(expected: execution); + + actual.AppId + .Should() + .Be(expected: instanceData.FlowDefinition.AppId); + + actual.Result.Id + .Should() + .Be(expected: instanceData.Id); + + actual.Result.State + .Should() + .Be(expected: workflowContext.ExecutionState); + + actual.Script + .Should() + .BeSameAs(expected: scriptBrokerMock.Object); + + Activity actualStart = actual.Flow.Activities.Single( + predicate: activity => activity.Ref == start.Ref); + + Activity actualInfo = actual.Flow.Activities.Single( + predicate: activity => activity.Ref == info.Ref); + + actualStart.Next + .Should() + .ContainSingle() + .Which + .Should() + .BeSameAs(expected: actualInfo); + + actualInfo.Previous + .Should() + .ContainSingle() + .Which + .Should() + .BeSameAs(expected: actualStart); + + actualInfo.AssignCode + .Should() + .Contain(expected: "flow.GetActivity"); + + workflowHttpClientBrokerMock.VerifyAll(); + workflowContextBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldRejectInvalidFlowExecutionAsync() + { + // Given + var service = CreateService(); + + // When + Func action = async () => await service + .ExecuteFlowExecutionAsync(flowExecution: null); + + // Then + await action + .Should() + .ThrowAsync(); + + workflowHttpClientBrokerMock.VerifyNoOtherCalls(); + workflowContextBrokerMock.VerifyNoOtherCalls(); + scriptBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldLogInvalidFlowInstanceResponseAsync() + { + // Given + FlowExecution execution = CreateFlowExecution(); + List messages = []; + + execution.Log = (level, message) => + { + messages.Add(item: message); + return Task.CompletedTask; + }; + + workflowHttpClientBrokerMock + .Setup(expression: broker => broker.GetStringAsync( + apiRoot: execution.Request.Api, + authToken: execution.Request.AuthToken, + requestUri: It.IsAny())) + .Returns(value: ValueTask.FromResult(result: "invalid")); + + var service = CreateService(); + + // When + Func action = async () => await service + .ExecuteFlowExecutionAsync(flowExecution: execution); + + // Then + await action + .Should() + .ThrowAsync(); + + messages + .Should() + .ContainSingle(predicate: message => message.Contains( + value: "Failed to deserialize flow instance")); + } + + [Fact] + public async Task ShouldLogInvalidWorkflowContextAsync() + { + // Given + FlowExecution execution = CreateFlowExecution(); + List messages = []; + + execution.Log = (level, message) => + { + messages.Add(item: message); + return Task.CompletedTask; + }; + + string rawInstance = SerializeFlowInstanceData( + execution: execution, + contextString: "invalid"); + + workflowHttpClientBrokerMock + .Setup(expression: broker => broker.GetStringAsync( + apiRoot: execution.Request.Api, + authToken: execution.Request.AuthToken, + requestUri: It.IsAny())) + .Returns(value: ValueTask.FromResult(result: rawInstance)); + + var service = CreateService(); + + // When + Func action = async () => await service + .ExecuteFlowExecutionAsync(flowExecution: execution); + + // Then + await action + .Should() + .ThrowAsync(); + + messages + .Should() + .ContainSingle(predicate: message => message.Contains( + value: "Failed to deserialize flow context")); + } + + [Fact] + public async Task ShouldLogMalformedFlowLinksAsync() + { + // Given + FlowExecution execution = CreateFlowExecution(); + List messages = []; + + execution.Log = (level, message) => + { + messages.Add(item: message); + + if (message.Contains(value: "previous activity selection")) + { + Activity activity = execution.Flow.Activities.Single(); + activity.Previous = [activity]; + execution.Flow.Links = Array.Empty(); + } + + return Task.CompletedTask; + }; + + WorkflowContext context = new() + { + Flow = new Flow + { + Name = "Malformed", + Activities = + [ + new InfoActivity + { + Ref = "info", + Message = "message" + } + ], + + Links = null + } + }; + + string rawInstance = SerializeFlowInstanceData( + execution: execution, + contextString: JsonConvert.SerializeObject( + value: context, + settings: ObjectExtensions.GetJsonSettings())); + + workflowHttpClientBrokerMock + .Setup(expression: broker => broker.GetStringAsync( + apiRoot: execution.Request.Api, + authToken: execution.Request.AuthToken, + requestUri: It.IsAny())) + .Returns(value: ValueTask.FromResult(result: rawInstance)); + + workflowContextBrokerMock + .Setup(expression: broker => broker.CreateWorkflowExecutionContext( + flowExecution: execution)) + .Throws(exception: new Exception("stop after stitching")); + + var service = CreateService(); + + // When + Func action = async () => await service + .ExecuteFlowExecutionAsync(flowExecution: execution); + + // Then + await action + .Should() + .ThrowAsync(); + + messages + .Should() + .HaveCount(expected: 2); + + messages + .Should() + .Contain(predicate: message => message.Contains( + value: "previous activity selection")); + + messages + .Should() + .Contain(predicate: message => message.Contains( + value: "one or more links")); + + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/FlowInstanceProcessingServiceTests.cs b/src/cCoder.Workflow.Engine.Tests/FlowInstanceProcessingServiceTests.cs new file mode 100644 index 0000000..8a57525 --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/FlowInstanceProcessingServiceTests.cs @@ -0,0 +1,62 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Activities.Models; +using cCoder.Data.Models.Workflow; +using cCoder.Workflow.Engine.Brokers; +using cCoder.Workflow.Engine.Extensions; +using cCoder.Workflow.Engine.Models; +using cCoder.Workflow.Engine.Services.Processings; +using Moq; +using Newtonsoft.Json; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class FlowInstanceProcessingServiceTests +{ + private readonly Mock scriptBrokerMock = + new(behavior: MockBehavior.Strict); + + private readonly Mock workflowContextBrokerMock = + new(behavior: MockBehavior.Strict); + + private readonly Mock + workflowHttpClientBrokerMock = + new(behavior: MockBehavior.Strict); + + private FlowInstanceProcessingService CreateService() => + new( + scriptBroker: scriptBrokerMock.Object, + workflowContextBroker: workflowContextBrokerMock.Object, + workflowHttpClientBroker: workflowHttpClientBrokerMock.Object); + + private static FlowExecution CreateFlowExecution() => + new() + { + Request = new( + api: "https://localhost/", + token: "token", + flowId: Guid.NewGuid(), + instanceId: Guid.NewGuid()), + + Log = (level, message) => Task.CompletedTask + }; + + private static string SerializeFlowInstanceData( + FlowExecution execution, + string contextString) => + JsonConvert.SerializeObject( + value: new FlowInstanceData + { + Id = execution.Request.InstanceId, + FlowDefinitionId = execution.Request.FlowId, + ContextString = contextString, + FlowDefinition = new() + { + Id = execution.Request.FlowId, + AppId = 7 + } + }, + settings: ObjectExtensions.GetJsonSettings()); +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/FlowResultProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Engine.Tests/FlowResultProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..be51ef1 --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/FlowResultProcessingServiceTests.Exceptions.cs @@ -0,0 +1,54 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using cCoder.Workflow.Engine.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class FlowResultProcessingServiceTests +{ + [Theory] + [MemberData( + nameof(WorkflowRequestOrchestrationServiceTests.ExceptionMappings), + MemberType = typeof(WorkflowRequestOrchestrationServiceTests))] + public async Task ShouldMapSaveFlowInstanceDataAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + const string apiRoot = "https://localhost/"; + const string authToken = "token"; + FlowInstanceData instanceData = CreateFlowInstanceData(); + + workflowHttpClientBrokerMock + .Setup(expression: broker => broker.PutJsonAsync( + apiRoot: apiRoot, + authToken: authToken, + requestUri: It.IsAny(), + payload: It.IsAny())) + .Throws(exception: exception); + + var service = CreateService(); + + // When + Func action = async () => await service + .SaveFlowInstanceDataAsync( + flowInstanceData: instanceData, + apiRoot: apiRoot, + authToken: authToken); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/FlowResultProcessingServiceTests.SaveFlowInstanceDataAsync.cs b/src/cCoder.Workflow.Engine.Tests/FlowResultProcessingServiceTests.SaveFlowInstanceDataAsync.cs new file mode 100644 index 0000000..26eb394 --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/FlowResultProcessingServiceTests.SaveFlowInstanceDataAsync.cs @@ -0,0 +1,129 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using cCoder.Workflow.Engine.Models; +using cCoder.Workflow.Engine.Models.Exceptions; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class FlowResultProcessingServiceTests +{ + [Fact] + public async Task ShouldSaveFlowInstanceDataAsync() + { + // Given + const string apiRoot = "https://localhost/"; + const string authToken = "token"; + FlowInstanceData instanceData = CreateFlowInstanceData(); + string capturedPayload = null; + + workflowHttpClientBrokerMock + .Setup(expression: broker => broker.PutJsonAsync( + apiRoot: apiRoot, + authToken: authToken, + requestUri: $"Workflow/FlowInstanceData({instanceData.Id})", + payload: It.IsAny())) + .Callback( + action: (_, _, _, payload) => capturedPayload = payload) + .Returns(value: ValueTask.FromResult( + result: new WorkflowHttpResult + { + IsSuccess = true, + StatusCode = 200, + Status = "OK" + })); + + var service = CreateService(); + + // When + await service.SaveFlowInstanceDataAsync( + flowInstanceData: instanceData, + apiRoot: apiRoot, + authToken: authToken); + + // Then + capturedPayload + .Should() + .Contain(expected: instanceData.Id.ToString()); + + capturedPayload + .Should() + .Contain(expected: instanceData.FlowDefinitionId.ToString()); + + workflowHttpClientBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldRejectUnsuccessfulFlowInstanceDataSaveAsync() + { + // Given + const string apiRoot = "https://localhost/"; + const string authToken = "token"; + FlowInstanceData instanceData = CreateFlowInstanceData(); + + workflowHttpClientBrokerMock + .Setup(expression: broker => broker.PutJsonAsync( + apiRoot: apiRoot, + authToken: authToken, + requestUri: It.IsAny(), + payload: It.IsAny())) + .Returns(value: ValueTask.FromResult( + result: new WorkflowHttpResult + { + IsSuccess = false, + StatusCode = 500, + Status = "InternalServerError", + Body = "failed" + })); + + var service = CreateService(); + + // When + Func action = async () => await service + .SaveFlowInstanceDataAsync( + flowInstanceData: instanceData, + apiRoot: apiRoot, + authToken: authToken); + + // Then + await action + .Should() + .ThrowAsync(); + } + + [Theory] + [InlineData(null, "https://localhost/", "token")] + [InlineData("instance", "", "token")] + [InlineData("instance", "https://localhost/", " ")] + public async Task ShouldRejectInvalidFlowInstanceDataSaveAsync( + string instanceMarker, + string apiRoot, + string authToken) + { + // Given + FlowInstanceData instanceData = instanceMarker is null + ? null + : CreateFlowInstanceData(); + + var service = CreateService(); + + // When + Func action = async () => await service + .SaveFlowInstanceDataAsync( + flowInstanceData: instanceData, + apiRoot: apiRoot, + authToken: authToken); + + // Then + await action + .Should() + .ThrowAsync(); + + workflowHttpClientBrokerMock.VerifyNoOtherCalls(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/FlowResultProcessingServiceTests.cs b/src/cCoder.Workflow.Engine.Tests/FlowResultProcessingServiceTests.cs new file mode 100644 index 0000000..2072c62 --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/FlowResultProcessingServiceTests.cs @@ -0,0 +1,29 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using cCoder.Workflow.Engine.Brokers; +using cCoder.Workflow.Engine.Services.Processings; +using Moq; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class FlowResultProcessingServiceTests +{ + private readonly Mock + workflowHttpClientBrokerMock = + new(behavior: MockBehavior.Strict); + + private FlowResultProcessingService CreateService() => + new(workflowHttpClientBroker: workflowHttpClientBrokerMock.Object); + + private static FlowInstanceData CreateFlowInstanceData() => + new() + { + Id = Guid.NewGuid(), + FlowDefinitionId = Guid.NewGuid(), + Name = "Flow", + ContextString = "{}" + }; +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/IServiceCollectionExtensionsTests.AddWorkflowEngine.cs b/src/cCoder.Workflow.Engine.Tests/IServiceCollectionExtensionsTests.AddWorkflowEngine.cs index de572ec..bb262cc 100644 --- a/src/cCoder.Workflow.Engine.Tests/IServiceCollectionExtensionsTests.AddWorkflowEngine.cs +++ b/src/cCoder.Workflow.Engine.Tests/IServiceCollectionExtensionsTests.AddWorkflowEngine.cs @@ -13,7 +13,7 @@ namespace cCoder.Workflow.Engine.Tests; public sealed partial class IServiceCollectionExtensionsTests { [Fact] - public void AddWorkflowEngine_RegistersResolvableEngineServices() + public async Task ShouldRegisterResolvableWorkflowEngineServicesAsync() { // Given @@ -21,16 +21,22 @@ public void AddWorkflowEngine_RegistersResolvableEngineServices() services.AddWorkflowEngineHostedServices(); // Then - using ServiceProvider serviceProvider = services.BuildServiceProvider( -options: new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true }); - - serviceProvider.GetRequiredService() + await using ServiceProvider serviceProvider = + services.BuildServiceProvider( + options: new ServiceProviderOptions + { + ValidateOnBuild = true, + ValidateScopes = true + }); + + serviceProvider + .GetRequiredService() .Should() .NotBeNull(); - serviceProvider.GetRequiredService() + serviceProvider + .GetRequiredService() .Should() .NotBeNull(); - } } \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/WorkflowRequestOrchestrationServiceTests.Exceptions.cs b/src/cCoder.Workflow.Engine.Tests/WorkflowRequestOrchestrationServiceTests.Exceptions.cs new file mode 100644 index 0000000..bff839d --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/WorkflowRequestOrchestrationServiceTests.Exceptions.cs @@ -0,0 +1,71 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using System.ComponentModel.DataAnnotations; +using cCoder.Workflow.Activities.Models; +using cCoder.Workflow.Engine.Models.Exceptions; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class WorkflowRequestOrchestrationServiceTests +{ + public static TheoryData ExceptionMappings => new() + { + { + new WorkflowEngineValidationException( + innerException: new Exception()), + typeof(WorkflowEngineValidationException) + }, + { + new WorkflowEngineDependencyException( + innerException: new Exception()), + typeof(WorkflowEngineDependencyException) + }, + { + new ValidationException(), + typeof(WorkflowEngineValidationException) + }, + { + new InvalidOperationException(), + typeof(WorkflowEngineDependencyException) + }, + { + new Exception(), + typeof(WorkflowEngineServiceException) + } + }; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapExecuteWorkflowRequestAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + WorkflowRequest request = CreateWorkflowRequest(); + + flowCommunicationProcessingServiceMock + .Setup(expression: service => service + .ConnectWorkflowRequestAsync(workflowRequest: request)) + .Throws(exception: exception); + + var service = CreateService(); + + // When + Func action = async () => await service + .ExecuteWorkflowRequestAsync(workflowRequest: request); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/WorkflowRequestOrchestrationServiceTests.ExecuteWorkflowRequestAsync.cs b/src/cCoder.Workflow.Engine.Tests/WorkflowRequestOrchestrationServiceTests.ExecuteWorkflowRequestAsync.cs new file mode 100644 index 0000000..2c350bf --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/WorkflowRequestOrchestrationServiceTests.ExecuteWorkflowRequestAsync.cs @@ -0,0 +1,123 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Activities.Models; +using cCoder.Workflow.Engine.Models; +using cCoder.Workflow.Engine.Models.Exceptions; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class WorkflowRequestOrchestrationServiceTests +{ + public static TheoryData InvalidWorkflowRequests => new() + { + null, + new WorkflowRequest + { + Api = "https://localhost/", + InstanceId = Guid.Empty + }, + new WorkflowRequest + { + Api = " ", + InstanceId = Guid.NewGuid() + } + }; + + [Fact] + public async Task ShouldExecuteWorkflowRequestAsync() + { + // Given + WorkflowRequest request = CreateWorkflowRequest(); + FlowExecution capturedExecution = null; + + flowCommunicationProcessingServiceMock + .Setup(expression: service => service + .ConnectWorkflowRequestAsync(workflowRequest: request)) + .Returns(value: ValueTask.CompletedTask); + + flowCommunicationProcessingServiceMock + .Setup(expression: service => service.LogWorkflowRequestAsync( + workflowRequest: request, + level: It.IsAny(), + message: It.IsAny())) + .Returns(value: ValueTask.CompletedTask); + + flowInstanceProcessingServiceMock + .Setup(expression: service => service.ExecuteFlowExecutionAsync( + flowExecution: It.IsAny())) + .Callback(action: execution => + capturedExecution = execution) + .Returns(valueFunction: execution => + ValueTask.FromResult( + result: CompleteExecution(execution: execution))); + + flowResultProcessingServiceMock + .Setup(expression: service => service.SaveFlowInstanceDataAsync( + flowInstanceData: + It.IsAny(), + apiRoot: request.Api, + authToken: request.AuthToken)) + .Returns(value: ValueTask.CompletedTask); + + var service = CreateService(); + + // When + await service.ExecuteWorkflowRequestAsync(workflowRequest: request); + + // Then + capturedExecution + .Should() + .NotBeNull(); + + capturedExecution.Request + .Should() + .BeSameAs(expected: request); + + capturedExecution.Log + .Should() + .NotBeNull(); + + flowCommunicationProcessingServiceMock.Verify( + expression: dependency => dependency.LogWorkflowRequestAsync( + workflowRequest: request, + level: It.IsAny(), + message: It.IsAny()), + times: Times.Exactly(callCount: 3)); + + flowCommunicationProcessingServiceMock.Verify( + expression: dependency => dependency + .ConnectWorkflowRequestAsync(workflowRequest: request), + times: Times.Once()); + + flowCommunicationProcessingServiceMock.VerifyNoOtherCalls(); + flowInstanceProcessingServiceMock.VerifyAll(); + flowResultProcessingServiceMock.VerifyAll(); + } + + [Theory] + [MemberData(nameof(InvalidWorkflowRequests))] + public async Task ShouldRejectInvalidWorkflowRequestAsync( + WorkflowRequest request) + { + // Given + var service = CreateService(); + + // When + Func action = async () => await service + .ExecuteWorkflowRequestAsync(workflowRequest: request); + + // Then + await action + .Should() + .ThrowAsync(); + + flowCommunicationProcessingServiceMock.VerifyNoOtherCalls(); + flowInstanceProcessingServiceMock.VerifyNoOtherCalls(); + flowResultProcessingServiceMock.VerifyNoOtherCalls(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/WorkflowRequestOrchestrationServiceTests.cs b/src/cCoder.Workflow.Engine.Tests/WorkflowRequestOrchestrationServiceTests.cs new file mode 100644 index 0000000..2f290ec --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/WorkflowRequestOrchestrationServiceTests.cs @@ -0,0 +1,53 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Activities.Models; +using cCoder.Workflow.Engine.Models; +using cCoder.Workflow.Engine.Services.Orchestrations; +using cCoder.Workflow.Engine.Services.Processings; +using Moq; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class WorkflowRequestOrchestrationServiceTests +{ + private readonly Mock + flowCommunicationProcessingServiceMock = + new(behavior: MockBehavior.Strict); + + private readonly Mock + flowInstanceProcessingServiceMock = + new(behavior: MockBehavior.Strict); + + private readonly Mock + flowResultProcessingServiceMock = + new(behavior: MockBehavior.Strict); + + private WorkflowRequestOrchestrationService CreateService() => + new WorkflowRequestOrchestrationService( + flowCommunicationProcessingService: + flowCommunicationProcessingServiceMock.Object, + flowInstanceProcessingService: + flowInstanceProcessingServiceMock.Object, + flowResultProcessingService: + flowResultProcessingServiceMock.Object); + + private static WorkflowRequest CreateWorkflowRequest() => + new( + api: "https://localhost/", + token: "token", + flowId: Guid.NewGuid(), + instanceId: Guid.NewGuid()); + + private static FlowExecution CompleteExecution(FlowExecution execution) + { + execution.Result = new cCoder.Data.Models.Workflow.FlowInstanceData() + { + Id = execution.Request.InstanceId, + FlowDefinitionId = execution.Request.FlowId + }; + + return execution; + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/WorkflowScriptExecutionOrchestrationAdapterTests.ExecuteAsync.cs b/src/cCoder.Workflow.Engine.Tests/WorkflowScriptExecutionOrchestrationAdapterTests.ExecuteAsync.cs new file mode 100644 index 0000000..1cdb730 --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/WorkflowScriptExecutionOrchestrationAdapterTests.ExecuteAsync.cs @@ -0,0 +1,47 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Engine.Exposures; +using cCoder.Workflow.Engine.Services.Processings; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class WorkflowScriptExecutionOrchestrationAdapterTests +{ + [Fact] + public async Task ShouldExecuteAsync() + { + // Given + const string payload = "return true"; + const string expected = "true"; + + Mock serviceMock = + new(behavior: MockBehavior.Strict); + + serviceMock + .Setup(expression: service => service + .ExecuteWorkflowScriptAsync( + payload: payload, + useDetails: true)) + .Returns(value: ValueTask.FromResult(result: expected)); + + WorkflowScriptExecutionOrchestrationAdapter adapter = + new(workflowScriptExecutionProcessingService: serviceMock.Object); + + // When + string actual = await adapter.ExecuteAsync( + payload: payload, + useDetails: true); + + // Then + actual + .Should() + .Be(expected: expected); + + serviceMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/WorkflowScriptExecutionProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Engine.Tests/WorkflowScriptExecutionProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..f673059 --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/WorkflowScriptExecutionProcessingServiceTests.Exceptions.cs @@ -0,0 +1,48 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Activities.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class WorkflowScriptExecutionProcessingServiceTests +{ + [Theory] + [MemberData( + nameof(WorkflowRequestOrchestrationServiceTests.ExceptionMappings), + MemberType = typeof(WorkflowRequestOrchestrationServiceTests))] + public async Task ShouldMapExecuteWorkflowScriptAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + const string payload = "return value"; + + scriptBrokerMock + .Setup(expression: broker => broker.Run( + code: payload, + imports: It.IsAny(), + args: null, + log: It.IsAny>())) + .Throws(exception: exception); + + var service = CreateService(); + + // When + Func action = async () => await service + .ExecuteWorkflowScriptAsync(payload: payload, useDetails: false); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/WorkflowScriptExecutionProcessingServiceTests.ExecuteWorkflowScriptAsync.cs b/src/cCoder.Workflow.Engine.Tests/WorkflowScriptExecutionProcessingServiceTests.ExecuteWorkflowScriptAsync.cs new file mode 100644 index 0000000..8a5e61d --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/WorkflowScriptExecutionProcessingServiceTests.ExecuteWorkflowScriptAsync.cs @@ -0,0 +1,172 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Activities.Models; +using cCoder.Workflow.Engine.Models; +using cCoder.Workflow.Engine.Models.Exceptions; +using FluentAssertions; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class WorkflowScriptExecutionProcessingServiceTests +{ + [Fact] + public async Task ShouldExecuteWorkflowScriptAsync() + { + // Given + const string payload = "return value"; + var result = new { Value = 7 }; + + scriptBrokerMock + .Setup(expression: broker => broker.Run( + code: payload, + imports: It.IsAny(), + args: null, + log: It.IsAny>())) + .ReturnsAsync(value: result); + + var service = CreateService(); + + // When + string actual = await service.ExecuteWorkflowScriptAsync( + payload: payload, + useDetails: false); + + // Then + actual + .Should() + .Contain(expected: "\"Value\":7"); + + scriptBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldExecuteWorkflowScriptDetailsAsync() + { + // Given + const string script = "return value"; + const string expected = "result"; + JObject model = JObject.FromObject(o: new { Value = 7 }); + + string payload = JsonConvert.SerializeObject( + value: new ExecutionDetails + { + Script = script, + Model = model + }); + + scriptBrokerMock + .Setup(expression: broker => broker.Run( + code: script, + imports: It.IsAny(), + args: It.Is(match: argument => + JToken.DeepEquals( + t1: argument as JToken, + t2: model)), + log: It.IsAny>())) + .ReturnsAsync(value: expected); + + var service = CreateService(); + + // When + string actual = await service.ExecuteWorkflowScriptAsync( + payload: payload, + useDetails: true); + + // Then + actual + .Should() + .Be(expected: expected); + + scriptBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldRouteWorkflowScriptLogsByLevelAsync() + { + // Given + const string payload = "return value"; + const string message = "message"; + Action log = null; + + scriptBrokerMock + .Setup(expression: broker => broker.Run( + code: payload, + imports: It.IsAny(), + args: null, + log: It.IsAny>())) + .Callback>( + action: (_, _, _, callback) => log = callback) + .ReturnsAsync(value: new object()); + + var service = CreateService(); + + await service.ExecuteWorkflowScriptAsync( + payload: payload, + useDetails: false); + + // When + log.Invoke(arg1: WorkflowLogLevel.Debug, arg2: message); + log.Invoke(arg1: WorkflowLogLevel.Info, arg2: message); + log.Invoke(arg1: WorkflowLogLevel.Warning, arg2: message); + log.Invoke(arg1: WorkflowLogLevel.Error, arg2: message); + log.Invoke(arg1: WorkflowLogLevel.Fatal, arg2: message); + + // Then + loggingBrokerMock.Verify( + expression: broker => broker.LogDebug( + message: "{Message}", + args: It.Is(match: arguments => + arguments.Single() as string == message)), + times: Times.Once()); + + loggingBrokerMock.Verify( + expression: broker => broker.LogInformation( + message: "{Message}", + args: It.Is(match: arguments => + arguments.Single() as string == message)), + times: Times.Once()); + + loggingBrokerMock.Verify( + expression: broker => broker.LogWarning( + message: "{Message}", + args: It.Is(match: arguments => + arguments.Single() as string == message)), + times: Times.Once()); + + loggingBrokerMock.Verify( + expression: broker => broker.LogError( + message: "{Message}", + args: It.Is(match: arguments => + arguments.Single() as string == message)), + times: Times.Exactly(callCount: 2)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task ShouldRejectInvalidWorkflowScriptPayloadAsync( + string payload) + { + // Given + var service = CreateService(); + + // When + Func action = async () => await service + .ExecuteWorkflowScriptAsync(payload: payload, useDetails: false); + + // Then + await action + .Should() + .ThrowAsync(); + + scriptBrokerMock.VerifyNoOtherCalls(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine.Tests/WorkflowScriptExecutionProcessingServiceTests.cs b/src/cCoder.Workflow.Engine.Tests/WorkflowScriptExecutionProcessingServiceTests.cs new file mode 100644 index 0000000..b202da6 --- /dev/null +++ b/src/cCoder.Workflow.Engine.Tests/WorkflowScriptExecutionProcessingServiceTests.cs @@ -0,0 +1,23 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Engine.Brokers; +using cCoder.Workflow.Engine.Brokers.Loggings; +using cCoder.Workflow.Engine.Services.Processings; +using Moq; + +namespace cCoder.Workflow.Engine.Tests; + +public sealed partial class WorkflowScriptExecutionProcessingServiceTests +{ + private readonly Mock scriptBrokerMock = + new(behavior: MockBehavior.Strict); + + private readonly Mock loggingBrokerMock = new(); + + private WorkflowScriptExecutionProcessingService CreateService() => + new( + scriptBroker: scriptBrokerMock.Object, + logger: loggingBrokerMock.Object); +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine/Brokers/IWorkflowHttpClientBroker.cs b/src/cCoder.Workflow.Engine/Brokers/IWorkflowHttpClientBroker.cs new file mode 100644 index 0000000..c394568 --- /dev/null +++ b/src/cCoder.Workflow.Engine/Brokers/IWorkflowHttpClientBroker.cs @@ -0,0 +1,21 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Engine.Models; + +namespace cCoder.Workflow.Engine.Brokers; + +internal interface IWorkflowHttpClientBroker +{ + ValueTask GetStringAsync( + string apiRoot, + string authToken, + string requestUri); + + ValueTask PutJsonAsync( + string apiRoot, + string authToken, + string requestUri, + string payload); +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine/Brokers/IWorkflowHubConnectionBroker.cs b/src/cCoder.Workflow.Engine/Brokers/IWorkflowHubConnectionBroker.cs new file mode 100644 index 0000000..9b72771 --- /dev/null +++ b/src/cCoder.Workflow.Engine/Brokers/IWorkflowHubConnectionBroker.cs @@ -0,0 +1,17 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +namespace cCoder.Workflow.Engine.Brokers; + +internal interface IWorkflowHubConnectionBroker +{ + Task ConnectAsync(string url); + + Task SendAsync( + string level, + string message, + string instanceId); + + ValueTask DisconnectAsync(); +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine/Brokers/WorkflowHttpClientBroker.cs b/src/cCoder.Workflow.Engine/Brokers/WorkflowHttpClientBroker.cs new file mode 100644 index 0000000..f758a9b --- /dev/null +++ b/src/cCoder.Workflow.Engine/Brokers/WorkflowHttpClientBroker.cs @@ -0,0 +1,38 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Engine.Dependencies; +using cCoder.Workflow.Engine.Models; + +namespace cCoder.Workflow.Engine.Brokers; + +internal sealed class WorkflowHttpClientBroker + : IWorkflowHttpClientBroker +{ + public async ValueTask GetStringAsync( + string apiRoot, + string authToken, + string requestUri) + { + using WorkflowHttpClientDependency dependency = + new(apiRoot: apiRoot, authToken: authToken); + + return await dependency.GetStringAsync( + requestUri: requestUri); + } + + public async ValueTask PutJsonAsync( + string apiRoot, + string authToken, + string requestUri, + string payload) + { + using WorkflowHttpClientDependency dependency = + new(apiRoot: apiRoot, authToken: authToken); + + return await dependency.PutJsonAsync( + requestUri: requestUri, + payload: payload); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine/Brokers/WorkflowHubConnectionBroker.cs b/src/cCoder.Workflow.Engine/Brokers/WorkflowHubConnectionBroker.cs new file mode 100644 index 0000000..f621497 --- /dev/null +++ b/src/cCoder.Workflow.Engine/Brokers/WorkflowHubConnectionBroker.cs @@ -0,0 +1,31 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Engine.Dependencies; + +namespace cCoder.Workflow.Engine.Brokers; + +internal sealed class WorkflowHubConnectionBroker + : IWorkflowHubConnectionBroker +{ + private WorkflowHubConnectionDependency connection; + + public async Task ConnectAsync(string url) + { + connection = new(url: url); + await connection.ConnectAsync(); + } + + public Task SendAsync( + string level, + string message, + string instanceId) => + connection.SendAsync( + level: level, + message: message, + instanceId: instanceId); + + public ValueTask DisconnectAsync() => + connection.DisposeAsync(); +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Engine/IServiceCollectionExtensions.cs b/src/cCoder.Workflow.Engine/IServiceCollectionExtensions.cs index e638ca4..8d094c6 100644 --- a/src/cCoder.Workflow.Engine/IServiceCollectionExtensions.cs +++ b/src/cCoder.Workflow.Engine/IServiceCollectionExtensions.cs @@ -27,9 +27,17 @@ public static IServiceCollection AddWorkflowEngineHostedServices( private static void AddBrokers( this IServiceCollection services) { - services.AddTransient(); + services.AddTransient< + Brokers.Loggings.ILoggingBroker, + Brokers.Loggings.LoggingBroker>(); services.AddTransient(); services.AddTransient(); + services.AddTransient< + IWorkflowHttpClientBroker, + WorkflowHttpClientBroker>(); + services.AddTransient< + IWorkflowHubConnectionBroker, + WorkflowHubConnectionBroker>(); services.AddTransient< IWorkflowContextBroker, WorkflowContextBroker>(); diff --git a/src/cCoder.Workflow.Engine/Services/Processings/FlowCommunicationProcessingService.cs b/src/cCoder.Workflow.Engine/Services/Processings/FlowCommunicationProcessingService.cs index 8c5c8c0..4b45d43 100644 --- a/src/cCoder.Workflow.Engine/Services/Processings/FlowCommunicationProcessingService.cs +++ b/src/cCoder.Workflow.Engine/Services/Processings/FlowCommunicationProcessingService.cs @@ -4,17 +4,18 @@ using cCoder.Workflow.Activities.Models; using cCoder.Workflow.Activities.Support; +using cCoder.Workflow.Engine.Brokers; using cCoder.Workflow.Engine.Extensions; -using cCoder.Workflow.Engine.Dependencies; using Microsoft.Extensions.Logging; namespace cCoder.Workflow.Engine.Services.Processings; internal sealed partial class FlowCommunicationProcessingService( - cCoder.Workflow.Engine.Brokers.Loggings.ILoggingBroker logger) + cCoder.Workflow.Engine.Brokers.Loggings.ILoggingBroker logger, + IWorkflowHubConnectionBroker workflowHubConnectionBroker) : IFlowCommunicationProcessingService { - private WorkflowHubConnectionDependency connection; + private bool isConnected; public ValueTask ConnectWorkflowRequestAsync( WorkflowRequest workflowRequest) => @@ -24,10 +25,10 @@ public ValueTask ConnectWorkflowRequestAsync( try { - connection = new( + await workflowHubConnectionBroker.ConnectAsync( url: $"{workflowRequest.Api}Hubs/Workflow"); - await connection.ConnectAsync(); + isConnected = true; await ExecuteLogWorkflowRequestAsync( workflowRequest: workflowRequest, @@ -38,12 +39,8 @@ await ExecuteLogWorkflowRequestAsync( } catch (Exception exception) { - if (connection is not null) - { - await connection.DisposeAsync(); - } - - connection = null; + await workflowHubConnectionBroker.DisconnectAsync(); + isConnected = false; await ExecuteLogWorkflowRequestAsync( workflowRequest: workflowRequest, @@ -103,9 +100,9 @@ private async ValueTask ExecuteLogWorkflowRequestAsync( try { - if (connection is not null) + if (isConnected) { - await connection.SendAsync( + await workflowHubConnectionBroker.SendAsync( level: level.ToString() .ToLowerInvariant(), message: message, @@ -114,12 +111,8 @@ await connection.SendAsync( } catch (Exception exception) { - if (connection is not null) - { - await connection.DisposeAsync(); - } - - connection = null; + await workflowHubConnectionBroker.DisconnectAsync(); + isConnected = false; await ExecuteLogWorkflowRequestAsync( workflowRequest: workflowRequest, diff --git a/src/cCoder.Workflow.Engine/Services/Processings/FlowInstanceProcessingService.cs b/src/cCoder.Workflow.Engine/Services/Processings/FlowInstanceProcessingService.cs index af42af9..d34b68f 100644 --- a/src/cCoder.Workflow.Engine/Services/Processings/FlowInstanceProcessingService.cs +++ b/src/cCoder.Workflow.Engine/Services/Processings/FlowInstanceProcessingService.cs @@ -18,7 +18,8 @@ namespace cCoder.Workflow.Engine.Services.Processings; internal sealed partial class FlowInstanceProcessingService( IScriptBroker scriptBroker, - IWorkflowContextBroker workflowContextBroker) + IWorkflowContextBroker workflowContextBroker, + IWorkflowHttpClientBroker workflowHttpClientBroker) : IFlowInstanceProcessingService { public ValueTask ExecuteFlowExecutionAsync( @@ -31,12 +32,9 @@ public ValueTask ExecuteFlowExecutionAsync( flowExecution.Start = DateTimeOffset.UtcNow; flowExecution.Script = scriptBroker; - using WorkflowHttpClientDependency api = - new( - apiRoot: request.Api, - authToken: request.AuthToken); - - string rawInstance = await api.GetStringAsync( + string rawInstance = await workflowHttpClientBroker.GetStringAsync( + apiRoot: request.Api, + authToken: request.AuthToken, requestUri: $"Workflow/FlowInstanceData({request.InstanceId})" + "?$expand=FlowDefinition($expand=App)"); diff --git a/src/cCoder.Workflow.Engine/Services/Processings/FlowResultProcessingService.cs b/src/cCoder.Workflow.Engine/Services/Processings/FlowResultProcessingService.cs index 2e5b2ff..0dd85a7 100644 --- a/src/cCoder.Workflow.Engine/Services/Processings/FlowResultProcessingService.cs +++ b/src/cCoder.Workflow.Engine/Services/Processings/FlowResultProcessingService.cs @@ -4,14 +4,14 @@ using System.Net; using cCoder.Data.Models.Workflow; -using cCoder.Workflow.Activities.Support; +using cCoder.Workflow.Engine.Brokers; using Newtonsoft.Json; -using cCoder.Workflow.Engine.Dependencies; using cCoder.Workflow.Engine.Models; namespace cCoder.Workflow.Engine.Services.Processings; -internal sealed partial class FlowResultProcessingService +internal sealed partial class FlowResultProcessingService( + IWorkflowHttpClientBroker workflowHttpClientBroker) : IFlowResultProcessingService { public ValueTask SaveFlowInstanceDataAsync( @@ -28,11 +28,6 @@ public ValueTask SaveFlowInstanceDataAsync( authToken ]); - using WorkflowHttpClientDependency api = - new( - apiRoot: apiRoot, - authToken: authToken); - string payload = JsonConvert.SerializeObject( value: new { @@ -48,11 +43,14 @@ public ValueTask SaveFlowInstanceDataAsync( }, formatting: Formatting.None); - WorkflowHttpResult response = await api.PutJsonAsync( + WorkflowHttpResult response = + await workflowHttpClientBroker.PutJsonAsync( + apiRoot: apiRoot, + authToken: authToken, requestUri: $"Workflow/FlowInstanceData" + $"({flowInstanceData.Id})", - payload: payload); + payload: payload); if (!response.IsSuccess) { diff --git a/src/cCoder.Workflow.Engine/project.stxjson b/src/cCoder.Workflow.Engine/project.stxjson index 8f4f3e0..9488202 100644 --- a/src/cCoder.Workflow.Engine/project.stxjson +++ b/src/cCoder.Workflow.Engine/project.stxjson @@ -481,6 +481,226 @@ } ] }, + { + "Name": "cCoder.Workflow.Engine.Brokers.WorkflowHttpClientBroker", + "StandardElementType": "Broker", + "LineNumber": 10, + "IsPublic": false, + "Kind": "Class", + "BaseType": null, + "Interfaces": [ + { + "Id": "cCoder.Workflow.Engine:cCoder.Workflow.Engine.Brokers.IWorkflowHttpClientBroker", + "FullName": "cCoder.Workflow.Engine.Brokers.IWorkflowHttpClientBroker", + "Name": "IWorkflowHttpClientBroker", + "Namespace": "cCoder.Workflow.Engine.Brokers", + "AssemblyName": "cCoder.Workflow.Engine", + "Kind": "Interface", + "IsInCurrentProject": true, + "StandardElementType": "Broker" + } + ], + "Properties": [], + "Methods": [ + { + "Id": "cCoder.Workflow.Engine.Brokers.WorkflowHttpClientBroker.GetStringAsync(System.String,System.String,System.String)", + "Name": "GetStringAsync", + "LineNumber": 13, + "Inputs": [ + { + "Name": "apiRoot", + "Type": "System.String" + }, + { + "Name": "authToken", + "Type": "System.String" + }, + { + "Name": "requestUri", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.ValueTask\u003CSystem.String\u003E", + "Implements": [ + "cCoder.Workflow.Engine.Brokers.IWorkflowHttpClientBroker.GetStringAsync(System.String,System.String,System.String)" + ], + "Calls": [ + { + "TypeName": "System.Net.Http.HttpClient", + "MethodName": "GetStringAsync", + "MethodId": "System.Net.Http.HttpClient.GetStringAsync(System.String?)", + "StandardElementType": "Dependency", + "IsDependencyBoundary": true + } + ], + "PossibleExceptionTypes": [], + "IncomingExceptionTypes": [], + "ThrowsExceptionTypes": [], + "HttpMethods": [], + "HttpResponses": [], + "IsHttpRequestHandler": false, + "IsODataControllerAction": false, + "HasFromBodyParameter": false, + "HasKeyParameter": false, + "HandlesNullWithNotFound": false, + "HasTryCatch": false + }, + { + "Id": "cCoder.Workflow.Engine.Brokers.WorkflowHttpClientBroker.PutJsonAsync(System.String,System.String,System.String,System.String)", + "Name": "PutJsonAsync", + "LineNumber": 25, + "Inputs": [ + { + "Name": "apiRoot", + "Type": "System.String" + }, + { + "Name": "authToken", + "Type": "System.String" + }, + { + "Name": "requestUri", + "Type": "System.String" + }, + { + "Name": "payload", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.ValueTask\u003CcCoder.Workflow.Engine.Models.WorkflowHttpResult\u003E", + "Implements": [ + "cCoder.Workflow.Engine.Brokers.IWorkflowHttpClientBroker.PutJsonAsync(System.String,System.String,System.String,System.String)" + ], + "Calls": [], + "PossibleExceptionTypes": [], + "IncomingExceptionTypes": [], + "ThrowsExceptionTypes": [], + "HttpMethods": [], + "HttpResponses": [], + "IsHttpRequestHandler": false, + "IsODataControllerAction": false, + "HasFromBodyParameter": false, + "HasKeyParameter": false, + "HandlesNullWithNotFound": false, + "HasTryCatch": false + } + ] + }, + { + "Name": "cCoder.Workflow.Engine.Brokers.WorkflowHubConnectionBroker", + "StandardElementType": "Broker", + "LineNumber": 9, + "IsPublic": false, + "Kind": "Class", + "BaseType": null, + "Interfaces": [ + { + "Id": "cCoder.Workflow.Engine:cCoder.Workflow.Engine.Brokers.IWorkflowHubConnectionBroker", + "FullName": "cCoder.Workflow.Engine.Brokers.IWorkflowHubConnectionBroker", + "Name": "IWorkflowHubConnectionBroker", + "Namespace": "cCoder.Workflow.Engine.Brokers", + "AssemblyName": "cCoder.Workflow.Engine", + "Kind": "Interface", + "IsInCurrentProject": true, + "StandardElementType": "Broker" + } + ], + "Properties": [], + "Methods": [ + { + "Id": "cCoder.Workflow.Engine.Brokers.WorkflowHubConnectionBroker.ConnectAsync(System.String)", + "Name": "ConnectAsync", + "LineNumber": 14, + "Inputs": [ + { + "Name": "url", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Implements": [ + "cCoder.Workflow.Engine.Brokers.IWorkflowHubConnectionBroker.ConnectAsync(System.String)" + ], + "Calls": [], + "PossibleExceptionTypes": [], + "IncomingExceptionTypes": [], + "ThrowsExceptionTypes": [], + "HttpMethods": [], + "HttpResponses": [], + "IsHttpRequestHandler": false, + "IsODataControllerAction": false, + "HasFromBodyParameter": false, + "HasKeyParameter": false, + "HandlesNullWithNotFound": false, + "HasTryCatch": false + }, + { + "Id": "cCoder.Workflow.Engine.Brokers.WorkflowHubConnectionBroker.DisconnectAsync()", + "Name": "DisconnectAsync", + "LineNumber": 29, + "Inputs": [], + "ReturnType": "System.Threading.Tasks.ValueTask", + "Implements": [ + "cCoder.Workflow.Engine.Brokers.IWorkflowHubConnectionBroker.DisconnectAsync()" + ], + "Calls": [ + { + "TypeName": "cCoder.Workflow.Engine.Dependencies.WorkflowHubConnectionDependency", + "MethodName": "DisposeAsync", + "MethodId": "cCoder.Workflow.Engine.Dependencies.WorkflowHubConnectionDependency.DisposeAsync()", + "StandardElementType": "Dependency", + "IsDependencyBoundary": false + } + ], + "PossibleExceptionTypes": [], + "IncomingExceptionTypes": [], + "ThrowsExceptionTypes": [], + "HttpMethods": [], + "HttpResponses": [], + "IsHttpRequestHandler": false, + "IsODataControllerAction": false, + "HasFromBodyParameter": false, + "HasKeyParameter": false, + "HandlesNullWithNotFound": false, + "HasTryCatch": false + }, + { + "Id": "cCoder.Workflow.Engine.Brokers.WorkflowHubConnectionBroker.SendAsync(System.String,System.String,System.String)", + "Name": "SendAsync", + "LineNumber": 20, + "Inputs": [ + { + "Name": "level", + "Type": "System.String" + }, + { + "Name": "message", + "Type": "System.String" + }, + { + "Name": "instanceId", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Implements": [ + "cCoder.Workflow.Engine.Brokers.IWorkflowHubConnectionBroker.SendAsync(System.String,System.String,System.String)" + ], + "Calls": [], + "PossibleExceptionTypes": [], + "IncomingExceptionTypes": [], + "ThrowsExceptionTypes": [], + "HttpMethods": [], + "HttpResponses": [], + "IsHttpRequestHandler": false, + "IsODataControllerAction": false, + "HasFromBodyParameter": false, + "HasKeyParameter": false, + "HandlesNullWithNotFound": false, + "HasTryCatch": false + } + ] + }, { "Name": "cCoder.Workflow.Engine.Dependencies.RoslynScriptDependency", "StandardElementType": "Dependency", @@ -1711,7 +1931,7 @@ { "Id": "cCoder.Workflow.Engine.Services.Processings.FlowCommunicationProcessingService.ConnectWorkflowRequestAsync(cCoder.Workflow.Activities.Models.WorkflowRequest)", "Name": "ConnectWorkflowRequestAsync", - "LineNumber": 19, + "LineNumber": 20, "Inputs": [ { "Name": "workflowRequest", @@ -1724,10 +1944,17 @@ ], "Calls": [ { - "TypeName": "cCoder.Workflow.Engine.Dependencies.WorkflowHubConnectionDependency", - "MethodName": "DisposeAsync", - "MethodId": "cCoder.Workflow.Engine.Dependencies.WorkflowHubConnectionDependency.DisposeAsync()", - "StandardElementType": "Dependency", + "TypeName": "cCoder.Workflow.Engine.Brokers.IWorkflowHubConnectionBroker", + "MethodName": "ConnectAsync", + "MethodId": "cCoder.Workflow.Engine.Brokers.IWorkflowHubConnectionBroker.ConnectAsync(System.String)", + "StandardElementType": "Broker", + "IsDependencyBoundary": false + }, + { + "TypeName": "cCoder.Workflow.Engine.Brokers.IWorkflowHubConnectionBroker", + "MethodName": "DisconnectAsync", + "MethodId": "cCoder.Workflow.Engine.Brokers.IWorkflowHubConnectionBroker.DisconnectAsync()", + "StandardElementType": "Broker", "IsDependencyBoundary": false } ], @@ -1756,7 +1983,7 @@ { "Id": "cCoder.Workflow.Engine.Services.Processings.FlowCommunicationProcessingService.LogWorkflowRequestAsync(cCoder.Workflow.Activities.Models.WorkflowRequest,cCoder.Workflow.Activities.Models.WorkflowLogLevel,System.String)", "Name": "LogWorkflowRequestAsync", - "LineNumber": 57, + "LineNumber": 54, "Inputs": [ { "Name": "workflowRequest", @@ -1828,7 +2055,7 @@ { "Id": "cCoder.Workflow.Engine.Services.Processings.FlowInstanceProcessingService.ExecuteFlowExecutionAsync(cCoder.Workflow.Engine.Models.FlowExecution)", "Name": "ExecuteFlowExecutionAsync", - "LineNumber": 24, + "LineNumber": 25, "Inputs": [ { "Name": "flowExecution", @@ -1847,13 +2074,6 @@ "StandardElementType": "Dependency", "IsDependencyBoundary": true }, - { - "TypeName": "System.Net.Http.HttpClient", - "MethodName": "GetStringAsync", - "MethodId": "System.Net.Http.HttpClient.GetStringAsync(System.String?)", - "StandardElementType": "Dependency", - "IsDependencyBoundary": true - }, { "TypeName": "cCoder.Workflow.Engine.Brokers.IWorkflowContextBroker", "MethodName": "CreateWorkflowExecutionContext", @@ -1867,6 +2087,13 @@ "MethodId": "cCoder.Workflow.Engine.Brokers.IWorkflowContextBroker.ExecuteWorkflowExecutionContextAsync(cCoder.Workflow.Engine.Dependencies.WorkflowExecutionContext,System.String,System.String)", "StandardElementType": "Broker", "IsDependencyBoundary": false + }, + { + "TypeName": "cCoder.Workflow.Engine.Brokers.IWorkflowHttpClientBroker", + "MethodName": "GetStringAsync", + "MethodId": "cCoder.Workflow.Engine.Brokers.IWorkflowHttpClientBroker.GetStringAsync(System.String,System.String,System.String)", + "StandardElementType": "Broker", + "IsDependencyBoundary": false } ], "PossibleExceptionTypes": [ @@ -1902,7 +2129,7 @@ { "Name": "cCoder.Workflow.Engine.Services.Processings.FlowResultProcessingService", "StandardElementType": "ProcessingService", - "LineNumber": 14, + "LineNumber": 13, "IsPublic": false, "Kind": "Class", "BaseType": null, @@ -1956,6 +2183,13 @@ "MethodId": "System.Net.Http.HttpRequestException..ctor(System.String?)", "StandardElementType": "Dependency", "IsDependencyBoundary": true + }, + { + "TypeName": "cCoder.Workflow.Engine.Brokers.IWorkflowHttpClientBroker", + "MethodName": "PutJsonAsync", + "MethodId": "cCoder.Workflow.Engine.Brokers.IWorkflowHttpClientBroker.PutJsonAsync(System.String,System.String,System.String,System.String)", + "StandardElementType": "Broker", + "IsDependencyBoundary": false } ], "PossibleExceptionTypes": [ @@ -2188,6 +2422,178 @@ } ] }, + { + "Name": "cCoder.Workflow.Engine.Brokers.IWorkflowHttpClientBroker", + "StandardElementType": "Broker", + "LineNumber": 9, + "IsPublic": false, + "Kind": "Interface", + "BaseType": null, + "Interfaces": [], + "Properties": [], + "Methods": [ + { + "Id": "cCoder.Workflow.Engine.Brokers.IWorkflowHttpClientBroker.GetStringAsync(System.String,System.String,System.String)", + "Name": "GetStringAsync", + "LineNumber": 11, + "Inputs": [ + { + "Name": "apiRoot", + "Type": "System.String" + }, + { + "Name": "authToken", + "Type": "System.String" + }, + { + "Name": "requestUri", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.ValueTask\u003CSystem.String\u003E", + "Implements": [], + "Calls": [], + "PossibleExceptionTypes": [], + "IncomingExceptionTypes": [], + "ThrowsExceptionTypes": [], + "HttpMethods": [], + "HttpResponses": [], + "IsHttpRequestHandler": false, + "IsODataControllerAction": false, + "HasFromBodyParameter": false, + "HasKeyParameter": false, + "HandlesNullWithNotFound": false, + "HasTryCatch": false + }, + { + "Id": "cCoder.Workflow.Engine.Brokers.IWorkflowHttpClientBroker.PutJsonAsync(System.String,System.String,System.String,System.String)", + "Name": "PutJsonAsync", + "LineNumber": 16, + "Inputs": [ + { + "Name": "apiRoot", + "Type": "System.String" + }, + { + "Name": "authToken", + "Type": "System.String" + }, + { + "Name": "requestUri", + "Type": "System.String" + }, + { + "Name": "payload", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.ValueTask\u003CcCoder.Workflow.Engine.Models.WorkflowHttpResult\u003E", + "Implements": [], + "Calls": [], + "PossibleExceptionTypes": [], + "IncomingExceptionTypes": [], + "ThrowsExceptionTypes": [], + "HttpMethods": [], + "HttpResponses": [], + "IsHttpRequestHandler": false, + "IsODataControllerAction": false, + "HasFromBodyParameter": false, + "HasKeyParameter": false, + "HandlesNullWithNotFound": false, + "HasTryCatch": false + } + ] + }, + { + "Name": "cCoder.Workflow.Engine.Brokers.IWorkflowHubConnectionBroker", + "StandardElementType": "Broker", + "LineNumber": 7, + "IsPublic": false, + "Kind": "Interface", + "BaseType": null, + "Interfaces": [], + "Properties": [], + "Methods": [ + { + "Id": "cCoder.Workflow.Engine.Brokers.IWorkflowHubConnectionBroker.ConnectAsync(System.String)", + "Name": "ConnectAsync", + "LineNumber": 9, + "Inputs": [ + { + "Name": "url", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Implements": [], + "Calls": [], + "PossibleExceptionTypes": [], + "IncomingExceptionTypes": [], + "ThrowsExceptionTypes": [], + "HttpMethods": [], + "HttpResponses": [], + "IsHttpRequestHandler": false, + "IsODataControllerAction": false, + "HasFromBodyParameter": false, + "HasKeyParameter": false, + "HandlesNullWithNotFound": false, + "HasTryCatch": false + }, + { + "Id": "cCoder.Workflow.Engine.Brokers.IWorkflowHubConnectionBroker.DisconnectAsync()", + "Name": "DisconnectAsync", + "LineNumber": 16, + "Inputs": [], + "ReturnType": "System.Threading.Tasks.ValueTask", + "Implements": [], + "Calls": [], + "PossibleExceptionTypes": [], + "IncomingExceptionTypes": [], + "ThrowsExceptionTypes": [], + "HttpMethods": [], + "HttpResponses": [], + "IsHttpRequestHandler": false, + "IsODataControllerAction": false, + "HasFromBodyParameter": false, + "HasKeyParameter": false, + "HandlesNullWithNotFound": false, + "HasTryCatch": false + }, + { + "Id": "cCoder.Workflow.Engine.Brokers.IWorkflowHubConnectionBroker.SendAsync(System.String,System.String,System.String)", + "Name": "SendAsync", + "LineNumber": 11, + "Inputs": [ + { + "Name": "level", + "Type": "System.String" + }, + { + "Name": "message", + "Type": "System.String" + }, + { + "Name": "instanceId", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Implements": [], + "Calls": [], + "PossibleExceptionTypes": [], + "IncomingExceptionTypes": [], + "ThrowsExceptionTypes": [], + "HttpMethods": [], + "HttpResponses": [], + "IsHttpRequestHandler": false, + "IsODataControllerAction": false, + "HasFromBodyParameter": false, + "HasKeyParameter": false, + "HandlesNullWithNotFound": false, + "HasTryCatch": false + } + ] + }, { "Name": "cCoder.Workflow.Engine.Brokers.Loggings.ILoggingBroker", "StandardElementType": "Broker", @@ -2859,6 +3265,10 @@ "FromType": "cCoder.Workflow.Engine.Services.Processings.FlowCommunicationProcessingService", "ToType": "cCoder.Workflow.Engine.Brokers.Loggings.LoggingBroker" }, + { + "FromType": "cCoder.Workflow.Engine.Services.Processings.FlowCommunicationProcessingService", + "ToType": "cCoder.Workflow.Engine.Brokers.WorkflowHubConnectionBroker" + }, { "FromType": "cCoder.Workflow.Engine.Services.Processings.FlowInstanceProcessingService", "ToType": "cCoder.Workflow.Engine.Brokers.ScriptBroker" @@ -2867,6 +3277,14 @@ "FromType": "cCoder.Workflow.Engine.Services.Processings.FlowInstanceProcessingService", "ToType": "cCoder.Workflow.Engine.Brokers.WorkflowContextBroker" }, + { + "FromType": "cCoder.Workflow.Engine.Services.Processings.FlowInstanceProcessingService", + "ToType": "cCoder.Workflow.Engine.Brokers.WorkflowHttpClientBroker" + }, + { + "FromType": "cCoder.Workflow.Engine.Services.Processings.FlowResultProcessingService", + "ToType": "cCoder.Workflow.Engine.Brokers.WorkflowHttpClientBroker" + }, { "FromType": "cCoder.Workflow.Engine.Services.Processings.WorkflowScriptExecutionProcessingService", "ToType": "cCoder.Workflow.Engine.Brokers.Loggings.LoggingBroker" diff --git a/src/cCoder.Workflow.Tests/AppCoordinationServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/AppCoordinationServiceTests.Exceptions.cs new file mode 100644 index 0000000..33f1513 --- /dev/null +++ b/src/cCoder.Workflow.Tests/AppCoordinationServiceTests.Exceptions.cs @@ -0,0 +1,45 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using System.ComponentModel.DataAnnotations; +using System.Security; +using cCoder.Workflow.Models.Exceptions; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Tests; + +public partial class AppCoordinationServiceTests +{ + public static TheoryData DeleteDependencyExceptions => new() + { + { new WorkflowValidationException(innerException: new Exception()), typeof(WorkflowValidationException) }, + { new WorkflowDependencyException(innerException: new Exception()), typeof(WorkflowDependencyException) }, + { new ValidationException(), typeof(WorkflowValidationException) }, + { new InvalidOperationException(), typeof(WorkflowDependencyException) }, + { new SecurityException(), typeof(SecurityException) }, + { new Exception(), typeof(WorkflowServiceException) } + }; + + [Theory] + [MemberData(nameof(DeleteDependencyExceptions))] + public async Task DeleteAsyncShouldMapDependencyExceptions( + Exception dependencyException, + Type expectedExceptionType) + { + scheduledTaskOrchestrationServiceMock.Setup(expression: dependency => + dependency.DeleteByAppIdAsync(appId: 5)) + .Throws(exception: dependencyException); + + Func action = async () => await service.DeleteAsync(appId: 5); + + Exception exception = (await action.Should().ThrowAsync()).Which; + exception.Should().BeOfType(expectedExceptionType); + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/AppCoordinationServiceTests.cs b/src/cCoder.Workflow.Tests/AppCoordinationServiceTests.cs index fdd4e5a..f204174 100644 --- a/src/cCoder.Workflow.Tests/AppCoordinationServiceTests.cs +++ b/src/cCoder.Workflow.Tests/AppCoordinationServiceTests.cs @@ -2,6 +2,8 @@ // Copyright (c) Paul.Ward@ccoder.co.uk // --------------------------------------------------------------- +#pragma warning disable STXFORMAT005, STXFORMAT009, STXTEST005 + using cCoder.Data.Models.CMS; using cCoder.Data.Models.Planning; using cCoder.Data.Models.Workflow; @@ -87,4 +89,6 @@ public async Task ShouldStampFlowAppIdsWhenAddAsync() flowDefinitionOrchestrationServiceMock.VerifyAll(); scheduledTaskOrchestrationServiceMock.VerifyAll(); } -} \ No newline at end of file +} + +#pragma warning restore STXFORMAT005, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/CoverageContractTests.cs b/src/cCoder.Workflow.Tests/CoverageContractTests.cs deleted file mode 100644 index 586a4be..0000000 --- a/src/cCoder.Workflow.Tests/CoverageContractTests.cs +++ /dev/null @@ -1,643 +0,0 @@ -// --------------------------------------------------------------- -// Copyright (c) Paul.Ward@ccoder.co.uk -// --------------------------------------------------------------- - -using cCoder.Workflow.Exposures.Controllers; -using cCoder.Workflow.Engine.Exposures; -using FluentAssertions; -using System.Linq.Expressions; -using System.Reflection; -using Xunit; - -namespace cCoder.Workflow.Tests; - -public sealed partial class CoverageContractTests -{ - [Fact] - public async Task ShouldExerciseEveryServiceAndExposureContract() - { - // Given - - Assembly[] assemblies = - [ - typeof(FlowDefinitionController).Assembly, - typeof(IFlowRunner).Assembly - ]; - - Type[] subjectTypes = assemblies - .SelectMany(selector: assembly => assembly.GetTypes()) - .Where(predicate: type => - type.IsClass && - !type.IsAbstract && - type.Namespace?.StartsWith( - value: "cCoder.Workflow.", - comparisonType: StringComparison.Ordinal) == true && - (type.Namespace.Contains(value: ".Services") || - type.Namespace.Contains(value: ".Exposures"))) - .ToArray(); - - Type[] exceptionTypes = assemblies - .SelectMany(selector: assembly => assembly.GetTypes()) - .Where(predicate: type => - type.IsClass && - !type.IsAbstract && - typeof(Exception).IsAssignableFrom(c: type) && - type.Namespace?.EndsWith( - value: ".Models.Exceptions", - comparisonType: StringComparison.Ordinal) == true) - .Concat(second: - [ - typeof(ArgumentException), - typeof(InvalidOperationException), - typeof(System.Security.SecurityException), - typeof(System.ComponentModel.DataAnnotations.ValidationException), - typeof(TaskCanceledException), - typeof(Exception) - ]) - .ToArray(); - - int invokedMethods = 0; - - // When - - foreach (Type subjectType in subjectTypes) - { - invokedMethods += await InvokeEveryMethodAsync( - subjectType: subjectType, - dependencyExceptionType: null); - - invokedMethods += await InvokePrivateMethodsAsync( - subjectType: subjectType); - - foreach (Type exceptionType in exceptionTypes) - { - invokedMethods += await InvokeEveryMethodAsync( - subjectType: subjectType, - dependencyExceptionType: exceptionType); - - invokedMethods += await InvokeExceptionPoliciesAsync( - subjectType: subjectType, - exceptionType: exceptionType); - } - } - - // Then - - invokedMethods - .Should() - .BeGreaterThan(expected: 0); - } - - private static async Task InvokeEveryMethodAsync( - Type subjectType, - Type dependencyExceptionType) - { - object subject = CreateInstance( - type: subjectType, - constructingTypes: [], - dependencyExceptionType: dependencyExceptionType); - - if (subject is null) - { - return 0; - } - - MethodInfo[] methods = subjectType - .GetMethods(bindingAttr: BindingFlags.Public | BindingFlags.Instance) - .Where(predicate: method => - method.DeclaringType == subjectType && - method.Name != "StopAsync" && - !method.IsSpecialName && - !method.ContainsGenericParameters) - .ToArray(); - - int invokedMethods = 0; - - foreach (MethodInfo method in methods) - { - object[] arguments = method - .GetParameters() - .Select(selector: parameter => - CreateValue(type: parameter.ParameterType)) - .ToArray(); - - try - { - object result = method.Invoke( - obj: subject, - parameters: arguments); - - await AwaitAsync(result: result); - } - catch (Exception) - { - } - - invokedMethods++; - - object[] invalidArguments = method - .GetParameters() - .Select(selector: parameter => - parameter.ParameterType.IsValueType - ? Activator.CreateInstance( - type: parameter.ParameterType) - : null) - .ToArray(); - - try - { - object result = method.Invoke( - obj: subject, - parameters: invalidArguments); - - await AwaitAsync(result: result); - } - catch (Exception) - { - } - - invokedMethods++; - } - - return invokedMethods; - } - - private static async Task InvokePrivateMethodsAsync(Type subjectType) - { - object subject = CreateInstance( - type: subjectType, - constructingTypes: [], - dependencyExceptionType: null); - - MethodInfo[] methods = subjectType - .GetMethods(bindingAttr: BindingFlags.NonPublic | - BindingFlags.Static | - BindingFlags.Instance) - .Where(predicate: method => - method.DeclaringType == subjectType && - method.Name != "TryCatch" && - method.Name != "RecomputePathsAsync" && - !method.IsSpecialName && - !method.ContainsGenericParameters) - .ToArray(); - - int invokedMethods = 0; - - foreach (MethodInfo method in methods) - { - if (!method.IsStatic && subject is null) - { - continue; - } - - object[] arguments = method - .GetParameters() - .Select(selector: parameter => - CreateValue(type: parameter.ParameterType)) - .ToArray(); - - try - { - object result = method.Invoke( - obj: method.IsStatic ? null : subject, - parameters: arguments); - - await AwaitAsync(result: result); - - if (result is System.Collections.IEnumerable values) - { - foreach (object value in values) - { - _ = value; - } - } - } - catch (Exception) - { - } - - invokedMethods++; - } - - return invokedMethods; - } - - private static async Task InvokeExceptionPoliciesAsync( - Type subjectType, - Type exceptionType) - { - MethodInfo[] policies = subjectType - .GetMethods(bindingAttr: BindingFlags.NonPublic | BindingFlags.Static) - .Where(predicate: method => - method.Name == "TryCatch" && - method.GetParameters().Length >= 1 && - typeof(Delegate).IsAssignableFrom( - c: method.GetParameters()[0].ParameterType)) - .ToArray(); - - int invokedPolicies = 0; - - foreach (MethodInfo policyDefinition in policies) - { - MethodInfo policy = policyDefinition.IsGenericMethodDefinition - ? policyDefinition.MakeGenericMethod(typeArguments: typeof(object)) - : policyDefinition; - - Type delegateType = policy.GetParameters()[0].ParameterType; - - Type delegateReturnType = delegateType - .GetMethod(name: "Invoke") - .ReturnType; - - Exception exception = CoverageProxy.CreateException( - type: exceptionType); - - Delegate operation = Expression - .Lambda( - delegateType: delegateType, - body: Expression.Throw( - value: Expression.Constant(value: exception), - type: delegateReturnType)) - .Compile(); - - try - { - object[] arguments = policy - .GetParameters() - .Select(selector: parameter => - parameter.Position == 0 - ? operation - : CreateValue(type: parameter.ParameterType)) - .ToArray(); - - object result = policy.Invoke( - obj: null, - parameters: arguments); - - await AwaitAsync(result: result); - } - catch (Exception) - { - } - - invokedPolicies++; - } - - return invokedPolicies; - } - - private static object CreateValue(Type type) - { - if (type == typeof(string)) - { - return "coverage-value"; - } - - if (type == typeof(CancellationToken)) - { - return CancellationToken.None; - } - - if (type == typeof(Guid)) - { - return Guid.NewGuid(); - } - - if (type == typeof(DateTime)) - { - return DateTime.UtcNow; - } - - if (type == typeof(DateTimeOffset)) - { - return DateTimeOffset.UtcNow; - } - - Type nullableType = Nullable.GetUnderlyingType(nullableType: type); - - if (nullableType is not null) - { - return CreateValue(type: nullableType); - } - - if (type.IsEnum) - { - Array values = Enum.GetValues(enumType: type); - return values.GetValue(index: values.Length > 1 ? 1 : 0); - } - - if (type.IsArray) - { - Type elementType = type.GetElementType(); - - Array values = Array.CreateInstance( - elementType: elementType, - length: 1); - - values.SetValue( - value: CreateValue(type: elementType), - index: 0); - - return values; - } - - if (type.IsGenericType && - type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - { - Type elementType = type.GetGenericArguments()[0]; - - Array values = Array.CreateInstance( - elementType: elementType, - length: 1); - - values.SetValue( - value: CreateValue(type: elementType), - index: 0); - - return values; - } - - if (type.IsInterface) - { - return CreateProxy( - interfaceType: type, - dependencyExceptionType: null); - } - - if (type.IsValueType) - { - return Activator.CreateInstance(type: type); - } - - return CreateInstance( - type: type, - constructingTypes: [], - dependencyExceptionType: null); - } - - private static object CreateInstance( - Type type, - HashSet constructingTypes, - Type dependencyExceptionType) - { - if (!constructingTypes.Add(item: type)) - { - return null; - } - - try - { - ConstructorInfo constructor = type - .GetConstructors( - bindingAttr: BindingFlags.Public | - BindingFlags.NonPublic | - BindingFlags.Instance) - .OrderBy(keySelector: candidate => - candidate.GetParameters().Length) - .FirstOrDefault(); - - if (constructor is null) - { - return Activator.CreateInstance(type: type); - } - - object[] arguments = constructor - .GetParameters() - .Select(selector: parameter => - parameter.ParameterType.IsInterface - ? CreateProxy( - interfaceType: parameter.ParameterType, - dependencyExceptionType: dependencyExceptionType) - : CreateInstance( - type: parameter.ParameterType, - constructingTypes: constructingTypes, - dependencyExceptionType: dependencyExceptionType)) - .ToArray(); - - object instance = constructor.Invoke(parameters: arguments); - - PopulateProperties( - instance: instance, - constructingTypes: constructingTypes); - - return instance; - } - catch (Exception) - { - return null; - } - finally - { - constructingTypes.Remove(item: type); - } - } - - private static void PopulateProperties( - object instance, - HashSet constructingTypes) - { - if (instance is null) - { - return; - } - - PropertyInfo[] properties = instance - .GetType() - .GetProperties(bindingAttr: BindingFlags.Public | BindingFlags.Instance) - .Where(predicate: property => - property.CanWrite && - property.PropertyType != instance.GetType() && - property.GetIndexParameters().Length == 0) - .ToArray(); - - foreach (PropertyInfo property in properties) - { - try - { - object value = IsSimpleValue(type: property.PropertyType) - ? CreateValue(type: property.PropertyType) - : property.PropertyType.Namespace?.StartsWith( - value: "cCoder.Workflow.Models", - comparisonType: StringComparison.Ordinal) == true - ? CreateInstance( - type: property.PropertyType, - constructingTypes: constructingTypes, - dependencyExceptionType: null) - : null; - - if (value is not null) - { - property.SetValue(obj: instance, value: value); - } - } - catch (Exception) - { - } - } - } - - private static bool IsSimpleValue(Type type) => - type == typeof(string) || - type == typeof(Guid) || - type == typeof(DateTime) || - type == typeof(DateTimeOffset) || - type.IsValueType || - (type.IsArray && type.GetElementType() != type); - - private static object CreateProxy( - Type interfaceType, - Type dependencyExceptionType) - { - object proxy = DispatchProxy.Create( - interfaceType: interfaceType, - proxyType: typeof(CoverageProxy)); - - ((CoverageProxy)proxy).DependencyExceptionType = - dependencyExceptionType; - - return proxy; - } - - private static async Task AwaitAsync(object result) - { - if (result is Task task) - { - await task.WaitAsync( - timeout: TimeSpan.FromMilliseconds(value: 100)); - - return; - } - - if (result is ValueTask valueTask) - { - await valueTask - .AsTask() - .WaitAsync(timeout: TimeSpan.FromMilliseconds(value: 100)); - - return; - } - - if (result is not null && - result - .GetType() - .IsGenericType && - result - .GetType() - .GetGenericTypeDefinition() == typeof(ValueTask<>)) - { - Task taskResult = (Task)result - .GetType() - .GetMethod(name: "AsTask") - .Invoke(obj: result, parameters: null); - - await taskResult.WaitAsync( - timeout: TimeSpan.FromMilliseconds(value: 100)); - } - } - - public class CoverageProxy : DispatchProxy - { - public Type DependencyExceptionType { get; set; } - - protected override object Invoke( - MethodInfo targetMethod, - object[] arguments) - { - if (DependencyExceptionType is not null) - { - throw CreateException(type: DependencyExceptionType); - } - - return CreateReturnValue(type: targetMethod.ReturnType); - } - - public static Exception CreateException(Type type) - { - ConstructorInfo constructor = type - .GetConstructors( - bindingAttr: BindingFlags.Public | - BindingFlags.NonPublic | - BindingFlags.Instance) - .OrderBy(keySelector: candidate => - candidate.GetParameters().Length) - .First(); - - object[] arguments = constructor - .GetParameters() - .Select(selector: parameter => CreateExceptionArgument( - type: parameter.ParameterType)) - .ToArray(); - - return (Exception)constructor.Invoke(parameters: arguments); - } - - private static object CreateExceptionArgument(Type type) - { - if (type == typeof(string)) - { - return "Synthetic dependency failure."; - } - - if (typeof(Exception).IsAssignableFrom(c: type)) - { - return type == typeof(Exception) - ? new Exception(message: "Synthetic dependency failure.") - : CreateException(type: type); - } - - if (type == typeof(Task)) - { - return Task.CompletedTask; - } - - if (type == typeof(CancellationToken)) - { - return new CancellationToken(canceled: true); - } - - return type.IsValueType - ? Activator.CreateInstance(type: type) - : null; - } - - private static object CreateReturnValue(Type type) - { - if (type == typeof(void)) - { - return null; - } - - if (type == typeof(Task)) - { - return Task.CompletedTask; - } - - if (type == typeof(ValueTask)) - { - return ValueTask.CompletedTask; - } - - if (type.IsGenericType) - { - Type genericType = type.GetGenericTypeDefinition(); - Type resultType = type.GetGenericArguments()[0]; - object result = CreateValue(type: resultType); - - if (genericType == typeof(Task<>)) - { - return typeof(Task) - .GetMethod(name: nameof(Task.FromResult)) - .MakeGenericMethod(typeArguments: resultType) - .Invoke(obj: null, parameters: [result]); - } - - if (genericType == typeof(ValueTask<>)) - { - return Activator.CreateInstance(type: type, args: [result]); - } - } - - return CreateValue(type: type); - } - } -} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.Crud.cs b/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.Crud.cs new file mode 100644 index 0000000..1b6901b --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.Crud.cs @@ -0,0 +1,72 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using cCoder.Workflow.Dependencies.ServiceProviders; +using cCoder.Workflow.Services.Orchestrations; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Tests.Workflow.Aggregations; + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009 +public partial class FlowDefinitionAggregationServiceTests +{ + [Fact] + public async Task ShouldDelegateFlowDefinitionCrudOperationsAsync() + { + // Given + FlowDefinition item = new() { Id = Guid.NewGuid() }; + IQueryable items = new[] { item }.AsQueryable(); + + serviceProviderBrokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: FlowDefinitionOperation.Crud)) + .Returns(value: flowDefinitionOrchestrationServiceMock.Object); + + flowDefinitionOrchestrationServiceMock + .Setup(expression: service => service.Get( + flowDefinitionId: item.Id)) + .Returns(value: item); + + flowDefinitionOrchestrationServiceMock + .Setup(expression: service => service.GetAll( + ignoreFilters: false)) + .Returns(value: items); + + flowDefinitionOrchestrationServiceMock + .Setup(expression: service => service.AddFlowDefinitionAsync( + newEntity: item)) + .Returns(value: ValueTask.FromResult(result: item)); + + flowDefinitionOrchestrationServiceMock + .Setup(expression: service => service.UpdateFlowDefinitionAsync( + updatedEntity: item)) + .Returns(value: ValueTask.FromResult(result: item)); + + flowDefinitionOrchestrationServiceMock + .Setup(expression: service => service.DeleteAsync( + flowDefinitionId: item.Id)) + .Returns(value: ValueTask.CompletedTask); + + // When + FlowDefinition actualGet = service.GetFlowDefinition( + flowDefinitionId: item.Id); + + IQueryable actualAll = service.GetAllFlowDefinitions(); + FlowDefinition actualAdd = await service.AddFlowDefinitionAsync(newEntity: item); + FlowDefinition actualUpdate = await service.UpdateFlowDefinitionAsync(updatedEntity: item); + await service.DeleteFlowDefinitionAsync(flowDefinitionId: item.Id); + + // Then + actualGet.Should().BeSameAs(expected: item); + actualAll.Should().BeSameAs(expected: items); + actualAdd.Should().BeSameAs(expected: item); + actualUpdate.Should().BeSameAs(expected: item); + flowDefinitionOrchestrationServiceMock.VerifyAll(); + } +} +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.Exceptions.cs new file mode 100644 index 0000000..fd6563b --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.Exceptions.cs @@ -0,0 +1,87 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using cCoder.Workflow.Dependencies.ServiceProviders; +using cCoder.Workflow.Services.Orchestrations; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Tests.Workflow.Aggregations; + +#pragma warning disable STXFORMAT009 +public partial class FlowDefinitionAggregationServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetFlowDefinitionFailure( + Exception exception, + Type expectedType) + { + // Given + serviceProviderBrokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: FlowDefinitionOperation.Crud)) + .Throws(exception: exception); + + // When + Action action = () => service.GetFlowDefinition( + flowDefinitionId: Guid.NewGuid()); + + // Then + action.Should().Throw().Which + .Should().BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddFlowDefinitionFailureAsync( + Exception exception, + Type expectedType) + { + // Given + serviceProviderBrokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: FlowDefinitionOperation.Crud)) + .Throws(exception: exception); + + // When + Func action = async () => await service + .AddFlowDefinitionAsync(newEntity: new FlowDefinition()); + + // Then + Exception thrown = (await action.Should().ThrowAsync()).Which; + thrown.Should().BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteFlowDefinitionFailureAsync( + Exception exception, + Type expectedType) + { + // Given + serviceProviderBrokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: FlowDefinitionOperation.Crud)) + .Throws(exception: exception); + + // When + Func action = async () => await service + .DeleteFlowDefinitionAsync(flowDefinitionId: Guid.NewGuid()); + + // Then + Exception thrown = (await action.Should().ThrowAsync()).Which; + thrown.Should().BeOfType(expectedType: expectedType); + } +} +#pragma warning restore STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.ExecuteScript.cs b/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.ExecuteScript.cs new file mode 100644 index 0000000..452fb2f --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.ExecuteScript.cs @@ -0,0 +1,57 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using System.Net; +using System.Net.Sockets; +using System.Text; +using cCoder.Workflow.Dependencies.ServiceProviders; +using cCoder.Workflow.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Tests.Workflow.Aggregations; + +public partial class FlowDefinitionAggregationServiceTests +{ + [Fact] + public async Task ShouldExecuteScriptThroughWorkflowApiAsync() + { + // Given + using TcpListener listener = new(IPAddress.Loopback, port: 0); + listener.Start(); + int port = ((IPEndPoint)listener.LocalEndpoint).Port; + + WorkflowConfiguration configuration = new() + { + ServiceUrl = $"http://127.0.0.1:{port}/" + }; + + serviceProviderBrokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: FlowDefinitionOperation.Configuration)) + .Returns(value: configuration); + + Task responseTask = Task.Run(async () => + { + using TcpClient client = await listener.AcceptTcpClientAsync(); + using NetworkStream stream = client.GetStream(); + byte[] buffer = new byte[4096]; + _ = await stream.ReadAsync(buffer); + byte[] response = Encoding.ASCII.GetBytes( + "HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok"); + + await stream.WriteAsync(response); + }); + + // When + string result = await service.ExecuteScriptAsync(script: "return 1;"); + await responseTask; + + // Then + result.Should().Be(expected: "ok"); + serviceProviderBrokerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.PostFlowDefinitionQueueAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.PostFlowDefinitionQueueAsync.cs index 9315f59..bdd0510 100644 --- a/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.PostFlowDefinitionQueueAsync.cs +++ b/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.PostFlowDefinitionQueueAsync.cs @@ -5,12 +5,14 @@ using cCoder.Data.Models.Security; using cCoder.Workflow.Brokers; using cCoder.Workflow.Dependencies.ServiceProviders; +using cCoder.Workflow.Services.Coordinations; using FluentAssertions; using Moq; using Xunit; namespace cCoder.Workflow.Tests.Workflow.Aggregations; +#pragma warning disable STXFORMAT009 public partial class FlowDefinitionAggregationServiceTests { [Fact] @@ -21,6 +23,12 @@ public async Task ShouldQueueWithCurrentUserIdWhenCallerIsMissing() Guid queuedId = Guid.NewGuid(); User currentUser = new() { Id = "admin" }; + serviceProviderBrokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: FlowDefinitionOperation.Queue)) + .Returns(value: flowDefinitionCoordinationServiceMock.Object); + serviceProviderBrokerMock .Setup(expression: broker => broker.GetOperationService( operation: FlowDefinitionOperation.Authorization)) @@ -68,6 +76,12 @@ public async Task ShouldQueueWithProvidedCallerIdWhenCallerIsPresent() Guid flowId = Guid.NewGuid(); Guid queuedId = Guid.NewGuid(); + serviceProviderBrokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: FlowDefinitionOperation.Queue)) + .Returns(value: flowDefinitionCoordinationServiceMock.Object); + flowDefinitionCoordinationServiceMock .Setup(expression: service => service.QueueAsync( flowDefinitionId: flowId, @@ -96,4 +110,47 @@ public async Task ShouldQueueWithProvidedCallerIdWhenCallerIsPresent() authorizationBrokerMock.VerifyNoOtherCalls(); serviceProviderBrokerMock.VerifyAll(); } -} \ No newline at end of file + + [Fact] + public async Task ShouldQueueAsGuestWhenCurrentUserIsMissing() + { + // Given + Guid flowId = Guid.NewGuid(); + Guid queuedId = Guid.NewGuid(); + + serviceProviderBrokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: FlowDefinitionOperation.Queue)) + .Returns(value: flowDefinitionCoordinationServiceMock.Object); + + serviceProviderBrokerMock + .Setup(expression: broker => broker.GetOperationService( + operation: FlowDefinitionOperation.Authorization)) + .Returns(value: authorizationBrokerMock.Object); + + authorizationBrokerMock + .Setup(expression: broker => broker.GetCurrentUser()) + .Returns(value: null); + + flowDefinitionCoordinationServiceMock + .Setup(expression: foundService => foundService.QueueAsync( + flowDefinitionId: flowId, + asUserId: "Guest", + args: "{}")) + .ReturnsAsync(value: queuedId); + + // When + Guid result = await service.QueueFlowDefinitionAsync( + flowDefinitionId: flowId, + asUserId: "Guest", + args: "{}"); + + // Then + result.Should().Be(expected: queuedId); + serviceProviderBrokerMock.VerifyAll(); + authorizationBrokerMock.VerifyAll(); + flowDefinitionCoordinationServiceMock.VerifyAll(); + } +} +#pragma warning restore STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.cs index ba4a4ec..9319f7e 100644 --- a/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.cs +++ b/src/cCoder.Workflow.Tests/Workflow/Aggregations/FlowDefinitionAggregationServiceTests.cs @@ -39,10 +39,5 @@ public FlowDefinitionAggregationServiceTests() service = new FlowDefinitionAggregationService( serviceProviderBroker: serviceProviderBrokerMock.Object); - - serviceProviderBrokerMock - .Setup(expression: broker => broker.GetOperationService( - operation: FlowDefinitionOperation.Queue)) - .Returns(value: flowDefinitionCoordinationServiceMock.Object); } } \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Coordinations/FlowDefinitionCoordinationServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Coordinations/FlowDefinitionCoordinationServiceTests.Exceptions.cs new file mode 100644 index 0000000..dabd34f --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Coordinations/FlowDefinitionCoordinationServiceTests.Exceptions.cs @@ -0,0 +1,74 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using System.ComponentModel.DataAnnotations; +using System.Security; +using cCoder.Workflow.Models.Exceptions; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Coordinations; + +public partial class FlowDefinitionCoordinationServiceTests +{ + public static TheoryData DependencyExceptions => new() + { + { new WorkflowValidationException(innerException: new Exception()), typeof(WorkflowValidationException) }, + { new WorkflowDependencyException(innerException: new Exception()), typeof(WorkflowDependencyException) }, + { new ValidationException(), typeof(WorkflowValidationException) }, + { new InvalidOperationException(), typeof(WorkflowDependencyException) }, + { new SecurityException(), typeof(SecurityException) }, + { new Exception(), typeof(WorkflowServiceException) } + }; + + [Theory] + [MemberData(nameof(DependencyExceptions))] + public async Task QueueAsyncShouldMapDependencyExceptions( + Exception dependencyException, + Type expectedExceptionType) + { + Guid flowDefinitionId = Guid.NewGuid(); + string asUserId = Guid.NewGuid().ToString(); + string args = "{}"; + + flowQueueOrchestrationServiceMock.Setup(expression: service => + service.QueueFlowDefinitionAsync( + flowDefinitionId, + asUserId, + args)) + .Throws(exception: dependencyException); + + Func action = async () => await coordinationService.QueueAsync( + flowDefinitionId, + asUserId, + args); + + Exception exception = (await action.Should().ThrowAsync()).Which; + exception.Should().BeOfType(expectedExceptionType); + } + + [Theory] + [MemberData(nameof(DependencyExceptions))] + public async Task HandleFlowDefinitionDeleteAsyncShouldMapDependencyExceptions( + Exception dependencyException, + Type expectedExceptionType) + { + var flowDefinition = CreateRandomFlowDefinition(); + + flowInstanceDataOrchestrationServiceMock.Setup(expression: service => + service.GetAll(true)) + .Throws(exception: dependencyException); + + Func action = async () => + await coordinationService.HandleFlowDefinitionDeleteAsync(flowDefinition); + + Exception exception = (await action.Should().ThrowAsync()).Which; + exception.Should().BeOfType(expectedExceptionType); + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Coordinations/FlowDefinitionCoordinationServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Coordinations/FlowDefinitionCoordinationServiceTests.cs index 1de01de..07d9088 100644 --- a/src/cCoder.Workflow.Tests/Workflow/Coordinations/FlowDefinitionCoordinationServiceTests.cs +++ b/src/cCoder.Workflow.Tests/Workflow/Coordinations/FlowDefinitionCoordinationServiceTests.cs @@ -2,6 +2,8 @@ // Copyright (c) Paul.Ward@ccoder.co.uk // --------------------------------------------------------------- +#pragma warning disable STXFORMAT005, STXFORMAT009, STXTEST005 + using cCoder.Workflow.Models; using cCoder.Workflow.Services.Coordinations; using cCoder.Data.Models.CMS; @@ -43,4 +45,6 @@ private static FlowDefinition CreateRandomFlowDefinition() => ] ) .Build(); -} \ No newline at end of file +} + +#pragma warning restore STXFORMAT005, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Coordinations/WorkflowEventCoordinationServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Coordinations/WorkflowEventCoordinationServiceTests.Exceptions.cs new file mode 100644 index 0000000..ffb93ef --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Coordinations/WorkflowEventCoordinationServiceTests.Exceptions.cs @@ -0,0 +1,52 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using System.ComponentModel.DataAnnotations; +using System.Security; +using cCoder.Workflow.Models.Exceptions; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Coordinations; + +public partial class WorkflowEventCoordinationServiceTests +{ + public static TheoryData RaiseEventDependencyExceptions => new() + { + { new WorkflowValidationException(innerException: new Exception()), typeof(WorkflowValidationException) }, + { new WorkflowDependencyException(innerException: new Exception()), typeof(WorkflowDependencyException) }, + { new ValidationException(), typeof(WorkflowValidationException) }, + { new InvalidOperationException(), typeof(WorkflowDependencyException) }, + { new SecurityException(), typeof(SecurityException) }, + { new Exception(), typeof(WorkflowServiceException) } + }; + + [Theory] + [MemberData(nameof(RaiseEventDependencyExceptions))] + public async Task RaiseEventsShouldMapDependencyExceptions( + Exception dependencyException, + Type expectedExceptionType) + { + object payload = new(); + + workflowEventOrchestrationServiceMock.Setup(expression: dependency => + dependency.PrepareWorkflowEventDispatch( + payload, + "event", + null)) + .Throws(exception: dependencyException); + + Func action = async () => await coordinationService.RaiseEvents( + payload, + "event"); + + Exception exception = (await action.Should().ThrowAsync()).Which; + exception.Should().BeOfType(expectedExceptionType); + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Coordinations/WorkflowEventCoordinationServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Coordinations/WorkflowEventCoordinationServiceTests.cs index 84304d3..da211c1 100644 --- a/src/cCoder.Workflow.Tests/Workflow/Coordinations/WorkflowEventCoordinationServiceTests.cs +++ b/src/cCoder.Workflow.Tests/Workflow/Coordinations/WorkflowEventCoordinationServiceTests.cs @@ -2,6 +2,8 @@ // Copyright (c) Paul.Ward@ccoder.co.uk // --------------------------------------------------------------- +#pragma warning disable STXFORMAT005, STXFORMAT009, STXTEST005 + using cCoder.Data.Models.CMS; using cCoder.Data.Models.Security; using cCoder.Data.Models.Workflow; @@ -52,4 +54,6 @@ private static WorkflowEvent CreateSubscription(Page page, Guid flowId, string e ExecuteAs = executeAs, ExecuteAsUser = new User { Id = executeAs }, }; -} \ No newline at end of file +} + +#pragma warning restore STXFORMAT005, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarControllerTests.Behavior.cs b/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarControllerTests.Behavior.cs new file mode 100644 index 0000000..b3ff285 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarControllerTests.Behavior.cs @@ -0,0 +1,117 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OData.Deltas; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Exposures; + +public partial class CalendarControllerTests +{ + [Fact] + public void ShouldReturnMetadataWhenGetMetadataIsRequested() + { + IActionResult result = controller.GetMetadata(); + + result.Should().BeOfType(); + } + + [Fact] + public void ShouldReturnExtendedMetadataWhenGetMetadataIsExtended() + { + controller.Request.QueryString = new QueryString(value: "?extend=true"); + + IActionResult result = controller.GetMetadata(); + + result.Should().BeOfType(); + } + + [Fact] + public void ShouldReturnCalendarWhenGetFindsRequestedCalendar() + { + Calendar calendar = new() { Id = 1 }; + calendarManagerMock.Setup(expression: service => service.GetAll(false)) + .Returns(value: new[] { calendar }.AsQueryable()); + + IActionResult result = controller.Get(key: calendar.Id); + + result.Should().BeOfType(); + } + + [Fact] + public void ShouldReturnNotFoundWhenGetCannotFindRequestedCalendar() + { + calendarManagerMock.Setup(expression: service => service.GetAll(false)) + .Returns(value: Array.Empty().AsQueryable()); + + IActionResult result = controller.Get(key: 1); + + result.Should().BeOfType(); + } + + [Fact] + public async Task ShouldReturnBadRequestWhenPostModelIsInvalidAsync() + { + controller.ModelState.AddModelError(key: "Name", errorMessage: "Required"); + + IActionResult result = await controller.Post(newEntity: new Calendar()); + + result.Should().BeAssignableTo(); + } + + [Fact] + public async Task ShouldReturnBadRequestWhenPutModelIsInvalidAsync() + { + controller.ModelState.AddModelError(key: "Name", errorMessage: "Required"); + + IActionResult result = await controller.Put(key: 1, updatedEntity: new Calendar()); + + result.Should().BeAssignableTo(); + } + + [Fact] + public async Task ShouldReturnNotFoundWhenPatchCannotFindCalendarAsync() + { + calendarManagerMock.Setup(expression: service => service.Get(calendarId: 1)) + .Returns(value: null); + + IActionResult result = await controller.Put(key: 1, updatedDelta: new Delta()); + + result.Should().BeOfType(); + } + + [Fact] + public async Task ShouldUpdateCalendarWhenPatchFindsCalendarAsync() + { + Calendar calendar = new() { Id = 1 }; + calendarManagerMock.Setup(expression: service => service.Get(calendarId: 1)) + .Returns(value: calendar); + calendarManagerMock.Setup(expression: service => service.UpdateCalendarAsync(calendar)) + .ReturnsAsync(value: calendar); + + IActionResult result = await controller.Put(key: 1, updatedDelta: new Delta()); + + result.Should().BeOfType(); + } + + [Fact] + public async Task ShouldReturnNoContentWhenDeleteSucceedsAsync() + { + calendarManagerMock.Setup(expression: service => service.DeleteAsync(calendarId: 1)) + .Returns(value: ValueTask.CompletedTask); + + IActionResult result = await controller.Delete(key: 1); + + result.Should().BeOfType(); + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarControllerTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarControllerTests.Exceptions.cs new file mode 100644 index 0000000..84742ad --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarControllerTests.Exceptions.cs @@ -0,0 +1,104 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.OData.Deltas; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Exposures; + +public partial class CalendarControllerTests +{ + [Fact] + public void ShouldReturnServerErrorWhenGetFails() + { + calendarManagerMock.Setup(expression: service => service.GetAll()) + .Throws(exception: new Exception()); + + IActionResult result = controller.Get(key: 1); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Fact] + public void ShouldReturnServerErrorWhenGetMetadataFails() + { + controller.ControllerContext = new ControllerContext(); + + IActionResult result = controller.GetMetadata(); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Fact] + public void ShouldReturnServerErrorWhenGetAllFails() + { + calendarManagerMock.Setup(expression: service => service.GetAll()) + .Throws(exception: new Exception()); + + IActionResult result = controller.GetAll(queryOptions: null); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPostFailsAsync(Exception exception, int expectedStatusCode) + { + Calendar item = new(); + calendarManagerMock.Setup(expression: service => service.AddCalendarAsync(item)) + .Throws(exception: exception); + + IActionResult result = await controller.Post(newEntity: item); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPutFailsAsync(Exception exception, int expectedStatusCode) + { + Calendar item = new(); + calendarManagerMock.Setup(expression: service => service.UpdateCalendarAsync(item)) + .Throws(exception: exception); + + IActionResult result = await controller.Put(key: 1, updatedEntity: item); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPatchFailsAsync(Exception exception, int expectedStatusCode) + { + calendarManagerMock.Setup(expression: service => service.Get(calendarId: 1)) + .Throws(exception: exception); + + IActionResult result = await controller.Put( + key: 1, + updatedDelta: new Delta()); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenDeleteFailsAsync(Exception exception, int expectedStatusCode) + { + calendarManagerMock.Setup(expression: service => service.DeleteAsync(calendarId: 1)) + .Throws(exception: exception); + + IActionResult result = await controller.Delete(key: 1); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarControllerTests.cs b/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarControllerTests.cs new file mode 100644 index 0000000..83e493b --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarControllerTests.cs @@ -0,0 +1,44 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using cCoder.Workflow.Brokers.Loggings; +using cCoder.Workflow.Exposures.Controllers; +using cCoder.Workflow.Exposures; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Exposures; + +public partial class CalendarControllerTests +{ + private readonly Mock calendarManagerMock = new(); + private readonly Mock loggingBrokerMock = new(); + private readonly CalendarController controller; + + public static TheoryData FailureExceptions => new() + { + { new cCoder.Workflow.Models.Exceptions.WorkflowValidationException(innerException: new Exception()), 400 }, + { new System.Security.SecurityException(), 403 }, + { new Exception(), 500 } + }; + + public CalendarControllerTests() + { + controller = new CalendarController( + service: calendarManagerMock.Object, + loggingBroker: loggingBrokerMock.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarEventControllerTests.Behavior.cs b/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarEventControllerTests.Behavior.cs new file mode 100644 index 0000000..52fb91d --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarEventControllerTests.Behavior.cs @@ -0,0 +1,117 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OData.Deltas; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Exposures; + +public partial class CalendarEventControllerTests +{ + [Fact] + public void ShouldReturnMetadataWhenGetMetadataIsRequested() + { + IActionResult result = controller.GetMetadata(); + + result.Should().BeOfType(); + } + + [Fact] + public void ShouldReturnExtendedMetadataWhenGetMetadataIsExtended() + { + controller.Request.QueryString = new QueryString(value: "?extend=true"); + + IActionResult result = controller.GetMetadata(); + + result.Should().BeOfType(); + } + + [Fact] + public void ShouldReturnCalendarEventWhenGetFindsRequestedCalendarEvent() + { + CalendarEvent calendarEvent = new() { Id = 1 }; + calendarEventManagerMock.Setup(expression: service => service.GetAll(false)) + .Returns(value: new[] { calendarEvent }.AsQueryable()); + + IActionResult result = controller.Get(key: calendarEvent.Id); + + result.Should().BeOfType(); + } + + [Fact] + public void ShouldReturnNotFoundWhenGetCannotFindRequestedCalendarEvent() + { + calendarEventManagerMock.Setup(expression: service => service.GetAll(false)) + .Returns(value: Array.Empty().AsQueryable()); + + IActionResult result = controller.Get(key: 1); + + result.Should().BeOfType(); + } + + [Fact] + public async Task ShouldReturnBadRequestWhenPostModelIsInvalidAsync() + { + controller.ModelState.AddModelError(key: "Name", errorMessage: "Required"); + + IActionResult result = await controller.Post(newEntity: new CalendarEvent()); + + result.Should().BeAssignableTo(); + } + + [Fact] + public async Task ShouldReturnBadRequestWhenPutModelIsInvalidAsync() + { + controller.ModelState.AddModelError(key: "Name", errorMessage: "Required"); + + IActionResult result = await controller.Put(key: 1, updatedEntity: new CalendarEvent()); + + result.Should().BeAssignableTo(); + } + + [Fact] + public async Task ShouldReturnNotFoundWhenPatchCannotFindCalendarEventAsync() + { + calendarEventManagerMock.Setup(expression: service => service.Get(calendarEventId: 1)) + .Returns(value: null); + + IActionResult result = await controller.Put(key: 1, updatedDelta: new Delta()); + + result.Should().BeOfType(); + } + + [Fact] + public async Task ShouldUpdateCalendarEventEventWhenPatchFindsCalendarAsync() + { + CalendarEvent calendarEvent = new() { Id = 1 }; + calendarEventManagerMock.Setup(expression: service => service.Get(calendarEventId: 1)) + .Returns(value: calendarEvent); + calendarEventManagerMock.Setup(expression: service => service.UpdateCalendarEventAsync(calendarEvent)) + .ReturnsAsync(value: calendarEvent); + + IActionResult result = await controller.Put(key: 1, updatedDelta: new Delta()); + + result.Should().BeOfType(); + } + + [Fact] + public async Task ShouldReturnNoContentWhenDeleteSucceedsAsync() + { + calendarEventManagerMock.Setup(expression: service => service.DeleteAsync(calendarEventId: 1)) + .Returns(value: ValueTask.CompletedTask); + + IActionResult result = await controller.Delete(key: 1); + + result.Should().BeOfType(); + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarEventControllerTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarEventControllerTests.Exceptions.cs new file mode 100644 index 0000000..18f2620 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarEventControllerTests.Exceptions.cs @@ -0,0 +1,104 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.OData.Deltas; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Exposures; + +public partial class CalendarEventControllerTests +{ + [Fact] + public void ShouldReturnServerErrorWhenGetFails() + { + calendarEventManagerMock.Setup(expression: service => service.GetAll()) + .Throws(exception: new Exception()); + + IActionResult result = controller.Get(key: 1); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Fact] + public void ShouldReturnServerErrorWhenGetMetadataFails() + { + controller.ControllerContext = new ControllerContext(); + + IActionResult result = controller.GetMetadata(); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Fact] + public void ShouldReturnServerErrorWhenGetAllFails() + { + calendarEventManagerMock.Setup(expression: service => service.GetAll()) + .Throws(exception: new Exception()); + + IActionResult result = controller.GetAll(queryOptions: null); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPostFailsAsync(Exception exception, int expectedStatusCode) + { + CalendarEvent item = new(); + calendarEventManagerMock.Setup(expression: service => service.AddCalendarEventAsync(item)) + .Throws(exception: exception); + + IActionResult result = await controller.Post(newEntity: item); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPutFailsAsync(Exception exception, int expectedStatusCode) + { + CalendarEvent item = new(); + calendarEventManagerMock.Setup(expression: service => service.UpdateCalendarEventAsync(item)) + .Throws(exception: exception); + + IActionResult result = await controller.Put(key: 1, updatedEntity: item); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPatchFailsAsync(Exception exception, int expectedStatusCode) + { + calendarEventManagerMock.Setup(expression: service => service.Get(calendarEventId: 1)) + .Throws(exception: exception); + + IActionResult result = await controller.Put( + key: 1, + updatedDelta: new Delta()); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenDeleteFailsAsync(Exception exception, int expectedStatusCode) + { + calendarEventManagerMock.Setup(expression: service => service.DeleteAsync(calendarEventId: 1)) + .Throws(exception: exception); + + IActionResult result = await controller.Delete(key: 1); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarEventControllerTests.cs b/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarEventControllerTests.cs new file mode 100644 index 0000000..2d1e06b --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Exposures/CalendarEventControllerTests.cs @@ -0,0 +1,44 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using cCoder.Workflow.Brokers.Loggings; +using cCoder.Workflow.Exposures.Controllers; +using cCoder.Workflow.Exposures; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Exposures; + +public partial class CalendarEventControllerTests +{ + private readonly Mock calendarEventManagerMock = new(); + private readonly Mock loggingBrokerMock = new(); + private readonly CalendarEventController controller; + + public static TheoryData FailureExceptions => new() + { + { new cCoder.Workflow.Models.Exceptions.WorkflowValidationException(innerException: new Exception()), 400 }, + { new System.Security.SecurityException(), 403 }, + { new Exception(), 500 } + }; + + public CalendarEventControllerTests() + { + controller = new CalendarEventController( + service: calendarEventManagerMock.Object, + loggingBroker: loggingBrokerMock.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Exposures/FlowDefinitionControllerTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Exposures/FlowDefinitionControllerTests.Exceptions.cs new file mode 100644 index 0000000..bbfb0c2 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Exposures/FlowDefinitionControllerTests.Exceptions.cs @@ -0,0 +1,138 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using cCoder.Data.Models.Planning; +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.OData.Deltas; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Exposures; + +public partial class FlowDefinitionControllerTests +{ + [Fact] + public void ShouldReturnServerErrorWhenGetFails() + { + flowDefinitionManagerMock.Setup(expression: service => + service.GetFlowDefinition(flowDefinitionId: Guid.Empty)) + .Throws(exception: new Exception()); + + IActionResult result = controller.Get(key: Guid.Empty); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Fact] + public void ShouldReturnServerErrorWhenGetMetadataFails() + { + controller.ControllerContext = new ControllerContext(); + + IActionResult result = controller.GetMetadata(); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Fact] + public void ShouldReturnServerErrorWhenGetAllFails() + { + flowDefinitionManagerMock.Setup(expression: service => service.GetAllFlowDefinitions()) + .Throws(exception: new Exception()); + + IActionResult result = controller.GetAll(queryOptions: null); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPostFailsAsync(Exception exception, int expectedStatusCode) + { + FlowDefinition item = new(); + flowDefinitionManagerMock.Setup(expression: service => service.AddFlowDefinitionAsync(item)) + .Throws(exception: exception); + + IActionResult result = await controller.Post(newEntity: item); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPutFailsAsync(Exception exception, int expectedStatusCode) + { + FlowDefinition item = new(); + flowDefinitionManagerMock.Setup(expression: service => service.UpdateFlowDefinitionAsync(item)) + .Throws(exception: exception); + + IActionResult result = await controller.Put(key: Guid.Empty, updatedEntity: item); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPatchFailsAsync(Exception exception, int expectedStatusCode) + { + flowDefinitionManagerMock.Setup(expression: service => service.GetFlowDefinition(flowDefinitionId: Guid.Empty)) + .Throws(exception: exception); + + IActionResult result = await controller.Put( + key: Guid.Empty, + updatedDelta: new Delta()); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenDeleteFailsAsync(Exception exception, int expectedStatusCode) + { + flowDefinitionManagerMock.Setup(expression: service => service.DeleteFlowDefinitionAsync(flowDefinitionId: Guid.Empty)) + .Throws(exception: exception); + + IActionResult result = await controller.Delete(key: Guid.Empty); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenExecuteFailsAsync( + Exception exception, + int expectedStatusCode) + { + flowDefinitionManagerMock.Setup(expression: service => service.QueueFlowDefinitionAsync( + flowDefinitionId: Guid.Empty, + asUserId: It.IsAny(), + args: It.IsAny())) + .Throws(exception: exception); + + IActionResult result = await controller.PostAsync(key: Guid.Empty); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenExecuteScriptFailsAsync( + Exception exception, + int expectedStatusCode) + { + flowDefinitionManagerMock.Setup(expression: service => + service.ExecuteScriptAsync(It.IsAny())) + .Throws(exception: exception); + + IActionResult result = await controller.PostScript(); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Exposures/FlowDefinitionControllerTests.cs b/src/cCoder.Workflow.Tests/Workflow/Exposures/FlowDefinitionControllerTests.cs new file mode 100644 index 0000000..79905bc --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Exposures/FlowDefinitionControllerTests.cs @@ -0,0 +1,47 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using cCoder.Workflow.Brokers.Loggings; +using cCoder.Workflow.Exposures.Controllers; +using cCoder.Workflow.Exposures; +using cCoder.Security.Models.Configurations; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Exposures; + +public partial class FlowDefinitionControllerTests +{ + private readonly Mock flowDefinitionManagerMock = new(); + private readonly Mock loggingBrokerMock = new(); + private readonly Mock authInfoMock = new(); + private readonly FlowDefinitionController controller; + + public static TheoryData FailureExceptions => new() + { + { new cCoder.Workflow.Models.Exceptions.WorkflowValidationException(innerException: new Exception()), 400 }, + { new System.Security.SecurityException(), 403 }, + { new Exception(), 500 } + }; + + public FlowDefinitionControllerTests() + { + controller = new FlowDefinitionController( + service: flowDefinitionManagerMock.Object, + authInfo: authInfoMock.Object, + loggingBroker: loggingBrokerMock.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Exposures/FlowInstanceDataControllerTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Exposures/FlowInstanceDataControllerTests.Exceptions.cs new file mode 100644 index 0000000..ec02094 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Exposures/FlowInstanceDataControllerTests.Exceptions.cs @@ -0,0 +1,105 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using cCoder.Data.Models.Planning; +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.OData.Deltas; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Exposures; + +public partial class FlowInstanceDataControllerTests +{ + [Fact] + public void ShouldReturnServerErrorWhenGetFails() + { + flowInstanceDataManagerMock.Setup(expression: service => service.GetAll()) + .Throws(exception: new Exception()); + + IActionResult result = controller.Get(key: Guid.Empty); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Fact] + public void ShouldReturnServerErrorWhenGetMetadataFails() + { + controller.ControllerContext = new ControllerContext(); + + IActionResult result = controller.GetMetadata(); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Fact] + public void ShouldReturnServerErrorWhenGetAllFails() + { + flowInstanceDataManagerMock.Setup(expression: service => service.GetAll()) + .Throws(exception: new Exception()); + + IActionResult result = controller.GetAll(queryOptions: null); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPostFailsAsync(Exception exception, int expectedStatusCode) + { + FlowInstanceData item = new(); + flowInstanceDataManagerMock.Setup(expression: service => service.AddFlowInstanceDataAsync(item)) + .Throws(exception: exception); + + IActionResult result = await controller.Post(newEntity: item); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPutFailsAsync(Exception exception, int expectedStatusCode) + { + FlowInstanceData item = new(); + flowInstanceDataManagerMock.Setup(expression: service => service.UpdateFlowInstanceDataAsync(item)) + .Throws(exception: exception); + + IActionResult result = await controller.Put(key: Guid.Empty, updatedEntity: item); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPatchFailsAsync(Exception exception, int expectedStatusCode) + { + flowInstanceDataManagerMock.Setup(expression: service => service.Get(flowInstanceDataId: Guid.Empty)) + .Throws(exception: exception); + + IActionResult result = await controller.Put( + key: Guid.Empty, + updatedDelta: new Delta()); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenDeleteFailsAsync(Exception exception, int expectedStatusCode) + { + flowInstanceDataManagerMock.Setup(expression: service => service.DeleteAsync(flowInstanceDataId: Guid.Empty)) + .Throws(exception: exception); + + IActionResult result = await controller.Delete(key: Guid.Empty); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Exposures/FlowInstanceDataControllerTests.cs b/src/cCoder.Workflow.Tests/Workflow/Exposures/FlowInstanceDataControllerTests.cs new file mode 100644 index 0000000..24d673e --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Exposures/FlowInstanceDataControllerTests.cs @@ -0,0 +1,44 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using cCoder.Workflow.Brokers.Loggings; +using cCoder.Workflow.Exposures.Controllers; +using cCoder.Workflow.Exposures; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Exposures; + +public partial class FlowInstanceDataControllerTests +{ + private readonly Mock flowInstanceDataManagerMock = new(); + private readonly Mock loggingBrokerMock = new(); + private readonly FlowInstanceDataController controller; + + public static TheoryData FailureExceptions => new() + { + { new cCoder.Workflow.Models.Exceptions.WorkflowValidationException(innerException: new Exception()), 400 }, + { new System.Security.SecurityException(), 403 }, + { new Exception(), 500 } + }; + + public FlowInstanceDataControllerTests() + { + controller = new FlowInstanceDataController( + service: flowInstanceDataManagerMock.Object, + loggingBroker: loggingBrokerMock.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Exposures/ScheduledTaskControllerTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Exposures/ScheduledTaskControllerTests.Exceptions.cs new file mode 100644 index 0000000..e5d2bbb --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Exposures/ScheduledTaskControllerTests.Exceptions.cs @@ -0,0 +1,118 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.OData.Deltas; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Exposures; + +public partial class ScheduledTaskControllerTests +{ + [Fact] + public void ShouldReturnServerErrorWhenGetFails() + { + scheduledTaskManagerMock.Setup(expression: service => service.GetAll()) + .Throws(exception: new Exception()); + + IActionResult result = controller.Get(key: 1); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Fact] + public void ShouldReturnServerErrorWhenGetMetadataFails() + { + controller.ControllerContext = new ControllerContext(); + + IActionResult result = controller.GetMetadata(); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Fact] + public void ShouldReturnServerErrorWhenGetAllFails() + { + scheduledTaskManagerMock.Setup(expression: service => service.GetAll()) + .Throws(exception: new Exception()); + + IActionResult result = controller.GetAll(queryOptions: null); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPostFailsAsync(Exception exception, int expectedStatusCode) + { + ScheduledTask item = new(); + scheduledTaskManagerMock.Setup(expression: service => service.AddScheduledTaskAsync(item)) + .Throws(exception: exception); + + IActionResult result = await controller.Post(newEntity: item); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPutFailsAsync(Exception exception, int expectedStatusCode) + { + ScheduledTask item = new(); + scheduledTaskManagerMock.Setup(expression: service => service.UpdateScheduledTaskAsync(item)) + .Throws(exception: exception); + + IActionResult result = await controller.Put(key: 1, updatedEntity: item); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPatchFailsAsync(Exception exception, int expectedStatusCode) + { + scheduledTaskManagerMock.Setup(expression: service => service.Get(scheduledTaskId: 1)) + .Throws(exception: exception); + + IActionResult result = await controller.Put( + key: 1, + updatedDelta: new Delta()); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenDeleteFailsAsync(Exception exception, int expectedStatusCode) + { + scheduledTaskManagerMock.Setup(expression: service => service.DeleteAsync(scheduledTaskId: 1)) + .Throws(exception: exception); + + IActionResult result = await controller.Delete(key: 1); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Fact] + public async Task ShouldReturnServerErrorWhenExecuteFailsAsync() + { + scheduledTaskManagerMock.Setup(expression: service => + service.ExecuteAsync(scheduledTaskId: 1, incrementNextExecution: true)) + .Throws(exception: new Exception()); + + IActionResult result = await controller.PostAsync( + key: 1, + incrementNextExecution: true); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Exposures/ScheduledTaskControllerTests.cs b/src/cCoder.Workflow.Tests/Workflow/Exposures/ScheduledTaskControllerTests.cs new file mode 100644 index 0000000..5eb4138 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Exposures/ScheduledTaskControllerTests.cs @@ -0,0 +1,44 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using cCoder.Workflow.Brokers.Loggings; +using cCoder.Workflow.Exposures.Controllers; +using cCoder.Workflow.Exposures; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Exposures; + +public partial class ScheduledTaskControllerTests +{ + private readonly Mock scheduledTaskManagerMock = new(); + private readonly Mock loggingBrokerMock = new(); + private readonly ScheduledTaskController controller; + + public static TheoryData FailureExceptions => new() + { + { new cCoder.Workflow.Models.Exceptions.WorkflowValidationException(innerException: new Exception()), 400 }, + { new System.Security.SecurityException(), 403 }, + { new Exception(), 500 } + }; + + public ScheduledTaskControllerTests() + { + controller = new ScheduledTaskController( + service: scheduledTaskManagerMock.Object, + loggingBroker: loggingBrokerMock.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Exposures/WorkflowEventControllerTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Exposures/WorkflowEventControllerTests.Exceptions.cs new file mode 100644 index 0000000..e6a14e3 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Exposures/WorkflowEventControllerTests.Exceptions.cs @@ -0,0 +1,105 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using cCoder.Data.Models.Planning; +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.OData.Deltas; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Exposures; + +public partial class WorkflowEventControllerTests +{ + [Fact] + public void ShouldReturnServerErrorWhenGetFails() + { + workflowEventManagerMock.Setup(expression: service => service.GetAll()) + .Throws(exception: new Exception()); + + IActionResult result = controller.Get(key: Guid.Empty); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Fact] + public void ShouldReturnServerErrorWhenGetMetadataFails() + { + controller.ControllerContext = new ControllerContext(); + + IActionResult result = controller.GetMetadata(); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Fact] + public void ShouldReturnServerErrorWhenGetAllFails() + { + workflowEventManagerMock.Setup(expression: service => service.GetAll()) + .Throws(exception: new Exception()); + + IActionResult result = controller.GetAll(queryOptions: null); + + result.Should().BeOfType().Which.StatusCode.Should().Be(500); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPostFailsAsync(Exception exception, int expectedStatusCode) + { + WorkflowEvent item = new(); + workflowEventManagerMock.Setup(expression: service => service.AddWorkflowEventAsync(item)) + .Throws(exception: exception); + + IActionResult result = await controller.Post(newEntity: item); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPutFailsAsync(Exception exception, int expectedStatusCode) + { + WorkflowEvent item = new(); + workflowEventManagerMock.Setup(expression: service => service.UpdateWorkflowEventAsync(item)) + .Throws(exception: exception); + + IActionResult result = await controller.Put(key: Guid.Empty, updatedEntity: item); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenPatchFailsAsync(Exception exception, int expectedStatusCode) + { + workflowEventManagerMock.Setup(expression: service => service.Get(workflowEventId: Guid.Empty)) + .Throws(exception: exception); + + IActionResult result = await controller.Put( + key: Guid.Empty, + updatedDelta: new Delta()); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(FailureExceptions))] + public async Task ShouldReturnServerErrorWhenDeleteFailsAsync(Exception exception, int expectedStatusCode) + { + workflowEventManagerMock.Setup(expression: service => service.DeleteAsync(workflowEventId: Guid.Empty)) + .Throws(exception: exception); + + IActionResult result = await controller.Delete(key: Guid.Empty); + + result.Should().BeAssignableTo().Which.StatusCode.Should().Be(expectedStatusCode); + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Exposures/WorkflowEventControllerTests.cs b/src/cCoder.Workflow.Tests/Workflow/Exposures/WorkflowEventControllerTests.cs new file mode 100644 index 0000000..541c7dd --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Exposures/WorkflowEventControllerTests.cs @@ -0,0 +1,44 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using cCoder.Workflow.Brokers.Loggings; +using cCoder.Workflow.Exposures.Controllers; +using cCoder.Workflow.Exposures; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Exposures; + +public partial class WorkflowEventControllerTests +{ + private readonly Mock workflowEventManagerMock = new(); + private readonly Mock loggingBrokerMock = new(); + private readonly WorkflowEventController controller; + + public static TheoryData FailureExceptions => new() + { + { new cCoder.Workflow.Models.Exceptions.WorkflowValidationException(innerException: new Exception()), 400 }, + { new System.Security.SecurityException(), 403 }, + { new Exception(), 500 } + }; + + public WorkflowEventControllerTests() + { + controller = new WorkflowEventController( + service: workflowEventManagerMock.Object, + loggingBroker: loggingBrokerMock.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarEventServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarEventServiceTests.Exceptions.cs new file mode 100644 index 0000000..f53304e --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarEventServiceTests.Exceptions.cs @@ -0,0 +1,91 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class CalendarEventServiceTests +{ + public static TheoryData ExceptionMappings => + FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetAllFailure(Exception exception, Type expectedType) + { + // Given + calendarEventBrokerMock + .Setup(expression: broker => broker.SelectAllCalendarEvents()) + .Throws(exception: exception); + + // When + Action action = () => calendarEventService.GetAll(); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddCalendarEventAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + CalendarEvent calendarEvent = CreateCalendarEvent(); + + calendarEventBrokerMock + .Setup(expression: broker => broker.SelectAppId( + entity: calendarEvent)) + .Throws(exception: exception); + + // When + Func action = async () => await calendarEventService + .AddCalendarEventAsync(newCalendarEvent: calendarEvent); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteAllByAppIdAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + calendarEventBrokerMock + .Setup(expression: broker => broker.DeleteAllCalendarEventsByAppIdAsync( + appId: 1)) + .Throws(exception: exception); + + // When + Func action = async () => await calendarEventService + .DeleteAllByAppIdAsync(appId: 1); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarEventServiceTests.Get.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarEventServiceTests.Get.cs new file mode 100644 index 0000000..5801611 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarEventServiceTests.Get.cs @@ -0,0 +1,119 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using System.Security; +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class CalendarEventServiceTests +{ + [Fact] + public void ShouldGetCalendar() + { + // Given + CalendarEvent expected = CreateCalendarEvent(); + + calendarEventBrokerMock + .Setup(expression: broker => broker.SelectAllCalendarEvents()) + .Returns(value: new[] { expected } + .AsQueryable()); + + // When + CalendarEvent actual = calendarEventService.Get( + calendarEventId: expected.Id); + + // Then + actual + .Should() + .BeSameAs(expected: expected); + } + + [Fact] + public void ShouldReturnNullForMissingCalendar() + { + // Given + calendarEventBrokerMock + .Setup(expression: broker => broker.SelectAllCalendarEvents()) + .Returns(value: Array.Empty() + .AsQueryable()); + + calendarEventBrokerMock + .Setup(expression: broker => broker + .SelectAllCalendarEventsIgnoringQueryFilters()) + .Returns(value: Array.Empty() + .AsQueryable()); + + // When + CalendarEvent actual = calendarEventService.Get(calendarEventId: 1); + + // Then + actual + .Should() + .BeNull(); + } + + [Fact] + public void ShouldRejectFilteredCalendar() + { + // Given + CalendarEvent restricted = CreateCalendarEvent(); + + calendarEventBrokerMock + .Setup(expression: broker => broker.SelectAllCalendarEvents()) + .Returns(value: Array.Empty() + .AsQueryable()); + + calendarEventBrokerMock + .Setup(expression: broker => broker + .SelectAllCalendarEventsIgnoringQueryFilters()) + .Returns(value: new[] { restricted } + .AsQueryable()); + + // When + Action action = () => calendarEventService.Get( + calendarEventId: restricted.Id); + + // Then + action + .Should() + .Throw(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ShouldGetAllCalendars(bool ignoreFilters) + { + // Given + IQueryable expected = new[] { CreateCalendarEvent() } + .AsQueryable(); + + if (ignoreFilters) + { + calendarEventBrokerMock + .Setup(expression: broker => broker + .SelectAllCalendarEventsIgnoringQueryFilters()) + .Returns(value: expected); + } + else + { + calendarEventBrokerMock + .Setup(expression: broker => broker.SelectAllCalendarEvents()) + .Returns(value: expected); + } + + // When + IQueryable actual = calendarEventService.GetAll( + ignoreFilters: ignoreFilters); + + // Then + actual + .Should() + .BeSameAs(expected: expected); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarEventServiceTests.Mutations.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarEventServiceTests.Mutations.cs new file mode 100644 index 0000000..e0b4f67 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarEventServiceTests.Mutations.cs @@ -0,0 +1,183 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class CalendarEventServiceTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ShouldSaveCalendarAsync(bool isUpdate) + { + // Given + CalendarEvent input = CreateCalendarEvent(); + CalendarEvent stored = CreateCalendarEvent(); + const int appId = 7; + string privilege = isUpdate + ? "CalendarEvent_update" + : "CalendarEvent_create"; + + calendarEventBrokerMock + .Setup(expression: broker => broker.SelectAppId(entity: input)) + .Returns(value: appId); + + authorizationBrokerMock + .Setup(expression: broker => broker.Authorize( + appId: appId, + privilege: privilege)); + + if (isUpdate) + { + calendarEventBrokerMock + .Setup(expression: broker => broker.UpdateCalendarEventAsync( + updatedEntity: It.Is(match: item => + item.Id == input.Id))) + .Returns(value: ValueTask.FromResult(result: stored)); + } + else + { + calendarEventBrokerMock + .Setup(expression: broker => broker.InsertCalendarEventAsync( + newEntity: It.Is(match: item => + item.Name == input.Name))) + .Returns(value: ValueTask.FromResult(result: stored)); + } + + // When + CalendarEvent actual = isUpdate + ? await calendarEventService.UpdateCalendarEventAsync( + updatedCalendarEvent: input) + : await calendarEventService.AddCalendarEventAsync( + newCalendarEvent: input); + + // Then + actual + .Should() + .BeSameAs(expected: input); + + actual.Id + .Should() + .Be(expected: stored.Id); + + actual.Name + .Should() + .Be(expected: stored.Name); + + calendarEventBrokerMock.VerifyAll(); + authorizationBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldDeleteCalendarEventAsync() + { + // Given + CalendarEvent calendarEvent = CreateCalendarEvent(); + const int appId = 7; + + calendarEventBrokerMock + .Setup(expression: broker => broker + .SelectAllCalendarEventsIgnoringQueryFilters()) + .Returns(value: new[] { calendarEvent } + .AsQueryable()); + + calendarEventBrokerMock + .Setup(expression: broker => broker.SelectAppId( + entity: calendarEvent)) + .Returns(value: appId); + + authorizationBrokerMock + .Setup(expression: broker => broker.Authorize( + appId: appId, + privilege: "CalendarEvent_delete")); + + calendarEventBrokerMock + .Setup(expression: broker => broker.DeleteCalendarEventAsync( + deletedEntity: It.Is(match: deleted => + deleted.Id == calendarEvent.Id))) + .Returns(value: ValueTask.FromResult(result: 1)); + + // When + await calendarEventService.DeleteAsync( + calendarEventId: calendarEvent.Id); + + // Then + calendarEventBrokerMock.VerifyAll(); + authorizationBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldIgnoreMissingCalendarWhenDeleteAsync() + { + // Given + calendarEventBrokerMock + .Setup(expression: broker => broker + .SelectAllCalendarEventsIgnoringQueryFilters()) + .Returns(value: Array.Empty() + .AsQueryable()); + + // When + await calendarEventService.DeleteAsync(calendarEventId: 1); + + // Then + calendarEventBrokerMock.VerifyAll(); + authorizationBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldDeleteAllCalendarEventsAsync() + { + // Given + CalendarEvent calendarEvent = CreateCalendarEvent(); + + calendarEventBrokerMock + .Setup(expression: broker => broker.DeleteAllCalendarEventsAsync( + deletedItems: It.Is>(match: items => + items.Single().Id == calendarEvent.Id))) + .Returns(value: ValueTask.CompletedTask); + + // When + await calendarEventService.DeleteAllForAppCalendarEventAsync( + deletedItems: new[] { calendarEvent }); + + // Then + calendarEventBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldIgnoreEmptyDeleteAllCalendarEventsAsync() + { + // Given + + // When + await calendarEventService.DeleteAllForAppCalendarEventAsync( + deletedItems: Array.Empty()); + + // Then + calendarEventBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldDeleteAllCalendarEventsByAppIdAsync() + { + // Given + const int appId = 7; + + calendarEventBrokerMock + .Setup(expression: broker => broker + .DeleteAllCalendarEventsByAppIdAsync(appId: appId)) + .Returns(value: ValueTask.CompletedTask); + + // When + await calendarEventService.DeleteAllByAppIdAsync(appId: appId); + + // Then + calendarEventBrokerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarEventServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarEventServiceTests.cs new file mode 100644 index 0000000..4916e64 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarEventServiceTests.cs @@ -0,0 +1,40 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Brokers; +using cCoder.Workflow.Brokers.Storage; +using cCoder.Workflow.Services.Foundations; +using Moq; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +#pragma warning disable STXFORMAT008 +public partial class CalendarEventServiceTests +{ + private readonly Mock calendarEventBrokerMock = new(); + private readonly Mock authorizationBrokerMock = new(); + private readonly CalendarEventService calendarEventService; + + public CalendarEventServiceTests() + { + + calendarEventService = new CalendarEventService( + calendarEventBroker: calendarEventBrokerMock.Object, + authorizationBroker: authorizationBrokerMock.Object); + + } + + private static CalendarEvent CreateCalendarEvent() => + new() + { + Id = Random.Shared.Next(minValue: 1, maxValue: int.MaxValue), + CalendarId = Random.Shared.Next(minValue: 1, maxValue: int.MaxValue), + Name = "Calendar event", + Description = "Description", + Start = DateTimeOffset.UtcNow, + DurationInTicks = TimeSpan.FromMinutes(value: 30).Ticks + }; +} +#pragma warning restore STXFORMAT008 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarServiceTests.Exceptions.cs new file mode 100644 index 0000000..e9ac1f9 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarServiceTests.Exceptions.cs @@ -0,0 +1,92 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class CalendarServiceTests +{ + public static TheoryData ExceptionMappings => + FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetAllFailure(Exception exception, Type expectedType) + { + // Given + calendarBrokerMock + .Setup(expression: broker => broker.SelectAllCalendars()) + .Throws(exception: exception); + + // When + Action action = () => calendarService.GetAll(); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddCalendarAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + Calendar calendar = CreateCalendar(); + + authorizationBrokerMock + .Setup(expression: broker => broker.Authorize( + appId: calendar.AppId, + privilege: "Calendar_create")) + .Throws(exception: exception); + + // When + Func action = async () => await calendarService + .AddCalendarAsync(newCalendar: calendar); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteAllByAppIdAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + calendarBrokerMock + .Setup(expression: broker => broker.DeleteAllCalendarsByAppIdAsync( + appId: 1)) + .Throws(exception: exception); + + // When + Func action = async () => await calendarService + .DeleteAllByAppIdAsync(appId: 1); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarServiceTests.Get.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarServiceTests.Get.cs new file mode 100644 index 0000000..a5d67cc --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarServiceTests.Get.cs @@ -0,0 +1,118 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using System.Security; +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class CalendarServiceTests +{ + [Fact] + public void ShouldGetCalendar() + { + // Given + Calendar expected = CreateCalendar(); + + calendarBrokerMock + .Setup(expression: broker => broker.SelectAllCalendars()) + .Returns(value: new[] { expected } + .AsQueryable()); + + // When + Calendar actual = calendarService.Get(calendarId: expected.Id); + + // Then + actual + .Should() + .BeSameAs(expected: expected); + } + + [Fact] + public void ShouldReturnNullForMissingCalendar() + { + // Given + calendarBrokerMock + .Setup(expression: broker => broker.SelectAllCalendars()) + .Returns(value: Array.Empty() + .AsQueryable()); + + calendarBrokerMock + .Setup(expression: broker => broker + .SelectAllCalendarsIgnoringQueryFilters()) + .Returns(value: Array.Empty() + .AsQueryable()); + + // When + Calendar actual = calendarService.Get(calendarId: 1); + + // Then + actual + .Should() + .BeNull(); + } + + [Fact] + public void ShouldRejectFilteredCalendar() + { + // Given + Calendar restricted = CreateCalendar(); + + calendarBrokerMock + .Setup(expression: broker => broker.SelectAllCalendars()) + .Returns(value: Array.Empty() + .AsQueryable()); + + calendarBrokerMock + .Setup(expression: broker => broker + .SelectAllCalendarsIgnoringQueryFilters()) + .Returns(value: new[] { restricted } + .AsQueryable()); + + // When + Action action = () => calendarService.Get( + calendarId: restricted.Id); + + // Then + action + .Should() + .Throw(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ShouldGetAllCalendars(bool ignoreFilters) + { + // Given + IQueryable expected = new[] { CreateCalendar() } + .AsQueryable(); + + if (ignoreFilters) + { + calendarBrokerMock + .Setup(expression: broker => broker + .SelectAllCalendarsIgnoringQueryFilters()) + .Returns(value: expected); + } + else + { + calendarBrokerMock + .Setup(expression: broker => broker.SelectAllCalendars()) + .Returns(value: expected); + } + + // When + IQueryable actual = calendarService.GetAll( + ignoreFilters: ignoreFilters); + + // Then + actual + .Should() + .BeSameAs(expected: expected); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarServiceTests.Mutations.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarServiceTests.Mutations.cs new file mode 100644 index 0000000..201bd14 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarServiceTests.Mutations.cs @@ -0,0 +1,171 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class CalendarServiceTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ShouldSaveCalendarAsync(bool isUpdate) + { + // Given + Calendar input = CreateCalendar(); + Calendar stored = CreateCalendar(); + string privilege = isUpdate + ? "Calendar_update" + : "Calendar_create"; + + authorizationBrokerMock + .Setup(expression: broker => broker.Authorize( + appId: input.AppId, + privilege: privilege)); + + if (isUpdate) + { + calendarBrokerMock + .Setup(expression: broker => broker.UpdateCalendarAsync( + updatedEntity: It.Is(match: item => + item.Id == input.Id))) + .Returns(value: ValueTask.FromResult(result: stored)); + } + else + { + calendarBrokerMock + .Setup(expression: broker => broker.InsertCalendarAsync( + newEntity: It.Is(match: item => + item.Name == input.Name))) + .Returns(value: ValueTask.FromResult(result: stored)); + } + + // When + Calendar actual = isUpdate + ? await calendarService.UpdateCalendarAsync( + updatedCalendar: input) + : await calendarService.AddCalendarAsync( + newCalendar: input); + + // Then + actual + .Should() + .BeSameAs(expected: input); + + actual.Id + .Should() + .Be(expected: stored.Id); + + actual.Name + .Should() + .Be(expected: stored.Name); + + calendarBrokerMock.VerifyAll(); + authorizationBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldDeleteCalendarAsync() + { + // Given + Calendar calendar = CreateCalendar(); + + calendarBrokerMock + .Setup(expression: broker => broker + .SelectAllCalendarsIgnoringQueryFilters()) + .Returns(value: new[] { calendar } + .AsQueryable()); + + authorizationBrokerMock + .Setup(expression: broker => broker.Authorize( + appId: calendar.AppId, + privilege: "Calendar_delete")); + + calendarBrokerMock + .Setup(expression: broker => broker.DeleteCalendarAsync( + deletedEntity: It.Is(match: deleted => + deleted.Id == calendar.Id))) + .Returns(value: ValueTask.FromResult(result: 1)); + + // When + await calendarService.DeleteAsync(calendarId: calendar.Id); + + // Then + calendarBrokerMock.VerifyAll(); + authorizationBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldIgnoreMissingCalendarWhenDeleteAsync() + { + // Given + calendarBrokerMock + .Setup(expression: broker => broker + .SelectAllCalendarsIgnoringQueryFilters()) + .Returns(value: Array.Empty() + .AsQueryable()); + + // When + await calendarService.DeleteAsync(calendarId: 1); + + // Then + calendarBrokerMock.VerifyAll(); + authorizationBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldDeleteAllCalendarsAsync() + { + // Given + Calendar calendar = CreateCalendar(); + + calendarBrokerMock + .Setup(expression: broker => broker.DeleteAllCalendarsAsync( + deletedItems: It.Is>(match: items => + items.Single().Id == calendar.Id))) + .Returns(value: ValueTask.CompletedTask); + + // When + await calendarService.DeleteAllForAppCalendarAsync( + deletedItems: new[] { calendar }); + + // Then + calendarBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldIgnoreEmptyDeleteAllCalendarsAsync() + { + // Given + + // When + await calendarService.DeleteAllForAppCalendarAsync( + deletedItems: Array.Empty()); + + // Then + calendarBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldDeleteAllCalendarsByAppIdAsync() + { + // Given + const int appId = 7; + + calendarBrokerMock + .Setup(expression: broker => broker + .DeleteAllCalendarsByAppIdAsync(appId: appId)) + .Returns(value: ValueTask.CompletedTask); + + // When + await calendarService.DeleteAllByAppIdAsync(appId: appId); + + // Then + calendarBrokerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarServiceTests.cs new file mode 100644 index 0000000..c97b2a6 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/CalendarServiceTests.cs @@ -0,0 +1,38 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Brokers; +using cCoder.Workflow.Brokers.Storage; +using cCoder.Workflow.Services.Foundations; +using Moq; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +#pragma warning disable STXFORMAT008 +public partial class CalendarServiceTests +{ + private readonly Mock calendarBrokerMock = new(); + private readonly Mock authorizationBrokerMock = new(); + private readonly CalendarService calendarService; + + public CalendarServiceTests() + { + + calendarService = new CalendarService( + calendarBroker: calendarBrokerMock.Object, + authorizationBroker: authorizationBrokerMock.Object); + + } + + private static Calendar CreateCalendar() => + new() + { + Id = Random.Shared.Next(minValue: 1, maxValue: int.MaxValue), + AppId = Random.Shared.Next(minValue: 1, maxValue: int.MaxValue), + Name = "Calendar", + Description = "Description" + }; +} +#pragma warning restore STXFORMAT008 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEntityEventServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEntityEventServiceTests.Exceptions.cs new file mode 100644 index 0000000..ec5286b --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEntityEventServiceTests.Exceptions.cs @@ -0,0 +1,46 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class CalendarEntityEventServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .CalendarServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapRaiseCalendarAddEventAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + Calendar calendar = new() { Id = 1 }; + + calendarEntityEventBrokerMock + .Setup(expression: broker => broker.RaiseCalendarAddEventAsync( + message: It.Is>(match: _ => true))) + .Throws(exception: exception); + + // When + Func action = async () => await service + .RaiseCalendarAddEventAsync(entity: calendar); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEntityEventServiceTests.RaiseCalendarAddEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEntityEventServiceTests.RaiseCalendarAddEventAsync.cs new file mode 100644 index 0000000..5b231a0 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEntityEventServiceTests.RaiseCalendarAddEventAsync.cs @@ -0,0 +1,58 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class CalendarEntityEventServiceTests +{ + [Fact] + public async Task ShouldMapAndCallBrokerWhenRaiseCalendarAddEventAsync() + { + // Given + Calendar entity = new(); + EventMessage actualMessage = null; + + calendarEntityEventBrokerMock + .Setup(expression: x => + x.RaiseCalendarAddEventAsync(message: It.IsAny>()) + ) + .Callback>(action: message => actualMessage = message) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseCalendarAddEventAsync(entity: entity); + + // Then + actualMessage.Should() + .NotBeNull(); + + actualMessage!.Data.Should() + .BeEquivalentTo(expectation: entity); + + actualMessage.AuthInfo.Should() + .NotBeNull(); + + actualMessage.AuthInfo.SSOUserId.Should() + .Be(expected: CurrentUserId); + + calendarEntityEventBrokerMock.Verify( +expression: x => x.RaiseCalendarAddEventAsync(message: It.IsAny>()), +times: Times.Once + ); + + calendarEntityEventBrokerMock.Verify( + expression: x => x.GetCurrentUserId(), + times: Times.Once); + + calendarEntityEventBrokerMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEntityEventServiceTests.RaiseCalendarDeleteEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEntityEventServiceTests.RaiseCalendarDeleteEventAsync.cs new file mode 100644 index 0000000..4b8dc81 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEntityEventServiceTests.RaiseCalendarDeleteEventAsync.cs @@ -0,0 +1,58 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class CalendarEntityEventServiceTests +{ + [Fact] + public async Task ShouldMapAndCallBrokerWhenRaiseCalendarDeleteEventAsync() + { + // Given + Calendar entity = new(); + EventMessage actualMessage = null; + + calendarEntityEventBrokerMock + .Setup(expression: x => + x.RaiseCalendarDeleteEventAsync(message: It.IsAny>()) + ) + .Callback>(action: message => actualMessage = message) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseCalendarDeleteEventAsync(entity: entity); + + // Then + actualMessage.Should() + .NotBeNull(); + + actualMessage!.Data.Should() + .BeEquivalentTo(expectation: entity); + + actualMessage.AuthInfo.Should() + .NotBeNull(); + + actualMessage.AuthInfo.SSOUserId.Should() + .Be(expected: CurrentUserId); + + calendarEntityEventBrokerMock.Verify( +expression: x => x.RaiseCalendarDeleteEventAsync(message: It.IsAny>()), +times: Times.Once + ); + + calendarEntityEventBrokerMock.Verify( + expression: x => x.GetCurrentUserId(), + times: Times.Once); + + calendarEntityEventBrokerMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEntityEventServiceTests.RaiseCalendarUpdateEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEntityEventServiceTests.RaiseCalendarUpdateEventAsync.cs new file mode 100644 index 0000000..932aef0 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEntityEventServiceTests.RaiseCalendarUpdateEventAsync.cs @@ -0,0 +1,58 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class CalendarEntityEventServiceTests +{ + [Fact] + public async Task ShouldMapAndCallBrokerWhenRaiseCalendarUpdateEventAsync() + { + // Given + Calendar entity = new(); + EventMessage actualMessage = null; + + calendarEntityEventBrokerMock + .Setup(expression: x => + x.RaiseCalendarUpdateEventAsync(message: It.IsAny>()) + ) + .Callback>(action: message => actualMessage = message) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseCalendarUpdateEventAsync(entity: entity); + + // Then + actualMessage.Should() + .NotBeNull(); + + actualMessage!.Data.Should() + .BeEquivalentTo(expectation: entity); + + actualMessage.AuthInfo.Should() + .NotBeNull(); + + actualMessage.AuthInfo.SSOUserId.Should() + .Be(expected: CurrentUserId); + + calendarEntityEventBrokerMock.Verify( +expression: x => x.RaiseCalendarUpdateEventAsync(message: It.IsAny>()), +times: Times.Once + ); + + calendarEntityEventBrokerMock.Verify( + expression: x => x.GetCurrentUserId(), + times: Times.Once); + + calendarEntityEventBrokerMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEntityEventServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEntityEventServiceTests.cs new file mode 100644 index 0000000..8166a23 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEntityEventServiceTests.cs @@ -0,0 +1,27 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Brokers.Events; +using Moq; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class CalendarEntityEventServiceTests +{ + private readonly Mock calendarEntityEventBrokerMock; + private readonly cCoder.Workflow.Services.Foundations.Events.CalendarEntityEventService service; + private const string CurrentUserId = "test-user"; + + public CalendarEntityEventServiceTests() + { + calendarEntityEventBrokerMock = new Mock(behavior: MockBehavior.Strict); + + calendarEntityEventBrokerMock + .Setup(expression: broker => broker.GetCurrentUserId()) + .Returns(value: CurrentUserId); + + service = new cCoder.Workflow.Services.Foundations.Events.CalendarEntityEventService( + calendarEventBroker: calendarEntityEventBrokerMock.Object); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEventEventServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEventEventServiceTests.Exceptions.cs new file mode 100644 index 0000000..2779020 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEventEventServiceTests.Exceptions.cs @@ -0,0 +1,46 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class CalendarEventEventServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .CalendarEventServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapRaiseCalendarEventAddEventAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + CalendarEvent calendarEvent = new() { Id = 1 }; + + calendarEventEventBrokerMock + .Setup(expression: broker => broker.RaiseCalendarEventAddEventAsync( + message: It.Is>(match: _ => true))) + .Throws(exception: exception); + + // When + Func action = async () => await service + .RaiseCalendarEventAddEventAsync(entity: calendarEvent); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEventEventServiceTests.RaiseCalendarEventAddEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEventEventServiceTests.RaiseCalendarEventAddEventAsync.cs new file mode 100644 index 0000000..3ae1296 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEventEventServiceTests.RaiseCalendarEventAddEventAsync.cs @@ -0,0 +1,58 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class CalendarEventEventServiceTests +{ + [Fact] + public async Task ShouldMapAndCallBrokerWhenRaiseCalendarEventAddEventAsync() + { + // Given + CalendarEvent entity = new(); + EventMessage actualMessage = null; + + calendarEventEventBrokerMock + .Setup(expression: x => + x.RaiseCalendarEventAddEventAsync(message: It.IsAny>()) + ) + .Callback>(action: message => actualMessage = message) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseCalendarEventAddEventAsync(entity: entity); + + // Then + actualMessage.Should() + .NotBeNull(); + + actualMessage!.Data.Should() + .BeEquivalentTo(expectation: entity); + + actualMessage.AuthInfo.Should() + .NotBeNull(); + + actualMessage.AuthInfo.SSOUserId.Should() + .Be(expected: CurrentUserId); + + calendarEventEventBrokerMock.Verify( +expression: x => x.RaiseCalendarEventAddEventAsync(message: It.IsAny>()), +times: Times.Once + ); + + calendarEventEventBrokerMock.Verify( + expression: x => x.GetCurrentUserId(), + times: Times.Once); + + calendarEventEventBrokerMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEventEventServiceTests.RaiseCalendarEventDeleteEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEventEventServiceTests.RaiseCalendarEventDeleteEventAsync.cs new file mode 100644 index 0000000..d59f480 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEventEventServiceTests.RaiseCalendarEventDeleteEventAsync.cs @@ -0,0 +1,58 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class CalendarEventEventServiceTests +{ + [Fact] + public async Task ShouldMapAndCallBrokerWhenRaiseCalendarEventDeleteEventAsync() + { + // Given + CalendarEvent entity = new(); + EventMessage actualMessage = null; + + calendarEventEventBrokerMock + .Setup(expression: x => + x.RaiseCalendarEventDeleteEventAsync(message: It.IsAny>()) + ) + .Callback>(action: message => actualMessage = message) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseCalendarEventDeleteEventAsync(entity: entity); + + // Then + actualMessage.Should() + .NotBeNull(); + + actualMessage!.Data.Should() + .BeEquivalentTo(expectation: entity); + + actualMessage.AuthInfo.Should() + .NotBeNull(); + + actualMessage.AuthInfo.SSOUserId.Should() + .Be(expected: CurrentUserId); + + calendarEventEventBrokerMock.Verify( +expression: x => x.RaiseCalendarEventDeleteEventAsync(message: It.IsAny>()), +times: Times.Once + ); + + calendarEventEventBrokerMock.Verify( + expression: x => x.GetCurrentUserId(), + times: Times.Once); + + calendarEventEventBrokerMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEventEventServiceTests.RaiseCalendarEventUpdateEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEventEventServiceTests.RaiseCalendarEventUpdateEventAsync.cs new file mode 100644 index 0000000..87984dd --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEventEventServiceTests.RaiseCalendarEventUpdateEventAsync.cs @@ -0,0 +1,58 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class CalendarEventEventServiceTests +{ + [Fact] + public async Task ShouldMapAndCallBrokerWhenRaiseCalendarEventUpdateEventAsync() + { + // Given + CalendarEvent entity = new(); + EventMessage actualMessage = null; + + calendarEventEventBrokerMock + .Setup(expression: x => + x.RaiseCalendarEventUpdateEventAsync(message: It.IsAny>()) + ) + .Callback>(action: message => actualMessage = message) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseCalendarEventUpdateEventAsync(entity: entity); + + // Then + actualMessage.Should() + .NotBeNull(); + + actualMessage!.Data.Should() + .BeEquivalentTo(expectation: entity); + + actualMessage.AuthInfo.Should() + .NotBeNull(); + + actualMessage.AuthInfo.SSOUserId.Should() + .Be(expected: CurrentUserId); + + calendarEventEventBrokerMock.Verify( +expression: x => x.RaiseCalendarEventUpdateEventAsync(message: It.IsAny>()), +times: Times.Once + ); + + calendarEventEventBrokerMock.Verify( + expression: x => x.GetCurrentUserId(), + times: Times.Once); + + calendarEventEventBrokerMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEventEventServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEventEventServiceTests.cs new file mode 100644 index 0000000..2c2271f --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/CalendarEventEventServiceTests.cs @@ -0,0 +1,27 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Brokers.Events; +using Moq; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class CalendarEventEventServiceTests +{ + private readonly Mock calendarEventEventBrokerMock; + private readonly cCoder.Workflow.Services.Foundations.Events.CalendarEventEventService service; + private const string CurrentUserId = "test-user"; + + public CalendarEventEventServiceTests() + { + calendarEventEventBrokerMock = new Mock(behavior: MockBehavior.Strict); + + calendarEventEventBrokerMock + .Setup(expression: broker => broker.GetCurrentUserId()) + .Returns(value: CurrentUserId); + + service = new cCoder.Workflow.Services.Foundations.Events.CalendarEventEventService( + calendarEventEventBroker: calendarEventEventBrokerMock.Object); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/EventHandlerServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/EventHandlerServiceTests.Exceptions.cs new file mode 100644 index 0000000..660800e --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/EventHandlerServiceTests.Exceptions.cs @@ -0,0 +1,55 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using System.ComponentModel.DataAnnotations; +using System.Security; +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Brokers.Events; +using cCoder.Workflow.Models.Exceptions; +using cCoder.Workflow.Services.Coordinations; +using cCoder.Workflow.Services.Foundations.Events; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public sealed partial class EventHandlerServiceExceptionTests +{ + public static TheoryData ListenerDependencyExceptions => new() + { + { new WorkflowValidationException(innerException: new Exception()), typeof(WorkflowValidationException) }, + { new WorkflowDependencyException(innerException: new Exception()), typeof(WorkflowDependencyException) }, + { new ValidationException(), typeof(WorkflowValidationException) }, + { new InvalidOperationException(), typeof(WorkflowDependencyException) }, + { new SecurityException(), typeof(SecurityException) }, + { new Exception(), typeof(WorkflowServiceException) } + }; + + [Theory] + [MemberData(nameof(ListenerDependencyExceptions))] + public void ListenToScheduledTaskExecuteEventsShouldMapDependencyExceptions( + Exception dependencyException, + Type expectedExceptionType) + { + var eventHubBrokerMock = new Mock(); + + eventHubBrokerMock.Setup(expression: broker => + broker.ListenToEvent( + "scheduled_task_execute", + It.IsAny>())) + .Throws(exception: dependencyException); + + var service = new EventHandlerService(eventHubBroker: eventHubBrokerMock.Object); + + Action action = service.ListenToScheduledTaskExecuteEvents; + + Exception exception = action.Should().Throw().Which; + exception.Should().BeOfType(expectedExceptionType); + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/FlowDefinitionEventServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/FlowDefinitionEventServiceTests.Exceptions.cs new file mode 100644 index 0000000..9949af7 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/FlowDefinitionEventServiceTests.Exceptions.cs @@ -0,0 +1,46 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class FlowDefinitionEventServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapRaiseFlowDefinitionAddEventAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + FlowDefinition flowDefinition = new() { Id = Guid.NewGuid() }; + + flowDefinitionEventBrokerMock + .Setup(expression: broker => broker.RaiseFlowDefinitionAddEventAsync( + message: It.Is>(match: _ => true))) + .Throws(exception: exception); + + // When + Func action = async () => await service + .RaiseFlowDefinitionAddEventAsync(entity: flowDefinition); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/FlowInstanceDataEventServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/FlowInstanceDataEventServiceTests.Exceptions.cs new file mode 100644 index 0000000..3695e34 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/FlowInstanceDataEventServiceTests.Exceptions.cs @@ -0,0 +1,46 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class FlowInstanceDataEventServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapRaiseFlowInstanceDataAddEventAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + FlowInstanceData entity = new(); + + flowInstanceDataEventBrokerMock + .Setup(expression: broker => broker.RaiseFlowInstanceDataAddEventAsync( + message: It.Is>(match: _ => true))) + .Throws(exception: exception); + + // When + Func action = async () => await service + .RaiseFlowInstanceDataAddEventAsync(entity: entity); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.Exceptions.cs new file mode 100644 index 0000000..a6da524 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.Exceptions.cs @@ -0,0 +1,46 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class ScheduledTaskEventServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .ScheduledTaskServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapRaiseScheduledTaskAddEventAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + ScheduledTask scheduledTask = new() { Id = 1 }; + + scheduledTaskEventBrokerMock + .Setup(expression: broker => broker.RaiseScheduledTaskAddEventAsync( + message: It.Is>(match: _ => true))) + .Throws(exception: exception); + + // When + Func action = async () => await service + .RaiseScheduledTaskAddEventAsync(entity: scheduledTask); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.RaiseScheduledTaskAddEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.RaiseScheduledTaskAddEventAsync.cs new file mode 100644 index 0000000..9bcb4d4 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.RaiseScheduledTaskAddEventAsync.cs @@ -0,0 +1,58 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class ScheduledTaskEventServiceTests +{ + [Fact] + public async Task ShouldMapAndCallBrokerWhenRaiseScheduledTaskAddEventAsync() + { + // Given + ScheduledTask entity = new(); + EventMessage actualMessage = null; + + scheduledTaskEventBrokerMock + .Setup(expression: x => + x.RaiseScheduledTaskAddEventAsync(message: It.IsAny>()) + ) + .Callback>(action: message => actualMessage = message) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseScheduledTaskAddEventAsync(entity: entity); + + // Then + actualMessage.Should() + .NotBeNull(); + + actualMessage!.Data.Should() + .BeEquivalentTo(expectation: entity); + + actualMessage.AuthInfo.Should() + .NotBeNull(); + + actualMessage.AuthInfo.SSOUserId.Should() + .Be(expected: CurrentUserId); + + scheduledTaskEventBrokerMock.Verify( +expression: x => x.RaiseScheduledTaskAddEventAsync(message: It.IsAny>()), +times: Times.Once + ); + + scheduledTaskEventBrokerMock.Verify( + expression: x => x.GetCurrentUserId(), + times: Times.Once); + + scheduledTaskEventBrokerMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.RaiseScheduledTaskDeleteEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.RaiseScheduledTaskDeleteEventAsync.cs new file mode 100644 index 0000000..25b9058 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.RaiseScheduledTaskDeleteEventAsync.cs @@ -0,0 +1,58 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class ScheduledTaskEventServiceTests +{ + [Fact] + public async Task ShouldMapAndCallBrokerWhenRaiseScheduledTaskDeleteEventAsync() + { + // Given + ScheduledTask entity = new(); + EventMessage actualMessage = null; + + scheduledTaskEventBrokerMock + .Setup(expression: x => + x.RaiseScheduledTaskDeleteEventAsync(message: It.IsAny>()) + ) + .Callback>(action: message => actualMessage = message) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseScheduledTaskDeleteEventAsync(entity: entity); + + // Then + actualMessage.Should() + .NotBeNull(); + + actualMessage!.Data.Should() + .BeEquivalentTo(expectation: entity); + + actualMessage.AuthInfo.Should() + .NotBeNull(); + + actualMessage.AuthInfo.SSOUserId.Should() + .Be(expected: CurrentUserId); + + scheduledTaskEventBrokerMock.Verify( +expression: x => x.RaiseScheduledTaskDeleteEventAsync(message: It.IsAny>()), +times: Times.Once + ); + + scheduledTaskEventBrokerMock.Verify( + expression: x => x.GetCurrentUserId(), + times: Times.Once); + + scheduledTaskEventBrokerMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.RaiseScheduledTaskExecuteEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.RaiseScheduledTaskExecuteEventAsync.cs new file mode 100644 index 0000000..e6172aa --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.RaiseScheduledTaskExecuteEventAsync.cs @@ -0,0 +1,58 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class ScheduledTaskEventServiceTests +{ + [Fact] + public async Task ShouldMapAndCallBrokerWhenRaiseScheduledTaskExecuteEventAsync() + { + // Given + ScheduledTask entity = new(); + EventMessage actualMessage = null; + + scheduledTaskEventBrokerMock + .Setup(expression: x => + x.RaiseScheduledTaskExecuteEventAsync(message: It.IsAny>()) + ) + .Callback>(action: message => actualMessage = message) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseScheduledTaskExecuteEventAsync(entity: entity); + + // Then + actualMessage.Should() + .NotBeNull(); + + actualMessage!.Data.Should() + .BeEquivalentTo(expectation: entity); + + actualMessage.AuthInfo.Should() + .NotBeNull(); + + actualMessage.AuthInfo.SSOUserId.Should() + .Be(expected: CurrentUserId); + + scheduledTaskEventBrokerMock.Verify( +expression: x => x.RaiseScheduledTaskExecuteEventAsync(message: It.IsAny>()), +times: Times.Once + ); + + scheduledTaskEventBrokerMock.Verify( + expression: x => x.GetCurrentUserId(), + times: Times.Once); + + scheduledTaskEventBrokerMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.RaiseScheduledTaskUpdateEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.RaiseScheduledTaskUpdateEventAsync.cs new file mode 100644 index 0000000..9fa65d3 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.RaiseScheduledTaskUpdateEventAsync.cs @@ -0,0 +1,58 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class ScheduledTaskEventServiceTests +{ + [Fact] + public async Task ShouldMapAndCallBrokerWhenRaiseScheduledTaskUpdateEventAsync() + { + // Given + ScheduledTask entity = new(); + EventMessage actualMessage = null; + + scheduledTaskEventBrokerMock + .Setup(expression: x => + x.RaiseScheduledTaskUpdateEventAsync(message: It.IsAny>()) + ) + .Callback>(action: message => actualMessage = message) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseScheduledTaskUpdateEventAsync(entity: entity); + + // Then + actualMessage.Should() + .NotBeNull(); + + actualMessage!.Data.Should() + .BeEquivalentTo(expectation: entity); + + actualMessage.AuthInfo.Should() + .NotBeNull(); + + actualMessage.AuthInfo.SSOUserId.Should() + .Be(expected: CurrentUserId); + + scheduledTaskEventBrokerMock.Verify( +expression: x => x.RaiseScheduledTaskUpdateEventAsync(message: It.IsAny>()), +times: Times.Once + ); + + scheduledTaskEventBrokerMock.Verify( + expression: x => x.GetCurrentUserId(), + times: Times.Once); + + scheduledTaskEventBrokerMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.cs new file mode 100644 index 0000000..19fa088 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/ScheduledTaskEventServiceTests.cs @@ -0,0 +1,27 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Brokers.Events; +using Moq; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class ScheduledTaskEventServiceTests +{ + private readonly Mock scheduledTaskEventBrokerMock; + private readonly cCoder.Workflow.Services.Foundations.Events.ScheduledTaskEventService service; + private const string CurrentUserId = "test-user"; + + public ScheduledTaskEventServiceTests() + { + scheduledTaskEventBrokerMock = new Mock(behavior: MockBehavior.Strict); + + scheduledTaskEventBrokerMock + .Setup(expression: broker => broker.GetCurrentUserId()) + .Returns(value: CurrentUserId); + + service = new cCoder.Workflow.Services.Foundations.Events.ScheduledTaskEventService( + scheduledTaskEventBroker: scheduledTaskEventBrokerMock.Object); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/WorkflowEventEventServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/WorkflowEventEventServiceTests.Exceptions.cs new file mode 100644 index 0000000..b73c6d1 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/Events/WorkflowEventEventServiceTests.Exceptions.cs @@ -0,0 +1,46 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using cCoder.Eventing.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations.Events; + +public partial class WorkflowEventEventServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapRaiseWorkflowEventAddEventAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + WorkflowEvent entity = new(); + + workflowEventEventBrokerMock + .Setup(expression: broker => broker.RaiseWorkflowEventAddEventAsync( + message: It.Is>(match: _ => true))) + .Throws(exception: exception); + + // When + Func action = async () => await service + .RaiseWorkflowEventAddEventAsync(entity: entity); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/FlowDefinitionServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/FlowDefinitionServiceTests.Exceptions.cs new file mode 100644 index 0000000..3e2295b --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/FlowDefinitionServiceTests.Exceptions.cs @@ -0,0 +1,111 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using System.ComponentModel.DataAnnotations; +using System.Security; +using cCoder.Data.Models.Workflow; +using cCoder.Workflow.Models.Exceptions; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class FlowDefinitionServiceTests +{ + public static TheoryData ExceptionMappings => + new() + { + { + new WorkflowValidationException( + innerException: new ArgumentException()), + typeof(WorkflowValidationException) + }, + { + new WorkflowDependencyException( + innerException: new InvalidOperationException()), + typeof(WorkflowDependencyException) + }, + { new ValidationException(), typeof(WorkflowValidationException) }, + { new InvalidOperationException(), typeof(WorkflowDependencyException) }, + { new SecurityException(), typeof(SecurityException) }, + { new Exception(), typeof(WorkflowServiceException) } + }; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetAllFailure(Exception exception, Type expectedType) + { + // Given + flowDefinitionBrokerMock + .Setup(expression: broker => broker.SelectAllFlowDefinitions()) + .Throws(exception: exception); + + // When + Action action = () => flowDefinitionService.GetAll(); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddFlowDefinitionAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + FlowDefinition flowDefinition = CreateRandomFlowDefinition(); + + authorizationBrokerMock + .Setup(expression: broker => broker.Authorize( + appId: flowDefinition.AppId, + privilege: "FlowDefinition_create")) + .Throws(exception: exception); + + // When + Func action = async () => await flowDefinitionService + .AddFlowDefinitionAsync(newFlowDefinition: flowDefinition); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteWithInstancesByAppIdAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + flowDefinitionBrokerMock + .Setup(expression: broker => broker + .DeleteFlowDefinitionsWithInstancesByAppIdAsync(appId: 7)) + .Throws(exception: exception); + + // When + Func action = async () => await flowDefinitionService + .DeleteWithInstancesByAppIdAsync(appId: 7); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/FlowInstanceDataServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/FlowInstanceDataServiceTests.Exceptions.cs new file mode 100644 index 0000000..2dd205c --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/FlowInstanceDataServiceTests.Exceptions.cs @@ -0,0 +1,98 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class FlowInstanceDataServiceTests +{ + public static TheoryData ExceptionMappings => + FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetAllFailure(Exception exception, Type expectedType) + { + // Given + flowInstanceDataBrokerMock + .Setup(expression: broker => broker.SelectAllFlowInstanceData()) + .Throws(exception: exception); + + // When + Action action = () => flowInstanceDataService.GetAll(); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddQueuedFlowInstanceDataAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + FlowInstanceData flowInstanceData = CreateRandomFlowInstanceData(); + + flowInstanceDataBrokerMock + .Setup(expression: broker => broker.AddFlowInstanceDataAsync( + newEntity: It.Is(match: _ => true))) + .Throws(exception: exception); + + // When + Func action = async () => await flowInstanceDataService + .AddQueuedFlowInstanceDataAsync( + newFlowInstanceData: flowInstanceData); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + FlowInstanceData flowInstanceData = CreateRandomFlowInstanceData(); + + flowInstanceDataBrokerMock + .Setup(expression: broker => broker.SelectAllFlowInstanceData()) + .Returns(value: new[] { flowInstanceData }.AsQueryable()); + + flowInstanceDataBrokerMock + .Setup(expression: broker => broker.SelectAppId( + entity: flowInstanceData)) + .Throws(exception: exception); + + // When + Func action = async () => await flowInstanceDataService + .DeleteAsync(flowInstanceDataId: flowInstanceData.Id); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.AddScheduledTaskAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.AddScheduledTaskAsync.cs new file mode 100644 index 0000000..84bcf79 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.AddScheduledTaskAsync.cs @@ -0,0 +1,122 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using System.Security; +using cCoder.Data.Models.Planning; +using cCoder.Data.Models.Security; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class ScheduledTaskServiceTests +{ + [Fact] + public async Task ShouldAddScheduledTaskAsync() + { + // Given + ScheduledTask input = CreateScheduledTask(); + ScheduledTask stored = CreateScheduledTask(); + const string userId = "user"; + + authorizationBrokerMock + .Setup(expression: broker => broker.IsAdminOfApp( + appId: input.AppId)) + .Returns(value: true); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker + .SelectExecuteAsUserBelongsToApp( + executeAs: input.ExecuteAs, + appId: input.AppId)) + .Returns(value: true); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker.SelectFlowBelongsToApp( + flowId: input.FlowId, + appId: input.AppId)) + .Returns(value: true); + + authorizationBrokerMock + .Setup(expression: broker => broker.Authorize( + appId: input.AppId, + privilege: "ScheduledTask_create")); + + authorizationBrokerMock + .Setup(expression: broker => broker.GetCurrentUser()) + .Returns(value: new User { Id = userId }); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker.InsertScheduledTaskAsync( + newEntity: It.Is(match: added => + added.Name == input.Name + && added.CreatedBy == userId + && added.UpdatedBy == userId))) + .Returns(value: ValueTask.FromResult(result: stored)); + + // When + ScheduledTask actual = await scheduledTaskService + .AddScheduledTaskAsync(newScheduledTask: input); + + // Then + actual + .Should() + .BeSameAs(expected: input); + + actual.Id + .Should() + .Be(expected: stored.Id); + + actual.FlowId + .Should() + .Be(expected: stored.FlowId); + + authorizationBrokerMock.VerifyAll(); + scheduledTaskBrokerMock.VerifyAll(); + } + + [Theory] + [InlineData(false, true, true)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + public async Task ShouldRejectUnauthorizedScheduledTaskOnAddAsync( + bool isAppAdmin, + bool userBelongsToApp, + bool flowBelongsToApp) + { + // Given + ScheduledTask input = CreateScheduledTask(); + + authorizationBrokerMock + .Setup(expression: broker => broker.IsAdminOfApp( + appId: input.AppId)) + .Returns(value: isAppAdmin); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker + .SelectExecuteAsUserBelongsToApp( + executeAs: input.ExecuteAs, + appId: input.AppId)) + .Returns(value: userBelongsToApp); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker.SelectFlowBelongsToApp( + flowId: input.FlowId, + appId: input.AppId)) + .Returns(value: flowBelongsToApp); + + // When + Func action = async () => await scheduledTaskService + .AddScheduledTaskAsync(newScheduledTask: input); + + // Then + await action + .Should() + .ThrowAsync(); + + authorizationBrokerMock.VerifyAll(); + scheduledTaskBrokerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.DeleteAllByAppIdAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.DeleteAllByAppIdAsync.cs new file mode 100644 index 0000000..92e5cb3 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.DeleteAllByAppIdAsync.cs @@ -0,0 +1,29 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class ScheduledTaskServiceTests +{ + [Fact] + public async Task ShouldDeleteAllByAppIdAsync() + { + // Given + const int appId = 7; + + scheduledTaskBrokerMock + .Setup(expression: broker => broker + .DeleteAllScheduledTasksByAppIdAsync(appId: appId)) + .Returns(value: ValueTask.CompletedTask); + + // When + await scheduledTaskService.DeleteAllByAppIdAsync(appId: appId); + + // Then + scheduledTaskBrokerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.DeleteAllForAppScheduledTaskAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.DeleteAllForAppScheduledTaskAsync.cs new file mode 100644 index 0000000..ae48977 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.DeleteAllForAppScheduledTaskAsync.cs @@ -0,0 +1,53 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class ScheduledTaskServiceTests +{ + [Fact] + public async Task ShouldDeleteAllForAppScheduledTaskAsync() + { + // Given + ScheduledTask scheduledTask = CreateScheduledTask(); + IEnumerable captured = null; + + scheduledTaskBrokerMock + .Setup(expression: broker => broker.DeleteAllScheduledTasksAsync( + deletedItems: It.IsAny>())) + .Callback>(action: deletedItems => + captured = deletedItems) + .Returns(value: ValueTask.CompletedTask); + + // When + await scheduledTaskService.DeleteAllForAppScheduledTaskAsync( + deletedItems: new[] { scheduledTask }); + + // Then + captured + .Should() + .ContainSingle(predicate: deleted => + deleted.Id == scheduledTask.Id); + + scheduledTaskBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldIgnoreEmptyDeleteAllForAppScheduledTaskAsync() + { + // Given + + // When + await scheduledTaskService.DeleteAllForAppScheduledTaskAsync( + deletedItems: Array.Empty()); + + // Then + scheduledTaskBrokerMock.VerifyNoOtherCalls(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.DeleteAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.DeleteAsync.cs new file mode 100644 index 0000000..715e439 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.DeleteAsync.cs @@ -0,0 +1,62 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class ScheduledTaskServiceTests +{ + [Fact] + public async Task ShouldDeleteScheduledTaskAsync() + { + // Given + ScheduledTask scheduledTask = CreateScheduledTask(); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker + .SelectAllScheduledTasksIgnoringQueryFilters()) + .Returns(value: new[] { scheduledTask } + .AsQueryable()); + + authorizationBrokerMock + .Setup(expression: broker => broker.Authorize( + appId: scheduledTask.AppId, + privilege: "ScheduledTask_delete")); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker.DeleteScheduledTaskAsync( + deletedEntity: It.Is(match: deleted => + deleted.Id == scheduledTask.Id))) + .Returns(value: ValueTask.FromResult(result: 1)); + + // When + await scheduledTaskService.DeleteAsync( + scheduledTaskId: scheduledTask.Id); + + // Then + scheduledTaskBrokerMock.VerifyAll(); + authorizationBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldIgnoreMissingScheduledTaskWhenDeleteAsync() + { + // Given + scheduledTaskBrokerMock + .Setup(expression: broker => broker + .SelectAllScheduledTasksIgnoringQueryFilters()) + .Returns(value: Array.Empty() + .AsQueryable()); + + // When + await scheduledTaskService.DeleteAsync(scheduledTaskId: 1); + + // Then + scheduledTaskBrokerMock.VerifyAll(); + authorizationBrokerMock.VerifyNoOtherCalls(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.Exceptions.cs new file mode 100644 index 0000000..10b29c7 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.Exceptions.cs @@ -0,0 +1,95 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class ScheduledTaskServiceTests +{ + public static TheoryData ExceptionMappings => + FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetAllFailure(Exception exception, Type expectedType) + { + // Given + scheduledTaskBrokerMock + .Setup(expression: broker => broker.SelectAllScheduledTasks()) + .Throws(exception: exception); + + // When + Action action = () => scheduledTaskService.GetAll(); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapMarkExecutedAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + ScheduledTask scheduledTask = CreateScheduledTask(); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker.SelectScheduledTaskForExecution( + scheduledTaskId: scheduledTask.Id)) + .Throws(exception: exception); + + // When + Func action = async () => await scheduledTaskService + .MarkExecutedAsync( + scheduledTaskId: scheduledTask.Id, + incrementNextExecution: true); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteAllByAppIdAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + int appId = Random.Shared.Next(minValue: 1, maxValue: int.MaxValue); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker + .DeleteAllScheduledTasksByAppIdAsync(appId: appId)) + .Throws(exception: exception); + + // When + Func action = async () => await scheduledTaskService + .DeleteAllByAppIdAsync(appId: appId); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.Get.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.Get.cs new file mode 100644 index 0000000..cde4daf --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.Get.cs @@ -0,0 +1,92 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using System.Security; +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class ScheduledTaskServiceTests +{ + [Fact] + public void ShouldGetScheduledTask() + { + // Given + ScheduledTask expected = CreateScheduledTask(); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker.SelectAllScheduledTasks()) + .Returns(value: new[] { expected } + .AsQueryable()); + + // When + ScheduledTask actual = scheduledTaskService.Get( + scheduledTaskId: expected.Id); + + // Then + actual + .Should() + .BeSameAs(expected: expected); + + scheduledTaskBrokerMock.VerifyAll(); + } + + [Fact] + public void ShouldReturnNullWhenScheduledTaskDoesNotExist() + { + // Given + scheduledTaskBrokerMock + .Setup(expression: broker => broker.SelectAllScheduledTasks()) + .Returns(value: Array.Empty() + .AsQueryable()); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker + .SelectAllScheduledTasksIgnoringQueryFilters()) + .Returns(value: Array.Empty() + .AsQueryable()); + + // When + ScheduledTask actual = scheduledTaskService.Get(scheduledTaskId: 1); + + // Then + actual + .Should() + .BeNull(); + + scheduledTaskBrokerMock.VerifyAll(); + } + + [Fact] + public void ShouldRejectFilteredScheduledTask() + { + // Given + ScheduledTask restricted = CreateScheduledTask(); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker.SelectAllScheduledTasks()) + .Returns(value: Array.Empty() + .AsQueryable()); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker + .SelectAllScheduledTasksIgnoringQueryFilters()) + .Returns(value: new[] { restricted } + .AsQueryable()); + + // When + Action action = () => scheduledTaskService.Get( + scheduledTaskId: restricted.Id); + + // Then + action + .Should() + .Throw(); + + scheduledTaskBrokerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.GetAll.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.GetAll.cs new file mode 100644 index 0000000..4ef442d --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.GetAll.cs @@ -0,0 +1,49 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class ScheduledTaskServiceTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ShouldGetAllScheduledTasks(bool ignoreFilters) + { + // Given + IQueryable expected = + new[] { CreateScheduledTask() } + .AsQueryable(); + + if (ignoreFilters) + { + scheduledTaskBrokerMock + .Setup(expression: broker => broker + .SelectAllScheduledTasksIgnoringQueryFilters()) + .Returns(value: expected); + } + else + { + scheduledTaskBrokerMock + .Setup(expression: broker => broker.SelectAllScheduledTasks()) + .Returns(value: expected); + } + + // When + IQueryable actual = scheduledTaskService.GetAll( + ignoreFilters: ignoreFilters); + + // Then + actual + .Should() + .BeSameAs(expected: expected); + + scheduledTaskBrokerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.GetExecuteAsUserBelongsToApp.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.GetExecuteAsUserBelongsToApp.cs new file mode 100644 index 0000000..7b0785d --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.GetExecuteAsUserBelongsToApp.cs @@ -0,0 +1,39 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class ScheduledTaskServiceTests +{ + [Fact] + public void ShouldGetExecuteAsUserBelongsToApp() + { + // Given + const string executeAs = "user"; + const int appId = 7; + + scheduledTaskBrokerMock + .Setup(expression: broker => broker + .SelectExecuteAsUserBelongsToApp( + executeAs: executeAs, + appId: appId)) + .Returns(value: true); + + // When + bool actual = scheduledTaskService.GetExecuteAsUserBelongsToApp( + executeAs: executeAs, + appId: appId); + + // Then + actual + .Should() + .BeTrue(); + + scheduledTaskBrokerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.GetFlowBelongsToApp.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.GetFlowBelongsToApp.cs new file mode 100644 index 0000000..71c8ba2 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.GetFlowBelongsToApp.cs @@ -0,0 +1,38 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class ScheduledTaskServiceTests +{ + [Fact] + public void ShouldGetFlowBelongsToApp() + { + // Given + Guid flowId = Guid.NewGuid(); + const int appId = 7; + + scheduledTaskBrokerMock + .Setup(expression: broker => broker.SelectFlowBelongsToApp( + flowId: flowId, + appId: appId)) + .Returns(value: true); + + // When + bool actual = scheduledTaskService.GetFlowBelongsToApp( + flowId: flowId, + appId: appId); + + // Then + actual + .Should() + .BeTrue(); + + scheduledTaskBrokerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.GetForExecution.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.GetForExecution.cs new file mode 100644 index 0000000..1714ee3 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.GetForExecution.cs @@ -0,0 +1,36 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class ScheduledTaskServiceTests +{ + [Fact] + public void ShouldGetScheduledTaskForExecution() + { + // Given + ScheduledTask expected = CreateScheduledTask(); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker.SelectScheduledTaskForExecution( + scheduledTaskId: expected.Id)) + .Returns(value: expected); + + // When + ScheduledTask actual = scheduledTaskService.GetForExecution( + scheduledTaskId: expected.Id); + + // Then + actual + .Should() + .BeSameAs(expected: expected); + + scheduledTaskBrokerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.MarkExecutedAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.MarkExecutedAsync.cs index 2389f64..1d99abb 100644 --- a/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.MarkExecutedAsync.cs +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.MarkExecutedAsync.cs @@ -3,6 +3,7 @@ // --------------------------------------------------------------- using cCoder.Data.Models.Planning; +using System.Security; using FluentAssertions; using Moq; using Xunit; @@ -46,4 +47,89 @@ await scheduledTaskService.MarkExecutedAsync( authorizationBrokerMock.VerifyNoOtherCalls(); scheduledTaskBrokerMock.VerifyAll(); } + + [Fact] + public async Task ShouldNotIncrementNextExecutionWhenMarkExecutedAsync() + { + // Given + ScheduledTask scheduledTask = CreateScheduledTask(); + DateTimeOffset? expectedNextExecution = scheduledTask.NextExecution; + + scheduledTaskBrokerMock + .Setup(expression: broker => broker + .SelectScheduledTaskForExecution( + scheduledTaskId: scheduledTask.Id)) + .Returns(value: scheduledTask); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker.UpdateScheduledTaskAsync( + updatedEntity: scheduledTask)) + .Returns(value: ValueTask.FromResult(result: scheduledTask)); + + // When + ScheduledTask actual = await scheduledTaskService.MarkExecutedAsync( + scheduledTaskId: scheduledTask.Id, + incrementNextExecution: false); + + // Then + actual.NextExecution + .Should() + .Be(expected: expectedNextExecution); + + scheduledTaskBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldClearNextExecutionForNonRepeatingScheduledTaskAsync() + { + // Given + ScheduledTask scheduledTask = CreateScheduledTask(); + scheduledTask.ScheduleInTicks = 0; + + scheduledTaskBrokerMock + .Setup(expression: broker => broker + .SelectScheduledTaskForExecution( + scheduledTaskId: scheduledTask.Id)) + .Returns(value: scheduledTask); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker.UpdateScheduledTaskAsync( + updatedEntity: scheduledTask)) + .Returns(value: ValueTask.FromResult(result: scheduledTask)); + + // When + ScheduledTask actual = await scheduledTaskService.MarkExecutedAsync( + scheduledTaskId: scheduledTask.Id, + incrementNextExecution: true); + + // Then + actual.NextExecution + .Should() + .BeNull(); + + scheduledTaskBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldRejectMissingScheduledTaskWhenMarkExecutedAsync() + { + // Given + scheduledTaskBrokerMock + .Setup(expression: broker => broker + .SelectScheduledTaskForExecution(scheduledTaskId: 1)) + .Returns(value: null); + + // When + Func action = async () => await scheduledTaskService + .MarkExecutedAsync( + scheduledTaskId: 1, + incrementNextExecution: true); + + // Then + await action + .Should() + .ThrowAsync(); + + scheduledTaskBrokerMock.VerifyAll(); + } } \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.UpdateScheduledTaskAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.UpdateScheduledTaskAsync.cs new file mode 100644 index 0000000..5d8d11b --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.UpdateScheduledTaskAsync.cs @@ -0,0 +1,77 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Data.Models.Security; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class ScheduledTaskServiceTests +{ + [Fact] + public async Task ShouldUpdateScheduledTaskAsync() + { + // Given + ScheduledTask input = CreateScheduledTask(); + ScheduledTask stored = CreateScheduledTask(); + const string userId = "user"; + + authorizationBrokerMock + .Setup(expression: broker => broker.IsAdminOfApp( + appId: input.AppId)) + .Returns(value: true); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker + .SelectExecuteAsUserBelongsToApp( + executeAs: input.ExecuteAs, + appId: input.AppId)) + .Returns(value: true); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker.SelectFlowBelongsToApp( + flowId: input.FlowId, + appId: input.AppId)) + .Returns(value: true); + + authorizationBrokerMock + .Setup(expression: broker => broker.Authorize( + appId: input.AppId, + privilege: "ScheduledTask_update")); + + authorizationBrokerMock + .Setup(expression: broker => broker.GetCurrentUser()) + .Returns(value: new User { Id = userId }); + + scheduledTaskBrokerMock + .Setup(expression: broker => broker.UpdateScheduledTaskAsync( + updatedEntity: It.Is(match: updated => + updated.Id == input.Id + && updated.UpdatedBy == userId))) + .Returns(value: ValueTask.FromResult(result: stored)); + + // When + ScheduledTask actual = await scheduledTaskService + .UpdateScheduledTaskAsync(updatedScheduledTask: input); + + // Then + actual + .Should() + .BeSameAs(expected: input); + + actual.Id + .Should() + .Be(expected: stored.Id); + + actual.FlowId + .Should() + .Be(expected: stored.FlowId); + + authorizationBrokerMock.VerifyAll(); + scheduledTaskBrokerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.cs index 9896531..bd86d34 100644 --- a/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.cs +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/ScheduledTaskServiceTests.cs @@ -34,8 +34,13 @@ private static ScheduledTask CreateScheduledTask() => { Id = Random.Shared.Next(minValue: 1, maxValue: int.MaxValue), AppId = Random.Shared.Next(minValue: 1, maxValue: int.MaxValue), + FlowId = Guid.NewGuid(), + Name = "Task", + ExecuteAs = "user", LastExecuted = DateTimeOffset.UtcNow.AddDays(days: -1), NextExecution = DateTimeOffset.UtcNow.AddMinutes(minutes: -1), - ScheduleInTicks = TimeSpan.FromMinutes(minutes: 5).Ticks + ScheduleInTicks = TimeSpan + .FromMinutes(minutes: 5) + .Ticks }; } \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Foundations/WorkflowEventServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Foundations/WorkflowEventServiceTests.Exceptions.cs new file mode 100644 index 0000000..72602e3 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Foundations/WorkflowEventServiceTests.Exceptions.cs @@ -0,0 +1,95 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Foundations; + +public partial class WorkflowEventServiceTests +{ + public static TheoryData ExceptionMappings => + FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetAllFailure(Exception exception, Type expectedType) + { + // Given + workflowEventBrokerMock + .Setup(expression: broker => broker.SelectAllWorkflowEvents()) + .Throws(exception: exception); + + // When + Action action = () => workflowEventService.GetAll(); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddWorkflowEventAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + WorkflowEvent workflowEvent = CreateRandomWorkflowEvent(); + + workflowEventBrokerMock + .Setup(expression: broker => broker.SelectAppId( + entity: workflowEvent)) + .Throws(exception: exception); + + // When + Func action = async () => await workflowEventService + .AddWorkflowEventAsync(newWorkflowEvent: workflowEvent); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteAsyncFailure(Exception exception, Type expectedType) + { + // Given + WorkflowEvent workflowEvent = CreateRandomWorkflowEvent(); + + workflowEventBrokerMock + .Setup(expression: broker => broker.SelectAllWorkflowEvents()) + .Returns(value: new[] { workflowEvent }.AsQueryable()); + + workflowEventBrokerMock + .Setup(expression: broker => broker.SelectAppId( + entity: workflowEvent)) + .Throws(exception: exception); + + // When + Func action = async () => await workflowEventService.DeleteAsync( + workflowEventId: workflowEvent.Id); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.AddAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.AddAsync.cs new file mode 100644 index 0000000..1a2787b --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.AddAsync.cs @@ -0,0 +1,55 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +#pragma warning disable STXFORMAT008, STXFORMAT009 +public partial class CalendarEventOrchestrationServiceTests +{ + [Fact] + public async Task ShouldCallProcessingThenRaiseAddEventAsyncWhenAddAsync() + { + // Given + CalendarEvent entity = CreateRandomCalendarEvent(); + + calendarEventProcessingServiceMock + .Setup(expression: service => service.AddCalendarEventAsync( + newEntity: entity)) + .ReturnsAsync(value: entity); + + calendarEventEventProcessingServiceMock + .Setup(expression: service => service.RaiseCalendarEventAddEventAsync( + entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + CalendarEvent result = await orchestrationService + .AddCalendarEventAsync(newEntity: entity); + + // Then + result.Should() + .BeSameAs(expected: entity); + + calendarEventProcessingServiceMock.Verify( + expression: service => service.AddCalendarEventAsync( + newEntity: entity), + times: Times.Once); + + calendarEventEventProcessingServiceMock.Verify( + expression: service => service.RaiseCalendarEventAddEventAsync( + entity: entity), + times: Times.Once); + } + +} +#pragma warning restore STXFORMAT008, STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.AddOrUpdate.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.AddOrUpdate.cs new file mode 100644 index 0000000..f16704b --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.AddOrUpdate.cs @@ -0,0 +1,40 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarEventOrchestrationServiceTests +{ + [Fact] + public async Task ShouldDelegateAddOrUpdateCalendarEventsAsync() + { + // Given + CalendarEvent item = CreateRandomCalendarEvent(); + CalendarEvent[] items = [item]; + Result[] expected = [new() { Success = true, Item = item }]; + + calendarEventProcessingServiceMock + .Setup(expression: service => service.AddOrUpdateCalendarEvent( + items: items)) + .Returns(value: ValueTask.FromResult>>( + result: expected)); + + // When + IEnumerable> actual = await orchestrationService + .AddOrUpdateCalendarEvent(items: items); + + // Then + actual + .Should() + .BeSameAs(expected: expected); + + calendarEventProcessingServiceMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.DeleteAllAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.DeleteAllAsync.cs new file mode 100644 index 0000000..e7073b5 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.DeleteAllAsync.cs @@ -0,0 +1,35 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarEventOrchestrationServiceTests +{ + [Fact] + public async Task ShouldDelegateToProcessingServiceWhenDeleteAllAsync() + { + // Given + CalendarEvent[] entities = [CreateRandomCalendarEvent()]; + + calendarEventProcessingServiceMock.Setup(expression: x => x.DeleteAllCalendarEventAsync(deletedItems: entities)) + .Returns(value: ValueTask.CompletedTask); + + // When + await orchestrationService.DeleteAllCalendarEventAsync(deletedItems: entities); + + // Then + calendarEventProcessingServiceMock.Verify(expression: x => x.DeleteAllCalendarEventAsync(deletedItems: entities), times: Times.Once); + calendarEventProcessingServiceMock.VerifyNoOtherCalls(); + calendarEventEventProcessingServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.DeleteAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.DeleteAsync.cs new file mode 100644 index 0000000..c61bb83 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.DeleteAsync.cs @@ -0,0 +1,62 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarEventOrchestrationServiceTests +{ + [Fact] + public async Task ShouldGetThenDeleteThenRaiseDeleteEventAsyncWhenDeleteAsync() + { + // Given + const int id = 1; + CalendarEvent entity = CreateRandomCalendarEvent(); + + calendarEventProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { entity }.AsQueryable()); + + calendarEventProcessingServiceMock.Setup(expression: x => x.DeleteAsync(calendarEventId: id)) + .Returns(value: ValueTask.CompletedTask); + + calendarEventEventProcessingServiceMock + .Setup(expression: x => x.RaiseCalendarEventDeleteEventAsync(entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + await orchestrationService.DeleteAsync(calendarEventId: id); + + // Then + calendarEventProcessingServiceMock.Verify( + expression: service => service.GetAll(ignoreFilters: true), + times: Times.Once); + calendarEventProcessingServiceMock.Verify(expression: x => x.DeleteAsync(calendarEventId: id), times: Times.Once); + calendarEventEventProcessingServiceMock.Verify(expression: x => x.RaiseCalendarEventDeleteEventAsync(entity: entity), times: Times.Once); + } + + [Fact] + public async Task ShouldIgnoreMissingCalendarEventWhenDeleteAsync() + { + // Given + calendarEventProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: Array.Empty().AsQueryable()); + + // When + await orchestrationService.DeleteAsync(calendarEventId: 1); + + // Then + calendarEventProcessingServiceMock.VerifyAll(); + calendarEventEventProcessingServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.Exceptions.cs new file mode 100644 index 0000000..83f50d2 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.Exceptions.cs @@ -0,0 +1,90 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarEventOrchestrationServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetAllFailure(Exception exception, Type expectedType) + { + // Given + calendarEventProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: false)) + .Throws(exception: exception); + + // When + Action action = () => orchestrationService.GetAll(); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddCalendarEventAsyncFailure(Exception exception, Type expectedType) + { + // Given + CalendarEvent item = CreateRandomCalendarEvent(); + + calendarEventProcessingServiceMock + .Setup(expression: service => service.AddCalendarEventAsync( + newEntity: item)) + .Throws(exception: exception); + + // When + Func action = async () => await orchestrationService + .AddCalendarEventAsync(newEntity: item); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteAllCalendarEventAsyncFailure(Exception exception, Type expectedType) + { + // Given + CalendarEvent[] items = [CreateRandomCalendarEvent()]; + + calendarEventProcessingServiceMock + .Setup(expression: service => service.DeleteAllCalendarEventAsync( + deletedItems: items)) + .Throws(exception: exception); + + // When + Func action = async () => await orchestrationService + .DeleteAllCalendarEventAsync(deletedItems: items); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.Get.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.Get.cs new file mode 100644 index 0000000..18e1bd7 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.Get.cs @@ -0,0 +1,40 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarEventOrchestrationServiceTests +{ + [Fact] + public void ShouldReturnProcessingResultWhenGet() + { + // Given + const int id = 1; + CalendarEvent entity = CreateRandomCalendarEvent(); + + calendarEventProcessingServiceMock.Setup(expression: x => x.Get(calendarEventId: id)) + .Returns(value: entity); + + // When + CalendarEvent result = orchestrationService.Get(calendarEventId: id); + + // Then + result.Should() + .BeSameAs(expected: entity); + + calendarEventProcessingServiceMock.Verify(expression: x => x.Get(calendarEventId: id), times: Times.Once); + calendarEventProcessingServiceMock.VerifyNoOtherCalls(); + calendarEventEventProcessingServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.GetAll.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.GetAll.cs new file mode 100644 index 0000000..abcec35 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.GetAll.cs @@ -0,0 +1,39 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarEventOrchestrationServiceTests +{ + [Fact] + public void ShouldReturnProcessingResultsWhenGetAll() + { + // Given + IQueryable entities = new[] { CreateRandomCalendarEvent() }.AsQueryable(); + + calendarEventProcessingServiceMock.Setup(expression: x => x.GetAll(ignoreFilters: true)) + .Returns(value: entities); + + // When + IQueryable result = orchestrationService.GetAll(ignoreFilters: true); + + // Then + result.Should() + .BeSameAs(expected: entities); + + calendarEventProcessingServiceMock.Verify(expression: x => x.GetAll(ignoreFilters: true), times: Times.Once); + calendarEventProcessingServiceMock.VerifyNoOtherCalls(); + calendarEventEventProcessingServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.UpdateAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.UpdateAsync.cs new file mode 100644 index 0000000..33d8b3a --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.UpdateAsync.cs @@ -0,0 +1,42 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarEventOrchestrationServiceTests +{ + [Fact] + public async Task ShouldCallProcessingThenRaiseUpdateEventAsyncWhenUpdateAsync() + { + // Given + CalendarEvent entity = CreateRandomCalendarEvent(); + + calendarEventProcessingServiceMock.Setup(expression: x => x.UpdateCalendarEventAsync(updatedEntity: entity)) + .ReturnsAsync(value: entity); + + calendarEventEventProcessingServiceMock + .Setup(expression: x => x.RaiseCalendarEventUpdateEventAsync(entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + CalendarEvent result = await orchestrationService.UpdateCalendarEventAsync(updatedEntity: entity); + + // Then + result.Should() + .BeSameAs(expected: entity); + + calendarEventProcessingServiceMock.Verify(expression: x => x.UpdateCalendarEventAsync(updatedEntity: entity), times: Times.Once); + calendarEventEventProcessingServiceMock.Verify(expression: x => x.RaiseCalendarEventUpdateEventAsync(entity: entity), times: Times.Once); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.cs new file mode 100644 index 0000000..99e611a --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarEventOrchestrationServiceTests.cs @@ -0,0 +1,36 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Services.Orchestrations; +using cCoder.Workflow.Services.Processings; +using FizzWare.NBuilder; +using Moq; + + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarEventOrchestrationServiceTests +{ + private readonly Mock calendarEventProcessingServiceMock; + private readonly Mock calendarEventEventProcessingServiceMock; + private readonly CalendarEventOrchestrationService orchestrationService; + + public CalendarEventOrchestrationServiceTests() + { + calendarEventProcessingServiceMock = new Mock(behavior: MockBehavior.Strict); + calendarEventEventProcessingServiceMock = new Mock(behavior: MockBehavior.Strict); + orchestrationService = new CalendarEventOrchestrationService( + calendarEventProcessingServiceMock.Object, + calendarEventEventProcessingServiceMock.Object + ); + } + + private static CalendarEvent CreateRandomCalendarEvent() => + Builder.CreateNew() + .Build(); +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.AddAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.AddAsync.cs new file mode 100644 index 0000000..b6056a5 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.AddAsync.cs @@ -0,0 +1,55 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +#pragma warning disable STXFORMAT008, STXFORMAT009 +public partial class CalendarOrchestrationServiceTests +{ + [Fact] + public async Task ShouldCallProcessingThenRaiseAddEventAsyncWhenAddAsync() + { + // Given + Calendar entity = CreateRandomCalendar(); + + calendarProcessingServiceMock + .Setup(expression: service => service.AddCalendarAsync( + newEntity: entity)) + .ReturnsAsync(value: entity); + + eventServiceMock + .Setup(expression: service => service.RaiseCalendarAddEventAsync( + entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + Calendar result = await orchestrationService + .AddCalendarAsync(newEntity: entity); + + // Then + result.Should() + .BeSameAs(expected: entity); + + calendarProcessingServiceMock.Verify( + expression: service => service.AddCalendarAsync( + newEntity: entity), + times: Times.Once); + + eventServiceMock.Verify( + expression: service => service.RaiseCalendarAddEventAsync( + entity: entity), + times: Times.Once); + } + +} +#pragma warning restore STXFORMAT008, STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.AddOrUpdate.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.AddOrUpdate.cs new file mode 100644 index 0000000..dec14e3 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.AddOrUpdate.cs @@ -0,0 +1,40 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarOrchestrationServiceTests +{ + [Fact] + public async Task ShouldDelegateAddOrUpdateCalendarsAsync() + { + // Given + Calendar item = CreateRandomCalendar(); + Calendar[] items = [item]; + Result[] expected = [new() { Success = true, Item = item }]; + + calendarProcessingServiceMock + .Setup(expression: service => service.AddOrUpdateCalendar( + items: items)) + .Returns(value: ValueTask.FromResult>>( + result: expected)); + + // When + IEnumerable> actual = await orchestrationService + .AddOrUpdateCalendar(items: items); + + // Then + actual + .Should() + .BeSameAs(expected: expected); + + calendarProcessingServiceMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.DeleteAllAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.DeleteAllAsync.cs new file mode 100644 index 0000000..8596e30 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.DeleteAllAsync.cs @@ -0,0 +1,35 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarOrchestrationServiceTests +{ + [Fact] + public async Task ShouldDelegateToProcessingServiceWhenDeleteAllAsync() + { + // Given + Calendar[] entities = [CreateRandomCalendar()]; + + calendarProcessingServiceMock.Setup(expression: x => x.DeleteAllCalendarAsync(deletedItems: entities)) + .Returns(value: ValueTask.CompletedTask); + + // When + await orchestrationService.DeleteAllCalendarAsync(deletedItems: entities); + + // Then + calendarProcessingServiceMock.Verify(expression: x => x.DeleteAllCalendarAsync(deletedItems: entities), times: Times.Once); + calendarProcessingServiceMock.VerifyNoOtherCalls(); + calendarEventProcessingServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.DeleteAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.DeleteAsync.cs new file mode 100644 index 0000000..93b1c63 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.DeleteAsync.cs @@ -0,0 +1,78 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +#pragma warning disable STXFORMAT009 +public partial class CalendarOrchestrationServiceTests +{ + [Fact] + public async Task ShouldGetThenDeleteThenRaiseDeleteEventAsyncWhenDeleteAsync() + { + // Given + const int id = 1; + Calendar entity = CreateRandomCalendar(); + CalendarEvent calendarEvent = new() { CalendarId = entity.Id }; + + calendarProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { entity }.AsQueryable()); + + calendarProcessingServiceMock.Setup(expression: x => x.DeleteAsync(calendarId: id)) + .Returns(value: ValueTask.CompletedTask); + + calendarEventProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: false)) + .Returns(value: new[] { calendarEvent }.AsQueryable()); + + calendarEventProcessingServiceMock + .Setup(expression: service => service.DeleteAllCalendarEventAsync( + deletedItems: It.Is>( + match: items => items.Single() == calendarEvent))) + .Returns(value: ValueTask.CompletedTask); + + eventServiceMock + .Setup(expression: service => service.RaiseCalendarDeleteEventAsync( + entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + await orchestrationService.DeleteAsync(calendarId: id); + + // Then + calendarProcessingServiceMock.Verify( + expression: service => service.GetAll(ignoreFilters: true), + times: Times.Once); + calendarProcessingServiceMock.Verify(expression: x => x.DeleteAsync(calendarId: id), times: Times.Once); + calendarEventProcessingServiceMock.VerifyAll(); + eventServiceMock.VerifyAll(); + } + + [Fact] + public async Task ShouldIgnoreMissingCalendarWhenDeleteAsync() + { + // Given + calendarProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: Array.Empty().AsQueryable()); + + // When + await orchestrationService.DeleteAsync(calendarId: 1); + + // Then + calendarProcessingServiceMock.VerifyAll(); + calendarEventProcessingServiceMock.VerifyNoOtherCalls(); + eventServiceMock.VerifyNoOtherCalls(); + } + +} +#pragma warning restore STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.DeleteByAppId.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.DeleteByAppId.cs new file mode 100644 index 0000000..9850b0a --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.DeleteByAppId.cs @@ -0,0 +1,31 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarOrchestrationServiceTests +{ + [Fact] + public async Task ShouldDeleteCalendarEventsBeforeCalendarsByAppIdAsync() + { + // Given + calendarEventProcessingServiceMock + .Setup(expression: service => service.DeleteAllByAppIdAsync(appId: 7)) + .Returns(value: ValueTask.CompletedTask); + + calendarProcessingServiceMock + .Setup(expression: service => service.DeleteByAppIdAsync(appId: 7)) + .Returns(value: ValueTask.CompletedTask); + + // When + await orchestrationService.DeleteByAppIdAsync(appId: 7); + + // Then + calendarEventProcessingServiceMock.VerifyAll(); + calendarProcessingServiceMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.Exceptions.cs new file mode 100644 index 0000000..3de6c70 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.Exceptions.cs @@ -0,0 +1,90 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarOrchestrationServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetAllFailure(Exception exception, Type expectedType) + { + // Given + calendarProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: false)) + .Throws(exception: exception); + + // When + Action action = () => orchestrationService.GetAll(); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddCalendarAsyncFailure(Exception exception, Type expectedType) + { + // Given + Calendar item = CreateRandomCalendar(); + + calendarProcessingServiceMock + .Setup(expression: service => service.AddCalendarAsync( + newEntity: item)) + .Throws(exception: exception); + + // When + Func action = async () => await orchestrationService + .AddCalendarAsync(newEntity: item); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteAllCalendarAsyncFailure(Exception exception, Type expectedType) + { + // Given + Calendar[] items = [CreateRandomCalendar()]; + + calendarProcessingServiceMock + .Setup(expression: service => service.DeleteAllCalendarAsync( + deletedItems: items)) + .Throws(exception: exception); + + // When + Func action = async () => await orchestrationService + .DeleteAllCalendarAsync(deletedItems: items); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.Get.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.Get.cs new file mode 100644 index 0000000..9396f7e --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.Get.cs @@ -0,0 +1,40 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarOrchestrationServiceTests +{ + [Fact] + public void ShouldReturnProcessingResultWhenGet() + { + // Given + const int id = 1; + Calendar entity = CreateRandomCalendar(); + + calendarProcessingServiceMock.Setup(expression: x => x.Get(calendarId: id)) + .Returns(value: entity); + + // When + Calendar result = orchestrationService.Get(calendarId: id); + + // Then + result.Should() + .BeSameAs(expected: entity); + + calendarProcessingServiceMock.Verify(expression: x => x.Get(calendarId: id), times: Times.Once); + calendarProcessingServiceMock.VerifyNoOtherCalls(); + calendarEventProcessingServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.GetAll.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.GetAll.cs new file mode 100644 index 0000000..c3742ca --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.GetAll.cs @@ -0,0 +1,39 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarOrchestrationServiceTests +{ + [Fact] + public void ShouldReturnProcessingResultsWhenGetAll() + { + // Given + IQueryable entities = new[] { CreateRandomCalendar() }.AsQueryable(); + + calendarProcessingServiceMock.Setup(expression: x => x.GetAll(ignoreFilters: true)) + .Returns(value: entities); + + // When + IQueryable result = orchestrationService.GetAll(ignoreFilters: true); + + // Then + result.Should() + .BeSameAs(expected: entities); + + calendarProcessingServiceMock.Verify(expression: x => x.GetAll(ignoreFilters: true), times: Times.Once); + calendarProcessingServiceMock.VerifyNoOtherCalls(); + calendarEventProcessingServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.UpdateAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.UpdateAsync.cs new file mode 100644 index 0000000..3f5ea93 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.UpdateAsync.cs @@ -0,0 +1,42 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarOrchestrationServiceTests +{ + [Fact] + public async Task ShouldCallProcessingThenRaiseUpdateEventAsyncWhenUpdateAsync() + { + // Given + Calendar entity = CreateRandomCalendar(); + + calendarProcessingServiceMock.Setup(expression: x => x.UpdateCalendarAsync(updatedEntity: entity)) + .ReturnsAsync(value: entity); + + eventServiceMock + .Setup(expression: x => x.RaiseCalendarUpdateEventAsync(entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + Calendar result = await orchestrationService.UpdateCalendarAsync(updatedEntity: entity); + + // Then + result.Should() + .BeSameAs(expected: entity); + + calendarProcessingServiceMock.Verify(expression: x => x.UpdateCalendarAsync(updatedEntity: entity), times: Times.Once); + eventServiceMock.Verify(expression: x => x.RaiseCalendarUpdateEventAsync(entity: entity), times: Times.Once); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.cs new file mode 100644 index 0000000..60467c3 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/CalendarOrchestrationServiceTests.cs @@ -0,0 +1,38 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Services.Orchestrations; +using cCoder.Workflow.Services.Processings; +using FizzWare.NBuilder; +using Moq; + + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class CalendarOrchestrationServiceTests +{ + private readonly Mock calendarProcessingServiceMock; + private readonly Mock calendarEventProcessingServiceMock; + private readonly Mock eventServiceMock; + private readonly CalendarOrchestrationService orchestrationService; + + public CalendarOrchestrationServiceTests() + { + calendarProcessingServiceMock = new Mock(behavior: MockBehavior.Strict); + calendarEventProcessingServiceMock = new Mock(behavior: MockBehavior.Strict); + eventServiceMock = new Mock(behavior: MockBehavior.Strict); + orchestrationService = new CalendarOrchestrationService( + processingService: calendarProcessingServiceMock.Object, + calendarEventProcessingService: calendarEventProcessingServiceMock.Object, + eventService: eventServiceMock.Object); + } + + private static Calendar CreateRandomCalendar() => + Builder.CreateNew() + .Build(); +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/FlowDefinitionOrchestrationServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/FlowDefinitionOrchestrationServiceTests.Exceptions.cs new file mode 100644 index 0000000..ac78ec9 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/FlowDefinitionOrchestrationServiceTests.Exceptions.cs @@ -0,0 +1,91 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class FlowDefinitionOrchestrationServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetAllFailure(Exception exception, Type expectedType) + { + // Given + flowDefinitionProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: false)) + .Throws(exception: exception); + + // When + Action action = () => orchestrationService.GetAll(); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddFlowDefinitionAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + FlowDefinition flowDefinition = CreateRandomFlowDefinition(); + + flowDefinitionProcessingServiceMock + .Setup(expression: service => service.AddFlowDefinitionAsync( + newEntity: flowDefinition)) + .Throws(exception: exception); + + // When + Func action = async () => await orchestrationService + .AddFlowDefinitionAsync(newEntity: flowDefinition); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteByAppIdAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + flowDefinitionProcessingServiceMock + .Setup(expression: service => service.DeleteByAppIdAsync(appId: 7)) + .Throws(exception: exception); + + // When + Func action = async () => await orchestrationService + .DeleteByAppIdAsync(appId: 7); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/FlowInstanceDataOrchestrationServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/FlowInstanceDataOrchestrationServiceTests.Exceptions.cs new file mode 100644 index 0000000..87ce4b7 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/FlowInstanceDataOrchestrationServiceTests.Exceptions.cs @@ -0,0 +1,94 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class FlowInstanceDataOrchestrationServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetAllFailure(Exception exception, Type expectedType) + { + // Given + flowInstanceDataProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: false)) + .Throws(exception: exception); + + // When + Action action = () => orchestrationService.GetAll(); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddQueuedFlowInstanceDataAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + FlowInstanceData item = CreateRandomFlowInstanceData(); + + flowInstanceDataProcessingServiceMock + .Setup(expression: service => service.AddQueuedFlowInstanceDataAsync( + newEntity: item)) + .Throws(exception: exception); + + // When + Func action = async () => await orchestrationService + .AddQueuedFlowInstanceDataAsync(newEntity: item); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteAllFlowInstanceDataAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + FlowInstanceData[] items = [CreateRandomFlowInstanceData()]; + + flowInstanceDataProcessingServiceMock + .Setup(expression: service => service.DeleteAllFlowInstanceDataAsync( + deletedItems: items)) + .Throws(exception: exception); + + // When + Func action = async () => await orchestrationService + .DeleteAllFlowInstanceDataAsync(deletedItems: items); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/FlowQueueOrchestrationServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/FlowQueueOrchestrationServiceTests.Exceptions.cs new file mode 100644 index 0000000..c2bd3c3 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/FlowQueueOrchestrationServiceTests.Exceptions.cs @@ -0,0 +1,52 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 + +using System.ComponentModel.DataAnnotations; +using System.Security; +using cCoder.Workflow.Models.Exceptions; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class FlowQueueOrchestrationServiceTests +{ + public static TheoryData QueueDependencyExceptions => new() + { + { new WorkflowValidationException(innerException: new Exception()), typeof(WorkflowValidationException) }, + { new WorkflowDependencyException(innerException: new Exception()), typeof(WorkflowDependencyException) }, + { new ValidationException(), typeof(WorkflowValidationException) }, + { new InvalidOperationException(), typeof(WorkflowDependencyException) }, + { new SecurityException(), typeof(SecurityException) }, + { new Exception(), typeof(WorkflowServiceException) } + }; + + [Theory] + [MemberData(nameof(QueueDependencyExceptions))] + public async Task QueueFlowDefinitionAsyncShouldMapDependencyExceptions( + Exception dependencyException, + Type expectedExceptionType) + { + Guid flowDefinitionId = Guid.NewGuid(); + string asUserId = Guid.NewGuid().ToString(); + string args = "{}"; + + flowDefinitionProcessingServiceMock.Setup(expression: dependency => + dependency.GetAll(true)) + .Throws(exception: dependencyException); + + Func action = async () => await orchestrationService.QueueFlowDefinitionAsync( + flowDefinitionId, + asUserId, + args); + + Exception exception = (await action.Should().ThrowAsync()).Which; + exception.Should().BeOfType(expectedExceptionType); + } +} + +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/FlowQueueOrchestrationServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/FlowQueueOrchestrationServiceTests.cs index f268abd..e9644fe 100644 --- a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/FlowQueueOrchestrationServiceTests.cs +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/FlowQueueOrchestrationServiceTests.cs @@ -2,6 +2,8 @@ // Copyright (c) Paul.Ward@ccoder.co.uk // --------------------------------------------------------------- +#pragma warning disable STXFORMAT005, STXFORMAT009, STXTEST005 + using cCoder.Data.Models.Workflow; using cCoder.Workflow.Activities; using cCoder.Workflow.Activities.Models; @@ -51,4 +53,6 @@ private static Flow CreateFlow() => { Activities = [new Start()] }; -} \ No newline at end of file +} + +#pragma warning restore STXFORMAT005, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/ScheduledTaskOrchestrationServiceTests.Delegation.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/ScheduledTaskOrchestrationServiceTests.Delegation.cs new file mode 100644 index 0000000..1943bd1 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/ScheduledTaskOrchestrationServiceTests.Delegation.cs @@ -0,0 +1,67 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 +public partial class ScheduledTaskOrchestrationServiceTests +{ + [Fact] + public async Task ShouldDelegateScheduledTaskOperationsAsync() + { + // Given + ScheduledTask task = CreateScheduledTask(); + IQueryable tasks = new[] { task }.AsQueryable(); + Result[] results = [new() { Success = true, Item = task }]; + + processingServiceMock.Setup(expression: found => found.Get(task.Id)).Returns(task); + processingServiceMock.Setup(expression: found => found.GetAll(true)).Returns(tasks); + processingServiceMock.Setup(expression: found => found.AddScheduledTaskAsync(task)) + .ReturnsAsync(task); + processingServiceMock.Setup(expression: found => found.UpdateScheduledTaskAsync(task)) + .ReturnsAsync(task); + processingServiceMock.Setup(expression: found => found.DeleteByAppIdAsync(7)) + .Returns(value: ValueTask.CompletedTask); + processingServiceMock.Setup(expression: found => found.AddOrUpdateScheduledTask(tasks)) + .ReturnsAsync(results); + processingServiceMock.Setup(expression: found => found.DeleteAllScheduledTaskAsync(tasks)) + .Returns(value: ValueTask.CompletedTask); + processingServiceMock.Setup(expression: found => found.ExecuteScheduledTaskAsync(task.Id, false)) + .ReturnsAsync(task); + + eventServiceMock.Setup(expression: found => found.RaiseScheduledTaskAddEventAsync(task)) + .Returns(value: ValueTask.CompletedTask); + eventServiceMock.Setup(expression: found => found.RaiseScheduledTaskUpdateEventAsync(task)) + .Returns(value: ValueTask.CompletedTask); + eventServiceMock.Setup(expression: found => found.RaiseScheduledTaskExecuteEventAsync(task)) + .Returns(value: ValueTask.CompletedTask); + + // When + ScheduledTask actualGet = service.Get(scheduledTaskId: task.Id); + IQueryable actualAll = service.GetAll(ignoreFilters: true); + ScheduledTask actualAdd = await service.AddScheduledTaskAsync(newEntity: task); + ScheduledTask actualUpdate = await service.UpdateScheduledTaskAsync(updatedEntity: task); + await service.DeleteByAppIdAsync(appId: 7); + IEnumerable> actualResults = await service + .AddOrUpdateScheduledTask(items: tasks); + await service.DeleteAllScheduledTaskAsync(deletedItems: tasks); + await service.ExecuteAsync(scheduledTaskId: task.Id, incrementNextExecution: false); + + // Then + actualGet.Should().BeSameAs(expected: task); + actualAll.Should().BeSameAs(expected: tasks); + actualAdd.Should().BeSameAs(expected: task); + actualUpdate.Should().BeSameAs(expected: task); + actualResults.Should().BeSameAs(expected: results); + processingServiceMock.VerifyAll(); + eventServiceMock.VerifyAll(); + } +} +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/ScheduledTaskOrchestrationServiceTests.Delete.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/ScheduledTaskOrchestrationServiceTests.Delete.cs new file mode 100644 index 0000000..c9fc056 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/ScheduledTaskOrchestrationServiceTests.Delete.cs @@ -0,0 +1,50 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 +public partial class ScheduledTaskOrchestrationServiceTests +{ + [Fact] + public async Task ShouldRaiseDeleteEventBeforeDeletingScheduledTaskAsync() + { + // Given + ScheduledTask task = CreateScheduledTask(); + + processingServiceMock.Setup(expression: found => found.GetAll(true)) + .Returns(new[] { task }.AsQueryable()); + eventServiceMock.Setup(expression: found => found.RaiseScheduledTaskDeleteEventAsync(task)) + .Returns(value: ValueTask.CompletedTask); + processingServiceMock.Setup(expression: found => found.DeleteAsync(task.Id)) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.DeleteAsync(scheduledTaskId: task.Id); + + // Then + processingServiceMock.VerifyAll(); + eventServiceMock.VerifyAll(); + } + + [Fact] + public async Task ShouldIgnoreMissingScheduledTaskWhenDeletingAsync() + { + // Given + processingServiceMock.Setup(expression: found => found.GetAll(true)) + .Returns(Array.Empty().AsQueryable()); + + // When + await service.DeleteAsync(scheduledTaskId: 1); + + // Then + processingServiceMock.VerifyAll(); + eventServiceMock.VerifyNoOtherCalls(); + } +} +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/ScheduledTaskOrchestrationServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/ScheduledTaskOrchestrationServiceTests.Exceptions.cs new file mode 100644 index 0000000..3ff8635 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/ScheduledTaskOrchestrationServiceTests.Exceptions.cs @@ -0,0 +1,57 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 +public partial class ScheduledTaskOrchestrationServiceTests +{ + public static TheoryData ExceptionMappings => + Foundations.FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetFailure(Exception exception, Type expectedType) + { + processingServiceMock.Setup(expression: found => found.Get(1)) + .Throws(exception); + + Action action = () => service.Get(scheduledTaskId: 1); + + action.Should().Throw().Which.Should().BeOfType(expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteFailureAsync(Exception exception, Type expectedType) + { + processingServiceMock.Setup(expression: found => found.GetAll(true)) + .Throws(exception); + + Func action = async () => await service.DeleteAsync(scheduledTaskId: 1); + + Exception thrown = (await action.Should().ThrowAsync()).Which; + thrown.Should().BeOfType(expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddFailureAsync(Exception exception, Type expectedType) + { + ScheduledTask task = CreateScheduledTask(); + processingServiceMock.Setup(expression: found => found.AddScheduledTaskAsync(task)) + .Throws(exception); + + Func action = async () => await service.AddScheduledTaskAsync(task); + + Exception thrown = (await action.Should().ThrowAsync()).Which; + thrown.Should().BeOfType(expectedType); + } +} +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/ScheduledTaskOrchestrationServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/ScheduledTaskOrchestrationServiceTests.cs new file mode 100644 index 0000000..d4869d5 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/ScheduledTaskOrchestrationServiceTests.cs @@ -0,0 +1,34 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Services.Orchestrations; +using cCoder.Workflow.Services.Processings; +using Moq; + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 +public partial class ScheduledTaskOrchestrationServiceTests +{ + private readonly Mock processingServiceMock = new(); + private readonly Mock eventServiceMock = new(); + private readonly ScheduledTaskOrchestrationService service; + + public ScheduledTaskOrchestrationServiceTests() + { + service = new ScheduledTaskOrchestrationService( + processingService: processingServiceMock.Object, + eventService: eventServiceMock.Object); + } + + private static ScheduledTask CreateScheduledTask() => + new() + { + Id = Random.Shared.Next(minValue: 1, maxValue: int.MaxValue), + AppId = 7, + Name = "Task" + }; +} +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009, STXTEST005 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/TaskRunnerOrchestrationServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/TaskRunnerOrchestrationServiceTests.Exceptions.cs new file mode 100644 index 0000000..35c0d99 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/TaskRunnerOrchestrationServiceTests.Exceptions.cs @@ -0,0 +1,42 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Services.Orchestrations; + +public partial class TaskRunnerOrchestrationServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapRunFailureAsync( + Exception exception, + Type expectedType) + { + // Given + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Throws(exception: exception); + + // When + Func action = async () => await taskRunnerOrchestrationService + .RunAsync(); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/TaskRunnerOrchestrationServiceTests.RunAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/TaskRunnerOrchestrationServiceTests.RunAsync.cs new file mode 100644 index 0000000..23e1d21 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/TaskRunnerOrchestrationServiceTests.RunAsync.cs @@ -0,0 +1,206 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Services.Orchestrations; + +#pragma warning disable STXFORMAT005, STXFORMAT009 +public partial class TaskRunnerOrchestrationServiceTests +{ + [Fact] + public async Task ShouldExecuteDueScheduledTaskAsync() + { + // Given + ScheduledTask task = CreateDueScheduledTask(); + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { task }.AsQueryable()); + + calendarEventProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: Array.Empty().AsQueryable()); + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.LogScheduledTasksRunningAsync( + scheduledTaskCount: 1)) + .Returns(value: ValueTask.CompletedTask); + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.LogScheduledTaskRunningAsync( + scheduledTask: task)) + .Returns(value: ValueTask.CompletedTask); + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.ExecuteScheduledTaskAsync( + scheduledTaskId: task.Id, + incrementNextExecution: true)) + .Returns(value: ValueTask.FromResult(result: task)); + + scheduledTaskEventProcessingServiceMock + .Setup(expression: service => service + .RaiseScheduledTaskExecuteEventAsync(entity: task)) + .Returns(value: ValueTask.CompletedTask); + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.LogScheduledTaskCompleteAsync( + scheduledTask: task)) + .Returns(value: ValueTask.CompletedTask); + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.LogScheduledTasksExecutedAsync( + scheduledTaskCount: 1)) + .Returns(value: ValueTask.CompletedTask); + + // When + await taskRunnerOrchestrationService.RunAsync(); + + // Then + scheduledTaskProcessingServiceMock.VerifyAll(); + scheduledTaskEventProcessingServiceMock.VerifyAll(); + } + + [Fact] + public async Task ShouldSkipScheduledTaskForMatchingCalendarEventAsync() + { + // Given + ScheduledTask task = CreateDueScheduledTask(); + task.ExcludedEventsCalendarId = 3; + task.ExcludedEventsName = "Holiday,Shutdown"; + + CalendarEvent calendarEvent = new() + { + CalendarId = 3, + Name = "Holiday", + Start = DateTimeOffset.Now.Date + }; + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { task }.AsQueryable()); + + calendarEventProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { calendarEvent }.AsQueryable()); + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.LogScheduledTasksRunningAsync(1)) + .Returns(value: ValueTask.CompletedTask); + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.LogScheduledTaskRunningAsync(task)) + .Returns(value: ValueTask.CompletedTask); + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.LogScheduledTaskSkippedAsync(task)) + .Returns(value: ValueTask.CompletedTask); + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.LogScheduledTaskCompleteAsync(task)) + .Returns(value: ValueTask.CompletedTask); + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.LogScheduledTasksExecutedAsync(1)) + .Returns(value: ValueTask.CompletedTask); + + // When + await taskRunnerOrchestrationService.RunAsync(); + + // Then + scheduledTaskProcessingServiceMock.VerifyAll(); + scheduledTaskEventProcessingServiceMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldRejectMissingUpdatedScheduledTaskAsync() + { + // Given + ScheduledTask task = CreateDueScheduledTask(); + SetupDueTask(task: task); + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.ExecuteScheduledTaskAsync( + scheduledTaskId: task.Id, + incrementNextExecution: true)) + .Returns(value: ValueTask.FromResult(result: null)); + + // When + Func action = async () => await taskRunnerOrchestrationService.RunAsync(); + + // Then + await action.Should().ThrowAsync(); + } + + [Fact] + public async Task ShouldRejectScheduledTaskWithMissingUserAsync() + { + // Given + ScheduledTask task = CreateDueScheduledTask(); + task.ExecuteAsUser = null; + SetupDueTask(task: task); + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.ExecuteScheduledTaskAsync( + scheduledTaskId: task.Id, + incrementNextExecution: true)) + .Returns(value: ValueTask.FromResult(result: task)); + + // When + Func action = async () => await taskRunnerOrchestrationService.RunAsync(); + + // Then + await action.Should().ThrowAsync(); + } + + [Fact] + public async Task ShouldSkipContinuousRunnerDuringMigrationAsync() + { + // Given + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.IsScheduledTaskMigrationActive()) + .Returns(value: true); + + // When + await taskRunnerOrchestrationService.RunContinuouslyAsync(); + + // Then + scheduledTaskProcessingServiceMock.VerifyAll(); + } + + private void SetupDueTask(ScheduledTask task) + { + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { task }.AsQueryable()); + + calendarEventProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: Array.Empty().AsQueryable()); + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.LogScheduledTasksRunningAsync(1)) + .Returns(value: ValueTask.CompletedTask); + + scheduledTaskProcessingServiceMock + .Setup(expression: service => service.LogScheduledTaskRunningAsync(task)) + .Returns(value: ValueTask.CompletedTask); + } + + private static ScheduledTask CreateDueScheduledTask() => + new() + { + Id = 1, + NextExecution = DateTimeOffset.UtcNow.AddMinutes(minutes: -1), + ScheduleInTicks = TimeSpan.FromMinutes(value: 5).Ticks, + ExecuteAsUser = new User(), + Flow = new FlowDefinition() + }; +} +#pragma warning restore STXFORMAT005, STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/TaskRunnerOrchestrationServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/TaskRunnerOrchestrationServiceTests.cs index 0fb6b84..6ef2e3a 100644 --- a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/TaskRunnerOrchestrationServiceTests.cs +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/TaskRunnerOrchestrationServiceTests.cs @@ -7,6 +7,7 @@ namespace cCoder.Workflow.Services.Orchestrations; +#pragma warning disable STXFORMAT009 public partial class TaskRunnerOrchestrationServiceTests { private readonly Mock calendarEventProcessingServiceMock; @@ -25,4 +26,5 @@ public TaskRunnerOrchestrationServiceTests() calendarEventProcessingService: calendarEventProcessingServiceMock.Object, scheduledTaskEventProcessingService: scheduledTaskEventProcessingServiceMock.Object); } -} \ No newline at end of file +} +#pragma warning restore STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/WorkflowEventOrchestrationServiceTests.AddAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/WorkflowEventOrchestrationServiceTests.AddAsync.cs index cdc41be..7353256 100644 --- a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/WorkflowEventOrchestrationServiceTests.AddAsync.cs +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/WorkflowEventOrchestrationServiceTests.AddAsync.cs @@ -13,6 +13,7 @@ namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; +#pragma warning disable STXFORMAT005, STXFORMAT009 public partial class WorkflowEventOrchestrationServiceTests { [Fact] @@ -39,4 +40,5 @@ public async Task ShouldCallProcessingThenRaiseAddEventAsyncWhenAddAsync() workflowEventEventProcessingServiceMock.Verify(expression: x => x.RaiseWorkflowEventAddEventAsync(entity: entity), times: Times.Once); } -} \ No newline at end of file +} +#pragma warning restore STXFORMAT005, STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/WorkflowEventOrchestrationServiceTests.Dispatch.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/WorkflowEventOrchestrationServiceTests.Dispatch.cs new file mode 100644 index 0000000..bced6e8 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/WorkflowEventOrchestrationServiceTests.Dispatch.cs @@ -0,0 +1,121 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +#pragma warning disable STXFORMAT005, STXFORMAT009 +public partial class WorkflowEventOrchestrationServiceTests +{ + [Fact] + public void ShouldPrepareWorkflowEventDispatch() + { + // Given + object payload = new { AppId = 7 }; + (int? AppId, string EventContext) expected = (7, "event"); + + workflowEventProcessingServiceMock + .Setup(expression: service => service.PrepareWorkflowEventDispatch( + payload: payload, + eventName: "event", + appIdOverride: 7)) + .Returns(value: expected); + + // When + (int? AppId, string EventContext) actual = orchestrationService + .PrepareWorkflowEventDispatch(payload, "event", 7); + + // Then + actual.Should().Be(expected: expected); + workflowEventProcessingServiceMock.VerifyAll(); + } + + [Fact] + public void ShouldSerializeWorkflowEventPayload() + { + // Given + object payload = new { Value = 1 }; + + workflowEventProcessingServiceMock + .Setup(expression: service => service.SerializeWorkflowEventPayload( + payload: payload)) + .Returns(value: "serialized"); + + // When + string actual = orchestrationService.SerializeWorkflowEventPayload( + payload: payload); + + // Then + actual.Should().Be(expected: "serialized"); + workflowEventProcessingServiceMock.VerifyAll(); + } + + [Fact] + public async Task ShouldGetWorkflowEventSubscriptionsAsync() + { + // Given + WorkflowEvent[] expected = [CreateRandomWorkflowEvent()]; + + workflowEventProcessingServiceMock + .Setup(expression: service => service.GetSubscriptionsAsync( + appId: 7, + eventContext: "event")) + .ReturnsAsync(value: expected); + + // When + WorkflowEvent[] actual = await orchestrationService + .GetWorkflowEventSubscriptionsAsync(appId: 7, eventContext: "event"); + + // Then + actual.Should().BeSameAs(expected: expected); + workflowEventProcessingServiceMock.VerifyAll(); + } + + [Fact] + public async Task ShouldLogWorkflowEventQueueFailureAsync() + { + // Given + WorkflowEvent item = CreateRandomWorkflowEvent(); + Exception exception = new(message: "failed"); + + workflowEventProcessingServiceMock + .Setup(expression: service => service.LogWorkflowEventQueueFailureAsync( + workflowEvent: item, + exception: exception)) + .Returns(value: ValueTask.CompletedTask); + + // When + await orchestrationService.LogWorkflowEventQueueFailureAsync( + workflowEvent: item, + exception: exception); + + // Then + workflowEventProcessingServiceMock.VerifyAll(); + } + + [Fact] + public async Task ShouldDelegateAddOrUpdateWorkflowEventsAsync() + { + // Given + WorkflowEvent[] items = [CreateRandomWorkflowEvent()]; + Result[] expected = [new() { Success = true, Item = items[0] }]; + + workflowEventProcessingServiceMock + .Setup(expression: service => service.AddOrUpdateWorkflowEvent(items)) + .ReturnsAsync(value: expected); + + // When + IEnumerable> actual = await orchestrationService + .AddOrUpdateWorkflowEvent(items: items); + + // Then + actual.Should().BeSameAs(expected: expected); + workflowEventProcessingServiceMock.VerifyAll(); + } +} +#pragma warning restore STXFORMAT005, STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Orchestrations/WorkflowEventOrchestrationServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/WorkflowEventOrchestrationServiceTests.Exceptions.cs new file mode 100644 index 0000000..6603df5 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Orchestrations/WorkflowEventOrchestrationServiceTests.Exceptions.cs @@ -0,0 +1,90 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Orchestrations; + +public partial class WorkflowEventOrchestrationServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetAllFailure(Exception exception, Type expectedType) + { + // Given + workflowEventProcessingServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: false)) + .Throws(exception: exception); + + // When + Action action = () => orchestrationService.GetAll(); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddWorkflowEventAsyncFailure(Exception exception, Type expectedType) + { + // Given + WorkflowEvent item = CreateRandomWorkflowEvent(); + + workflowEventProcessingServiceMock + .Setup(expression: service => service.AddWorkflowEventAsync( + newEntity: item)) + .Throws(exception: exception); + + // When + Func action = async () => await orchestrationService + .AddWorkflowEventAsync(newEntity: item); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteAllWorkflowEventAsyncFailure(Exception exception, Type expectedType) + { + // Given + WorkflowEvent[] items = [CreateRandomWorkflowEvent()]; + + workflowEventProcessingServiceMock + .Setup(expression: service => service.DeleteAllWorkflowEventAsync( + deletedItems: items)) + .Throws(exception: exception); + + // When + Func action = async () => await orchestrationService + .DeleteAllWorkflowEventAsync(deletedItems: items); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEntityEventProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEntityEventProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..c8d4b91 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEntityEventProcessingServiceTests.Exceptions.cs @@ -0,0 +1,45 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class CalendarEntityEventProcessingServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapRaiseCalendarAddEventAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + Calendar entity = CreateRandomCalendar(); + + calendarEntityEventServiceMock + .Setup(expression: dependency => dependency + .RaiseCalendarAddEventAsync(entity: entity)) + .Throws(exception: exception); + + // When + Func action = async () => await service + .RaiseCalendarAddEventAsync(entity: entity); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEntityEventProcessingServiceTests.RaiseCalendarAddEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEntityEventProcessingServiceTests.RaiseCalendarAddEventAsync.cs new file mode 100644 index 0000000..e074b68 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEntityEventProcessingServiceTests.RaiseCalendarAddEventAsync.cs @@ -0,0 +1,35 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class CalendarEntityEventProcessingServiceTests +{ + [Fact] + public async Task ShouldPassThroughCallWhenRaiseCalendarAddEventAsync() + { + // Given + Calendar entity = CreateRandomCalendar(); + + calendarEntityEventServiceMock + .Setup(expression: x => x.RaiseCalendarAddEventAsync(entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseCalendarAddEventAsync(entity: entity); + + // Then + calendarEntityEventServiceMock.Verify(expression: x => x.RaiseCalendarAddEventAsync(entity: entity), times: Times.Once); + calendarEntityEventServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEntityEventProcessingServiceTests.RaiseCalendarDeleteEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEntityEventProcessingServiceTests.RaiseCalendarDeleteEventAsync.cs new file mode 100644 index 0000000..97a4ff6 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEntityEventProcessingServiceTests.RaiseCalendarDeleteEventAsync.cs @@ -0,0 +1,35 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class CalendarEntityEventProcessingServiceTests +{ + [Fact] + public async Task ShouldPassThroughCallWhenRaiseCalendarDeleteEventAsync() + { + // Given + Calendar entity = CreateRandomCalendar(); + + calendarEntityEventServiceMock + .Setup(expression: x => x.RaiseCalendarDeleteEventAsync(entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseCalendarDeleteEventAsync(entity: entity); + + // Then + calendarEntityEventServiceMock.Verify(expression: x => x.RaiseCalendarDeleteEventAsync(entity: entity), times: Times.Once); + calendarEntityEventServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEntityEventProcessingServiceTests.RaiseCalendarUpdateEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEntityEventProcessingServiceTests.RaiseCalendarUpdateEventAsync.cs new file mode 100644 index 0000000..3d6da91 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEntityEventProcessingServiceTests.RaiseCalendarUpdateEventAsync.cs @@ -0,0 +1,35 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class CalendarEntityEventProcessingServiceTests +{ + [Fact] + public async Task ShouldPassThroughCallWhenRaiseCalendarUpdateEventAsync() + { + // Given + Calendar entity = CreateRandomCalendar(); + + calendarEntityEventServiceMock + .Setup(expression: x => x.RaiseCalendarUpdateEventAsync(entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseCalendarUpdateEventAsync(entity: entity); + + // Then + calendarEntityEventServiceMock.Verify(expression: x => x.RaiseCalendarUpdateEventAsync(entity: entity), times: Times.Once); + calendarEntityEventServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEntityEventProcessingServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEntityEventProcessingServiceTests.cs new file mode 100644 index 0000000..03598c0 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEntityEventProcessingServiceTests.cs @@ -0,0 +1,31 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Services.Foundations.Events; +using cCoder.Workflow.Services.Processings; +using FizzWare.NBuilder; +using Moq; + + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class CalendarEntityEventProcessingServiceTests +{ + private readonly Mock calendarEntityEventServiceMock; + private readonly CalendarEntityEventProcessingService service; + + public CalendarEntityEventProcessingServiceTests() + { + calendarEntityEventServiceMock = new Mock(behavior: MockBehavior.Strict); + service = new CalendarEntityEventProcessingService(calendarEntityEventServiceMock.Object); + } + + private static Calendar CreateRandomCalendar() => + Builder.CreateNew() + .Build(); +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventEventProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventEventProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..c2b0cdd --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventEventProcessingServiceTests.Exceptions.cs @@ -0,0 +1,45 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class CalendarEventEventProcessingServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapRaiseCalendarEventAddEventAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + CalendarEvent entity = CreateRandomCalendarEvent(); + + calendarEventEventServiceMock + .Setup(expression: dependency => dependency + .RaiseCalendarEventAddEventAsync(entity: entity)) + .Throws(exception: exception); + + // When + Func action = async () => await service + .RaiseCalendarEventAddEventAsync(entity: entity); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventEventProcessingServiceTests.RaiseCalendarEventAddEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventEventProcessingServiceTests.RaiseCalendarEventAddEventAsync.cs new file mode 100644 index 0000000..85489d0 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventEventProcessingServiceTests.RaiseCalendarEventAddEventAsync.cs @@ -0,0 +1,35 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class CalendarEventEventProcessingServiceTests +{ + [Fact] + public async Task ShouldPassThroughCallWhenRaiseCalendarEventAddEventAsync() + { + // Given + CalendarEvent entity = CreateRandomCalendarEvent(); + + calendarEventEventServiceMock + .Setup(expression: x => x.RaiseCalendarEventAddEventAsync(entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseCalendarEventAddEventAsync(entity: entity); + + // Then + calendarEventEventServiceMock.Verify(expression: x => x.RaiseCalendarEventAddEventAsync(entity: entity), times: Times.Once); + calendarEventEventServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventEventProcessingServiceTests.RaiseCalendarEventDeleteEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventEventProcessingServiceTests.RaiseCalendarEventDeleteEventAsync.cs new file mode 100644 index 0000000..f9ba014 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventEventProcessingServiceTests.RaiseCalendarEventDeleteEventAsync.cs @@ -0,0 +1,35 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class CalendarEventEventProcessingServiceTests +{ + [Fact] + public async Task ShouldPassThroughCallWhenRaiseCalendarEventDeleteEventAsync() + { + // Given + CalendarEvent entity = CreateRandomCalendarEvent(); + + calendarEventEventServiceMock + .Setup(expression: x => x.RaiseCalendarEventDeleteEventAsync(entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseCalendarEventDeleteEventAsync(entity: entity); + + // Then + calendarEventEventServiceMock.Verify(expression: x => x.RaiseCalendarEventDeleteEventAsync(entity: entity), times: Times.Once); + calendarEventEventServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventEventProcessingServiceTests.RaiseCalendarEventUpdateEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventEventProcessingServiceTests.RaiseCalendarEventUpdateEventAsync.cs new file mode 100644 index 0000000..6b5b52f --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventEventProcessingServiceTests.RaiseCalendarEventUpdateEventAsync.cs @@ -0,0 +1,35 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class CalendarEventEventProcessingServiceTests +{ + [Fact] + public async Task ShouldPassThroughCallWhenRaiseCalendarEventUpdateEventAsync() + { + // Given + CalendarEvent entity = CreateRandomCalendarEvent(); + + calendarEventEventServiceMock + .Setup(expression: x => x.RaiseCalendarEventUpdateEventAsync(entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseCalendarEventUpdateEventAsync(entity: entity); + + // Then + calendarEventEventServiceMock.Verify(expression: x => x.RaiseCalendarEventUpdateEventAsync(entity: entity), times: Times.Once); + calendarEventEventServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventEventProcessingServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventEventProcessingServiceTests.cs new file mode 100644 index 0000000..26f61c6 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventEventProcessingServiceTests.cs @@ -0,0 +1,31 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Services.Foundations.Events; +using cCoder.Workflow.Services.Processings; +using FizzWare.NBuilder; +using Moq; + + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class CalendarEventEventProcessingServiceTests +{ + private readonly Mock calendarEventEventServiceMock; + private readonly CalendarEventEventProcessingService service; + + public CalendarEventEventProcessingServiceTests() + { + calendarEventEventServiceMock = new Mock(behavior: MockBehavior.Strict); + service = new CalendarEventEventProcessingService(calendarEventEventServiceMock.Object); + } + + private static CalendarEvent CreateRandomCalendarEvent() => + Builder.CreateNew() + .Build(); +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventProcessingServiceTests.AddOrUpdateCalendarEvent.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventProcessingServiceTests.AddOrUpdateCalendarEvent.cs new file mode 100644 index 0000000..0060977 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventProcessingServiceTests.AddOrUpdateCalendarEvent.cs @@ -0,0 +1,99 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Models.Results; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +#pragma warning disable STXFORMAT008 +public partial class CalendarEventProcessingServiceTests +{ + [Fact] + public async Task ShouldAddAndUpdateCalendarEvents() + { + // Given + + CalendarEvent added = new() + { + Id = 0, + Name = "Added" + }; + + CalendarEvent updated = CreateCalendarEvent(); + + calendarEventServiceMock + .Setup(expression: service => service.AddCalendarEventAsync( + newCalendarEvent: added)) + .Returns(value: ValueTask.FromResult(result: added)); + + calendarEventServiceMock + .Setup(expression: service => service.UpdateCalendarEventAsync( + updatedCalendarEvent: updated)) + .Returns(value: ValueTask.FromResult(result: updated)); + + // When + Result[] results = (await processingService + .AddOrUpdateCalendarEvent(items: new[] { added, updated })) + .ToArray(); + + // Then + results + .Should() + .HaveCount(expected: 2); + + results + .Should() + .OnlyContain(predicate: result => result.Success); + + results[0].Message + .Should() + .Be(expected: "Added Successfully"); + + results[1].Message + .Should() + .Be(expected: "Updated Successfully"); + + calendarEventServiceMock.VerifyAll(); + } + + [Fact] + public async Task ShouldCaptureAddOrUpdateCalendarEventFailure() + { + // Given + + CalendarEvent item = new() + { + Id = 0, + Name = "Failed" + }; + + calendarEventServiceMock + .Setup(expression: service => service.AddCalendarEventAsync( + newCalendarEvent: item)) + .Throws(exception: new Exception("failed")); + + // When + Result result = (await processingService + .AddOrUpdateCalendarEvent(items: new[] { item })) + .Single(); + + // Then + result.Success + .Should() + .BeFalse(); + + result.Item + .Should() + .BeSameAs(expected: item); + + result.Message + .Should() + .Be(expected: "The Workflow service failed."); + } +} +#pragma warning restore STXFORMAT008 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventProcessingServiceTests.Delegation.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventProcessingServiceTests.Delegation.cs new file mode 100644 index 0000000..089fc69 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventProcessingServiceTests.Delegation.cs @@ -0,0 +1,121 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class CalendarEventProcessingServiceTests +{ + [Fact] + public async Task ShouldDelegateCalendarEventOperationsAsync() + { + // Given + CalendarEvent task = CreateCalendarEvent(); + IQueryable tasks = new[] { task } + .AsQueryable(); + + calendarEventServiceMock + .Setup(expression: service => service.Get( + calendarEventId: task.Id)) + .Returns(value: task); + + calendarEventServiceMock + .Setup(expression: service => service.GetAll( + ignoreFilters: true)) + .Returns(value: tasks); + + calendarEventServiceMock + .Setup(expression: service => service.AddCalendarEventAsync( + newCalendarEvent: task)) + .Returns(value: ValueTask.FromResult(result: task)); + + calendarEventServiceMock + .Setup(expression: service => service.UpdateCalendarEventAsync( + updatedCalendarEvent: task)) + .Returns(value: ValueTask.FromResult(result: task)); + + calendarEventServiceMock + .Setup(expression: service => service.DeleteAsync( + calendarEventId: task.Id)) + .Returns(value: ValueTask.CompletedTask); + + calendarEventServiceMock + .Setup(expression: service => service.DeleteAllByAppIdAsync( + appId: 7)) + .Returns(value: ValueTask.CompletedTask); + + calendarEventServiceMock + .Setup(expression: service => service + .DeleteAllForAppCalendarEventAsync( + deletedItems: It.Is>( + match: items => items.Single() == task))) + .Returns(value: ValueTask.CompletedTask); + + // When + CalendarEvent actualGet = processingService.Get(calendarEventId: task.Id); + + IQueryable actualAll = processingService.GetAll( + ignoreFilters: true); + + CalendarEvent actualAdded = await processingService.AddCalendarEventAsync( + newEntity: task); + + CalendarEvent actualUpdated = await processingService.UpdateCalendarEventAsync( + updatedEntity: task); + + await processingService.DeleteAsync(calendarEventId: task.Id); + await processingService.DeleteAllForAppCalendarEventAsync( + deletedItems: new[] { task }); + + await processingService.DeleteAllByAppIdAsync(appId: 7); + + // Then + actualGet + .Should() + .BeSameAs(expected: task); + + actualAll + .Should() + .BeSameAs(expected: tasks); + + actualAdded + .Should() + .BeSameAs(expected: task); + + actualUpdated + .Should() + .BeSameAs(expected: task); + + calendarEventServiceMock.VerifyAll(); + } + + [Fact] + public async Task ShouldDeleteAllCalendarEventsAsync() + { + // Given + CalendarEvent first = CreateCalendarEvent(); + CalendarEvent second = CreateCalendarEvent(); + + calendarEventServiceMock + .Setup(expression: service => service.DeleteAsync( + calendarEventId: first.Id)) + .Returns(value: ValueTask.CompletedTask); + + calendarEventServiceMock + .Setup(expression: service => service.DeleteAsync( + calendarEventId: second.Id)) + .Returns(value: ValueTask.CompletedTask); + + // When + await processingService.DeleteAllCalendarEventAsync( + deletedItems: new[] { first, second }); + + // Then + calendarEventServiceMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..aaf22cc --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventProcessingServiceTests.Exceptions.cs @@ -0,0 +1,90 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class CalendarEventProcessingServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetFailure(Exception exception, Type expectedType) + { + // Given + calendarEventServiceMock + .Setup(expression: service => service.Get(calendarEventId: 1)) + .Throws(exception: exception); + + // When + Action action = () => processingService.Get(calendarEventId: 1); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + calendarEventServiceMock + .Setup(expression: service => service.DeleteAsync( + calendarEventId: 1)) + .Throws(exception: exception); + + // When + Func action = async () => await processingService.DeleteAsync( + calendarEventId: 1); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddCalendarEventAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + calendarEventServiceMock + .Setup(expression: service => service.AddCalendarEventAsync( + newCalendarEvent: It.IsAny())) + .Throws(exception: exception); + + // When + Func action = async () => await processingService + .AddCalendarEventAsync(newEntity: CreateCalendarEvent()); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventProcessingServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventProcessingServiceTests.cs new file mode 100644 index 0000000..7fca704 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarEventProcessingServiceTests.cs @@ -0,0 +1,36 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Services.Foundations; +using cCoder.Workflow.Services.Processings; +using Moq; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +#pragma warning disable STXFORMAT008 +public partial class CalendarEventProcessingServiceTests +{ + private readonly Mock calendarEventServiceMock = new(); + + private readonly CalendarEventProcessingService processingService; + + public CalendarEventProcessingServiceTests() + { + processingService = new CalendarEventProcessingService( + service: calendarEventServiceMock.Object); + } + + private static CalendarEvent CreateCalendarEvent() => + new() + { + Id = Random.Shared.Next(minValue: 1, maxValue: int.MaxValue), + CalendarId = Random.Shared.Next(minValue: 1, maxValue: int.MaxValue), + Name = "Event", + Description = "Description", + Start = DateTimeOffset.UtcNow, + DurationInTicks = TimeSpan.FromMinutes(value: 30).Ticks + }; +} +#pragma warning restore STXFORMAT008 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarProcessingServiceTests.AddOrUpdateCalendar.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarProcessingServiceTests.AddOrUpdateCalendar.cs new file mode 100644 index 0000000..81840a6 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarProcessingServiceTests.AddOrUpdateCalendar.cs @@ -0,0 +1,103 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Models.Results; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +#pragma warning disable STXFORMAT008 +public partial class CalendarProcessingServiceTests +{ + [Fact] + public async Task ShouldAddAndUpdateCalendars() + { + // Given + + Calendar added = new() + { + Id = 0, + Name = "Added" + }; + + Calendar updated = CreateCalendar(); + + calendarServiceMock + .Setup(expression: service => service.AddCalendarAsync( + newCalendar: added)) + .Returns(value: ValueTask.FromResult(result: added)); + + calendarServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { updated }.AsQueryable()); + + calendarServiceMock + .Setup(expression: service => service.UpdateCalendarAsync( + updatedCalendar: updated)) + .Returns(value: ValueTask.FromResult(result: updated)); + + // When + Result[] results = (await processingService + .AddOrUpdateCalendar(items: new[] { added, updated })) + .ToArray(); + + // Then + results + .Should() + .HaveCount(expected: 2); + + results + .Should() + .OnlyContain(predicate: result => result.Success); + + results[0].Message + .Should() + .Be(expected: "Added Successfully"); + + results[1].Message + .Should() + .Be(expected: "Updated Successfully"); + + calendarServiceMock.VerifyAll(); + } + + [Fact] + public async Task ShouldCaptureAddOrUpdateCalendarFailure() + { + // Given + + Calendar item = new() + { + Id = 0, + Name = "Failed" + }; + + calendarServiceMock + .Setup(expression: service => service.AddCalendarAsync( + newCalendar: item)) + .Throws(exception: new Exception("failed")); + + // When + Result result = (await processingService + .AddOrUpdateCalendar(items: new[] { item })) + .Single(); + + // Then + result.Success + .Should() + .BeFalse(); + + result.Item + .Should() + .BeSameAs(expected: item); + + result.Message + .Should() + .Be(expected: "The Workflow service failed."); + } +} +#pragma warning restore STXFORMAT008 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarProcessingServiceTests.Delegation.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarProcessingServiceTests.Delegation.cs new file mode 100644 index 0000000..9a3c6a3 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarProcessingServiceTests.Delegation.cs @@ -0,0 +1,111 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class CalendarProcessingServiceTests +{ + [Fact] + public async Task ShouldDelegateCalendarOperationsAsync() + { + // Given + Calendar task = CreateCalendar(); + IQueryable tasks = new[] { task } + .AsQueryable(); + + calendarServiceMock + .Setup(expression: service => service.Get( + calendarId: task.Id)) + .Returns(value: task); + + calendarServiceMock + .Setup(expression: service => service.GetAll( + ignoreFilters: true)) + .Returns(value: tasks); + + calendarServiceMock + .Setup(expression: service => service.AddCalendarAsync( + newCalendar: task)) + .Returns(value: ValueTask.FromResult(result: task)); + + calendarServiceMock + .Setup(expression: service => service.UpdateCalendarAsync( + updatedCalendar: task)) + .Returns(value: ValueTask.FromResult(result: task)); + + calendarServiceMock + .Setup(expression: service => service.DeleteAsync( + calendarId: task.Id)) + .Returns(value: ValueTask.CompletedTask); + + calendarServiceMock + .Setup(expression: service => service.DeleteAllByAppIdAsync( + appId: 7)) + .Returns(value: ValueTask.CompletedTask); + + // When + Calendar actualGet = processingService.Get(calendarId: task.Id); + + IQueryable actualAll = processingService.GetAll( + ignoreFilters: true); + + Calendar actualAdded = await processingService.AddCalendarAsync( + newEntity: task); + + Calendar actualUpdated = await processingService.UpdateCalendarAsync( + updatedEntity: task); + + await processingService.DeleteAsync(calendarId: task.Id); + await processingService.DeleteByAppIdAsync(appId: 7); + + // Then + actualGet + .Should() + .BeSameAs(expected: task); + + actualAll + .Should() + .BeSameAs(expected: tasks); + + actualAdded + .Should() + .BeSameAs(expected: task); + + actualUpdated + .Should() + .BeSameAs(expected: task); + + calendarServiceMock.VerifyAll(); + } + + [Fact] + public async Task ShouldDeleteAllCalendarsAsync() + { + // Given + Calendar first = CreateCalendar(); + Calendar second = CreateCalendar(); + + calendarServiceMock + .Setup(expression: service => service.DeleteAsync( + calendarId: first.Id)) + .Returns(value: ValueTask.CompletedTask); + + calendarServiceMock + .Setup(expression: service => service.DeleteAsync( + calendarId: second.Id)) + .Returns(value: ValueTask.CompletedTask); + + // When + await processingService.DeleteAllCalendarAsync( + deletedItems: new[] { first, second }); + + // Then + calendarServiceMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..ff17698 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarProcessingServiceTests.Exceptions.cs @@ -0,0 +1,90 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class CalendarProcessingServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetFailure(Exception exception, Type expectedType) + { + // Given + calendarServiceMock + .Setup(expression: service => service.Get(calendarId: 1)) + .Throws(exception: exception); + + // When + Action action = () => processingService.Get(calendarId: 1); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + calendarServiceMock + .Setup(expression: service => service.DeleteAsync( + calendarId: 1)) + .Throws(exception: exception); + + // When + Func action = async () => await processingService.DeleteAsync( + calendarId: 1); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddCalendarAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + calendarServiceMock + .Setup(expression: service => service.AddCalendarAsync( + newCalendar: It.IsAny())) + .Throws(exception: exception); + + // When + Func action = async () => await processingService + .AddCalendarAsync(newEntity: CreateCalendar()); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarProcessingServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarProcessingServiceTests.cs new file mode 100644 index 0000000..819c69e --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/CalendarProcessingServiceTests.cs @@ -0,0 +1,34 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Services.Foundations; +using cCoder.Workflow.Services.Processings; +using Moq; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +#pragma warning disable STXFORMAT008 +public partial class CalendarProcessingServiceTests +{ + private readonly Mock calendarServiceMock = new(); + + private readonly CalendarProcessingService processingService; + + public CalendarProcessingServiceTests() + { + processingService = new CalendarProcessingService( + service: calendarServiceMock.Object); + } + + private static Calendar CreateCalendar() => + new() + { + Id = Random.Shared.Next(minValue: 1, maxValue: int.MaxValue), + AppId = Random.Shared.Next(minValue: 1, maxValue: int.MaxValue), + Name = "Calendar", + Description = "Description" + }; +} +#pragma warning restore STXFORMAT008 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionEventProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionEventProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..6dee84a --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionEventProcessingServiceTests.Exceptions.cs @@ -0,0 +1,45 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class FlowDefinitionEventProcessingServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapRaiseFlowDefinitionAddEventAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + FlowDefinition flowDefinition = CreateRandomFlowDefinition(); + + flowDefinitionEventServiceMock + .Setup(expression: foundation => foundation.RaiseFlowDefinitionAddEventAsync( + entity: flowDefinition)) + .Throws(exception: exception); + + // When + Func action = async () => await service + .RaiseFlowDefinitionAddEventAsync(entity: flowDefinition); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionProcessingServiceTests.AddAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionProcessingServiceTests.AddAsync.cs index b1076c5..75799ad 100644 --- a/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionProcessingServiceTests.AddAsync.cs +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionProcessingServiceTests.AddAsync.cs @@ -12,6 +12,7 @@ namespace cCoder.Core.Services.Tests.Workflow.Processings; +#pragma warning disable STXFORMAT009 public partial class FlowDefinitionProcessingServiceTests { [Fact] @@ -36,4 +37,5 @@ public async Task ShouldDelegateToFoundationServiceWhenAddAsync() flowDefinitionServiceMock.Verify(expression: x => x.AddFlowDefinitionAsync(newFlowDefinition: flow), times: Times.Once); } -} \ No newline at end of file +} +#pragma warning restore STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionProcessingServiceTests.AddOrUpdate.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionProcessingServiceTests.AddOrUpdate.cs new file mode 100644 index 0000000..35d4c2d --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionProcessingServiceTests.AddOrUpdate.cs @@ -0,0 +1,106 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using cCoder.Workflow.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +#pragma warning disable STXFORMAT009 +public partial class FlowDefinitionProcessingServiceTests +{ + [Fact] + public async Task ShouldAddAndUpdateFlowDefinitionsAsync() + { + // Given + FlowDefinition added = CreateRandomFlowDefinition(); + added.Id = Guid.Empty; + FlowDefinition updated = CreateRandomFlowDefinition(); + + jsonBrokerMock + .Setup(expression: broker => broker.Serialize( + value: It.IsAny())) + .Returns(value: "[]"); + + loggingBrokerMock + .Setup(expression: broker => broker.LogDebug( + message: "AddOrUpdate:\n[]", + args: It.IsAny())); + + flowDefinitionServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { updated }.AsQueryable()); + + flowDefinitionServiceMock + .Setup(expression: service => service.AddFlowDefinitionAsync( + newFlowDefinition: added)) + .Returns(value: ValueTask.FromResult(result: added)); + + flowDefinitionServiceMock + .Setup(expression: service => service.UpdateFlowDefinitionAsync( + updatedFlowDefinition: updated)) + .Returns(value: ValueTask.FromResult(result: updated)); + + // When + Result[] results = (await flowDefinitionProcessingService + .AddOrUpdateFlowDefinition(items: new[] { added, updated })) + .ToArray(); + + // Then + results.Should().OnlyContain(predicate: result => result.Success); + results[0].Message.Should().Be(expected: "Added Successfully"); + results[1].Message.Should().Be(expected: "Updated Successfully"); + flowDefinitionServiceMock.VerifyAll(); + jsonBrokerMock.VerifyAll(); + loggingBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldCaptureAddOrUpdateFlowDefinitionFailureAsync() + { + // Given + FlowDefinition item = CreateRandomFlowDefinition(); + item.Id = Guid.Empty; + + jsonBrokerMock + .Setup(expression: broker => broker.Serialize( + value: It.IsAny())) + .Returns(value: "[]"); + + flowDefinitionServiceMock + .Setup(expression: service => service.AddFlowDefinitionAsync( + newFlowDefinition: item)) + .Throws(exception: new Exception(message: "failed")); + + // When + Result result = (await flowDefinitionProcessingService + .AddOrUpdateFlowDefinition(items: new[] { item })) + .Single(); + + // Then + result.Success.Should().BeFalse(); + result.Item.Should().BeSameAs(expected: item); + result.Message.Should().Be(expected: "The Workflow service failed."); + } + + [Fact] + public async Task ShouldDeleteFlowDefinitionsByAppIdAsync() + { + // Given + flowDefinitionServiceMock + .Setup(expression: service => service + .DeleteWithInstancesByAppIdAsync(appId: 7)) + .Returns(value: ValueTask.CompletedTask); + + // When + await flowDefinitionProcessingService.DeleteByAppIdAsync(appId: 7); + + // Then + flowDefinitionServiceMock.VerifyAll(); + } +} +#pragma warning restore STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..4ee85e1 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionProcessingServiceTests.Exceptions.cs @@ -0,0 +1,92 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class FlowDefinitionProcessingServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetAllFailure(Exception exception, Type expectedType) + { + // Given + flowDefinitionServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: false)) + .Throws(exception: exception); + + // When + Action action = () => flowDefinitionProcessingService.GetAll(); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddFlowDefinitionAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + FlowDefinition flowDefinition = CreateRandomFlowDefinition(); + + flowDefinitionServiceMock + .Setup(expression: service => service.AddFlowDefinitionAsync( + newFlowDefinition: flowDefinition)) + .Throws(exception: exception); + + // When + Func action = async () => await flowDefinitionProcessingService + .AddFlowDefinitionAsync(newEntity: flowDefinition); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteByAppIdAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + flowDefinitionServiceMock + .Setup(expression: service => service + .DeleteWithInstancesByAppIdAsync(appId: 7)) + .Throws(exception: exception); + + // When + Func action = async () => await flowDefinitionProcessingService + .DeleteByAppIdAsync(appId: 7); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionProcessingServiceTests.Serialization.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionProcessingServiceTests.Serialization.cs new file mode 100644 index 0000000..7304d22 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowDefinitionProcessingServiceTests.Serialization.cs @@ -0,0 +1,89 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Activities.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class FlowDefinitionProcessingServiceTests +{ + [Fact] + public void ShouldAuthorizeFlowDefinitionExecution() + { + // Given + authorizationBrokerMock + .Setup(expression: broker => broker.Authorize( + userId: "user", + appId: 7, + privilege: "flowdefinition_execute")); + + // When + bool result = flowDefinitionProcessingService + .AuthorizeFlowDefinitionExecution(userId: "user", appId: 7); + + // Then + result.Should().BeTrue(); + authorizationBrokerMock.VerifyAll(); + } + + [Fact] + public void ShouldParseFlowDefinition() + { + // Given + Flow expected = new(); + + jsonBrokerMock + .Setup(expression: broker => broker.ParseJson(json: "{}")) + .Returns(value: expected); + + // When + object actual = flowDefinitionProcessingService + .ParseFlowDefinition(definitionJson: "{}"); + + // Then + actual.Should().BeSameAs(expected: expected); + jsonBrokerMock.VerifyAll(); + } + + [Fact] + public void ShouldParseFlowDefinitionData() + { + // Given + object expected = new(); + + jsonBrokerMock + .Setup(expression: broker => broker.ParseJson(json: "{}")) + .Returns(value: expected); + + // When + object actual = flowDefinitionProcessingService + .ParseFlowDefinitionData(args: "{}"); + + // Then + actual.Should().BeSameAs(expected: expected); + jsonBrokerMock.VerifyAll(); + } + + [Fact] + public void ShouldSerializeFlowDefinitionContext() + { + // Given + object context = new(); + + jsonBrokerMock + .Setup(expression: broker => broker.Serialize(value: context)) + .Returns(value: "serialized"); + + // When + string actual = flowDefinitionProcessingService + .SerializeFlowDefinitionContext(context: context); + + // Then + actual.Should().Be(expected: "serialized"); + jsonBrokerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/FlowInstanceDataEventProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowInstanceDataEventProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..f80701e --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowInstanceDataEventProcessingServiceTests.Exceptions.cs @@ -0,0 +1,45 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class FlowInstanceDataEventProcessingServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapRaiseFlowInstanceDataAddEventAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + FlowInstanceData entity = CreateRandomFlowInstanceData(); + + flowInstanceDataEventServiceMock + .Setup(expression: dependency => dependency + .RaiseFlowInstanceDataAddEventAsync(entity: entity)) + .Throws(exception: exception); + + // When + Func action = async () => await service + .RaiseFlowInstanceDataAddEventAsync(entity: entity); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/FlowInstanceDataProcessingServiceTests.AddAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowInstanceDataProcessingServiceTests.AddAsync.cs index fe0b34f..9061634 100644 --- a/src/cCoder.Workflow.Tests/Workflow/Processings/FlowInstanceDataProcessingServiceTests.AddAsync.cs +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowInstanceDataProcessingServiceTests.AddAsync.cs @@ -13,6 +13,7 @@ namespace cCoder.Core.Services.Tests.Workflow.Processings; +#pragma warning disable STXFORMAT005, STXFORMAT008 public partial class FlowInstanceDataProcessingServiceTests { [Fact] @@ -35,4 +36,5 @@ public async Task ShouldDelegateToFoundationServiceWhenAddAsync() flowInstanceDataServiceMock.VerifyNoOtherCalls(); } -} \ No newline at end of file +} +#pragma warning restore STXFORMAT005, STXFORMAT008 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/FlowInstanceDataProcessingServiceTests.AddOrUpdate.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowInstanceDataProcessingServiceTests.AddOrUpdate.cs new file mode 100644 index 0000000..32f9195 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowInstanceDataProcessingServiceTests.AddOrUpdate.cs @@ -0,0 +1,88 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using cCoder.Workflow.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +#pragma warning disable STXFORMAT005, STXFORMAT009 +public partial class FlowInstanceDataProcessingServiceTests +{ + [Fact] + public async Task ShouldAddAndUpdateFlowInstanceDataAsync() + { + // Given + FlowInstanceData added = CreateRandomFlowInstanceData(); + added.Id = Guid.Empty; + FlowInstanceData updated = CreateRandomFlowInstanceData(); + FlowInstanceData dbVersion = CreateRandomFlowInstanceData(); + dbVersion.Id = updated.Id; + + flowInstanceDataServiceMock + .Setup(expression: service => service.AddFlowInstanceDataAsync(added)) + .ReturnsAsync(value: added); + flowInstanceDataServiceMock + .Setup(expression: service => service.Get(updated.Id)) + .Returns(value: dbVersion); + flowInstanceDataServiceMock + .Setup(expression: service => service.UpdateFlowInstanceDataAsync(dbVersion)) + .ReturnsAsync(value: dbVersion); + + // When + Result[] results = (await flowInstanceDataProcessingService + .AddOrUpdateFlowInstanceData(items: new[] { added, updated })) + .ToArray(); + + // Then + results.Should().OnlyContain(predicate: result => result.Success); + results[0].Message.Should().Be(expected: "Added Successfully"); + results[1].Message.Should().Be(expected: "Updated Successfully"); + flowInstanceDataServiceMock.VerifyAll(); + } + + [Fact] + public async Task ShouldCaptureAddOrUpdateFlowInstanceDataFailureAsync() + { + // Given + FlowInstanceData item = CreateRandomFlowInstanceData(); + item.Id = Guid.Empty; + + flowInstanceDataServiceMock + .Setup(expression: service => service.AddFlowInstanceDataAsync(item)) + .Throws(exception: new Exception(message: "failed")); + + // When + Result result = (await flowInstanceDataProcessingService + .AddOrUpdateFlowInstanceData(items: new[] { item })) + .Single(); + + // Then + result.Success.Should().BeFalse(); + result.Item.Should().BeSameAs(expected: item); + result.Message.Should().Be(expected: "The Workflow service failed."); + } + + [Fact] + public async Task ShouldRejectMissingFlowInstanceDataWhenUpdatingAsync() + { + // Given + FlowInstanceData item = CreateRandomFlowInstanceData(); + + flowInstanceDataServiceMock + .Setup(expression: service => service.Get(item.Id)) + .Returns(value: null); + + // When + Func action = async () => await flowInstanceDataProcessingService + .UpdateFlowInstanceDataAsync(updatedEntity: item); + + // Then + await action.Should().ThrowAsync(); + } +} +#pragma warning restore STXFORMAT005, STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/FlowInstanceDataProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowInstanceDataProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..9696887 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/FlowInstanceDataProcessingServiceTests.Exceptions.cs @@ -0,0 +1,92 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class FlowInstanceDataProcessingServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetAllFailure(Exception exception, Type expectedType) + { + // Given + flowInstanceDataServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: false)) + .Throws(exception: exception); + + // When + Action action = () => flowInstanceDataProcessingService.GetAll(); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddQueuedFlowInstanceDataAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + FlowInstanceData item = CreateRandomFlowInstanceData(); + + flowInstanceDataServiceMock + .Setup(expression: service => service.AddQueuedFlowInstanceDataAsync( + newFlowInstanceData: item)) + .Throws(exception: exception); + + // When + Func action = async () => await flowInstanceDataProcessingService + .AddQueuedFlowInstanceDataAsync(newEntity: item); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteAsyncFailure(Exception exception, Type expectedType) + { + // Given + Guid id = Guid.NewGuid(); + + flowInstanceDataServiceMock + .Setup(expression: service => service.DeleteAsync( + flowInstanceDataId: id)) + .Throws(exception: exception); + + // When + Func action = async () => await flowInstanceDataProcessingService + .DeleteAsync(flowInstanceDataId: id); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..8fdda5a --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.Exceptions.cs @@ -0,0 +1,45 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class ScheduledTaskEventProcessingServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapRaiseScheduledTaskAddEventAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + ScheduledTask entity = CreateRandomScheduledTask(); + + scheduledTaskEventServiceMock + .Setup(expression: dependency => dependency + .RaiseScheduledTaskAddEventAsync(entity: entity)) + .Throws(exception: exception); + + // When + Func action = async () => await service + .RaiseScheduledTaskAddEventAsync(entity: entity); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.RaiseScheduledTaskAddEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.RaiseScheduledTaskAddEventAsync.cs new file mode 100644 index 0000000..2e0980b --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.RaiseScheduledTaskAddEventAsync.cs @@ -0,0 +1,35 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class ScheduledTaskEventProcessingServiceTests +{ + [Fact] + public async Task ShouldPassThroughCallWhenRaiseScheduledTaskAddEventAsync() + { + // Given + ScheduledTask entity = CreateRandomScheduledTask(); + + scheduledTaskEventServiceMock + .Setup(expression: x => x.RaiseScheduledTaskAddEventAsync(entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseScheduledTaskAddEventAsync(entity: entity); + + // Then + scheduledTaskEventServiceMock.Verify(expression: x => x.RaiseScheduledTaskAddEventAsync(entity: entity), times: Times.Once); + scheduledTaskEventServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.RaiseScheduledTaskDeleteEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.RaiseScheduledTaskDeleteEventAsync.cs new file mode 100644 index 0000000..5e2af33 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.RaiseScheduledTaskDeleteEventAsync.cs @@ -0,0 +1,35 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class ScheduledTaskEventProcessingServiceTests +{ + [Fact] + public async Task ShouldPassThroughCallWhenRaiseScheduledTaskDeleteEventAsync() + { + // Given + ScheduledTask entity = CreateRandomScheduledTask(); + + scheduledTaskEventServiceMock + .Setup(expression: x => x.RaiseScheduledTaskDeleteEventAsync(entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseScheduledTaskDeleteEventAsync(entity: entity); + + // Then + scheduledTaskEventServiceMock.Verify(expression: x => x.RaiseScheduledTaskDeleteEventAsync(entity: entity), times: Times.Once); + scheduledTaskEventServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.RaiseScheduledTaskExecuteEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.RaiseScheduledTaskExecuteEventAsync.cs new file mode 100644 index 0000000..eaf5dc6 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.RaiseScheduledTaskExecuteEventAsync.cs @@ -0,0 +1,35 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class ScheduledTaskEventProcessingServiceTests +{ + [Fact] + public async Task ShouldPassThroughCallWhenRaiseScheduledTaskExecuteEventAsync() + { + // Given + ScheduledTask entity = CreateRandomScheduledTask(); + + scheduledTaskEventServiceMock + .Setup(expression: x => x.RaiseScheduledTaskExecuteEventAsync(entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseScheduledTaskExecuteEventAsync(entity: entity); + + // Then + scheduledTaskEventServiceMock.Verify(expression: x => x.RaiseScheduledTaskExecuteEventAsync(entity: entity), times: Times.Once); + scheduledTaskEventServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.RaiseScheduledTaskUpdateEventAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.RaiseScheduledTaskUpdateEventAsync.cs new file mode 100644 index 0000000..a758479 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.RaiseScheduledTaskUpdateEventAsync.cs @@ -0,0 +1,35 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using Moq; +using Xunit; + + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class ScheduledTaskEventProcessingServiceTests +{ + [Fact] + public async Task ShouldPassThroughCallWhenRaiseScheduledTaskUpdateEventAsync() + { + // Given + ScheduledTask entity = CreateRandomScheduledTask(); + + scheduledTaskEventServiceMock + .Setup(expression: x => x.RaiseScheduledTaskUpdateEventAsync(entity: entity)) + .Returns(value: ValueTask.CompletedTask); + + // When + await service.RaiseScheduledTaskUpdateEventAsync(entity: entity); + + // Then + scheduledTaskEventServiceMock.Verify(expression: x => x.RaiseScheduledTaskUpdateEventAsync(entity: entity), times: Times.Once); + scheduledTaskEventServiceMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.cs new file mode 100644 index 0000000..b96aa1f --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskEventProcessingServiceTests.cs @@ -0,0 +1,31 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Models; +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Security; +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Services.Foundations.Events; +using cCoder.Workflow.Services.Processings; +using FizzWare.NBuilder; +using Moq; + + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class ScheduledTaskEventProcessingServiceTests +{ + private readonly Mock scheduledTaskEventServiceMock; + private readonly ScheduledTaskEventProcessingService service; + + public ScheduledTaskEventProcessingServiceTests() + { + scheduledTaskEventServiceMock = new Mock(behavior: MockBehavior.Strict); + service = new ScheduledTaskEventProcessingService(scheduledTaskEventServiceMock.Object); + } + + private static ScheduledTask CreateRandomScheduledTask() => + Builder.CreateNew() + .Build(); +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskProcessingServiceTests.AddOrUpdateScheduledTask.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskProcessingServiceTests.AddOrUpdateScheduledTask.cs new file mode 100644 index 0000000..a98958b --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskProcessingServiceTests.AddOrUpdateScheduledTask.cs @@ -0,0 +1,104 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Models.Results; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +#pragma warning disable STXFORMAT008 +public partial class ScheduledTaskProcessingServiceTests +{ + [Fact] + public async Task ShouldAddAndUpdateScheduledTasks() + { + // Given + + ScheduledTask added = new() + { + Id = 0, + Name = "Added" + }; + + ScheduledTask updated = CreateScheduledTask(); + + scheduledTaskServiceMock + .Setup(expression: service => service.AddScheduledTaskAsync( + newScheduledTask: added)) + .Returns(value: ValueTask.FromResult(result: added)); + + scheduledTaskServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { updated } + .AsQueryable()); + + scheduledTaskServiceMock + .Setup(expression: service => service.UpdateScheduledTaskAsync( + updatedScheduledTask: updated)) + .Returns(value: ValueTask.FromResult(result: updated)); + + // When + Result[] results = (await processingService + .AddOrUpdateScheduledTask(items: new[] { added, updated })) + .ToArray(); + + // Then + results + .Should() + .HaveCount(expected: 2); + + results + .Should() + .OnlyContain(predicate: result => result.Success); + + results[0].Message + .Should() + .Be(expected: "Added Successfully"); + + results[1].Message + .Should() + .Be(expected: "Updated Successfully"); + + scheduledTaskServiceMock.VerifyAll(); + } + + [Fact] + public async Task ShouldCaptureAddOrUpdateScheduledTaskFailure() + { + // Given + + ScheduledTask item = new() + { + Id = 0, + Name = "Failed" + }; + + scheduledTaskServiceMock + .Setup(expression: service => service.AddScheduledTaskAsync( + newScheduledTask: item)) + .Throws(exception: new Exception("failed")); + + // When + Result result = (await processingService + .AddOrUpdateScheduledTask(items: new[] { item })) + .Single(); + + // Then + result.Success + .Should() + .BeFalse(); + + result.Item + .Should() + .BeSameAs(expected: item); + + result.Message + .Should() + .Be(expected: "The Workflow service failed."); + } +} +#pragma warning restore STXFORMAT008 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskProcessingServiceTests.Delegation.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskProcessingServiceTests.Delegation.cs new file mode 100644 index 0000000..bee4400 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskProcessingServiceTests.Delegation.cs @@ -0,0 +1,126 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class ScheduledTaskProcessingServiceTests +{ + [Fact] + public async Task ShouldDelegateScheduledTaskOperationsAsync() + { + // Given + ScheduledTask task = CreateScheduledTask(); + IQueryable tasks = new[] { task } + .AsQueryable(); + + scheduledTaskServiceMock + .Setup(expression: service => service.Get( + scheduledTaskId: task.Id)) + .Returns(value: task); + + scheduledTaskServiceMock + .Setup(expression: service => service.GetAll( + ignoreFilters: true)) + .Returns(value: tasks); + + scheduledTaskServiceMock + .Setup(expression: service => service.MarkExecutedAsync( + scheduledTaskId: task.Id, + incrementNextExecution: false)) + .Returns(value: ValueTask.FromResult(result: task)); + + scheduledTaskServiceMock + .Setup(expression: service => service.AddScheduledTaskAsync( + newScheduledTask: task)) + .Returns(value: ValueTask.FromResult(result: task)); + + scheduledTaskServiceMock + .Setup(expression: service => service.UpdateScheduledTaskAsync( + updatedScheduledTask: task)) + .Returns(value: ValueTask.FromResult(result: task)); + + scheduledTaskServiceMock + .Setup(expression: service => service.DeleteAsync( + scheduledTaskId: task.Id)) + .Returns(value: ValueTask.CompletedTask); + + scheduledTaskServiceMock + .Setup(expression: service => service.DeleteAllByAppIdAsync( + appId: task.AppId)) + .Returns(value: ValueTask.CompletedTask); + + // When + ScheduledTask actualGet = processingService.Get(scheduledTaskId: task.Id); + + IQueryable actualAll = processingService.GetAll( + ignoreFilters: true); + + ScheduledTask actualExecuted = await processingService + .ExecuteScheduledTaskAsync( + scheduledTaskId: task.Id, + incrementNextExecution: false); + + ScheduledTask actualAdded = await processingService.AddScheduledTaskAsync( + newEntity: task); + + ScheduledTask actualUpdated = await processingService.UpdateScheduledTaskAsync( + updatedEntity: task); + + await processingService.DeleteAsync(scheduledTaskId: task.Id); + await processingService.DeleteByAppIdAsync(appId: task.AppId); + + // Then + actualGet + .Should() + .BeSameAs(expected: task); + + actualAll + .Should() + .BeSameAs(expected: tasks); + + actualExecuted + .Should() + .BeSameAs(expected: task); + + actualAdded + .Should() + .BeSameAs(expected: task); + + actualUpdated + .Should() + .BeSameAs(expected: task); + + scheduledTaskServiceMock.VerifyAll(); + } + + [Fact] + public async Task ShouldDeleteAllScheduledTasksAsync() + { + // Given + ScheduledTask first = CreateScheduledTask(); + ScheduledTask second = CreateScheduledTask(); + + scheduledTaskServiceMock + .Setup(expression: service => service.DeleteAsync( + scheduledTaskId: first.Id)) + .Returns(value: ValueTask.CompletedTask); + + scheduledTaskServiceMock + .Setup(expression: service => service.DeleteAsync( + scheduledTaskId: second.Id)) + .Returns(value: ValueTask.CompletedTask); + + // When + await processingService.DeleteAllScheduledTaskAsync( + deletedItems: new[] { first, second }); + + // Then + scheduledTaskServiceMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..50beda0 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskProcessingServiceTests.Exceptions.cs @@ -0,0 +1,91 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class ScheduledTaskProcessingServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetFailure(Exception exception, Type expectedType) + { + // Given + scheduledTaskServiceMock + .Setup(expression: service => service.Get(scheduledTaskId: 1)) + .Throws(exception: exception); + + // When + Action action = () => processingService.Get(scheduledTaskId: 1); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + scheduledTaskServiceMock + .Setup(expression: service => service.DeleteAsync( + scheduledTaskId: 1)) + .Throws(exception: exception); + + // When + Func action = async () => await processingService.DeleteAsync( + scheduledTaskId: 1); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapExecuteScheduledTaskAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + scheduledTaskServiceMock + .Setup(expression: service => service.MarkExecutedAsync( + scheduledTaskId: 1, + incrementNextExecution: true)) + .Throws(exception: exception); + + // When + Func action = async () => await processingService + .ExecuteScheduledTaskAsync(scheduledTaskId: 1); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskProcessingServiceTests.Logging.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskProcessingServiceTests.Logging.cs new file mode 100644 index 0000000..eb5d517 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskProcessingServiceTests.Logging.cs @@ -0,0 +1,55 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class ScheduledTaskProcessingServiceTests +{ + [Fact] + public void ShouldGetScheduledTaskMigrationState() + { + // Given + configuration.IsMigrating = true; + // When + bool actual = processingService.IsScheduledTaskMigrationActive(); + + // Then + actual + .Should() + .BeTrue(); + } + + [Fact] + public async Task ShouldLogScheduledTaskLifecycleAsync() + { + // Given + ScheduledTask task = CreateScheduledTask(); + task.NextExecution = DateTimeOffset.UtcNow; + // When + await processingService.LogNoScheduledTasksDueAsync(); + await processingService.LogScheduledTasksRunningAsync(scheduledTaskCount: 1); + await processingService.LogScheduledTaskRunningAsync(scheduledTask: task); + await processingService.LogScheduledTaskCompleteAsync(scheduledTask: task); + await processingService.LogScheduledTaskSkippedAsync(scheduledTask: task); + await processingService.LogScheduledTasksExecutedAsync(scheduledTaskCount: 1); + + // Then + loggingBrokerMock.Verify( + expression: broker => broker.LogDebug( + message: It.IsAny(), + args: It.IsAny()), + times: Times.Exactly(callCount: 4)); + + loggingBrokerMock.Verify( + expression: broker => broker.LogInformation( + message: It.IsAny(), + args: It.IsAny()), + times: Times.Exactly(callCount: 2)); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskProcessingServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskProcessingServiceTests.cs new file mode 100644 index 0000000..de0db27 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/ScheduledTaskProcessingServiceTests.cs @@ -0,0 +1,42 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Brokers.Loggings; +using cCoder.Workflow.Models; +using cCoder.Workflow.Services.Foundations; +using cCoder.Workflow.Services.Processings; +using Moq; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +#pragma warning disable STXFORMAT008 +public partial class ScheduledTaskProcessingServiceTests +{ + private readonly Mock scheduledTaskServiceMock = new(); + + private readonly Mock loggingBrokerMock = new(); + + private readonly WorkflowConfiguration configuration = new(); + + private readonly ScheduledTaskProcessingService processingService; + + public ScheduledTaskProcessingServiceTests() + { + processingService = new ScheduledTaskProcessingService( + service: scheduledTaskServiceMock.Object, + configuration: configuration, + logger: loggingBrokerMock.Object); + } + + private static ScheduledTask CreateScheduledTask() => + new() + { + Id = Random.Shared.Next(minValue: 1, maxValue: int.MaxValue), + AppId = Random.Shared.Next(minValue: 1, maxValue: int.MaxValue), + FlowId = Guid.NewGuid(), + Name = "Task" + }; +} +#pragma warning restore STXFORMAT008 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventEventProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventEventProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..9529fb7 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventEventProcessingServiceTests.Exceptions.cs @@ -0,0 +1,45 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class WorkflowEventEventProcessingServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapRaiseWorkflowEventAddEventAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + WorkflowEvent entity = CreateRandomWorkflowEvent(); + + workflowEventEventServiceMock + .Setup(expression: dependency => dependency + .RaiseWorkflowEventAddEventAsync(entity: entity)) + .Throws(exception: exception); + + // When + Func action = async () => await service + .RaiseWorkflowEventAddEventAsync(entity: entity); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventProcessingServiceTests.AddAsync.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventProcessingServiceTests.AddAsync.cs index 5f0d13d..067ebb5 100644 --- a/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventProcessingServiceTests.AddAsync.cs +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventProcessingServiceTests.AddAsync.cs @@ -10,6 +10,7 @@ namespace cCoder.Core.Services.Tests.Workflow.Processings; +#pragma warning disable STXFORMAT005, STXFORMAT009 public partial class WorkflowEventProcessingServiceTests { [Fact] @@ -71,4 +72,5 @@ await act.Should() authorizationBrokerMock.Verify(expression: x => x.Authorize(userId: workflowEvent.ExecuteAs, appId: 1, privilege: "app_admin"), times: Times.Once); authorizationBrokerMock.VerifyNoOtherCalls(); } -} \ No newline at end of file +} +#pragma warning restore STXFORMAT005, STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventProcessingServiceTests.AddOrUpdate.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventProcessingServiceTests.AddOrUpdate.cs new file mode 100644 index 0000000..ef975d6 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventProcessingServiceTests.AddOrUpdate.cs @@ -0,0 +1,84 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using cCoder.Workflow.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +#pragma warning disable STXFORMAT009 +public partial class WorkflowEventProcessingServiceTests +{ + [Fact] + public async Task ShouldAddAndUpdateWorkflowEventsAsync() + { + // Given + WorkflowEvent added = CreateRandomWorkflowEvent(); + added.Id = Guid.Empty; + WorkflowEvent updated = CreateRandomWorkflowEvent(); + + foreach (WorkflowEvent item in new[] { added, updated }) + { + workflowEventServiceMock + .Setup(expression: service => service.GetAppIdForWorkflowEvent( + workflowEvent: item)) + .Returns(value: 7); + + authorizationBrokerMock + .Setup(expression: broker => broker.Authorize( + userId: item.ExecuteAs, + appId: 7, + privilege: "app_admin")); + } + + workflowEventServiceMock + .Setup(expression: service => service.AddWorkflowEventAsync( + newWorkflowEvent: added)) + .Returns(value: ValueTask.FromResult(result: added)); + + workflowEventServiceMock + .Setup(expression: service => service.UpdateWorkflowEventAsync( + updatedWorkflowEvent: updated)) + .Returns(value: ValueTask.FromResult(result: updated)); + + // When + Result[] results = (await workflowEventProcessingService + .AddOrUpdateWorkflowEvent(items: new[] { added, updated })) + .ToArray(); + + // Then + results.Should().OnlyContain(predicate: result => result.Success); + results[0].Message.Should().Be(expected: "Added Successfully"); + results[1].Message.Should().Be(expected: "Updated Successfully"); + workflowEventServiceMock.VerifyAll(); + authorizationBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldCaptureAddOrUpdateWorkflowEventFailureAsync() + { + // Given + WorkflowEvent item = CreateRandomWorkflowEvent(); + item.Id = Guid.Empty; + + workflowEventServiceMock + .Setup(expression: service => service.GetAppIdForWorkflowEvent( + workflowEvent: item)) + .Throws(exception: new Exception(message: "failed")); + + // When + Result result = (await workflowEventProcessingService + .AddOrUpdateWorkflowEvent(items: new[] { item })) + .Single(); + + // Then + result.Success.Should().BeFalse(); + result.Item.Should().BeSameAs(expected: item); + result.Message.Should().Be(expected: "The Workflow service failed."); + } +} +#pragma warning restore STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventProcessingServiceTests.Dispatch.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventProcessingServiceTests.Dispatch.cs new file mode 100644 index 0000000..52756af --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventProcessingServiceTests.Dispatch.cs @@ -0,0 +1,101 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class WorkflowEventProcessingServiceTests +{ + [Fact] + public void ShouldPrepareWorkflowEventDispatchFromPayload() + { + // Given + var payload = new { AppId = 7, Path = "/home" }; + + loggingBrokerMock + .Setup(expression: broker => broker.LogDebug( + "Workflow trigger event: AppId {AppId}, Context {EventContext}", + It.IsAny())); + + // When + (int? AppId, string EventContext) result = workflowEventProcessingService + .PrepareWorkflowEventDispatch( + payload: payload, + eventName: "page_update"); + + // Then + result.AppId.Should().Be(expected: 7); + result.EventContext.Should().Be(expected: "page_update/home"); + loggingBrokerMock.VerifyAll(); + } + + [Fact] + public void ShouldPrepareWorkflowEventDispatchWithOverridesAndMissingPath() + { + // Given + var payload = new { Name = "Payload" }; + + loggingBrokerMock + .Setup(expression: broker => broker.LogDebug( + "Workflow trigger event: AppId {AppId}, Context {EventContext}", + It.IsAny())); + + // When + (int? AppId, string EventContext) result = workflowEventProcessingService + .PrepareWorkflowEventDispatch( + payload: payload, + eventName: "event", + appIdOverride: 9); + + // Then + result.AppId.Should().Be(expected: 9); + result.EventContext.Should().Be(expected: "event"); + loggingBrokerMock.VerifyAll(); + } + + [Fact] + public void ShouldSerializeWorkflowEventPayload() + { + // Given + object payload = new { Value = 1 }; + + jsonBrokerMock + .Setup(expression: broker => broker.Serialize(value: payload)) + .Returns(value: "serialized"); + + // When + string result = workflowEventProcessingService + .SerializeWorkflowEventPayload(payload: payload); + + // Then + result.Should().Be(expected: "serialized"); + jsonBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldLogWorkflowEventQueueFailureAsync() + { + // Given + WorkflowEvent workflowEvent = CreateRandomWorkflowEvent(); + Exception exception = new(message: "Queue failed"); + + loggingBrokerMock + .Setup(expression: broker => broker.LogWarning( + exception: exception, + message: "Failed to queue a new workflow instance for subscription {SubscriptionId}, flow {FlowId}.", + args: It.IsAny())); + + // When + await workflowEventProcessingService.LogWorkflowEventQueueFailureAsync( + workflowEvent: workflowEvent, + exception: exception); + + // Then + loggingBrokerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..701b8ae --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowEventProcessingServiceTests.Exceptions.cs @@ -0,0 +1,89 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public partial class WorkflowEventProcessingServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetAllFailure(Exception exception, Type expectedType) + { + // Given + workflowEventServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: false)) + .Throws(exception: exception); + + // When + Action action = () => workflowEventProcessingService.GetAll(); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapAddWorkflowEventAsyncFailure(Exception exception, Type expectedType) + { + // Given + WorkflowEvent item = CreateRandomWorkflowEvent(); + + workflowEventServiceMock + .Setup(expression: service => service.AddWorkflowEventAsync( + newWorkflowEvent: item)) + .Throws(exception: exception); + + // When + Func action = async () => await workflowEventProcessingService + .AddWorkflowEventAsync(newEntity: item); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapDeleteAsyncFailure(Exception exception, Type expectedType) + { + // Given + Guid id = Guid.NewGuid(); + + workflowEventServiceMock + .Setup(expression: service => service.DeleteAsync(workflowEventId: id)) + .Throws(exception: exception); + + // When + Func action = async () => await workflowEventProcessingService + .DeleteAsync(workflowEventId: id); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowInstanceProcessingServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowInstanceProcessingServiceTests.Exceptions.cs new file mode 100644 index 0000000..d89b1ed --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowInstanceProcessingServiceTests.Exceptions.cs @@ -0,0 +1,84 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public sealed partial class WorkflowInstanceProcessingServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapGetAllFailure(Exception exception, Type expectedType) + { + // Given + flowInstanceDataManagerMock + .Setup(expression: manager => manager.GetAll(ignoreFilters: false)) + .Throws(exception: exception); + + // When + Action action = () => processingService.GetAll(); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapExecuteWaitingQueuedInstanceFailureAsync( + Exception exception, + Type expectedType) + { + // Given + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.UpdateQueuedInstanceClaimAsync( + flowInstanceDataId: It.IsAny(), + cancellationToken: It.IsAny())) + .Throws(exception: exception); + + // When + Func action = async () => await processingService + .ExecuteWaitingQueuedInstanceByIdAsync( + flowInstanceDataId: Guid.NewGuid()); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown.Should().BeOfType(expectedType: expectedType); + } + + [Fact] + public async Task ShouldStopContinuousMaintenanceWhenCancelledAsync() + { + // Given + using CancellationTokenSource cancellation = new(); + cancellation.Cancel(); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.FlushOldInstancesAsync( + cutoff: It.IsAny(), + cancellationToken: cancellation.Token)) + .ReturnsAsync(value: 0); + + // When + await processingService.RunInstanceMaintenanceContinuouslyAsync( + cancellationToken: cancellation.Token); + + // Then + workflowInstanceManagementBrokerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowInstanceProcessingServiceTests.ExecuteWaiting.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowInstanceProcessingServiceTests.ExecuteWaiting.cs new file mode 100644 index 0000000..f2cfd09 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowInstanceProcessingServiceTests.ExecuteWaiting.cs @@ -0,0 +1,205 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Workflow; +using cCoder.Security.Exposures; +using cCoder.Security.Models.Entities; +using cCoder.Workflow.Activities.Models; +using System.Net; +using System.Net.Sockets; +using System.Text; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public sealed partial class WorkflowInstanceProcessingServiceTests +{ + [Fact] + public async Task ShouldIgnoreMissingClaimedWorkflowInstanceAsync() + { + // Given + Guid instanceId = Guid.NewGuid(); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.UpdateQueuedInstanceClaimAsync( + flowInstanceDataId: instanceId, + cancellationToken: It.IsAny())) + .ReturnsAsync(value: 1); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.SelectClaimedInstanceAsync( + flowInstanceDataId: instanceId, + cancellationToken: It.IsAny())) + .ReturnsAsync(value: (FlowInstanceData)null); + + // When + await processingService.ExecuteWaitingQueuedInstanceByIdAsync( + flowInstanceDataId: instanceId); + + // Then + workflowInstanceManagementBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldMarkClaimedWorkflowInstanceFailedWhenTokenIssueFailsAsync() + { + // Given + FlowInstanceData instance = CreateQueuedFlowInstanceData(); + Exception exception = new(message: "Token issue failed"); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.UpdateQueuedInstanceClaimAsync( + flowInstanceDataId: instance.Id, + cancellationToken: It.IsAny())) + .ReturnsAsync(value: 1); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.SelectClaimedInstanceAsync( + flowInstanceDataId: instance.Id, + cancellationToken: It.IsAny())) + .ReturnsAsync(value: instance); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.MarkInstanceFailedAsync( + flowInstanceDataId: instance.Id, + failedAt: It.IsAny(), + cancellationToken: It.IsAny())) + .ReturnsAsync(value: 1); + + serviceProviderMock + .Setup(expression: provider => provider.GetService( + serviceType: typeof(ITokenManager))) + .Throws(exception: exception); + + loggingBrokerMock + .Setup(expression: broker => broker.LogError( + exception: exception, + message: "Flow instance {InstanceId} execution failed.", + args: instance.Id)); + + // When + await processingService.ExecuteWaitingQueuedInstanceByIdAsync( + flowInstanceDataId: instance.Id); + + // Then + workflowInstanceManagementBrokerMock.VerifyAll(); + serviceProviderMock.VerifyAll(); + loggingBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldMarkClaimedWorkflowInstanceFailedWhenWorkflowApiFailsAsync() + { + // Given + FlowInstanceData instance = CreateQueuedFlowInstanceData(); + Mock tokenManagerMock = new(); + configuration.ServiceUrl = "http://127.0.0.1:1/"; + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.UpdateQueuedInstanceClaimAsync( + flowInstanceDataId: instance.Id, + cancellationToken: It.IsAny())) + .ReturnsAsync(value: 1); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.SelectClaimedInstanceAsync( + flowInstanceDataId: instance.Id, + cancellationToken: It.IsAny())) + .ReturnsAsync(value: instance); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.MarkInstanceFailedAsync( + flowInstanceDataId: instance.Id, + failedAt: It.IsAny(), + cancellationToken: It.IsAny())) + .ReturnsAsync(value: 1); + + serviceProviderMock + .Setup(expression: provider => provider.GetService( + serviceType: typeof(ITokenManager))) + .Returns(value: tokenManagerMock.Object); + + tokenManagerMock + .Setup(expression: manager => manager.IssueTokenAsync( + userId: instance.Caller, + tokenUse: TokenUse.WorkflowExecution)) + .ReturnsAsync(value: new Token { Id = "token" }); + + // When + await processingService.ExecuteWaitingQueuedInstanceByIdAsync( + flowInstanceDataId: instance.Id); + + // Then + workflowInstanceManagementBrokerMock.VerifyAll(); + serviceProviderMock.VerifyAll(); + tokenManagerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldMarkClaimedWorkflowInstanceFailedForUnsuccessfulResponseAsync() + { + // Given + FlowInstanceData instance = CreateQueuedFlowInstanceData(); + Mock tokenManagerMock = new(); + using TcpListener listener = new(IPAddress.Loopback, port: 0); + listener.Start(); + int port = ((IPEndPoint)listener.LocalEndpoint).Port; + configuration.ServiceUrl = $"http://127.0.0.1:{port}/"; + + Task responseTask = Task.Run(async () => + { + using TcpClient client = await listener.AcceptTcpClientAsync(); + using NetworkStream stream = client.GetStream(); + byte[] buffer = new byte[4096]; + _ = await stream.ReadAsync(buffer); + byte[] response = Encoding.ASCII.GetBytes( + "HTTP/1.1 500 Internal Server Error\r\n" + + "Content-Length: 6\r\nConnection: close\r\n\r\nfailed"); + + await stream.WriteAsync(response); + }); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.UpdateQueuedInstanceClaimAsync( + flowInstanceDataId: instance.Id, + cancellationToken: It.IsAny())) + .ReturnsAsync(value: 1); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.SelectClaimedInstanceAsync( + flowInstanceDataId: instance.Id, + cancellationToken: It.IsAny())) + .ReturnsAsync(value: instance); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.MarkInstanceFailedAsync( + flowInstanceDataId: instance.Id, + failedAt: It.IsAny(), + cancellationToken: It.IsAny())) + .ReturnsAsync(value: 1); + + serviceProviderMock + .Setup(expression: provider => provider.GetService( + serviceType: typeof(ITokenManager))) + .Returns(value: tokenManagerMock.Object); + + tokenManagerMock + .Setup(expression: manager => manager.IssueTokenAsync( + userId: instance.Caller, + tokenUse: TokenUse.WorkflowExecution)) + .ReturnsAsync(value: new Token { Id = "token" }); + + // When + await processingService.ExecuteWaitingQueuedInstanceByIdAsync( + flowInstanceDataId: instance.Id); + + await responseTask; + + // Then + workflowInstanceManagementBrokerMock.VerifyAll(); + serviceProviderMock.VerifyAll(); + tokenManagerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowInstanceProcessingServiceTests.Maintenance.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowInstanceProcessingServiceTests.Maintenance.cs new file mode 100644 index 0000000..d4d8985 --- /dev/null +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowInstanceProcessingServiceTests.Maintenance.cs @@ -0,0 +1,193 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Core.Services.Tests.Workflow.Processings; + +public sealed partial class WorkflowInstanceProcessingServiceTests +{ + [Fact] + public void ShouldReturnWorkflowExecutionStatistics() + { + // Given + object[] expected = [new { Failed = 2 }]; + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.GetFailedExecutionStats()) + .Returns(value: expected); + + // When + object[] actual = processingService.GetStats(); + + // Then + actual.Should().BeSameAs(expected: expected); + workflowInstanceManagementBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldLogDroppedWorkflowInstancesDuringMaintenanceAsync() + { + // Given + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.FlushOldInstancesAsync( + cutoff: It.IsAny(), + cancellationToken: It.IsAny())) + .ReturnsAsync(value: 3); + + loggingBrokerMock + .Setup(expression: broker => broker.LogInformation( + "Dropped {Count} Workflow instances older than {MaxAge}.", + It.IsAny())); + + // When + await processingService.RunInstanceMaintenanceAsync(); + + // Then + workflowInstanceManagementBrokerMock.VerifyAll(); + loggingBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldLogMaintenanceFailureAndInnerFailureAsync() + { + // Given + Exception innerException = new(message: "inner"); + Exception exception = new(message: "outer", innerException: innerException); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.FlushOldInstancesAsync( + cutoff: It.IsAny(), + cancellationToken: It.IsAny())) + .Throws(exception: exception); + + loggingBrokerMock + .Setup(expression: broker => broker.LogError( + exception: exception, + message: exception.Message)); + + loggingBrokerMock + .Setup(expression: broker => broker.LogError( + exception: innerException, + message: innerException.Message)); + + // When + await processingService.RunInstanceMaintenanceAsync(); + + // Then + loggingBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldSkipContinuousMaintenanceDuringMigrationAsync() + { + // Given + configuration.IsMigrating = true; + + // When + await processingService.RunInstanceMaintenanceContinuouslyAsync(); + + // Then + workflowInstanceManagementBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldSkipContinuousQueueProcessingDuringMigrationAsync() + { + // Given + configuration.IsMigrating = true; + + // When + await processingService + .RunQueueInstanceBackgroundServiceDependencyContinuouslyAsync(); + + // Then + workflowInstanceManagementBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldMapInvalidQueuePollingIntervalAsync() + { + // Given + using CancellationTokenSource cancellation = new(); + cancellation.Cancel(); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.GetQueuedInstances()) + .Returns(value: []); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.RequeueHungExecutingInstancesAsync( + cutoff: It.IsAny(), + cancellationToken: cancellation.Token)) + .ReturnsAsync(value: 0); + + // When + Func action = async () => await processingService + .RunQueueInstanceBackgroundServiceDependencyContinuouslyAsync( + cancellationToken: cancellation.Token); + + // Then + await action.Should().ThrowAsync(); + workflowInstanceManagementBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldStopContinuousQueueProcessingWhenCancelledAsync() + { + // Given + using CancellationTokenSource cancellation = new(); + cancellation.Cancel(); + configuration.QueueInstanceManagement.PollingIntervalMilliseconds = 10; + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.GetQueuedInstances()) + .Returns(value: []); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.RequeueHungExecutingInstancesAsync( + cutoff: It.IsAny(), + cancellationToken: cancellation.Token)) + .ReturnsAsync(value: 0); + + // When + await processingService + .RunQueueInstanceBackgroundServiceDependencyContinuouslyAsync( + cancellationToken: cancellation.Token); + + // Then + workflowInstanceManagementBrokerMock.VerifyAll(); + } + + [Fact] + public async Task ShouldLogQueueProcessingFailureAndInnerFailureAsync() + { + // Given + Exception innerException = new(message: "inner"); + Exception exception = new(message: "outer", innerException: innerException); + + workflowInstanceManagementBrokerMock + .Setup(expression: broker => broker.GetQueuedInstances()) + .Throws(exception: exception); + + loggingBrokerMock + .Setup(expression: broker => broker.LogError( + exception: exception, + message: exception.Message)); + + loggingBrokerMock + .Setup(expression: broker => broker.LogError( + exception: innerException, + message: innerException.Message)); + + // When + await processingService + .RunQueueInstanceBackgroundServiceDependencyAsync(); + + // Then + loggingBrokerMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowInstanceProcessingServiceTests.cs b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowInstanceProcessingServiceTests.cs index 136d359..c3650d5 100644 --- a/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowInstanceProcessingServiceTests.cs +++ b/src/cCoder.Workflow.Tests/Workflow/Processings/WorkflowInstanceProcessingServiceTests.cs @@ -16,19 +16,26 @@ namespace cCoder.Core.Services.Tests.Workflow.Processings; +#pragma warning disable STXFORMAT005, STXFORMAT008, STXFORMAT009 public sealed partial class WorkflowInstanceProcessingServiceTests { private readonly Mock workflowInstanceManagementBrokerMock; private readonly Mock flowInstanceDataManagerMock; private readonly Mock loggingBrokerMock; + private readonly Mock serviceProviderMock; + private readonly WorkflowConfiguration configuration; private readonly WorkflowInstanceProcessingService processingService; public WorkflowInstanceProcessingServiceTests() { - workflowInstanceManagementBrokerMock = new Mock(behavior: MockBehavior.Strict); - flowInstanceDataManagerMock = new Mock(behavior: MockBehavior.Strict); + workflowInstanceManagementBrokerMock = new( + behavior: MockBehavior.Strict); + + flowInstanceDataManagerMock = new( + behavior: MockBehavior.Strict); loggingBrokerMock = new(); - WorkflowConfiguration configuration = new() + serviceProviderMock = new(); + configuration = new() { ServiceUrl = "https://workflow.test/", SslPort = 7157, @@ -36,19 +43,18 @@ public WorkflowInstanceProcessingServiceTests() { MaxAgeDays = 5 }, - QueueInstanceManagement = - new WorkflowQueueInstanceManagementConfiguration - { - ExecutingTimeoutMinutes = 45 - } + QueueInstanceManagement = new() + { + ExecutingTimeoutMinutes = 45 + } }; processingService = new WorkflowInstanceProcessingService( - workflowInstanceManagementBrokerMock.Object, - flowInstanceDataManagerMock.Object, - Mock.Of(), - configuration, - loggingBrokerMock.Object); + workflowInstanceManagementBroker: workflowInstanceManagementBrokerMock.Object, + flowInstanceDataManager: flowInstanceDataManagerMock.Object, + serviceProvider: serviceProviderMock.Object, + workflowConfiguration: configuration, + log: loggingBrokerMock.Object); } [Theory] @@ -282,4 +288,5 @@ private static FlowInstanceData CreateQueuedFlowInstanceData() => App = new App { Domain = "tenant.test" }, }, }; -} \ No newline at end of file +} +#pragma warning restore STXFORMAT005, STXFORMAT008, STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.Exceptions.cs b/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.Exceptions.cs new file mode 100644 index 0000000..d85c879 --- /dev/null +++ b/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.Exceptions.cs @@ -0,0 +1,100 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Brokers.ServiceProviders; +using cCoder.Workflow.Dependencies.ServiceProviders; +using cCoder.Workflow.Models; +using cCoder.Workflow.Services.Aggregations; +using cCoder.Workflow.Services.Orchestrations; +using FluentAssertions; +using Moq; +using Xunit; +using IJsonBroker = cCoder.Workflow.Brokers.IJsonBroker; + +namespace cCoder.Workflow.Tests; + +#pragma warning disable STXFORMAT009 +public sealed partial class WorkflowMigrationAggregationServiceTests +{ + public static TheoryData ExceptionMappings => + cCoder.Core.Services.Tests.Workflow.Foundations + .FlowDefinitionServiceTests.ExceptionMappings; + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public void ShouldMapExportPackageFailure( + Exception exception, + Type expectedType) + { + // Given + Mock brokerMock = new(); + + brokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: WorkflowMigrationOperation.Calendar)) + .Throws(exception: exception); + + WorkflowMigrationAggregationService service = + new(serviceProviderBroker: brokerMock.Object); + + // When + Action action = () => service.ExportPackage( + appId: 1, + packageName: "Calendars"); + + // Then + action + .Should() + .Throw() + .Which + .Should() + .BeOfType(expectedType: expectedType); + } + + [Theory] + [MemberData(nameof(ExceptionMappings))] + public async Task ShouldMapImportPackageWorkflowPackageAsyncFailure( + Exception exception, + Type expectedType) + { + // Given + Mock brokerMock = new(); + + brokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: WorkflowMigrationOperation.Json)) + .Throws(exception: exception); + + WorkflowMigrationAggregationService service = + new(serviceProviderBroker: brokerMock.Object); + + WorkflowPackage package = new() + { + Items = + [ + new WorkflowPackageItem + { + Type = "Workflow/FlowDefinition", + Data = "[]" + } + ] + }; + + // When + Func action = async () => await service + .ImportPackageWorkflowPackageAsync(appId: 1, package: package); + + // Then + Exception thrown = (await action + .Should() + .ThrowAsync()).Which; + + thrown + .Should() + .BeOfType(expectedType: expectedType); + } +} +#pragma warning restore STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.ExportKnownPackages.cs b/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.ExportKnownPackages.cs new file mode 100644 index 0000000..209fd74 --- /dev/null +++ b/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.ExportKnownPackages.cs @@ -0,0 +1,194 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.CMS; +using cCoder.Data.Models.Planning; +using cCoder.Data.Models.Workflow; +using cCoder.Workflow.Brokers; +using cCoder.Workflow.Brokers.ServiceProviders; +using cCoder.Workflow.Dependencies.ServiceProviders; +using cCoder.Workflow.Models; +using cCoder.Workflow.Services.Aggregations; +using cCoder.Workflow.Services.Orchestrations; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Tests; + +#pragma warning disable STXFORMAT009 +public sealed partial class WorkflowMigrationAggregationServiceTests +{ + [Fact] + public void ShouldExportCalendarsForApp() + { + // Given + Mock brokerMock = new(); + Mock calendarServiceMock = new(); + Calendar included = new() { AppId = 7, Name = "Included" }; + Calendar excluded = new() { AppId = 8, Name = "Excluded" }; + + calendarServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { included, excluded }.AsQueryable()); + + brokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: WorkflowMigrationOperation.Calendar)) + .Returns(value: calendarServiceMock.Object); + + WorkflowMigrationAggregationService service = + new(serviceProviderBroker: brokerMock.Object); + + // When + WorkflowPackage result = service.ExportPackage( + appId: 7, + packageName: "Calendars"); + + // Then + result.Name.Should().Be(expected: "Calendars"); + result.Items.Should().ContainSingle(); + result.Items.Single().Data.Should().Contain(expected: "Included"); + result.Items.Single().Data.Should().NotContain(unexpected: "Excluded"); + calendarServiceMock.VerifyAll(); + } + + [Fact] + public void ShouldExportCalendarEventsForApp() + { + // Given + Mock brokerMock = new(); + Mock eventServiceMock = new(); + Calendar includedCalendar = new() { AppId = 7, Name = "Included calendar" }; + Calendar excludedCalendar = new() { AppId = 8, Name = "Excluded calendar" }; + + CalendarEvent included = new() + { + Name = "Included event", + Calendar = includedCalendar + }; + + CalendarEvent excluded = new() + { + Name = "Excluded event", + Calendar = excludedCalendar + }; + + eventServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { included, excluded, new CalendarEvent() } + .AsQueryable()); + + brokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: WorkflowMigrationOperation.CalendarEvent)) + .Returns(value: eventServiceMock.Object); + + WorkflowMigrationAggregationService service = + new(serviceProviderBroker: brokerMock.Object); + + // When + WorkflowPackage result = service.ExportPackage( + appId: 7, + packageName: "CalendarEvents"); + + // Then + result.Items.Single().Data.Should().Contain(expected: "Included event"); + result.Items.Single().Data.Should().NotContain(unexpected: "Excluded event"); + eventServiceMock.VerifyAll(); + } + + [Fact] + public void ShouldExportFlowDefinitionsForApp() + { + // Given + Mock brokerMock = new(); + Mock flowServiceMock = new(); + App app = new() { Name = "Process" }; + + FlowDefinition included = new() + { + AppId = 7, + App = app, + Name = "Included flow" + }; + + FlowDefinition excluded = new() + { + AppId = 8, + App = app, + Name = "Excluded flow" + }; + + flowServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { included, excluded }.AsQueryable()); + + brokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: WorkflowMigrationOperation.FlowDefinition)) + .Returns(value: flowServiceMock.Object); + + brokerMock + .Setup(expression: broker => broker.GetOperationService( + operation: WorkflowMigrationOperation.Json)) + .Returns(value: new JsonBroker()); + + WorkflowMigrationAggregationService service = + new(serviceProviderBroker: brokerMock.Object); + + // When + WorkflowPackage result = service.ExportPackage( + appId: 7, + packageName: "Workflows"); + + // Then + result.Items.Single().Data.Should().Contain(expected: "Included flow"); + result.Items.Single().Data.Should().NotContain(unexpected: "Excluded flow"); + flowServiceMock.VerifyAll(); + } + + [Fact] + public void ShouldExportScheduledTasksForApp() + { + // Given + Mock brokerMock = new(); + Mock taskServiceMock = new(); + FlowDefinition flow = new() { Name = "Flow" }; + ScheduledTask included = new() { AppId = 7, Name = "Included task", Flow = flow }; + ScheduledTask excluded = new() { AppId = 8, Name = "Excluded task", Flow = flow }; + + taskServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { included, excluded }.AsQueryable()); + + brokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: WorkflowMigrationOperation.ScheduledTask)) + .Returns(value: taskServiceMock.Object); + + brokerMock + .Setup(expression: broker => broker.GetOperationService( + operation: WorkflowMigrationOperation.Json)) + .Returns(value: new JsonBroker()); + + WorkflowMigrationAggregationService service = + new(serviceProviderBroker: brokerMock.Object); + + // When + WorkflowPackage result = service.ExportPackage( + appId: 7, + packageName: "ScheduledTasks"); + + // Then + result.Items.Single().Data.Should().Contain(expected: "Included task"); + result.Items.Single().Data.Should().NotContain(unexpected: "Excluded task"); + taskServiceMock.VerifyAll(); + } +} +#pragma warning restore STXFORMAT009 \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.ExportPackage.cs b/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.ExportPackage.cs new file mode 100644 index 0000000..0d75f81 --- /dev/null +++ b/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.ExportPackage.cs @@ -0,0 +1,42 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Workflow.Brokers.ServiceProviders; +using cCoder.Workflow.Models; +using cCoder.Workflow.Services.Aggregations; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Tests; + +public sealed partial class WorkflowMigrationAggregationServiceTests +{ + [Fact] + public void ShouldExportEmptyPackageForUnknownPackageName() + { + // Given + Mock brokerMock = + new(behavior: MockBehavior.Strict); + + WorkflowMigrationAggregationService service = + new(serviceProviderBroker: brokerMock.Object); + + // When + WorkflowPackage package = service.ExportPackage( + appId: 1, + packageName: "Unknown"); + + // Then + package.Name + .Should() + .Be(expected: "Unknown"); + + package.Items + .Should() + .BeEmpty(); + + brokerMock.VerifyNoOtherCalls(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.ImportCalendarEvents.cs b/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.ImportCalendarEvents.cs new file mode 100644 index 0000000..daa29e1 --- /dev/null +++ b/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.ImportCalendarEvents.cs @@ -0,0 +1,109 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Brokers; +using cCoder.Workflow.Brokers.ServiceProviders; +using cCoder.Workflow.Dependencies.ServiceProviders; +using cCoder.Workflow.Models; +using cCoder.Workflow.Models.Results; +using cCoder.Workflow.Services.Aggregations; +using cCoder.Workflow.Services.Orchestrations; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Tests; + +public sealed partial class WorkflowMigrationAggregationServiceTests +{ + [Fact] + public async Task ShouldImportOnlyNewCalendarEventsWithKnownCalendarsAsync() + { + // Given + Mock brokerMock = new(); + Mock calendarServiceMock = new(); + Mock eventServiceMock = new(); + Mock> loggerMock = new(); + Calendar calendar = new() { Id = 11, AppId = 7, Name = "Calendar" }; + + CalendarEvent existing = new() + { + Id = 12, + Name = "Existing", + Calendar = calendar, + CalendarId = calendar.Id + }; + + CalendarEvent[] captured = null; + + calendarServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { calendar }.AsQueryable()); + + eventServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { existing }.AsQueryable()); + + eventServiceMock + .Setup(expression: service => service.AddOrUpdateCalendarEvent( + items: It.IsAny>())) + .Callback>(action: items => captured = items.ToArray()) + .Returns(value: ValueTask.FromResult>>( + result: [new() { Success = true }])); + + brokerMock + .Setup(expression: broker => broker.GetOperationService( + operation: WorkflowMigrationOperation.Json)) + .Returns(value: new JsonBroker()); + + brokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: WorkflowMigrationOperation.Calendar)) + .Returns(value: calendarServiceMock.Object); + + brokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: WorkflowMigrationOperation.CalendarEvent)) + .Returns(value: eventServiceMock.Object); + + brokerMock + .Setup(expression: broker => broker + .GetOperationService>( + operation: WorkflowMigrationOperation.Logging)) + .Returns(value: loggerMock.Object); + + WorkflowMigrationAggregationService service = + new(serviceProviderBroker: brokerMock.Object); + + WorkflowPackage package = new() + { + Items = + [ + new() + { + Type = "Core/CalendarEvent", + Data = "[" + + "{\"Name\":\"New\",\"CalendarName\":\"Calendar\"}," + + "{\"Name\":\"Existing\",\"CalendarName\":\"Calendar\"}," + + "{\"Name\":\"Unknown\",\"CalendarName\":\"Missing\"}" + + "]" + } + ] + }; + + // When + await service.ImportPackageWorkflowPackageAsync(appId: 7, package: package); + + // Then + captured.Should().ContainSingle(); + captured.Single().Name.Should().Be(expected: "New"); + captured.Single().CalendarId.Should().Be(expected: calendar.Id); + calendarServiceMock.VerifyAll(); + eventServiceMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.ImportCalendars.cs b/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.ImportCalendars.cs new file mode 100644 index 0000000..600e769 --- /dev/null +++ b/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.ImportCalendars.cs @@ -0,0 +1,189 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Workflow.Brokers; +using cCoder.Workflow.Brokers.ServiceProviders; +using cCoder.Workflow.Dependencies.ServiceProviders; +using cCoder.Workflow.Models; +using cCoder.Workflow.Models.Results; +using cCoder.Workflow.Services.Aggregations; +using cCoder.Workflow.Services.Orchestrations; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Tests; + +public sealed partial class WorkflowMigrationAggregationServiceTests +{ + [Fact] + public async Task ShouldImportNewCalendarsAndIgnoreExistingCalendarsAsync() + { + // Given + Mock brokerMock = new(); + Mock calendarServiceMock = new(); + Calendar existing = new() { Id = 11, AppId = 7, Name = "Existing" }; + Calendar[] captured = null; + + calendarServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: new[] { existing }.AsQueryable()); + + calendarServiceMock + .Setup(expression: service => service.AddOrUpdateCalendar( + items: It.IsAny>())) + .Callback>(action: items => captured = items.ToArray()) + .Returns(value: ValueTask.FromResult>>( + result: [new() { Success = true }])); + + brokerMock + .Setup(expression: broker => broker.GetOperationService( + operation: WorkflowMigrationOperation.Json)) + .Returns(value: new JsonBroker()); + + brokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: WorkflowMigrationOperation.Calendar)) + .Returns(value: calendarServiceMock.Object); + + WorkflowMigrationAggregationService service = + new(serviceProviderBroker: brokerMock.Object); + + WorkflowPackage package = new() + { + Items = + [ + new() + { + Type = "Core/Calendar", + Data = "[{\"Name\":\"Existing\"},{\"Name\":\"New\"}]" + } + ] + }; + + // When + await service.ImportPackageWorkflowPackageAsync(appId: 7, package: package); + + // Then + captured.Should().ContainSingle(); + captured.Single().Name.Should().Be(expected: "New"); + captured.Single().AppId.Should().Be(expected: 7); + calendarServiceMock.VerifyAll(); + } + + [Fact] + public async Task ShouldImportSingleCalendarObjectAsync() + { + // Given + Mock brokerMock = new(); + Mock calendarServiceMock = new(); + + calendarServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: Array.Empty().AsQueryable()); + + calendarServiceMock + .Setup(expression: service => service.AddOrUpdateCalendar( + items: It.Is>( + match: items => items.Single().Name == "Single"))) + .Returns(value: ValueTask.FromResult>>( + result: [new() { Success = true }])); + + brokerMock + .Setup(expression: broker => broker.GetOperationService( + operation: WorkflowMigrationOperation.Json)) + .Returns(value: new JsonBroker()); + + brokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: WorkflowMigrationOperation.Calendar)) + .Returns(value: calendarServiceMock.Object); + + WorkflowMigrationAggregationService service = + new(serviceProviderBroker: brokerMock.Object); + + WorkflowPackage package = new() + { + Items = [new() { Type = "Core/Calendar", Data = "{\"Name\":\"Single\"}" }] + }; + + // When + await service.ImportPackageWorkflowPackageAsync(appId: 7, package: package); + + // Then + calendarServiceMock.VerifyAll(); + } + + [Fact] + public async Task ShouldIgnoreEmptyWorkflowPackageAsync() + { + // Given + Mock brokerMock = new( + behavior: MockBehavior.Strict); + + WorkflowMigrationAggregationService service = + new(serviceProviderBroker: brokerMock.Object); + + // When + await service.ImportPackageWorkflowPackageAsync( + appId: 7, + package: new WorkflowPackage { Items = [] }); + + // Then + brokerMock.VerifyNoOtherCalls(); + } + + [Theory] + [InlineData(null, null)] + [InlineData("calendar-1", null)] + [InlineData(null, "Import failed")] + public async Task ShouldRejectFailedCalendarImportAsync( + string resultId, + string message) + { + // Given + Mock brokerMock = new(); + Mock calendarServiceMock = new(); + + calendarServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: Array.Empty().AsQueryable()); + + calendarServiceMock + .Setup(expression: service => service.AddOrUpdateCalendar( + items: It.IsAny>())) + .Returns(value: ValueTask.FromResult>>( + result: [new() { Success = false, Id = resultId, Message = message }])); + + brokerMock + .Setup(expression: broker => broker.GetOperationService( + operation: WorkflowMigrationOperation.Json)) + .Returns(value: new JsonBroker()); + + brokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: WorkflowMigrationOperation.Calendar)) + .Returns(value: calendarServiceMock.Object); + + WorkflowMigrationAggregationService service = + new(serviceProviderBroker: brokerMock.Object); + + WorkflowPackage package = new() + { + Items = [new() { Type = "Core/Calendar", Data = "{\"Name\":\"Failed\"}" }] + }; + + // When + Func action = async () => await service + .ImportPackageWorkflowPackageAsync(appId: 7, package: package); + + // Then + await action.Should().ThrowAsync(); + calendarServiceMock.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.ImportScheduledTasks.cs b/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.ImportScheduledTasks.cs new file mode 100644 index 0000000..37659da --- /dev/null +++ b/src/cCoder.Workflow.Tests/WorkflowMigrationAggregationServiceTests.ImportScheduledTasks.cs @@ -0,0 +1,78 @@ +// --------------------------------------------------------------- +// Copyright (c) Paul.Ward@ccoder.co.uk +// --------------------------------------------------------------- + +using cCoder.Data.Models.Planning; +using cCoder.Data.Models.Workflow; +using cCoder.Workflow.Brokers; +using cCoder.Workflow.Brokers.ServiceProviders; +using cCoder.Workflow.Dependencies.ServiceProviders; +using cCoder.Workflow.Models; +using cCoder.Workflow.Services.Aggregations; +using cCoder.Workflow.Services.Orchestrations; +using FluentAssertions; +using Moq; +using Xunit; + +namespace cCoder.Workflow.Tests; + +public sealed partial class WorkflowMigrationAggregationServiceTests +{ + [Fact] + public async Task ShouldRejectScheduledTaskWithMissingFlowAsync() + { + // Given + Mock brokerMock = new(); + Mock flowServiceMock = new(); + Mock taskServiceMock = new(); + + flowServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: Array.Empty().AsQueryable()); + + taskServiceMock + .Setup(expression: service => service.GetAll(ignoreFilters: true)) + .Returns(value: Array.Empty().AsQueryable()); + + brokerMock + .Setup(expression: broker => broker.GetOperationService( + operation: WorkflowMigrationOperation.Json)) + .Returns(value: new JsonBroker()); + + brokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: WorkflowMigrationOperation.FlowDefinition)) + .Returns(value: flowServiceMock.Object); + + brokerMock + .Setup(expression: broker => broker + .GetOperationService( + operation: WorkflowMigrationOperation.ScheduledTask)) + .Returns(value: taskServiceMock.Object); + + WorkflowMigrationAggregationService service = + new(serviceProviderBroker: brokerMock.Object); + + WorkflowPackage package = new() + { + Items = + [ + new() + { + Type = "Workflow/ScheduledTask", + Data = "{\"Name\":\"Task\",\"FlowName\":\"Missing\"}" + } + ] + }; + + // When + Func action = async () => await service + .ImportPackageWorkflowPackageAsync(appId: 7, package: package); + + // Then + await action.Should().ThrowAsync(); + flowServiceMock.VerifyAll(); + taskServiceMock.VerifyAll(); + } +} \ No newline at end of file