diff --git a/src/Invictus.Testing.Tests.Integration/IntegrationTest.cs b/src/Invictus.Testing.Tests.Integration/IntegrationTest.cs new file mode 100644 index 0000000..19d24a1 --- /dev/null +++ b/src/Invictus.Testing.Tests.Integration/IntegrationTest.cs @@ -0,0 +1,59 @@ +using Arcus.Testing.Logging; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Invictus.Testing.Tests.Integration +{ + /// + /// Provides set of reusable information required for the logic app integration tests. + /// + public abstract class IntegrationTest + { + /// + /// Initializes a new instance of the class. + /// + protected IntegrationTest(ITestOutputHelper outputWriter) + { + Logger = new XunitTestLogger(outputWriter); + ResourceGroup = Configuration.GetAzureResourceGroup(); + LogicAppName = Configuration.GetTestLogicAppName(); + LogicAppMockingName = Configuration.GetTestMockingLogicAppName(); + + string subscriptionId = Configuration.GetAzureSubscriptionId(); + string tenantId = Configuration.GetAzureTenantId(); + string clientId = Configuration.GetAzureClientId(); + string clientSecret = Configuration.GetAzureClientSecret(); + Authentication = LogicAuthentication.UsingServicePrincipal(tenantId, subscriptionId, clientId, clientSecret); + } + + /// + /// Gets the logger to write diagnostic messages during tests. + /// + protected ILogger Logger { get; } + + /// + /// Gets the configuration available in the current integration test suite. + /// + protected TestConfig Configuration { get; } = TestConfig.Create(); + + /// + /// Gets the resource group where the logic app resources on Azure are located. + /// + protected string ResourceGroup { get; } + + /// + /// Gets the name of the logic app resource running on Azure to test stateless operations against. + /// + protected string LogicAppName { get; } + + /// + /// Gets the name of the logic app resource running on Azure to test stateful operations against. + /// + protected string LogicAppMockingName { get; } + + /// + /// Gets the authentication mechanism to authenticate with Azure. + /// + protected LogicAuthentication Authentication { get; } + } +} diff --git a/src/Invictus.Testing.Tests.Integration/Invictus.Testing.Tests.Integration.csproj b/src/Invictus.Testing.Tests.Integration/Invictus.Testing.Tests.Integration.csproj index f993de8..49faa08 100644 --- a/src/Invictus.Testing.Tests.Integration/Invictus.Testing.Tests.Integration.csproj +++ b/src/Invictus.Testing.Tests.Integration/Invictus.Testing.Tests.Integration.csproj @@ -5,6 +5,7 @@ + diff --git a/src/Invictus.Testing.Tests.Integration/LogicAppClientTests.cs b/src/Invictus.Testing.Tests.Integration/LogicAppClientTests.cs new file mode 100644 index 0000000..8bfaecc --- /dev/null +++ b/src/Invictus.Testing.Tests.Integration/LogicAppClientTests.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Invictus.Testing.Model; +using Invictus.Testing.Serialization; +using Newtonsoft.Json.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace Invictus.Testing.Tests.Integration +{ + public class LogicAppClientTests : IntegrationTest + { + /// + /// Initializes a new instance of the class. + /// + public LogicAppClientTests(ITestOutputHelper outputWriter) : base(outputWriter) + { + } + + [Fact] + public async Task GetLogicAppTriggerUrl_NoTriggerNameSpecified_Success() + { + // Arrange + using (var logicApp = await LogicAppClient.CreateAsync(ResourceGroup, LogicAppName, Authentication)) + { + // Act + LogicAppTriggerUrl logicAppTriggerUrl = await logicApp.GetTriggerUrlAsync(); + + // Assert + Assert.NotNull(logicAppTriggerUrl.Value); + Assert.Equal("POST", logicAppTriggerUrl.Method); + } + } + + [Fact] + public async Task GetLogicAppTriggerUrl_ByName_Success() + { + using (var logicApp = await LogicAppClient.CreateAsync(ResourceGroup, LogicAppName, Authentication)) + { + // Act + LogicAppTriggerUrl logicAppTriggerUrl = await logicApp.GetTriggerUrlByNameAsync(triggerName: "manual"); + + // Assert + Assert.NotNull(logicAppTriggerUrl.Value); + Assert.Equal("POST", logicAppTriggerUrl.Method); + } + } + + [Fact] + public async Task TemporaryEnableSuccessStaticResultForAction_WithoutConsumerStaticResult_Success() + { + // Arrange + const string actionName = "HTTP"; + string correlationId = $"correlationId-{Guid.NewGuid()}"; + var headers = new Dictionary + { + { "correlationId", correlationId }, + }; + + // Act + using (var logicApp = await LogicAppClient.CreateAsync(ResourceGroup, LogicAppMockingName, Authentication, Logger)) + { + await using (await logicApp.TemporaryEnableAsync()) + { + await using (await logicApp.TemporaryEnableSuccessStaticResultAsync(actionName)) + { + // Act + await logicApp.TriggerAsync(headers); + LogicAppAction enabledAction = await PollForLogicAppActionAsync(correlationId, actionName); + + Assert.Equal(actionName, enabledAction.Name); + Assert.Equal("200", enabledAction.Outputs.statusCode.ToString()); + Assert.Equal("Succeeded", enabledAction.Status); + } + + await logicApp.TriggerAsync(headers); + LogicAppAction disabledAction = await PollForLogicAppActionAsync(correlationId, actionName); + + Assert.NotEmpty(disabledAction.Outputs.headers); + } + } + } + + [Fact] + public async Task TemporaryEnableStaticResultsForAction_WithSuccessStaticResult_Success() + { + // Arrange + const string actionName = "HTTP"; + string correlationId = $"correlationId-{Guid.NewGuid()}"; + var headers = new Dictionary + { + { "correlationId", correlationId }, + }; + + var definition = new StaticResultDefinition + { + Outputs = new Outputs + { + Headers = new Dictionary { { "testheader", "testvalue" } }, + StatusCode = "200", + Body = JToken.Parse("{id : 12345, name : 'test body'}") + }, + Status = "Succeeded" + }; + + // Act + using (var logicApp = await LogicAppClient.CreateAsync(ResourceGroup, LogicAppMockingName, Authentication, Logger)) + { + await using (await logicApp.TemporaryEnableAsync()) + { + var definitions = new Dictionary { [actionName] = definition }; + await using (await logicApp.TemporaryEnableStaticResultsAsync(definitions)) + { + // Act + await logicApp.TriggerAsync(headers); + LogicAppAction enabledAction = await PollForLogicAppActionAsync(correlationId, actionName); + + Assert.Equal("200", enabledAction.Outputs.statusCode.ToString()); + Assert.Equal("testvalue", enabledAction.Outputs.headers["testheader"].ToString()); + Assert.Contains("test body", enabledAction.Outputs.body.ToString()); + } + + await logicApp.TriggerAsync(headers); + LogicAppAction disabledAction = await PollForLogicAppActionAsync(correlationId, actionName); + + Assert.DoesNotContain("test body", disabledAction.Outputs.body.ToString()); + } + } + } + + [Fact] + public async Task TemporaryEnableStaticResultForAction_WithSuccessStaticResult_Success() + { + // Arrange + const string actionName = "HTTP"; + string correlationId = $"correlationId-{Guid.NewGuid()}"; + var headers = new Dictionary + { + { "correlationId", correlationId }, + }; + + var definition = new StaticResultDefinition + { + Outputs = new Outputs + { + Headers = new Dictionary { { "testheader", "testvalue" } }, + StatusCode = "200", + Body = JToken.Parse("{id : 12345, name : 'test body'}") + }, + Status = "Succeeded" + }; + + // Act + using (var logicApp = await LogicAppClient.CreateAsync(ResourceGroup, LogicAppMockingName, Authentication)) + { + await using (await logicApp.TemporaryEnableAsync()) + { + await using (await logicApp.TemporaryEnableStaticResultAsync(actionName, definition)) + { + // Act + await logicApp.TriggerAsync(headers); + LogicAppAction enabledAction = await PollForLogicAppActionAsync(correlationId, actionName); + + Assert.Equal("200", enabledAction.Outputs.statusCode.ToString()); + Assert.Equal("testvalue", enabledAction.Outputs.headers["testheader"].ToString()); + Assert.Contains("test body", enabledAction.Outputs.body.ToString()); + } + + await logicApp.TriggerAsync(headers); + LogicAppAction disabledAction = await PollForLogicAppActionAsync(correlationId, actionName); + + Assert.DoesNotContain("test body", disabledAction.Outputs.body.ToString()); + } + } + } + + [Fact] + public async Task TemporaryEnableLogicApp_Success() + { + // Act + using (var logicApp = await LogicAppClient.CreateAsync(ResourceGroup, LogicAppMockingName, Authentication, Logger)) + { + await using (await logicApp.TemporaryEnableAsync()) + { + // Assert + LogicApp metadata = await logicApp.GetMetadataAsync(); + Assert.Equal("Enabled", metadata.State); + } + { + LogicApp metadata = await logicApp.GetMetadataAsync(); + Assert.Equal("Disabled", metadata.State); + } + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public async Task Constructor_WithBlankResourceGroup_Fails(string resourceGroup) + { + await Assert.ThrowsAsync( + () => LogicAppClient.CreateAsync(resourceGroup, LogicAppName, Authentication)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public async Task Constructor_WithBlankLogicApp_Fails(string logicApp) + { + await Assert.ThrowsAsync( + () => LogicAppClient.CreateAsync(ResourceGroup, logicApp, Authentication)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public async Task ConstructorWithLogger_WithBlankResourceGroup_Fails(string resourceGroup) + { + await Assert.ThrowsAsync( + () => LogicAppClient.CreateAsync(resourceGroup, LogicAppName, Authentication, Logger)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public async Task ConstructorWithLogger_WithBlankLogicApp_Fails(string logicApp) + { + await Assert.ThrowsAsync( + () => LogicAppClient.CreateAsync(ResourceGroup, logicApp, Authentication, Logger)); + } + + [Fact] + public async Task Constructor_WithNullAuthentication_Fails() + { + await Assert.ThrowsAnyAsync( + () => LogicAppClient.CreateAsync(ResourceGroup, LogicAppName, authentication: null)); + } + + [Fact] + public async Task ConstructorWithLogger_WithNullAuthentication_Fails() + { + await Assert.ThrowsAnyAsync( + () => LogicAppClient.CreateAsync(ResourceGroup, LogicAppName, authentication: null, logger: Logger)); + } + + private async Task PollForLogicAppActionAsync(string correlationId, string actionName) + { + LogicAppRun logicAppRun = await LogicAppsProvider + .LocatedAt(ResourceGroup, LogicAppMockingName, Authentication, Logger) + .WithStartTime(DateTimeOffset.UtcNow.AddMinutes(-1)) + .WithCorrelationId(correlationId) + .PollForSingleLogicAppRunAsync(); + + Assert.True(logicAppRun.Actions.Count() != 0); + LogicAppAction logicAppAction = logicAppRun.Actions.First(action => action.Name.Equals(actionName)); + Assert.NotNull(logicAppAction); + + return logicAppAction; + } + } +} diff --git a/src/Invictus.Testing.Tests.Integration/LogicAppConverterTests.cs b/src/Invictus.Testing.Tests.Integration/LogicAppConverterTests.cs new file mode 100644 index 0000000..39583d7 --- /dev/null +++ b/src/Invictus.Testing.Tests.Integration/LogicAppConverterTests.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Bogus; +using Bogus.Extensions; +using Invictus.Testing.Model; +using Microsoft.Azure.Management.Logic.Models; +using Newtonsoft.Json; +using Xunit; + +namespace Invictus.Testing.Tests.Integration +{ + public class LogicAppConverterTests + { + private static readonly Faker BogusGenerator = new Faker(); + + [Fact] + public void ToLogicApp_WithWorkflow_CreatesAlternative() + { + // Arrange + string state = BogusGenerator.PickRandom("NotSpecified", "Completed", "Enabled", "Disabled", "Deleted", "Suspended"); + var workflow = new Workflow( + name: BogusGenerator.Internet.DomainName().OrNull(BogusGenerator), + createdTime: BogusGenerator.Date.Recent(), + changedTime: BogusGenerator.Date.Recent(), + state: state.OrNull(BogusGenerator), + version: BogusGenerator.System.Version().ToString().OrNull(BogusGenerator), + accessEndpoint: BogusGenerator.Internet.IpAddress().ToString().OrNull(BogusGenerator), + definition: BogusGenerator.Random.String().OrNull(BogusGenerator)); + + // Act + var actual = LogicAppConverter.ToLogicApp(workflow); + + // Assert + Assert.NotNull(actual); + Assert.Equal(workflow.Name, actual.Name); + Assert.Equal(workflow.CreatedTime, actual.CreatedTime); + Assert.Equal(workflow.ChangedTime, actual.ChangedTime); + Assert.Equal(workflow.State, actual.State); + Assert.Equal(workflow.Version, actual.Version); + Assert.Equal(workflow.AccessEndpoint, actual.AccessEndpoint); + Assert.Equal(workflow.Definition, actual.Definition); + } + + [Fact] + public void ToLogicAppAction_WithInputOutput_CreatesAlternative() + { + // Arrange + var trackedProperties = new Dictionary + { + [Guid.NewGuid().ToString()] = BogusGenerator.Random.Word() + }; + + string trackedPropertiesJson = JsonConvert.SerializeObject(trackedProperties).OrNull(BogusGenerator); + + var workflowAction = new WorkflowRunAction( + name: BogusGenerator.Internet.DomainName().OrNull(BogusGenerator), + startTime: BogusGenerator.Date.Past(), + endTime: BogusGenerator.Date.Past(), + status: GenerateStatus(), + error: BogusGenerator.Random.Bytes(10), + trackedProperties: trackedPropertiesJson); + var inputs = BogusGenerator.Random.String(); + var outputs = BogusGenerator.Random.String(); + + // Act + var actual = LogicAppConverter.ToLogicAppAction(workflowAction, inputs, outputs); + + // Assert + Assert.NotNull(actual); + Assert.Equal(workflowAction.Name, actual.Name); + Assert.Equal(workflowAction.StartTime, actual.StartTime); + Assert.Equal(workflowAction.EndTime, actual.EndTime); + Assert.Equal(workflowAction.Status, actual.Status); + Assert.Equal(workflowAction.Error, actual.Error); + Assert.Equal(inputs, actual.Inputs); + Assert.Equal(outputs, actual.Outputs); + Assert.True(trackedPropertiesJson == null || trackedProperties.SequenceEqual(actual.TrackedProperties)); + } + + [Fact] + public void ToLogicAppRun_WithWorkflowRunAndActions_CreatesCombinedModel() + { + // Arrange + WorkflowRunTrigger trigger = CreateWorkflowRunTrigger(); + WorkflowRun workflowRun = CreateWorkflowRun(trigger); + IEnumerable actions = CreateLogicAppActions(); + + // Act + var actual = LogicAppConverter.ToLogicAppRun(workflowRun, actions); + + // Assert + Assert.NotNull(actual); + Assert.Equal(workflowRun.Name, actual.Id); + Assert.Equal(workflowRun.Status, actual.Status); + Assert.Equal(workflowRun.StartTime, actual.StartTime); + Assert.Equal(workflowRun.EndTime, actual.EndTime); + Assert.Equal(workflowRun.Error, actual.Error); + Assert.Equal(workflowRun.Correlation?.ClientTrackingId, actual.CorrelationId); + Assert.Equal(actions, actual.Actions); + + Assert.Equal(trigger.Name, actual.Trigger.Name); + Assert.Equal(trigger.Inputs, actual.Trigger.Inputs); + Assert.Equal(trigger.Outputs, actual.Trigger.Outputs); + Assert.Equal(trigger.StartTime, actual.Trigger.StartTime); + Assert.Equal(trigger.EndTime, actual.Trigger.EndTime); + Assert.Equal(trigger.Status, actual.Trigger.Status); + Assert.Equal(trigger.Error, actual.Trigger.Error); + + Assert.All(actions.Where(action => action.TrackedProperties != null), action => + { + Assert.All(action.TrackedProperties, prop => + { + Assert.Contains(prop, actual.TrackedProperties); + }); + }); + } + + private static WorkflowRunTrigger CreateWorkflowRunTrigger() + { + var trigger = new WorkflowRunTrigger( + name: BogusGenerator.Internet.DomainName().OrNull(BogusGenerator), + inputs: BogusGenerator.Random.Word().OrNull(BogusGenerator), + outputs: BogusGenerator.Random.Word().OrNull(BogusGenerator), + startTime: BogusGenerator.Date.Past(), + endTime: BogusGenerator.Date.Recent(), + status: GenerateStatus(), + error: BogusGenerator.Random.Bytes(10).OrNull(BogusGenerator)); + return trigger; + } + + private static WorkflowRun CreateWorkflowRun(WorkflowRunTrigger trigger) + { + var correlation = new Correlation(BogusGenerator.Random.String().OrNull(BogusGenerator)).OrNull(BogusGenerator); + var workflowRun = new WorkflowRun( + name: BogusGenerator.Internet.DomainWord().OrNull(BogusGenerator), + startTime: BogusGenerator.Date.Recent(), + status: GenerateStatus(), + error: BogusGenerator.Random.Bytes(10).OrNull(BogusGenerator), + correlation: correlation, + trigger: trigger); + return workflowRun; + } + + private static IEnumerable CreateLogicAppActions() + { + int actionCount = BogusGenerator.Random.Int(1, 10); + int propertyCount = BogusGenerator.Random.Int(1, 10); + + Dictionary trackedProperties = + BogusGenerator.Make(propertyCount, () => new KeyValuePair(Guid.NewGuid().ToString(), BogusGenerator.Random.Word())) + .ToDictionary(item => item.Key, item => item.Value); + + IList actions = BogusGenerator.Make(actionCount, () => + { + return new LogicAppAction + { + Name = BogusGenerator.Internet.DomainWord().OrNull(BogusGenerator), + Inputs = BogusGenerator.Random.Words().OrNull(BogusGenerator), + Outputs = BogusGenerator.Random.Words().OrNull(BogusGenerator), + Status = GenerateStatus(), + StartTime = BogusGenerator.Date.Past(), + EndTime = BogusGenerator.Date.Recent(), + Error = BogusGenerator.Random.Byte(10).OrNull(BogusGenerator), + TrackedProperties = trackedProperties + }; + }); + + return actions; + } + + private static string GenerateStatus() + { + return BogusGenerator.PickRandom("NotSpecified", "Paused", "Running", + "Waiting", "Succeeded", "Skipped", "Suspended", "Cancelled", "Failed", "Faulted", + "TimedOut", "Aborted", "Ignored"); + } + } +} diff --git a/src/Invictus.Testing.Tests.Integration/LogicAppExceptionTests.cs b/src/Invictus.Testing.Tests.Integration/LogicAppExceptionTests.cs new file mode 100644 index 0000000..38a1d51 --- /dev/null +++ b/src/Invictus.Testing.Tests.Integration/LogicAppExceptionTests.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Invictus.Testing.Tests.Integration +{ + public class LogicAppExceptionTests + { + [Fact] + public void CreateException_WithAllProperties_AssignsAllProperties() + { + // Arrange + string logicApp = "logic app name", resourceGroup = "resource group", subscriptionId = "subscription ID"; + string message = "There's something wrong with the logic app"; + var innerException = new KeyNotFoundException("Couldn't find the logic app"); + + // Act + var exception = new LogicAppException(subscriptionId, resourceGroup, logicApp, message, innerException); + + // Assert + Assert.Equal(message, exception.Message); + Assert.Equal(logicApp, exception.LogicAppName); + Assert.Equal(resourceGroup, exception.ResourceGroup); + Assert.Equal(subscriptionId, exception.SubscriptionId); + Assert.Equal(innerException, exception.InnerException); + } + } +} diff --git a/src/Invictus.Testing.Tests.Integration/LogicAppNotUpdatedExceptionTests.cs b/src/Invictus.Testing.Tests.Integration/LogicAppNotUpdatedExceptionTests.cs new file mode 100644 index 0000000..94cebb0 --- /dev/null +++ b/src/Invictus.Testing.Tests.Integration/LogicAppNotUpdatedExceptionTests.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; +using Xunit; + +namespace Invictus.Testing.Tests.Integration +{ + public class LogicAppNotUpdatedExceptionTests + { + [Fact] + public void CreateException_WithAllProperties_AssignsAllProperties() + { + // Arrange + string logicApp = "logic app name", resourceGroup = "resource group", subscriptionId = "subscription ID"; + string message = "There's something wrong with updating the logic app"; + var innerException = new KeyNotFoundException("Couldn't find the logic app"); + + // Act + var exception = new LogicAppNotUpdatedException(subscriptionId, resourceGroup, logicApp, message, innerException); + + // Assert + Assert.Equal(message, exception.Message); + Assert.Equal(logicApp, exception.LogicAppName); + Assert.Equal(resourceGroup, exception.ResourceGroup); + Assert.Equal(subscriptionId, exception.SubscriptionId); + Assert.Equal(innerException, exception.InnerException); + } + + [Fact] + public void SerializeException_WithoutProperties_SerializesWithoutProperties() + { + // Arrange + var innerException = new InvalidOperationException("Problem with update"); + var exception = new LogicAppNotUpdatedException("App not updated", innerException); + + var expected = exception.ToString(); + + // Act + LogicAppNotUpdatedException actual = SerializeDeserializeException(exception); + + // Assert + Assert.Equal(expected, actual.ToString()); + } + + [Fact] + public void SerializeException_WithProperties_SerializeWithProperties() + { + // Arrange + string logicApp = "logic app name", + resourceGroup = "resouce group", + subscriptionId = "subscription ID"; + + var innerException = new KeyNotFoundException("Problem with update"); + var exception = new LogicAppNotUpdatedException(subscriptionId, resourceGroup, logicApp, "App not updated", innerException); + + string expected = exception.ToString(); + + // Act + LogicAppNotUpdatedException actual = SerializeDeserializeException(exception); + + // Assert + Assert.Equal(expected, actual.ToString()); + Assert.Equal(logicApp, actual.LogicAppName); + Assert.Equal(resourceGroup, actual.ResourceGroup); + Assert.Equal(subscriptionId, actual.SubscriptionId); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Constructor_WithBlankLogicAppName_Fails(string logicAppName) + { + Assert.Throws( + () => new LogicAppNotUpdatedException("subscription ID", "resource group", logicAppName, "App not updated")); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Constructor_WithBlankResourceGroup_Fails(string resourceGroup) + { + Assert.Throws( + () => new LogicAppNotUpdatedException("subscription ID", resourceGroup, "logic app", "App not updated")); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Constructor_WithBlankSubscriptionId_Fails(string subscriptionId) + { + Assert.Throws( + () => new LogicAppNotUpdatedException(subscriptionId, "resource group", "logic app", "App not updated")); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void ConstructorInnerException_WithBlankLogicAppName_Fails(string logicAppName) + { + var innerException = new Exception("The cause of the exception"); + Assert.Throws( + () => new LogicAppNotUpdatedException("subscription ID", "resource group", logicAppName, "App not updated", innerException)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void ConstructorInnerException_WithBlankResourceGroup_Fails(string resourceGroup) + { + var innerException = new Exception("The cause of the exception"); + Assert.Throws( + () => new LogicAppNotUpdatedException("subscription ID", resourceGroup, "logic app", "App not updated", innerException)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void ConstructorInnerException_WithBlankSubscriptionId_Fails(string subscriptionId) + { + var innerException = new Exception("The cause of the exception"); + Assert.Throws( + () => new LogicAppNotUpdatedException(subscriptionId, "resource group", "logic app", "Trigger could not be found", innerException)); + } + + private static LogicAppNotUpdatedException SerializeDeserializeException(LogicAppNotUpdatedException exception) + { + var formatter = new BinaryFormatter(); + using (var contents = new MemoryStream()) + { + formatter.Serialize(contents, exception); + contents.Seek(0, 0); + + var deserialized = (LogicAppNotUpdatedException) formatter.Deserialize(contents); + return deserialized; + } + } + } +} diff --git a/src/Invictus.Testing.Tests.Integration/LogicAppTriggerNotFoundExceptionTests.cs b/src/Invictus.Testing.Tests.Integration/LogicAppTriggerNotFoundExceptionTests.cs new file mode 100644 index 0000000..44fdcfe --- /dev/null +++ b/src/Invictus.Testing.Tests.Integration/LogicAppTriggerNotFoundExceptionTests.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; +using Xunit; + +namespace Invictus.Testing.Tests.Integration +{ + public class LogicAppTriggerNotFoundExceptionTests + { + [Fact] + public void CreateException_WithAllProperties_AssignsAllProperties() + { + // Arrange + string logicApp = "logic app name", resourceGroup = "resource group", subscriptionId = "subscription ID"; + string message = "There's something wrong with finding the trigger in the logic app"; + var innerException = new KeyNotFoundException("Couldn't find the trigger in the logic app"); + + // Act + var exception = new LogicAppTriggerNotFoundException(subscriptionId, resourceGroup, logicApp, message, innerException); + + // Assert + Assert.Equal(message, exception.Message); + Assert.Equal(logicApp, exception.LogicAppName); + Assert.Equal(resourceGroup, exception.ResourceGroup); + Assert.Equal(subscriptionId, exception.SubscriptionId); + Assert.Equal(innerException, exception.InnerException); + } + + [Fact] + public void SerializeException_WithoutProperties_SerializesWithoutProperties() + { + // Arrange + var innerException = new KeyNotFoundException("No trigger with this key found"); + var exception = new LogicAppTriggerNotFoundException("Trigger could not be found", innerException); + + var expected = exception.ToString(); + + // Act + LogicAppTriggerNotFoundException actual = SerializeDeserializeException(exception); + + // Assert + Assert.Equal(expected, actual.ToString()); + } + + [Fact] + public void SerializeException_WithProperties_SerializeWithProperties() + { + // Arrange + string logicApp = "logic app name", + resourceGroup = "resource group", + subscriptionId = "subscription ID"; + + var innerException = new KeyNotFoundException("No trigger with this key found"); + var exception = new LogicAppTriggerNotFoundException(subscriptionId, resourceGroup, logicApp, "Trigger could not be found", innerException); + + string expected = exception.ToString(); + + // Act + LogicAppTriggerNotFoundException actual = SerializeDeserializeException(exception); + + // Assert + Assert.Equal(expected, actual.ToString()); + Assert.Equal(logicApp, actual.LogicAppName); + Assert.Equal(resourceGroup, actual.ResourceGroup); + Assert.Equal(subscriptionId, actual.SubscriptionId); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Constructor_WithBlankLogicAppName_Fails(string logicAppName) + { + Assert.Throws( + () => new LogicAppTriggerNotFoundException("subscription ID", "resource group", logicAppName, "Trigger could not be found")); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Constructor_WithBlankResourceGroup_Fails(string resourceGroup) + { + Assert.Throws( + () => new LogicAppTriggerNotFoundException("subscription ID", resourceGroup, "logic app", "Trigger could not be found")); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Constructor_WithBlankSubscriptionId_Fails(string subscriptionId) + { + Assert.Throws( + () => new LogicAppTriggerNotFoundException(subscriptionId, "resource group", "logic app", "Trigger could not be found")); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void ConstructorInnerException_WithBlankLogicAppName_Fails(string logicAppName) + { + var innerException = new Exception("The cause of the exception"); + Assert.Throws( + () => new LogicAppTriggerNotFoundException("subscription ID", "resource group", logicAppName, "Trigger could not be found", innerException)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void ConstructorInnerException_WithBlankResourceGroup_Fails(string resourceGroup) + { + var innerException = new Exception("The cause of the exception"); + Assert.Throws( + () => new LogicAppTriggerNotFoundException("subscription ID", resourceGroup, "logic app", "Trigger could not be found", innerException)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void ConstructorInnerException_WithBlankSubscriptionId_Fails(string subscriptionId) + { + var innerException = new Exception("The cause of the exception"); + Assert.Throws( + () => new LogicAppTriggerNotFoundException(subscriptionId, "resource group", "logic app", "Trigger could not be found", innerException)); + } + + private static LogicAppTriggerNotFoundException SerializeDeserializeException(LogicAppTriggerNotFoundException exception) + { + var formatter = new BinaryFormatter(); + using (var contents = new MemoryStream()) + { + formatter.Serialize(contents, exception); + contents.Seek(0, 0); + + var deserialized = (LogicAppTriggerNotFoundException) formatter.Deserialize(contents); + return deserialized; + } + } + } +} diff --git a/src/Invictus.Testing.Tests.Integration/LogicAppsHelperTests.cs b/src/Invictus.Testing.Tests.Integration/LogicAppsHelperTests.cs deleted file mode 100644 index 0083468..0000000 --- a/src/Invictus.Testing.Tests.Integration/LogicAppsHelperTests.cs +++ /dev/null @@ -1,475 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Invictus.Testing.Model; -using Invictus.Testing.Serialization; -using Newtonsoft.Json.Linq; -using Xunit; -using Xunit.Abstractions; - -namespace Invictus.Testing.Tests.Integration -{ - public class LogicAppsHelperTests : IDisposable - { - private readonly ITestOutputHelper _outputWriter; - private readonly string _resourceGroup, _logicAppName, _logicAppMockingName; - private readonly LogicAppsHelper _logicAppsHelper; - - private static readonly TestConfig Configuration = TestConfig.Create(); - - /// - /// Initializes a new instance of the class. - /// - public LogicAppsHelperTests(ITestOutputHelper outputWriter) - { - _outputWriter = outputWriter; - - _resourceGroup = Configuration.GetAzureResourceGroup(); - _logicAppName = Configuration.GetTestLogicAppName(); - _logicAppMockingName = Configuration.GetTestMockingLogicAppName(); - - string subscriptionId = Configuration.GetAzureSubscriptionId(); - string tenantId = Configuration.GetAzureTenantId(); - string clientId = Configuration.GetAzureClientId(); - string clientSecret = Configuration.GetAzureClientSecret(); - _logicAppsHelper = new LogicAppsHelper(subscriptionId, tenantId, clientId, clientSecret); - } - - [Fact] - public async Task GetLogicAppTriggerUrl_Success() - { - // Act - LogicAppTriggerUrl logicAppTriggerUrl = await _logicAppsHelper.GetLogicAppTriggerUrlAsync(_resourceGroup, _logicAppName); - - // Assert - Assert.NotNull(logicAppTriggerUrl.Value); - Assert.Equal("POST", logicAppTriggerUrl.Method); - } - - [Fact] - public async Task GetLogicAppTriggerUrl_ByName_Success() - { - // Act - LogicAppTriggerUrl logicAppTriggerUrl = - await _logicAppsHelper.GetLogicAppTriggerUrlAsync(_resourceGroup, _logicAppName, triggerName: "manual"); - - // Assert - Assert.NotNull(logicAppTriggerUrl.Value); - Assert.Equal("POST", logicAppTriggerUrl.Method); - } - - [Fact] - public async Task PollForLogicAppRun_ByCorrelationId_Success() - { - // Arrange - DateTime startTime = DateTime.UtcNow; - - string correlationId = $"correlationId-{Guid.NewGuid()}"; - var headers = new Dictionary - { - { "correlationId", correlationId } - }; - - // Act - LogicAppTriggerUrl logicAppTriggerUrl = await _logicAppsHelper.GetLogicAppTriggerUrlAsync(_resourceGroup, _logicAppName); - - // Assert - Task pollingTask = _logicAppsHelper.PollForLogicAppRunAsync(_resourceGroup, _logicAppName, startTime, correlationId); - Task postTask = PostHeadersToLogicAppTriggerAsync(logicAppTriggerUrl.Value, headers); - - await Task.WhenAll(pollingTask, postTask); - - Assert.NotNull(pollingTask.Result); - Assert.Equal(correlationId, pollingTask.Result.CorrelationId); - } - - [Fact] - public async Task PollForLogicAppRuns_ByCorrelationId_AfterTimeoutPeriod_Success() - { - // Arrange - TimeSpan timeout = TimeSpan.FromSeconds(5); - DateTime startTime = DateTime.UtcNow; - - string correlationId = $"correlationId-{Guid.NewGuid()}"; - var headers = new Dictionary - { - { "correlationId", correlationId } - }; - - // Act - LogicAppTriggerUrl logicAppTriggerUrl = await _logicAppsHelper.GetLogicAppTriggerUrlAsync(_resourceGroup, _logicAppName); - - // Assert - // Poll for all logic app runs with provided correlation id after timeout period expires. - Task> pollingTask = - _logicAppsHelper.PollForLogicAppRunsAsync(_resourceGroup, _logicAppName, startTime, correlationId, timeout); - - // Run logic app twice with the same correlation id. - Task postTask1 = PostHeadersToLogicAppTriggerAsync(logicAppTriggerUrl.Value, headers); - Task postTask2 = PostHeadersToLogicAppTriggerAsync(logicAppTriggerUrl.Value, headers); - - await Task.WhenAll(pollingTask, postTask1, postTask2); - - Assert.NotNull(pollingTask.Result); - Assert.Equal(2, pollingTask.Result.Count); - Assert.All(pollingTask.Result, logicAppRun => - { - Assert.Equal(correlationId, logicAppRun.CorrelationId); - }); - } - - [Fact] - public async Task PollForLogicAppRuns_ByCorrelationId_NumberOfRuns_Success() - { - // Arrange - const int numberOfRuns = 2; - TimeSpan timeout = TimeSpan.FromSeconds(30); - DateTime startTime = DateTime.UtcNow; - - string correlationId = $"correlationId-{Guid.NewGuid()}"; - var headers = new Dictionary - { - { "correlationId", correlationId } - }; - - // Act - LogicAppTriggerUrl logicAppTriggerUrl = await _logicAppsHelper.GetLogicAppTriggerUrlAsync(_resourceGroup, _logicAppName); - - // Assert - // Poll for a specific number of logic app runs with provided correlation id. - Task> pollingTask = - _logicAppsHelper.PollForLogicAppRunsAsync(_resourceGroup, _logicAppName, startTime, correlationId, timeout, numberOfRuns); - - // Run logic app twice with the same correlation id. - Task postTask1 = PostHeadersToLogicAppTriggerAsync(logicAppTriggerUrl.Value, headers); - Task postTask2 = PostHeadersToLogicAppTriggerAsync(logicAppTriggerUrl.Value, headers); - await Task.WhenAll(pollingTask, postTask1, postTask2); - - Assert.Equal(numberOfRuns, pollingTask.Result.Count); - Assert.All(pollingTask.Result, logicAppRun => - { - Assert.Equal(correlationId, logicAppRun.CorrelationId); - }); - } - - [Fact] - public async Task PollForLogicAppRun_ByTrackedProperty_Success() - { - // Arrange - const string trackedPropertyName = "trackedproperty"; - DateTime startTime = DateTime.UtcNow; - - string correlationId = $"correlationId-{Guid.NewGuid()}"; - string trackedPropertyValue = $"tracked-{Guid.NewGuid()}"; - - var headers = new Dictionary - { - { "correlationId", correlationId }, - { "trackedpropertyheader1", trackedPropertyValue }, - { "trackedpropertyheader2", trackedPropertyValue } - }; - - // Act - LogicAppTriggerUrl logicAppTriggerUrl = await _logicAppsHelper.GetLogicAppTriggerUrlAsync(_resourceGroup, _logicAppName); - - // Assert - Task pollingTask = - _logicAppsHelper.PollForLogicAppRunAsync(_resourceGroup, _logicAppName, startTime, trackedPropertyName, trackedPropertyValue); - - Task postTask = PostHeadersToLogicAppTriggerAsync(logicAppTriggerUrl.Value, headers); - - await Task.WhenAll(pollingTask, postTask); - - Assert.NotNull(pollingTask.Result); - Assert.True(pollingTask.Result.TrackedProperties.ContainsValue(trackedPropertyValue)); - } - - [Fact] - public async Task PollForLogicAppRun_ByTrackedProperty_DifferentValues_GetsLatest_Success() - { - // Arrange - const string trackedPropertyName = "trackedproperty"; - DateTime startTime = DateTime.UtcNow; - - string correlationId = $"correlationId-{Guid.NewGuid()}"; - string trackedPropertyValue1 = $"tracked-{Guid.NewGuid()}"; - string trackedPropertyValue2 = $"tracked-{Guid.NewGuid()}"; - - var headers = new Dictionary - { - { "correlationId", correlationId }, - { "trackedpropertyheader1", trackedPropertyValue1 }, - { "trackedpropertyheader2", trackedPropertyValue2 } - }; - - // Act - LogicAppTriggerUrl logicAppTriggerUrl = await _logicAppsHelper.GetLogicAppTriggerUrlAsync(_resourceGroup, _logicAppName); - - // Assert - Task pollingTask = - _logicAppsHelper.PollForLogicAppRunAsync(_resourceGroup, _logicAppName, startTime, trackedPropertyName, trackedPropertyValue1); - - Task postTask = PostHeadersToLogicAppTriggerAsync(logicAppTriggerUrl.Value, headers); - - await Task.WhenAll(pollingTask, postTask); - - Assert.NotNull(pollingTask.Result); - Assert.True(pollingTask.Result.TrackedProperties.ContainsValue(trackedPropertyValue2)); - } - - [Fact] - public async Task PollForLogicAppRuns_ByTrackedProperty_AfterTimeoutPeriod_Success() - { - // Arrange - const string trackedPropertyName = "trackedproperty"; - DateTime startTime = DateTime.UtcNow; - TimeSpan timeout = TimeSpan.FromSeconds(5); - - string correlationId = $"correlationId-{Guid.NewGuid()}"; - string trackedPropertyValue = $"tracked-{Guid.NewGuid()}"; - - var headers = new Dictionary - { - { "correlationId", correlationId }, - { "trackedpropertyheader1", trackedPropertyValue }, - { "trackedpropertyheader2", trackedPropertyValue } - }; - - // Act - LogicAppTriggerUrl logicAppTriggerUrl = await _logicAppsHelper.GetLogicAppTriggerUrlAsync(_resourceGroup, _logicAppName); - - // Assert - // Poll for all logic app runs with provided tracked property after timeout period expires. - Task> pollingTask = - _logicAppsHelper.PollForLogicAppRunsAsync(_resourceGroup, _logicAppName, startTime, trackedPropertyName, trackedPropertyValue, timeout); - - // Run logic app twice with the same tracked property. - Task postTask1 = PostHeadersToLogicAppTriggerAsync(logicAppTriggerUrl.Value, headers); - Task postTask2 = PostHeadersToLogicAppTriggerAsync(logicAppTriggerUrl.Value, headers); - - await Task.WhenAll(pollingTask, postTask1, postTask2); - - Assert.NotNull(pollingTask.Result); - Assert.Equal(2, pollingTask.Result.Count); - Assert.All(pollingTask.Result, logicAppRun => - { - Assert.True(logicAppRun.TrackedProperties.ContainsValue(trackedPropertyValue)); - }); - } - - [Fact] - public async Task PollForLogicAppRuns_ByTrackedProperty_NumberOfRuns_Success() - { - // Arrange - const string trackedPropertyName = "trackedproperty"; - const int numberOfRuns = 2; - DateTime startTime = DateTime.UtcNow; - TimeSpan timeout = TimeSpan.FromSeconds(30); - - string correlationId = $"correlationId-{Guid.NewGuid()}"; - var trackedPropertyValue = Guid.NewGuid().ToString(); - var headers = new Dictionary - { - { "correlationId", correlationId }, - { "trackedpropertyheader1", trackedPropertyValue }, - { "trackedpropertyheader2", trackedPropertyValue } - }; - - // Act - LogicAppTriggerUrl logicAppTriggerUrl = await _logicAppsHelper.GetLogicAppTriggerUrlAsync(_resourceGroup, _logicAppName); - - // Assert - // Poll for a specific number of logic app runs with provided tracked property. - Task> pollingTask = - _logicAppsHelper.PollForLogicAppRunsAsync(_resourceGroup, _logicAppName, startTime, trackedPropertyName, trackedPropertyValue, timeout, numberOfRuns); - - // Run logic app twice with the same tracked property. - Task postTask1 = PostHeadersToLogicAppTriggerAsync(logicAppTriggerUrl.Value, headers); - Task postTask2 = PostHeadersToLogicAppTriggerAsync(logicAppTriggerUrl.Value, headers); - - await Task.WhenAll(pollingTask, postTask1, postTask2); - - Assert.NotNull(pollingTask.Result); - Assert.Equal(numberOfRuns, pollingTask.Result.Count); - Assert.All(pollingTask.Result, logicAppRun => - { - Assert.True(logicAppRun.TrackedProperties.ContainsValue(trackedPropertyValue)); - }); - } - - [Fact] - public async Task EnableStaticResultForAction_Success() - { - // Arrange - const string actionName = "HTTP"; - - string correlationId = $"correlationId-{Guid.NewGuid()}"; - var headers = new Dictionary - { - { "correlationId", correlationId }, - }; - - var staticResultDefinition = new StaticResultDefinition - { - Outputs = new Outputs - { - Headers = new Dictionary { { "testheader", "testvalue" } }, - StatusCode = "200", - Body = JToken.Parse("{id : 12345, name : 'test body'}") - }, - Status = "Succeeded" - }; - - // Act - bool result = await _logicAppsHelper.EnableStaticResultForActionAsync(_resourceGroup, _logicAppMockingName, actionName, staticResultDefinition); - - // Assert - Assert.True(result); - - await RunLogicAppOnTriggerUrlAsync(headers); - LogicAppAction logicAppAction = await PollForLogicAppActionAsync(correlationId, actionName); - - Assert.Equal("200", logicAppAction.Outputs.statusCode.ToString()); - Assert.Equal("testvalue", logicAppAction.Outputs.headers["testheader"].ToString()); - Assert.True(logicAppAction.Outputs.body.ToString().Contains("test body")); - } - - [Fact] - public async Task EnableStaticResultForActions_Success() - { - // Arrange - const string actionName = "HTTP"; - - string correlationId = $"correlationId-{Guid.NewGuid()}"; - var headers = new Dictionary - { - { "correlationId", correlationId }, - }; - - var staticResultDefinition = new StaticResultDefinition - { - Outputs = new Outputs - { - Headers = new Dictionary { { "testheader", "testvalue" } }, - StatusCode = "200", - Body = "test body" - }, - Status = "Succeeded" - }; - - var actions = new Dictionary { { actionName, staticResultDefinition } }; - - // Act - bool isSuccess = await _logicAppsHelper.EnableStaticResultForActionsAsync(_resourceGroup, _logicAppMockingName, actions); - - // Assert - Assert.True(isSuccess); - - await RunLogicAppOnTriggerUrlAsync(headers); - LogicAppAction logicAppAction = await PollForLogicAppActionAsync(correlationId, actionName); - - Assert.Equal("200", logicAppAction.Outputs.statusCode.ToString()); - Assert.Equal("testvalue", logicAppAction.Outputs.headers["testheader"].ToString()); - Assert.Equal("test body", logicAppAction.Outputs.body.ToString()); - } - - [Fact] - public async Task DisableStaticResultForAction_Success() - { - // Arrange - const string actionName = "HTTP"; - - string correlationId = $"correlationId-{Guid.NewGuid()}"; - var headers = new Dictionary - { - { "correlationId", correlationId }, - }; - - // Act - bool isSuccess = await _logicAppsHelper.DisableStaticResultForActionAsync(_resourceGroup, _logicAppMockingName, actionName); - - // Assert - Assert.True(isSuccess); - - await RunLogicAppOnTriggerUrlAsync(headers); - LogicAppAction logicAppAction = await PollForLogicAppActionAsync(correlationId, actionName); - - string body = logicAppAction.Outputs.body; - Assert.NotEqual("test body", body); - } - - [Fact] - public async Task DisableStaticResultForAllActions_Success() - { - // Arrange - DateTime startTime = DateTime.UtcNow.AddMinutes(-1); - - string correlationId = $"correlationId-{Guid.NewGuid()}"; - var headers = new Dictionary - { - { "correlationId", correlationId }, - }; - - // Act - bool isSuccess = await _logicAppsHelper.DisableAllStaticResultsForLogicAppAsync(_resourceGroup, _logicAppMockingName); - - // Assert - Assert.True(isSuccess); - - await RunLogicAppOnTriggerUrlAsync(headers); - - // Check logic app run for static result - LogicAppRun logicAppRun = await _logicAppsHelper.PollForLogicAppRunAsync(_resourceGroup, _logicAppMockingName, startTime, correlationId); - Assert.All(logicAppRun.Actions, action => - { - string body = action.Outputs.body; - Assert.NotEqual("test body", body); - }); - } - - private async Task RunLogicAppOnTriggerUrlAsync(IDictionary headers) - { - LogicAppTriggerUrl logicAppTriggerUrl = await _logicAppsHelper.GetLogicAppTriggerUrlAsync(_resourceGroup, _logicAppMockingName); - await PostHeadersToLogicAppTriggerAsync(logicAppTriggerUrl.Value, headers); - } - - private async Task PollForLogicAppActionAsync(string correlationId, string actionName) - { - DateTime startTime = DateTime.UtcNow.AddMinutes(-1); - - LogicAppRun logicAppRun = await _logicAppsHelper.PollForLogicAppRunAsync(_resourceGroup, _logicAppMockingName, startTime, correlationId); - Assert.True(logicAppRun.Actions.Count != 0); - - LogicAppAction logicAppAction = logicAppRun.Actions.First(action => action.Name.Equals(actionName)); - Assert.NotNull(logicAppAction); - - return logicAppAction; - } - - private static async Task PostHeadersToLogicAppTriggerAsync(string uri, IDictionary headers) - { - using (var request = new HttpRequestMessage(HttpMethod.Post, uri)) - { - foreach ((string name, string value) in headers) - { - request.Headers.Add(name, value); - } - - using (var client = new HttpClient()) - using (HttpResponseMessage response = await client.SendAsync(request)) - { - } - } - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - _logicAppsHelper?.Dispose(); - } - } -} diff --git a/src/Invictus.Testing.Tests.Integration/LogicAppsProviderTests.cs b/src/Invictus.Testing.Tests.Integration/LogicAppsProviderTests.cs new file mode 100644 index 0000000..1771607 --- /dev/null +++ b/src/Invictus.Testing.Tests.Integration/LogicAppsProviderTests.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Invictus.Testing.Model; +using Xunit; +using Xunit.Abstractions; + +namespace Invictus.Testing.Tests.Integration +{ + public class LogicAppsProviderTests : IntegrationTest + { + /// + /// Initializes a new instance of the class. + /// + public LogicAppsProviderTests(ITestOutputHelper outputWriter) : base(outputWriter) + { + } + + [Fact] + public async Task PollForLogicAppRun_WithoutLogger_Success() + { + // Arrange + string correlationId = $"correlationId-{Guid.NewGuid()}"; + var headers = new Dictionary + { + { "correlationId", correlationId } + }; + + using (var logicApp = await LogicAppClient.CreateAsync(ResourceGroup, LogicAppName, Authentication, Logger)) + await using (await logicApp.TemporaryEnableAsync()) + { + Task postTask1 = logicApp.TriggerAsync(headers); + Task postTask2 = logicApp.TriggerAsync(headers); + + // Act + Task> pollingTask = + LogicAppsProvider.LocatedAt(ResourceGroup, LogicAppName, Authentication) + .WithCorrelationId(correlationId) + .PollForLogicAppRunsAsync(); + + await Task.WhenAll(pollingTask, postTask1, postTask2); + + // Assert + Assert.NotNull(pollingTask.Result); + Assert.NotEmpty(pollingTask.Result); + Assert.All(pollingTask.Result, logicAppRun => + { + Assert.Equal(correlationId, logicAppRun.CorrelationId); + }); + } + } + + [Fact] + public async Task PollForLogicAppRun_NotMatchedCorrelation_Fails() + { + // Arrange + var headers = new Dictionary + { + { "correlationId", $"correlationId-{Guid.NewGuid()}" } + }; + + // Act + Task pollingTask = + LogicAppsProvider.LocatedAt(ResourceGroup, LogicAppName, Authentication, Logger) + .WithTimeout(TimeSpan.FromSeconds(5)) + .WithCorrelationId("not-matched-correlation-ID") + .PollForSingleLogicAppRunAsync(); + + using (var logicApp = await LogicAppClient.CreateAsync(ResourceGroup, LogicAppName, Authentication, Logger)) + { + // Assert + await logicApp.TriggerAsync(headers); + await Assert.ThrowsAsync(() => pollingTask); + } + } + + [Fact] + public async Task PollForLogicAppRun_ByCorrelationId_Success() + { + // Arrange + string correlationId = $"correlationId-{Guid.NewGuid()}"; + var headers = new Dictionary + { + { "correlationId", correlationId } + }; + + using (var logicApp = await LogicAppClient.CreateAsync(ResourceGroup, LogicAppName, Authentication, Logger)) + await using (await logicApp.TemporaryEnableAsync()) + { + Task postTask = logicApp.TriggerAsync(headers); + + // Act + Task pollingTask = + LogicAppsProvider.LocatedAt(ResourceGroup, LogicAppName, Authentication, Logger) + .WithCorrelationId(correlationId) + .PollForSingleLogicAppRunAsync(); + + await Task.WhenAll(pollingTask, postTask); + + // Assert + Assert.NotNull(pollingTask.Result); + Assert.Equal(correlationId, pollingTask.Result.CorrelationId); + } + } + + [Fact] + public async Task PollForLogicAppRuns_ByCorrelationId_NumberOfRuns_Success() + { + // Arrange + const int numberOfRuns = 2; + TimeSpan timeout = TimeSpan.FromSeconds(30); + + string correlationId = $"correlationId-{Guid.NewGuid()}"; + var headers = new Dictionary + { + { "correlationId", correlationId } + }; + + using (var logicApp = await LogicAppClient.CreateAsync(ResourceGroup, LogicAppName, Authentication)) + await using (await logicApp.TemporaryEnableAsync()) + { + // Run logic app twice with the same correlation id. + Task postTask1 = logicApp.TriggerAsync(headers); + Task postTask2 = logicApp.TriggerAsync(headers); + + // Act + Task> pollingTask = + LogicAppsProvider.LocatedAt(ResourceGroup, LogicAppName, Authentication, Logger) + .WithCorrelationId(correlationId) + .WithTimeout(timeout) + .PollForLogicAppRunsAsync(numberOfRuns); + + await Task.WhenAll(pollingTask, postTask1, postTask2); + + // Assert + Assert.Equal(numberOfRuns, pollingTask.Result.Count()); + Assert.All(pollingTask.Result, logicAppRun => + { + Assert.Equal(correlationId, logicAppRun.CorrelationId); + }); + } + } + + [Fact] + public async Task PollForLogicAppRun_ByTrackedProperty_Success() + { + // Arrange + const string trackedPropertyName = "trackedproperty"; + string correlationId = $"correlationId-{Guid.NewGuid()}"; + string trackedPropertyValue = $"tracked-{Guid.NewGuid()}"; + + var headers = new Dictionary + { + { "correlationId", correlationId }, + { "trackedpropertyheader1", trackedPropertyValue }, + { "trackedpropertyheader2", trackedPropertyValue } + }; + + using (var logicApp = await LogicAppClient.CreateAsync(ResourceGroup, LogicAppName, Authentication)) + await using (await logicApp.TemporaryEnableAsync()) + { + Task postTask = logicApp.TriggerAsync(headers); + + // Act + Task pollingTask = + LogicAppsProvider.LocatedAt(ResourceGroup, LogicAppName, Authentication, Logger) + .WithTrackedProperty(trackedPropertyName, trackedPropertyValue) + .PollForSingleLogicAppRunAsync(); + + await Task.WhenAll(pollingTask, postTask); + + // Assert + Assert.NotNull(pollingTask.Result); + Assert.Contains(pollingTask.Result.TrackedProperties, property => property.Value == trackedPropertyValue); + } + } + + [Fact] + public async Task PollForLogicAppRun_ByTrackedProperty_DifferentValues_GetsLatest_Success() + { + // Arrange + const string trackedPropertyName = "trackedproperty"; + string correlationId = $"correlationId-{Guid.NewGuid()}"; + string trackedPropertyValue1 = $"tracked-{Guid.NewGuid()}"; + string trackedPropertyValue2 = $"tracked-{Guid.NewGuid()}"; + + var headers = new Dictionary + { + { "correlationId", correlationId }, + { "trackedpropertyheader1", trackedPropertyValue1 }, + { "trackedpropertyheader2", trackedPropertyValue2 } + }; + + using (var logicApp = await LogicAppClient.CreateAsync(ResourceGroup, LogicAppName, Authentication)) + await using (await logicApp.TemporaryEnableAsync()) + { + Task postTask = logicApp.TriggerAsync(headers); + + // Act + Task pollingTask = + LogicAppsProvider.LocatedAt(ResourceGroup, LogicAppName, Authentication, Logger) + .WithTrackedProperty(trackedPropertyName, trackedPropertyValue1) + .PollForSingleLogicAppRunAsync(); + + await Task.WhenAll(pollingTask, postTask); + + // Assert + Assert.NotNull(pollingTask.Result); + Assert.Contains(pollingTask.Result.TrackedProperties, property => property.Value == trackedPropertyValue2); + } + } + + [Fact] + public async Task PollForLogicAppRuns_ByTrackedProperty_NumberOfRuns_Success() + { + // Arrange + const string trackedPropertyName = "trackedproperty"; + const int numberOfRuns = 2; + TimeSpan timeout = TimeSpan.FromSeconds(40); + + string correlationId = $"correlationId-{Guid.NewGuid()}"; + var trackedPropertyValue = Guid.NewGuid().ToString(); + var headers = new Dictionary + { + { "correlationId", correlationId }, + { "trackedpropertyheader1", trackedPropertyValue }, + { "trackedpropertyheader2", trackedPropertyValue } + }; + + using (var logicApp = await LogicAppClient.CreateAsync(ResourceGroup, LogicAppName, Authentication)) + await using (await logicApp.TemporaryEnableAsync()) + { + // Run logic app twice with the same tracked property. + Task postTask1 = logicApp.TriggerAsync(headers); + Task postTask2 = logicApp.TriggerAsync(headers); + + // Act + // Poll for a specific number of logic app runs with provided tracked property. + Task> pollingTask = + LogicAppsProvider.LocatedAt(ResourceGroup, LogicAppName, Authentication, Logger) + .WithTrackedProperty(trackedPropertyName, trackedPropertyValue) + .WithTimeout(timeout) + .PollForLogicAppRunsAsync(numberOfRuns); + + await Task.WhenAll(pollingTask, postTask1, postTask2); + + // Assert + Assert.NotNull(pollingTask.Result); + Assert.Equal(numberOfRuns, pollingTask.Result.Count()); + Assert.All(pollingTask.Result, logicAppRun => + { + Assert.Contains(logicAppRun.TrackedProperties, property => property.Value == trackedPropertyValue); + }); + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void Constructor_WithBlankResourceGroup_Fails(string resourceGroup) + { + Assert.Throws( + () => LogicAppsProvider.LocatedAt(resourceGroup, LogicAppName, Authentication)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void Constructor_WithBlankLogicApp_Fails(string logicApp) + { + Assert.Throws( + () => LogicAppsProvider.LocatedAt(ResourceGroup, logicApp, Authentication)); + } + + [Fact] + public void Constructor_WithoutAuthentication_Fails() + { + Assert.ThrowsAny( + () => LogicAppsProvider.LocatedAt(ResourceGroup, LogicAppName, authentication: null)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void ConstructorWithLogger_WithBlankResourceGroup_Fails(string resourceGroup) + { + Assert.Throws( + () => LogicAppsProvider.LocatedAt(resourceGroup, LogicAppName, Authentication, Logger)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void ConstructorWithLogger_WithBlankLogicApp_Fails(string logicApp) + { + Assert.Throws( + () => LogicAppsProvider.LocatedAt(ResourceGroup, logicApp, Authentication, Logger)); + } + + [Fact] + public void ConstructorWithLogger_WithoutAuthentication_Fails() + { + Assert.ThrowsAny( + () => LogicAppsProvider.LocatedAt(ResourceGroup, LogicAppName, authentication: null, logger: Logger)); + } + } +} diff --git a/src/Invictus.Testing/AsyncDisposable.cs b/src/Invictus.Testing/AsyncDisposable.cs new file mode 100644 index 0000000..5734e25 --- /dev/null +++ b/src/Invictus.Testing/AsyncDisposable.cs @@ -0,0 +1,53 @@ +using System.Threading.Tasks; +using GuardNet; + +// ReSharper disable once CheckNamespace +namespace System +{ + /// + /// Represents an abstracted way to define setup/teardown functions in an implementation. + /// + public class AsyncDisposable : IAsyncDisposable + { + private readonly Func _teardown; + + private AsyncDisposable(Func teardown) + { + Guard.NotNull(teardown, nameof(teardown)); + _teardown = teardown; + } + + /// + /// Create an instance of the class to simulate setup/teardown actions. + /// + /// The action to run when the instance is being disposed. + public static AsyncDisposable Create(Func teardown) + { + Guard.NotNull(teardown, nameof(teardown)); + return new AsyncDisposable(teardown); + } + + /// + /// Create an instance of the class to simulate setup/teardown actions. + /// + /// The action to run when the instance is created (now). + /// The action to run when the instance is being disposed. + public static async Task CreateAsync(Func setup, Func teardown) + { + Guard.NotNull(setup, nameof(setup)); + Guard.NotNull(teardown, nameof(teardown)); + + await setup(); + return new AsyncDisposable(teardown); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously. + /// + /// A task that represents the asynchronous dispose operation. + public async ValueTask DisposeAsync() + { + await _teardown(); + } + } +} diff --git a/src/Invictus.Testing/Converter.cs b/src/Invictus.Testing/Converter.cs deleted file mode 100644 index a0317bf..0000000 --- a/src/Invictus.Testing/Converter.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Invictus.Testing.Model; -using Microsoft.Azure.Management.Logic.Models; -using Newtonsoft.Json.Linq; - -namespace Invictus.Testing -{ - public class Converter - { - /// - /// Convert to LogicAppRun. - /// - /// - /// - /// - /// - /// - public static LogicAppRun ToLogicAppRun(LogicAppsHelper helper, string resourceGroupName, string logicAppName, WorkflowRun workFlowRun) - { - var logicAppRun = (LogicAppRun)workFlowRun; - - logicAppRun.Actions = helper.GetLogicAppRunActionsAsync(resourceGroupName, logicAppName, workFlowRun.Name, false).Result; - - logicAppRun.TrackedProperties = GetAllTrackedProperties(logicAppRun.Actions); - - return logicAppRun; - } - - /// - /// Convert to LogicAppRun. - /// - /// - /// - /// - public static LogicAppRun ToLogicAppRun(WorkflowRun workFlowRun, List actions) - { - var logicAppRun = (LogicAppRun)workFlowRun; - - logicAppRun.Actions = actions; - logicAppRun.TrackedProperties = GetAllTrackedProperties(logicAppRun.Actions); - - return logicAppRun; - } - - /// - /// Convert to LogicAppAction. - /// - /// - /// - public static async Task ToLogicAppActionAsync(WorkflowRunAction workflowRunAction) - { - var logicAppAction = (LogicAppAction)workflowRunAction; - - if (workflowRunAction.InputsLink != null) - { - logicAppAction.Inputs = JToken.Parse(await DoHttpRequestAsync(workflowRunAction.InputsLink.Uri)); - } - if (workflowRunAction.OutputsLink != null) - { - logicAppAction.Outputs = JToken.Parse(await DoHttpRequestAsync(workflowRunAction.OutputsLink.Uri)); - } - - return logicAppAction; - } - - #region Private Methods - private static async Task DoHttpRequestAsync(string uri) - { - string responseString = string.Empty; - using (var httpClient = new HttpClient()) - { - responseString = await httpClient.GetStringAsync(uri); - } - - return responseString; - } - - private static Dictionary GetAllTrackedProperties(List actions) - { - return actions - .Where(x => x.TrackedProperties != null) - .OrderByDescending(x => x.StartTime) - .SelectMany(a => a.TrackedProperties) - .GroupBy(x => x.Key) - .Select(g => g.First()) - .ToDictionary(x => x.Key, x => x.Value); - } - #endregion - } -} diff --git a/src/Invictus.Testing/Invictus.Testing.csproj b/src/Invictus.Testing/Invictus.Testing.csproj index cd9560b..934cd7d 100644 --- a/src/Invictus.Testing/Invictus.Testing.csproj +++ b/src/Invictus.Testing/Invictus.Testing.csproj @@ -17,8 +17,10 @@ + + diff --git a/src/Invictus.Testing/LogicAppClient.cs b/src/Invictus.Testing/LogicAppClient.cs new file mode 100644 index 0000000..d397029 --- /dev/null +++ b/src/Invictus.Testing/LogicAppClient.cs @@ -0,0 +1,448 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using GuardNet; +using Invictus.Testing.Model; +using Invictus.Testing.Serialization; +using Microsoft.Azure.Management.Logic; +using Microsoft.Azure.Management.Logic.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Rest.Azure; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Invictus.Testing +{ + /// + /// Representing client operations on a given logic app running in Azure. + /// + public class LogicAppClient : IDisposable + { + private readonly string _resourceGroup, _logicAppName; + private readonly LogicManagementClient _logicManagementClient; + private readonly ILogger _logger; + + private static readonly HttpClient HttpClient = new HttpClient(); + + /// + /// Initializes a new instance of the class. + /// + /// The resource group where the logic app is located. + /// The name of the logic app resource running in Azure. + /// The logic management client to run REST operations during client operations. + /// The instance to write diagnostic trace messages while interacting with the logic app. + public LogicAppClient(string resourceGroup, string logicAppName, LogicManagementClient client, ILogger logger) + { + Guard.NotNullOrEmpty(resourceGroup, nameof(resourceGroup)); + Guard.NotNullOrEmpty(logicAppName, nameof(logicAppName)); + Guard.NotNull(client, nameof(client)); + + _resourceGroup = resourceGroup; + _logicAppName = logicAppName; + _logicManagementClient = client; + _logger = logger ?? NullLogger.Instance; + } + + /// + /// Initializes a new instance of the class. + /// + /// The resource group where the logic app is located. + /// The name of the logic app resource running in Azure. + /// The logic management client to run REST operations during client operations. + public LogicAppClient(string resourceGroup, string logicAppName, LogicManagementClient client) + : this(resourceGroup, logicAppName, client, NullLogger.Instance) + { + } + + /// + /// Creates a new authenticated instance of the . + /// + /// The resource group where the logic app is located. + /// The name of the logic app resource running in Azure. + /// The authentication mechanism to authenticate this client. + /// + /// An authenticated client capable of interacting with the logic app resource running in Azure. + /// + public static async Task CreateAsync( + string resourceGroup, + string logicAppName, + LogicAuthentication authentication) + { + Guard.NotNullOrEmpty(resourceGroup, nameof(resourceGroup)); + Guard.NotNullOrEmpty(logicAppName, nameof(logicAppName)); + Guard.NotNull(authentication, nameof(authentication)); + + return await CreateAsync(resourceGroup, logicAppName, authentication, NullLogger.Instance); + } + + /// + /// Creates a new authenticated instance of the . + /// + /// The resource group where the logic app is located. + /// The name of the logic app resource running in Azure. + /// The authentication mechanism to authenticate this client. + /// The instance to write diagnostic trace messages while interacting with the logic app. + /// + /// An authenticated client capable of interacting with the logic app resource running in Azure. + /// + public static async Task CreateAsync( + string resourceGroup, + string logicAppName, + LogicAuthentication authentication, + ILogger logger) + { + Guard.NotNullOrEmpty(resourceGroup, nameof(resourceGroup)); + Guard.NotNullOrEmpty(logicAppName, nameof(logicAppName)); + Guard.NotNull(authentication, nameof(authentication)); + + LogicManagementClient managementClient = await authentication.AuthenticateAsync(); + logger = logger ?? NullLogger.Instance; + return new LogicAppClient(resourceGroup, logicAppName, managementClient, logger); + } + + /// + /// Temporary enables the current logic app resource on Azure, and disables the logic app after the returned instance gets disposed. + /// + /// + /// An instance to control the removal of the updates. + /// + public async Task TemporaryEnableAsync() + { + return await AsyncDisposable.CreateAsync( + async () => + { + _logger.LogTrace("Enables (+) the workflow on logic app '{LogicAppName}' in resource group '{ResourceGroup}'", _logicAppName, _resourceGroup); + await _logicManagementClient.Workflows.EnableAsync(_resourceGroup, _logicAppName); + }, + async () => + { + _logger.LogTrace("Disables (-) the workflow on logic app '{LogicAppName}' in resource group '{ResourceGroup}'", _logicAppName, _resourceGroup); + await _logicManagementClient.Workflows.DisableAsync(_resourceGroup, _logicAppName); + }); + } + + /// + /// Updates the current JSON logic app definition with the given , + /// and removes this update after the returned instance gets disposed. + /// + /// Then JSON representation of the new definition. + /// + /// An instance to control the removal of the updates. + /// + public async Task TemporaryUpdateAsync(string logicAppDefinition) + { + Guard.NotNull(logicAppDefinition, nameof(logicAppDefinition)); + + Workflow workflow = await _logicManagementClient.Workflows.GetAsync(_resourceGroup, _logicAppName); + object originalAppDefinition = workflow.Definition; + + _logger.LogTrace("Updates (+) the logic app '{LogicAppName}' workflow definition in resource group '{ResourceGroup}'", _logicAppName, _resourceGroup); + await UpdateAsync(workflow, JObject.Parse(logicAppDefinition)); + return AsyncDisposable.Create(async () => + { + _logger.LogTrace("Reverts (-) the update of the logic app '{LogicAppName}' workflow definition in resource group '{ResourceGroup}'", _logicAppName, _resourceGroup); + await UpdateAsync(workflow, originalAppDefinition); + }); + } + + private async Task UpdateAsync(Workflow workflow, object logicAppDefinition) + { + workflow.Definition = logicAppDefinition; + Workflow resultWorkflow = + await _logicManagementClient.Workflows.CreateOrUpdateAsync(_resourceGroup, _logicAppName, workflow); + + if (resultWorkflow.Name != _logicAppName) + { + throw new InvalidOperationException( + $"Could not update the logic app '{_logicAppName}' workflow in resource group '{_resourceGroup}'correctly, " + + "please make sure the authentication on this resource is set correctly and has the right access to this resource"); + } + } + + /// + /// Deletes the current logic app resource on Azure. + /// + public async Task DeleteAsync() + { + _logger.LogTrace("Deletes the workflow of logic app '{LogicAppName}' in resource group '{ResourceGroup}'", _logicAppName, _resourceGroup); + await _logicManagementClient.Workflows.DeleteAsync(_resourceGroup, _logicAppName); + } + + /// + /// Runs the current logic app resource by searching for triggers on the logic app. + /// + /// When no trigger can be found on the logic app. + public async Task RunAsync() + { + string triggerName = await GetTriggerNameAsync(); + await RunByNameAsync(triggerName); + } + + /// + /// Runs the current logic app resource using the given . + /// + /// The name of the trigger that executes a workflow in the logic app. + public async Task RunByNameAsync(string triggerName) + { + Guard.NotNullOrEmpty(triggerName, nameof(triggerName)); + + _logger.LogTrace("Run the workflow trigger of logic app '{LogicAppName}' in resource group '{ResourceGroup}'", _logicAppName, _resourceGroup); + await _logicManagementClient.WorkflowTriggers.RunAsync(_resourceGroup, _logicAppName, triggerName); + } + + + /// + /// Gets the logic app definition information. + /// + public async Task GetMetadataAsync() + { + Workflow workflow = await _logicManagementClient.Workflows.GetAsync(_resourceGroup, _logicAppName); + return LogicAppConverter.ToLogicApp(workflow); + } + + /// + /// Run logic app on the current trigger URL, posting the given . + /// + /// The headers to send with the trigger URL of the current logic app. + public async Task TriggerAsync(IDictionary headers) + { + Guard.NotNull(headers, nameof(headers)); + Guard.NotAny(headers, nameof(headers)); + + LogicAppTriggerUrl triggerUrl = await GetTriggerUrlAsync(); + + _logger.LogTrace("Trigger the workflow of logic app '{LogicAppName}' in resource group '{ResourceGroup}'", _logicAppName, _resourceGroup); + using (var request = new HttpRequestMessage(HttpMethod.Post, triggerUrl.Value)) + { + foreach (KeyValuePair header in headers) + { + request.Headers.Add(header.Key, header.Value); + } + + await HttpClient.SendAsync(request); + } + } + + /// + /// Gets the URL on which the workflow with trigger can be run by searching the workflow for configured triggers. + /// + /// When no trigger can be found on the logic app. + public async Task GetTriggerUrlAsync() + { + string triggerName = await GetTriggerNameAsync(); + LogicAppTriggerUrl triggerUrl = await GetTriggerUrlByNameAsync(triggerName); + + return triggerUrl; + } + + /// + /// Gets the URL on which the workflow with the can be run. + /// + /// The name of the trigger that relates to a workflow. + public async Task GetTriggerUrlByNameAsync(string triggerName) + { + Guard.NotNullOrEmpty(triggerName, nameof(triggerName)); + + _logger.LogTrace("Request the workflow trigger URL of logic app '{LogicAppName}' in resource group '{ResourceGroup}'", _logicAppName, _resourceGroup); + WorkflowTriggerCallbackUrl callbackUrl = + await _logicManagementClient.WorkflowTriggers.ListCallbackUrlAsync(_resourceGroup, _logicAppName, triggerName); + + return new LogicAppTriggerUrl + { + Value = callbackUrl.Value, + Method = callbackUrl.Method + }; + } + + private async Task GetTriggerNameAsync() + { + IPage triggers = await _logicManagementClient.WorkflowTriggers.ListAsync(_resourceGroup, _logicAppName); + + if (triggers.Any()) + { + return triggers.First().Name; + } + + throw new LogicAppTriggerNotFoundException(_logicManagementClient.SubscriptionId, _resourceGroup, _logicAppName, $"Cannot find any trigger for logic app '{_logicAppName}' in resource group '{_resourceGroup}'"); + } + + /// + /// Temporary enables a static result for an action with the given on the logic app, + /// and disables the static result when the returned instance gets disposed. + /// + /// The name of the action to enable the static result. + /// + /// An instance to control when the static result for the action on the logic app should be disabled. + /// + public async Task TemporaryEnableSuccessStaticResultAsync(string actionName) + { + Guard.NotNullOrEmpty(actionName, nameof(actionName)); + + var successfulStaticResult = new StaticResultDefinition + { + Outputs = new Outputs { Headers = new Dictionary(), StatusCode = "OK" }, + Status = "Succeeded" + }; + + return await TemporaryEnableStaticResultAsync(actionName, successfulStaticResult); + } + + /// + /// Temporary enables a static result for an action with the given on the logic app, + /// and disables the static result when the returned instance gets disposed. + /// + /// The name of the action to enable the static result. + /// The definition that describes the static result for the action. + /// + /// An instance to control when the static result for the action on the logic app should be disabled. + /// + public async Task TemporaryEnableStaticResultAsync(string actionName, StaticResultDefinition definition) + { + Guard.NotNullOrEmpty(actionName, nameof(actionName)); + Guard.NotNull(definition, nameof(definition)); + + return await AsyncDisposable.CreateAsync( + async () => await EnableStaticResultForActionsAsync(new Dictionary { [actionName] = definition }), + async () => await DisableStaticResultForActionAsync(actionName)); + } + + /// + /// Enables static results for a given set of actions on the logic app. + /// + /// The set of action names and the corresponding static result. + /// + /// An instance to control when the static result for the actions on the logic app should be disabled. + /// + public async Task TemporaryEnableStaticResultsAsync(IDictionary actions) + { + Guard.NotNull(actions, nameof(actions)); + Guard.NotAny(actions, nameof(actions)); + + return await AsyncDisposable.CreateAsync( + async () => await EnableStaticResultForActionsAsync(actions), + async () => await DisableStaticResultsForActionsAsync(actions.Keys)); + } + + private async Task EnableStaticResultForActionsAsync(IDictionary actions) + { + Guard.NotNull(actions, nameof(actions)); + Guard.NotAny(actions, nameof(actions)); + Guard.For( + () => actions.Any(action => action.Key is null || action.Value is null), + "Cannot enable static result for actions when either the action or result is missing"); + + string actionNames = String.Join(", ", actions.Keys); + _logger.LogTrace("Enables (+) a static result definition for actions {ActionNames} of logic app '{LogicAppName}' in resource group '{ResourceGroup}'", actionNames, _logicAppName, _resourceGroup); + + Workflow workflow = await _logicManagementClient.Workflows.GetAsync(_resourceGroup, _logicAppName); + var logicAppDefinition = JsonConvert.DeserializeObject(workflow.Definition.ToString()); + + foreach (KeyValuePair action in actions) + { + if (logicAppDefinition.Actions[action.Key] is null) + { + continue; + } + + logicAppDefinition = UpdateLogicAppDefinitionWithStaticResult(logicAppDefinition, action.Key, action.Value); + } + + workflow.Definition = JObject.Parse(JsonConvert.SerializeObject(logicAppDefinition)); + + Workflow resultWorkflow = await _logicManagementClient.Workflows.CreateOrUpdateAsync(_resourceGroup, _logicAppName, workflow); + if (resultWorkflow.Name != _logicAppName) + { + throw new LogicAppNotUpdatedException(_logicManagementClient.SubscriptionId, _resourceGroup, _logicAppName, "Failed to enable a static result."); + } + } + + private static LogicAppDefinition UpdateLogicAppDefinitionWithStaticResult( + LogicAppDefinition logicAppDefinition, + string actionName, + StaticResultDefinition staticResultDefinition) + { + ActionDefinition actionDefinition = logicAppDefinition.Actions[actionName]; + if (actionDefinition.RuntimeConfiguration != null) + { + actionDefinition.RuntimeConfiguration.StaticResult.StaticResultOptions = "Enabled"; + } + else + { + string staticResultName = $"{actionName}0"; + actionDefinition.RuntimeConfiguration = new RuntimeConfiguration + { + StaticResult = new StaticResult { Name = staticResultName, StaticResultOptions = "Enabled" } + }; + + if (logicAppDefinition.StaticResults == null) + { + logicAppDefinition.StaticResults = new Dictionary(); + } + + logicAppDefinition.StaticResults[staticResultName] = staticResultDefinition; + } + + return logicAppDefinition; + } + + private async Task DisableStaticResultForActionAsync(string actionName) + { + Guard.NotNullOrEmpty(actionName, nameof(actionName)); + + _logger.LogTrace( + "Disables (-) a static result definition for action {ActionName} of logic app '{LogicAppName}' in resource group '{ResourceGroup}'", + actionName, _logicAppName, _resourceGroup); + + await DisableStaticResultsForActionAsync(name => name == actionName); + } + + private async Task DisableStaticResultsForActionsAsync(IEnumerable actionNames) + { + Guard.NotNull(actionNames, nameof(actionNames)); + Guard.NotAny(actionNames, nameof(actionNames)); + Guard.For( + () => actionNames.Any(action => action is null), + "Cannot disable static results for actions when one or more action names are missing"); + + _logger.LogTrace( + "Disables (-) a static result definition for actions {ActionNames} of logic app '{LogicAppName}' in resource group '{ResourceGroup}'", + actionNames, _logicAppName, _resourceGroup); + + await DisableStaticResultsForActionAsync(actionNames.Contains); + } + + private async Task DisableStaticResultsForActionAsync(Func shouldDisable) + { + Workflow workflow = await _logicManagementClient.Workflows.GetAsync(_resourceGroup, _logicAppName); + var logicAppDefinition = JsonConvert.DeserializeObject(workflow.Definition.ToString()); + + foreach (KeyValuePair item in logicAppDefinition.Actions) + { + ActionDefinition actionDefinition = item.Value; + if (shouldDisable(item.Key) && actionDefinition.RuntimeConfiguration != null) + { + actionDefinition.RuntimeConfiguration.StaticResult.StaticResultOptions = "Disabled"; + } + } + + workflow.Definition = JObject.Parse(JsonConvert.SerializeObject(logicAppDefinition)); + + Workflow resultWorkflow = await _logicManagementClient.Workflows.CreateOrUpdateAsync(_resourceGroup, _logicAppName, workflow); + if (resultWorkflow.Name != _logicAppName) + { + throw new LogicAppNotUpdatedException(_logicManagementClient.SubscriptionId, _resourceGroup, _logicAppName, "Failed to disable the static results."); + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + _logicManagementClient?.Dispose(); + } + } +} diff --git a/src/Invictus.Testing/LogicAppConverter.cs b/src/Invictus.Testing/LogicAppConverter.cs new file mode 100644 index 0000000..afd78e2 --- /dev/null +++ b/src/Invictus.Testing/LogicAppConverter.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using GuardNet; +using Invictus.Testing.Model; +using Microsoft.Azure.Management.Logic.Models; +using Newtonsoft.Json; + +namespace Invictus.Testing +{ + /// + /// Collection of conversion function to create custom models from Azure SDK models. + /// + public static class LogicAppConverter + { + /// + /// Convert to . + /// + public static LogicAppRun ToLogicAppRun(WorkflowRun workFlowRun, IEnumerable actions) + { + Guard.NotNull(workFlowRun, nameof(workFlowRun)); + Guard.NotNull(actions, nameof(actions)); + + return new LogicAppRun + { + Id = workFlowRun.Name, + StartTime = workFlowRun.StartTime, + EndTime = workFlowRun.EndTime, + Status = workFlowRun.Status, + Error = workFlowRun.Error, + CorrelationId = workFlowRun.Correlation?.ClientTrackingId, + Trigger = CreateLogicAppTriggerFrom(workFlowRun.Trigger), + Actions = actions, + TrackedProperties = new ReadOnlyDictionary(GetAllTrackedProperties(actions)) + }; + } + + private static LogicAppTrigger CreateLogicAppTriggerFrom(WorkflowRunTrigger workflowRunTrigger) + { + return new LogicAppTrigger + { + Name = workflowRunTrigger.Name, + Inputs = workflowRunTrigger.Inputs, + Outputs = workflowRunTrigger.Outputs, + StartTime = workflowRunTrigger.StartTime, + EndTime = workflowRunTrigger.EndTime, + Status = workflowRunTrigger.Status, + Error = workflowRunTrigger.Error + }; + } + + /// + /// Convert to . + /// + public static LogicAppAction ToLogicAppAction(WorkflowRunAction workflowRunAction, dynamic input, dynamic output) + { + var logicAppAction = new LogicAppAction + { + Name = workflowRunAction.Name, + StartTime = workflowRunAction.StartTime, + EndTime = workflowRunAction.EndTime, + Status = workflowRunAction.Status, + Error = workflowRunAction.Error, + Inputs = input, + Outputs = output + }; + + if (workflowRunAction.TrackedProperties != null) + { + logicAppAction.TrackedProperties = + JsonConvert.DeserializeObject>( + workflowRunAction.TrackedProperties.ToString()); + } + + return logicAppAction; + } + + private static IDictionary GetAllTrackedProperties(IEnumerable actions) + { + return actions + .Where(x => x.TrackedProperties != null) + .OrderByDescending(x => x.StartTime) + .SelectMany(a => a.TrackedProperties) + .GroupBy(x => x.Key) + .Select(g => g.First()) + .ToDictionary(x => x.Key, x => x.Value); + } + + /// + /// Convert to . + /// + public static LogicApp ToLogicApp(Workflow workflow) + { + Guard.NotNull(workflow, nameof(workflow)); + + return new LogicApp + { + Name = workflow.Name, + CreatedTime = workflow.CreatedTime, + ChangedTime = workflow.ChangedTime, + State = workflow.State, + Version = workflow.Version, + AccessEndpoint = workflow.AccessEndpoint, + Definition = workflow.Definition + }; + } + } +} diff --git a/src/Invictus.Testing/LogicAppException.cs b/src/Invictus.Testing/LogicAppException.cs new file mode 100644 index 0000000..5c71918 --- /dev/null +++ b/src/Invictus.Testing/LogicAppException.cs @@ -0,0 +1,127 @@ +using System; +using System.Runtime.Serialization; +using System.Security.Permissions; +using GuardNet; + +namespace Invictus.Testing +{ + /// + /// Thrown when a problem occurs during interaction with a logic app running in Azure. + /// + [Serializable] + public class LogicAppException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public LogicAppException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the exception. + public LogicAppException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the exception. + /// The exception that is the cause of the current exception + public LogicAppException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID that identifies the subscription on Azure. + /// The resource group where the logic app is located. + /// The name of the logic app resource running in Azure. + /// The message that describes the exception. + public LogicAppException( + string subscriptionId, + string resourceGroup, + string logicAppName, + string message) : base(message) + { + Guard.NotNullOrWhitespace(subscriptionId, nameof(subscriptionId)); + Guard.NotNullOrWhitespace(resourceGroup, nameof(resourceGroup)); + Guard.NotNullOrWhitespace(logicAppName, nameof(logicAppName)); + + SubscriptionId = subscriptionId; + ResourceGroup = resourceGroup; + LogicAppName = logicAppName; + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID that identifies the subscription on Azure. + /// The resource group where the logic app is located. + /// The name of the logic app resource running in Azure. + /// The message that describes the exception. + /// The exception that is the cause of the current exception + public LogicAppException( + string subscriptionId, + string resourceGroup, + string logicAppName, + string message, + Exception innerException) : base(message, innerException) + { + Guard.NotNullOrWhitespace(subscriptionId, nameof(subscriptionId)); + Guard.NotNullOrWhitespace(resourceGroup, nameof(resourceGroup)); + Guard.NotNullOrWhitespace(logicAppName, nameof(logicAppName)); + + SubscriptionId = subscriptionId; + ResourceGroup = resourceGroup; + LogicAppName = logicAppName; + } + + /// + /// Initializes a new instance of the class. + /// + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + protected LogicAppException(SerializationInfo info, StreamingContext context) : base(info, context) + { + LogicAppName = info.GetString(nameof(LogicAppName)); + ResourceGroup = info.GetString(nameof(ResourceGroup)); + SubscriptionId = info.GetString(nameof(SubscriptionId)); + } + + /// + /// Gets the ID of the subscription of + /// + public string SubscriptionId { get; } + + /// + /// Gets the resource group on Azure in which the logic app is located. + /// + public string ResourceGroup { get; } + + /// + /// Gets the name of the logic app running on Azure. + /// + public string LogicAppName { get; } + + /// + /// When overridden in a derived class, sets the with information about the exception. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + /// The info parameter is a null reference (Nothing in Visual Basic). + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + Guard.NotNull(info, nameof(info)); + + info.AddValue(nameof(SubscriptionId), SubscriptionId); + info.AddValue(nameof(ResourceGroup), ResourceGroup); + info.AddValue(nameof(LogicAppName), LogicAppName); + + base.GetObjectData(info, context); + } + } +} diff --git a/src/Invictus.Testing/LogicAppNotUpdatedException.cs b/src/Invictus.Testing/LogicAppNotUpdatedException.cs new file mode 100644 index 0000000..86aa39f --- /dev/null +++ b/src/Invictus.Testing/LogicAppNotUpdatedException.cs @@ -0,0 +1,77 @@ +using System; +using System.Runtime.Serialization; +using System.Security.Permissions; + +namespace Invictus.Testing +{ + /// + /// Thrown when the logic app running in Azure could not be updated. + /// + [Serializable] + public class LogicAppNotUpdatedException : LogicAppException + { + /// + /// Initializes a new instance of the class. + /// + public LogicAppNotUpdatedException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the exception. + public LogicAppNotUpdatedException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the exception. + /// The exception that is the cause of the current exception + public LogicAppNotUpdatedException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID that identifies the subscription on Azure. + /// The resource group where the logic app is located. + /// The name of the logic app resource running in Azure. + /// The message that describes the exception. + public LogicAppNotUpdatedException( + string subscriptionId, + string resourceGroup, + string logicAppName, + string message) : base(subscriptionId, resourceGroup, logicAppName, message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID that identifies the subscription on Azure. + /// The resource group where the logic app is located. + /// The name of the logic app resource running in Azure. + /// The message that describes the exception. + /// The exception that is the cause of the current exception + public LogicAppNotUpdatedException( + string subscriptionId, + string resourceGroup, + string logicAppName, + string message, + Exception innerException) : base(subscriptionId, resourceGroup, logicAppName, message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + protected LogicAppNotUpdatedException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/Invictus.Testing/LogicAppTriggerNotFoundException.cs b/src/Invictus.Testing/LogicAppTriggerNotFoundException.cs new file mode 100644 index 0000000..7cd4b01 --- /dev/null +++ b/src/Invictus.Testing/LogicAppTriggerNotFoundException.cs @@ -0,0 +1,77 @@ +using System; +using System.Runtime.Serialization; +using System.Security.Permissions; + +namespace Invictus.Testing +{ + /// + /// Exception thrown when no trigger can be found for a given logic app. + /// + [Serializable] + public class LogicAppTriggerNotFoundException : LogicAppException + { + /// + /// Initializes a new instance of the class. + /// + public LogicAppTriggerNotFoundException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the exception. + public LogicAppTriggerNotFoundException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the exception. + /// The exception that is the cause of the current exception + public LogicAppTriggerNotFoundException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID that identifies the subscription on Azure. + /// The resource group where the logic app is located. + /// The name of the logic app resource running in Azure. + /// The message that describes the exception. + public LogicAppTriggerNotFoundException( + string subscriptionId, + string resourceGroup, + string logicAppName, + string message) : base(subscriptionId, resourceGroup, logicAppName, message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID that identifies the subscription on Azure. + /// The resource group where the logic app is located. + /// The name of the logic app resource running in Azure. + /// The message that describes the exception. + /// The exception that is the cause of the current exception + public LogicAppTriggerNotFoundException( + string subscriptionId, + string resourceGroup, + string logicAppName, + string message, + Exception innerException) : base(subscriptionId, resourceGroup, logicAppName, message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + protected LogicAppTriggerNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/Invictus.Testing/LogicAppsHelper.cs b/src/Invictus.Testing/LogicAppsHelper.cs deleted file mode 100644 index b342b92..0000000 --- a/src/Invictus.Testing/LogicAppsHelper.cs +++ /dev/null @@ -1,729 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Invictus.Testing.Model; -using Invictus.Testing.Serialization; -using Microsoft.Azure.Management.Logic; -using Microsoft.Azure.Management.Logic.Models; -using Microsoft.IdentityModel.Clients.ActiveDirectory; -using Microsoft.Rest; -using Microsoft.Rest.Azure.OData; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Polly; -using Polly.Retry; - -namespace Invictus.Testing -{ - public class LogicAppsHelper : IDisposable - { - private readonly LogicManagementClient _logicManagementClient; - private readonly string _resourceGroupPrefix; - private readonly string _logicAppPrefix; - - private const int DefaultTimeoutInSeconds = 90; - private const int PollIntervalInSeconds = 5; - - /// - /// LogicAppsHelper constructor. - /// - /// The Id of the Azure Subscription hosting the Logic Apps. - /// The Directory Id of the Azure Subscription AD - /// The Object Id of the Service Principal with sufficient access to all required resource groups. - /// The corresponding key of the Service Principal - public LogicAppsHelper(string subscriptionId, string tenantId, string clientId, string clientSecret) - { - _logicManagementClient = GetLogicManagementClientAsync(subscriptionId, tenantId, clientId, clientSecret).Result; - } - - /// - /// LogicAppsHelper constructor. - /// - /// The Id of the Azure Subscription hosting the Logic Apps. - /// The Directory Id of the Azure Subscription AD - /// The Object Id of the Service Principal with sufficient access to all required resource groups. - /// The corresponding key of the Service Principal - /// Prefix for Resource Group - /// Prefix for Logic App name - public LogicAppsHelper(string subscriptionId, string tenantId, string clientId, string clientSecret, string resourceGroupPrefix = "", string logicAppPrefix = "") - { - _resourceGroupPrefix = resourceGroupPrefix; - _logicAppPrefix = logicAppPrefix; - - _logicManagementClient = GetLogicManagementClientAsync(subscriptionId, tenantId, clientId, clientSecret).Result; - } - - /// - /// Enable a Logic App. - /// - /// The Azure Resource Group - /// The LogicApp name - /// - public async Task EnableLogicAppAsync(string resourceGroupName, string logicAppName) - { - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - - await _logicManagementClient.Workflows.EnableAsync(resourceGroupName, logicAppName); - } - - /// - /// Disable a Logic App. - /// - /// The Azure Resource Group - /// The LogicApp name - /// - public async Task DisableLogicAppAsync(string resourceGroupName, string logicAppName) - { - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - - await _logicManagementClient.Workflows.DisableAsync(resourceGroupName, logicAppName); - } - - /// - /// Delete a Logic App. - /// - /// The Azure Resource Group - /// The LogicApp name - /// - public async Task DeleteLogicAppAsync(string resourceGroupName, string logicAppName) - { - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - - await _logicManagementClient.Workflows.DeleteAsync(resourceGroupName, logicAppName); - } - - /// - /// Get Logic App. - /// - /// The Azure Resource Group - /// The LogicApp name - /// A LogicApp object - public async Task GetLogicAppAsync(string resourceGroupName, string logicAppName) - { - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - - var workflow = await _logicManagementClient.Workflows.GetAsync(resourceGroupName, logicAppName); - return (LogicApp)workflow; - } - - /// - /// Update Logic App definition. - /// - /// The Azure Resource Group - /// The LogicApp name - /// The LogicApp definition in JSON format - /// True is operation succeeded, false otherwise - public async Task UpdateLogicAppAsync(string resourceGroupName, string logicAppName, string logicAppDefinition) - { - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - var success = false; - var workflow = await _logicManagementClient.Workflows.GetAsync(resourceGroupName, logicAppName); - - workflow.Definition = JObject.Parse(logicAppDefinition); - var resultWorkflow = await - _logicManagementClient.Workflows.CreateOrUpdateAsync(resourceGroupName, logicAppName, workflow); - - if (resultWorkflow.Name == logicAppName) - { - success = true; - } - - return success; - } - - /// - /// Enable default static result for Logic App action. - /// - /// The Azure Resource Group - /// The LogicApp name - /// The LogicApp action name - /// True is operation succeeded, false otherwise - public async Task EnableStaticResultForActionAsync(string resourceGroupName, string logicAppName, string actionName) - { - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - var success = false; - - var workflow = await _logicManagementClient.Workflows.GetAsync(resourceGroupName, logicAppName); - LogicAppDefinition logicAppDefinition = JsonConvert.DeserializeObject(workflow.Definition.ToString()); - - ActionDefinition actionDefinition = logicAppDefinition.Actions[actionName]; - if (actionDefinition == null) return false; - - if (actionDefinition.RuntimeConfiguration != null) - { - actionDefinition.RuntimeConfiguration.StaticResult.StaticResultOptions = "Enabled"; - } - else - { - var staticResultName = $"{actionName}0"; - actionDefinition.RuntimeConfiguration = new RuntimeConfiguration - { - StaticResult = new StaticResult { Name = staticResultName, StaticResultOptions = "Enabled" } - }; - - StaticResultDefinition staticResultDefinition = new StaticResultDefinition - { - Outputs = new Outputs { Headers = new Dictionary(), StatusCode = "OK" }, - Status = "Succeeded" - }; - if (logicAppDefinition.StaticResults == null) - logicAppDefinition.StaticResults = new Dictionary(); - - logicAppDefinition.StaticResults.Add(staticResultName, staticResultDefinition); - } - - workflow.Definition = JObject.Parse(JsonConvert.SerializeObject(logicAppDefinition)); - - var resultWorkflow = await _logicManagementClient.Workflows.CreateOrUpdateAsync(resourceGroupName, logicAppName, workflow); - if (resultWorkflow.Name == logicAppName) - { - success = true; - } - - return success; - } - - /// - /// Enable static result for Logic App action. - /// - /// The Azure Resource Group - /// The LogicApp name - /// The LogicApp action name - /// The Static Result definition - /// True is operation succeeded, false otherwise - public async Task EnableStaticResultForActionAsync(string resourceGroupName, string logicAppName, string actionName, StaticResultDefinition staticResultDefinition) - { - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - var success = false; - - var workflow = await _logicManagementClient.Workflows.GetAsync(resourceGroupName, logicAppName); - LogicAppDefinition logicAppDefinition = JsonConvert.DeserializeObject(workflow.Definition.ToString()); - - ActionDefinition actionDefinition = logicAppDefinition.Actions[actionName]; - if (actionDefinition == null) return false; - - var staticResultName = $"{actionName}0"; - if (actionDefinition.RuntimeConfiguration != null) - { - actionDefinition.RuntimeConfiguration.StaticResult.StaticResultOptions = "Enabled"; - } - else - { - actionDefinition.RuntimeConfiguration = new RuntimeConfiguration - { - StaticResult = new StaticResult { Name = staticResultName, StaticResultOptions = "Enabled" } - }; - } - - if (logicAppDefinition.StaticResults == null) - logicAppDefinition.StaticResults = new Dictionary(); - - if (logicAppDefinition.StaticResults.ContainsKey(staticResultName)) - logicAppDefinition.StaticResults[staticResultName] = staticResultDefinition; - else - logicAppDefinition.StaticResults.Add(staticResultName, staticResultDefinition); - - workflow.Definition = JObject.Parse(JsonConvert.SerializeObject(logicAppDefinition)); - - var resultWorkflow = await _logicManagementClient.Workflows.CreateOrUpdateAsync(resourceGroupName, logicAppName, workflow); - if (resultWorkflow.Name == logicAppName) - { - success = true; - } - - return success; - } - - /// - /// Enable static result for Logic App actions. - /// - /// The Azure Resource Group - /// The LogicApp name - /// List of LogicApp actions and static result definitions - /// True is operation succeeded, false otherwise - public async Task EnableStaticResultForActionsAsync(string resourceGroupName, string logicAppName, Dictionary actions) - { - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - var success = false; - - var workflow = await _logicManagementClient.Workflows.GetAsync(resourceGroupName, logicAppName); - LogicAppDefinition logicAppDefinition = JsonConvert.DeserializeObject(workflow.Definition.ToString()); - - foreach (var item in actions) - { - var actionName = item.Key; - var staticResultDefinition = item.Value; - ActionDefinition actionDefinition = logicAppDefinition.Actions[actionName]; - if (actionDefinition == null) continue; - - var staticResultName = $"{actionName}0"; - if (actionDefinition.RuntimeConfiguration != null) - { - actionDefinition.RuntimeConfiguration.StaticResult.StaticResultOptions = "Enabled"; - } - else - { - actionDefinition.RuntimeConfiguration = new RuntimeConfiguration - { - StaticResult = new StaticResult { Name = staticResultName, StaticResultOptions = "Enabled" } - }; - } - - if (logicAppDefinition.StaticResults == null) - logicAppDefinition.StaticResults = new Dictionary(); - - if (logicAppDefinition.StaticResults.ContainsKey(staticResultName)) - logicAppDefinition.StaticResults[staticResultName] = staticResultDefinition; - else - logicAppDefinition.StaticResults.Add(staticResultName, staticResultDefinition); - } - - - workflow.Definition = JObject.Parse(JsonConvert.SerializeObject(logicAppDefinition)); - var resultWorkflow = await _logicManagementClient.Workflows.CreateOrUpdateAsync(resourceGroupName, logicAppName, workflow); - if (resultWorkflow.Name == logicAppName) - { - success = true; - } - - return success; - } - - /// - /// Disable static result for Logic App action. - /// - /// The Azure Resource Group - /// The LogicApp name - /// The LogicApp action name - /// True is operation succeeded, false otherwise - public async Task DisableStaticResultForActionAsync(string resourceGroupName, string logicAppName, string actionName) - { - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - var success = false; - - var workflow = await _logicManagementClient.Workflows.GetAsync(resourceGroupName, logicAppName); - LogicAppDefinition logicAppDefinition = JsonConvert.DeserializeObject(workflow.Definition.ToString()); - - ActionDefinition actionDefinition = logicAppDefinition.Actions[actionName]; - if (actionDefinition == null) return false; - - if (actionDefinition.RuntimeConfiguration != null) - { - actionDefinition.RuntimeConfiguration.StaticResult.StaticResultOptions = "Disabled"; - } - - workflow.Definition = JObject.Parse(JsonConvert.SerializeObject(logicAppDefinition)); - - var resultWorkflow = await _logicManagementClient.Workflows.CreateOrUpdateAsync(resourceGroupName, logicAppName, workflow); - if (resultWorkflow.Name == logicAppName) - { - success = true; - } - - return success; - } - - /// - /// Disables static results for all actions of a Logic App. - /// - /// The Azure Resource Group - /// The LogicApp name - /// - public async Task DisableAllStaticResultsForLogicAppAsync(string resourceGroupName, string logicAppName) - { - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - var success = false; - - var workflow = await _logicManagementClient.Workflows.GetAsync(resourceGroupName, logicAppName); - LogicAppDefinition logicAppDefinition = JsonConvert.DeserializeObject(workflow.Definition.ToString()); - - foreach (var item in logicAppDefinition.Actions) - { - ActionDefinition actionDefinition = item.Value; - - if (actionDefinition.RuntimeConfiguration != null) - { - actionDefinition.RuntimeConfiguration.StaticResult.StaticResultOptions = "Disabled"; - } - - } - - workflow.Definition = JObject.Parse(JsonConvert.SerializeObject(logicAppDefinition)); - - var resultWorkflow = await _logicManagementClient.Workflows.CreateOrUpdateAsync(resourceGroupName, logicAppName, workflow); - if (resultWorkflow.Name == logicAppName) - { - success = true; - } - - return success; - } - - /// - /// Get Logic App Trigger Url - /// - /// The Azure Resource Group - /// The LogicApp name - /// The LogicApp trigger name - /// A LogicAppTriggerUrl object - public async Task GetLogicAppTriggerUrlAsync(string resourceGroupName, string logicAppName, string triggerName = "") - { - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - if (string.IsNullOrEmpty(triggerName)) - { - triggerName = await GetTriggerName(resourceGroupName, logicAppName); - } - var callbackUrl = await _logicManagementClient.WorkflowTriggers.ListCallbackUrlAsync(resourceGroupName, logicAppName, triggerName); - - return new LogicAppTriggerUrl { Value = callbackUrl.Value, Method = callbackUrl.Method }; - } - - /// - /// Runs a Logic App. - /// - /// The Azure Resource Group - /// The LogicApp name - /// The LogicApp trigger name - /// - public async Task RunLogicAppAsync(string resourceGroupName, string logicAppName, string triggerName = "") - { - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - if (string.IsNullOrEmpty(triggerName)) - { - triggerName = await GetTriggerName(resourceGroupName, logicAppName); - } - - await _logicManagementClient.WorkflowTriggers.RunAsync(resourceGroupName, logicAppName, triggerName); - } - - /// - /// Get Logic App Run By Id. - /// - /// The Azure Resource Group - /// The LogicApp name - /// The LogicApp run identifier - /// A LogicApp run - public async Task GetLogicAppRunAsync(string resourceGroupName, string logicAppName, string Id) - { - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - - var workflowRun = await _logicManagementClient.WorkflowRuns.GetAsync(resourceGroupName, logicAppName, Id); - return Converter.ToLogicAppRun(this, resourceGroupName, logicAppName, workflowRun); - } - - /// - /// Poll for logic app run by correlation id. - /// - /// The Azure Resource Group - /// The LogicApp name - /// The start time of the LogicApp run - /// The correlation Id - /// The timeout period in seconds - /// A LogicApp run - public async Task PollForLogicAppRunAsync(string resourceGroupName, string logicAppName, DateTime startTime, string correlationId, - TimeSpan? timeout = null) - { - if (timeout == null) { timeout = TimeSpan.FromSeconds(DefaultTimeoutInSeconds); } - - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - - var odataQuery = GetOdataQuery(startTime, correlationId); - - return await Poll( - async () => - { - var result = await _logicManagementClient.WorkflowRuns.ListAsync(resourceGroupName, logicAppName, odataQuery); - return result.Select(x => Converter.ToLogicAppRun(this, resourceGroupName, logicAppName, x)).FirstOrDefault(); - }, - PollIntervalInSeconds, - timeout.Value); - } - - /// - /// Poll for logic app runs after timeout expired. - /// - /// The Azure Resource Group - /// The LogicApp name - /// The start time of the LogicApp run - /// The correlation Id - /// The timeout period in seconds - /// Expected number of items - /// List of LogicApp runs - public async Task> PollForLogicAppRunsAsync(string resourceGroupName, string logicAppName, DateTime startTime, string correlationId, - TimeSpan? timeout = null, int numberOfItems = 0) - { - if (timeout == null) { timeout = TimeSpan.FromSeconds(DefaultTimeoutInSeconds); } - - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - - if (numberOfItems > 0) - { - return await Poll( - async () => await FindLogicAppRunsByCorrelationIdAsync(resourceGroupName, logicAppName, startTime, correlationId), - numberOfItems, - PollIntervalInSeconds, - timeout.Value); - } - else - { - return await PollAfterTimeout( - async () => await FindLogicAppRunsByCorrelationIdAsync(resourceGroupName, logicAppName, startTime, correlationId), - timeout.Value); - } - } - - /// - /// Poll for logic app run by tracked property. - /// - /// The Azure Resource Group - /// The LogicApp name - /// The start time of the LogicApp run - /// Tracked Property name - /// Tracked Property value - /// The timeout period in seconds - /// A LogicApp run - public async Task PollForLogicAppRunAsync(string resourceGroupName, string logicAppName, DateTime startTime, - string trackedPropertyName, string trackedProperyValue, TimeSpan? timeout = null) - { - if (timeout == null) { timeout = TimeSpan.FromSeconds(DefaultTimeoutInSeconds); } - - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - - return await Poll( - async () => await FindLogicAppRunByTrackedPropertyAsync(resourceGroupName, logicAppName, startTime, trackedPropertyName, trackedProperyValue), - PollIntervalInSeconds, - timeout.Value - ); - } - - /// - /// Poll for logic app runs after timeout expired. - /// - /// The Azure Resource Group - /// The LogicApp name - /// The start time of the LogicApp run - /// Tracked Property name - /// Tracked Property value - /// The timeout period in seconds - /// Expected number of items - /// List of LogicApp runs - public async Task> PollForLogicAppRunsAsync(string resourceGroupName, string logicAppName, DateTime startTime, - string trackedPropertyName, string trackedProperyValue, TimeSpan? timeout = null, int numberOfItems = 0) - { - if (timeout == null) { timeout = TimeSpan.FromSeconds(DefaultTimeoutInSeconds); } - - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - - if (numberOfItems > 0) - { - return await Poll( - async () => await FindLogicAppRunsByTrackedPropertyAsync(resourceGroupName, logicAppName, startTime, trackedPropertyName, trackedProperyValue), - numberOfItems, - PollIntervalInSeconds, - timeout.Value); - } - else - { - return await PollAfterTimeout( - async () => await FindLogicAppRunsByTrackedPropertyAsync(resourceGroupName, logicAppName, startTime, trackedPropertyName, trackedProperyValue), - timeout.Value); - } - } - - /// - /// Get LogicApp Run actions. - /// - /// The Azure Resource Group - /// The LogicApp name - /// The LogicApp Run Identifier - /// - /// List of LogicApp actions - public async Task> GetLogicAppRunActionsAsync(string resourceGroupName, string logicAppName, string runName, bool usePrefix = true) - { - if (usePrefix) - { - resourceGroupName = PrefixResourceGroupName(resourceGroupName); - logicAppName = PrefixLogicAppName(logicAppName); - } - - return await FindLogicAppRunActionsAsync(resourceGroupName, logicAppName, runName); - } - - #region Private Methods - private async Task GetLogicManagementClientAsync(string subscriptionId, string tenantId, string clientId, string clientSecret) - { - var authority = string.Format("{0}{1}", "https://login.windows.net/", tenantId); - - var authContext = new AuthenticationContext(authority); - var credential = new ClientCredential(clientId, clientSecret); - - var token = await authContext.AcquireTokenAsync("https://management.azure.com/", credential); - - return new LogicManagementClient(new TokenCredentials(token.AccessToken)) { SubscriptionId = subscriptionId }; - } - - private async Task FindLogicAppRunByTrackedPropertyAsync(string resourceGroupName, string logicAppName, DateTime startTime, string trackedPropertyName, string trackedProperyValue) - { - LogicAppRun result = null; - - var odataQuery = new ODataQuery - { - Filter = $"StartTime ge {startTime.ToString("O")} and Status ne 'Running'" - }; - var workFlowRuns = await _logicManagementClient.WorkflowRuns.ListAsync(resourceGroupName, logicAppName, odataQuery); - - foreach (var workFlowRun in workFlowRuns) - { - var actions = await FindLogicAppRunActionsAsync(resourceGroupName, logicAppName, workFlowRun.Name); - - bool trackedPropertyFound = actions - .Where(a => a.TrackedProperties != null) - .Any(a => a.TrackedProperties.Count(t => t.Key.Equals(trackedPropertyName, StringComparison.OrdinalIgnoreCase) && t.Value.Equals(trackedProperyValue, StringComparison.OrdinalIgnoreCase)) > 0); - - if (trackedPropertyFound) - { - result = Converter.ToLogicAppRun(workFlowRun, actions); - break; - } - } - - return result; - } - - private async Task> FindLogicAppRunsByTrackedPropertyAsync(string resourceGroupName, string logicAppName, DateTime startTime, string trackedPropertyName, string trackedProperyValue) - { - var result = new List(); - - var odataQuery = new ODataQuery - { - Filter = $"StartTime ge {startTime.ToString("O")} and Status ne 'Running'" - }; - var workFlowRuns = await _logicManagementClient.WorkflowRuns.ListAsync(resourceGroupName, logicAppName, odataQuery); - - Parallel.ForEach(workFlowRuns, (workFlowRun) => - { - var actions = FindLogicAppRunActionsAsync(resourceGroupName, logicAppName, workFlowRun.Name).Result; - - bool trackedPropertyFound = actions - .Where(a => a.TrackedProperties != null) - .Any(a => a.TrackedProperties.Count(t => t.Key.Equals(trackedPropertyName, StringComparison.OrdinalIgnoreCase) && t.Value.Equals(trackedProperyValue, StringComparison.OrdinalIgnoreCase)) > 0); - - if (trackedPropertyFound) - { - var logicAppRun = Converter.ToLogicAppRun(workFlowRun, actions); - result.Add(logicAppRun); - } - - }); - - return result; - } - - private async Task> FindLogicAppRunsByCorrelationIdAsync(string resourceGroupName, string logicAppName, DateTime startTime, string correlationId) - { - var odataQuery = GetOdataQuery(startTime, correlationId); - - var result = await _logicManagementClient.WorkflowRuns.ListAsync(resourceGroupName, logicAppName, odataQuery); - return result.Select(x => Converter.ToLogicAppRun(this, resourceGroupName, logicAppName, x)).ToList(); - } - - private async Task> FindLogicAppRunActionsAsync(string resourceGroupName, string logicAppName, string runName) - { - var actions = new List(); - - var workflowRunActions = await _logicManagementClient.WorkflowRunActions.ListAsync(resourceGroupName, logicAppName, runName); - foreach (var workflowRunAction in workflowRunActions) - { - actions.Add(await Converter.ToLogicAppActionAsync(workflowRunAction)); - } - - return actions; - } - - private async Task GetTriggerName(string resourceGroupName, string logicAppName) - { - var triggerName = string.Empty; - var triggers = await _logicManagementClient.WorkflowTriggers.ListAsync(resourceGroupName, logicAppName); - if (triggers.Count() > 0) - { - triggerName = triggers.First().Name; - } - - return triggerName; - } - - private string PrefixResourceGroupName(string resourceGroupName) - { - return $"{_resourceGroupPrefix}{resourceGroupName}"; - } - - private string PrefixLogicAppName(string logicAppName) - { - return $"{_logicAppPrefix}{logicAppName}"; - } - - private ODataQuery GetOdataQuery(DateTime startTime, string correlationId) - { - return new ODataQuery - { - Filter = $"StartTime ge {startTime.ToString("O")} and ClientTrackingId eq '{correlationId}' and Status ne 'Running'" - }; - } - - private async Task Poll(Func> condition, int pollIntervalSeconds, TimeSpan timeout) - { - RetryPolicy retryPolicy = - Policy.HandleResult(result => result == null) - .WaitAndRetryForeverAsync(index => TimeSpan.FromSeconds(pollIntervalSeconds)); - - return await Policy.TimeoutAsync(timeout) - .WrapAsync(retryPolicy) - .ExecuteAsync(condition); - } - - private async Task> Poll(Func>> condition, int count, int pollIntervalSeconds, TimeSpan timeout) - { - RetryPolicy> retryPolicy = - Policy.HandleResult>(results => results.Count < count) - .WaitAndRetryForeverAsync(index => TimeSpan.FromSeconds(pollIntervalSeconds)); - - return await Policy.TimeoutAsync(timeout) - .WrapAsync(retryPolicy) - .ExecuteAsync(condition); - } - - private async Task PollAfterTimeout(Func> returnDelegate, TimeSpan timeout) - { - await Task.Delay(timeout); - return await returnDelegate(); - } - #endregion - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - _logicManagementClient?.Dispose(); - } - } -} diff --git a/src/Invictus.Testing/LogicAppsProvider.cs b/src/Invictus.Testing/LogicAppsProvider.cs new file mode 100644 index 0000000..e8e328a --- /dev/null +++ b/src/Invictus.Testing/LogicAppsProvider.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using GuardNet; +using Invictus.Testing.Model; +using Microsoft.Azure.Management.Logic; +using Microsoft.Azure.Management.Logic.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Rest.Azure; +using Microsoft.Rest.Azure.OData; +using Newtonsoft.Json.Linq; +using Polly; +using Polly.Retry; +using Polly.Timeout; + +namespace Invictus.Testing +{ + /// + /// Component to provide access in a reliable manner on logic app resources running in Azure. + /// + public class LogicAppsProvider + { + private readonly string _resourceGroup, _logicAppName; + private readonly LogicAuthentication _authentication; + private readonly TimeSpan _retryInterval = TimeSpan.FromSeconds(1); + private readonly ILogger _logger; + + private DateTimeOffset _startTime = DateTimeOffset.UtcNow; + private TimeSpan _timeout = TimeSpan.FromSeconds(90); + private string _trackedPropertyName, _trackedPropertyValue, _correlationId; + private bool _hasTrackedProperty, _hasCorrelationId; + + private static readonly HttpClient HttpClient = new HttpClient(); + + private LogicAppsProvider( + string resourceGroup, + string logicAppName, + LogicAuthentication authentication, + ILogger logger) + { + Guard.NotNullOrWhitespace(resourceGroup, nameof(resourceGroup)); + Guard.NotNullOrWhitespace(logicAppName, nameof(logicAppName)); + Guard.NotNull(authentication, nameof(authentication)); + Guard.NotNull(logger, nameof(logger)); + + _resourceGroup = resourceGroup; + _logicAppName = logicAppName; + _authentication = authentication; + _logger = logger; + } + + /// + /// Creates a new instance of the class. + /// + /// The resource group where the logic apps should be located. + /// The name of the logic app resource running in Azure. + /// The authentication mechanism to authenticate with Azure. + public static LogicAppsProvider LocatedAt( + string resourceGroup, + string logicAppName, + LogicAuthentication authentication) + { + Guard.NotNullOrWhitespace(resourceGroup, nameof(resourceGroup)); + Guard.NotNullOrWhitespace(logicAppName, nameof(logicAppName)); + Guard.NotNull(authentication, nameof(authentication)); + + return LocatedAt(resourceGroup, logicAppName, authentication, NullLogger.Instance); + } + + /// + /// Creates a new instance of the class. + /// + /// The resource group where the logic apps should be located. + /// The name of the logic app resource running in Azure. + /// The authentication mechanism to authenticate with Azure. + /// The instance to write diagnostic trace messages while interacting with the provider. + public static LogicAppsProvider LocatedAt( + string resourceGroup, + string logicAppName, + LogicAuthentication authentication, + ILogger logger) + { + Guard.NotNullOrEmpty(resourceGroup, nameof(resourceGroup)); + Guard.NotNullOrEmpty(logicAppName, nameof(logicAppName)); + Guard.NotNull(authentication, nameof(authentication)); + + logger = logger ?? NullLogger.Instance; + return new LogicAppsProvider(resourceGroup, logicAppName, authentication, logger); + } + + /// + /// Sets the start time when the logic app runs were executed. + /// + /// The date that the logic app ran. + public LogicAppsProvider WithStartTime(DateTimeOffset startTime) + { + _startTime = startTime; + return this; + } + + /// + /// Sets the time period in which the retrieval of the logic app runs should succeed. + /// + /// The period to retrieve logic app runs. + public LogicAppsProvider WithTimeout(TimeSpan timeout) + { + _timeout = timeout; + return this; + } + + /// + /// Sets the correlation ID as client tracking when retrieving logic app runs. + /// + /// The client tracking of the logic app runs. + public LogicAppsProvider WithCorrelationId(string correlationId) + { + Guard.NotNull(correlationId, nameof(correlationId)); + + _hasCorrelationId = true; + _correlationId = correlationId; + return this; + } + + /// + /// Sets the tracking property as filter when retrieving logic app runs. + /// + /// The name of the tracked property to filter on. + /// The value of the tracked property to filter on. + public LogicAppsProvider WithTrackedProperty(string trackedPropertyName, string trackedPropertyValue) + { + Guard.NotNull(trackedPropertyName, nameof(trackedPropertyName)); + Guard.NotNull(trackedPropertyValue, nameof(trackedPropertyValue)); + + _hasTrackedProperty = true; + _trackedPropertyName = trackedPropertyName; + _trackedPropertyValue = trackedPropertyValue; + return this; + } + + /// + /// Starts polling for a series of logic app runs corresponding to the previously set filtering criteria. + /// + public async Task> PollForLogicAppRunsAsync() + { + var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.CancelAfter(_timeout); + + IEnumerable logicAppRuns = Enumerable.Empty(); + + while (!cancellationTokenSource.Token.IsCancellationRequested) + { + try + { + logicAppRuns = await GetLogicAppRunsAsync(); + await Task.Delay(TimeSpan.FromSeconds(1), cancellationTokenSource.Token); + } + catch (Exception exception) + { + _logger.LogError(exception, "Polling for logic app runs was faulted: {Message}", exception.Message); + } + } + + return logicAppRuns ?? Enumerable.Empty(); + } + + /// + /// Start polling for a single logic app run corresponding to the previously set filtering criteria. + /// + public async Task PollForSingleLogicAppRunAsync() + { + IEnumerable logicAppRuns = await PollForLogicAppRunsAsync(minimumNumberOfItems: 1); + return logicAppRuns.FirstOrDefault(); + } + + /// + /// Starts polling for a corresponding to the previously set filtering criteria. + /// + /// The minimum amount of logic app runs to retrieve. + public async Task> PollForLogicAppRunsAsync(int minimumNumberOfItems) + { + Guard.NotLessThanOrEqualTo(minimumNumberOfItems, 0, nameof(minimumNumberOfItems)); + + string amount = minimumNumberOfItems == 1 ? "any" : minimumNumberOfItems.ToString(); + + RetryPolicy> retryPolicy = + Policy.HandleResult>(currentLogicAppRuns => + { + int count = currentLogicAppRuns.Count(); + bool isStillPending = count < minimumNumberOfItems; + + _logger.LogTrace("Polling for {Amount} log app runs, whilst got now {Current} ", amount, count); + return isStillPending; + }).Or(ex => + { + _logger.LogError(ex, "Polling for logic app runs was faulted: {Message}", ex.Message); + return true; + }) + .WaitAndRetryForeverAsync(index => + { + _logger.LogTrace("Could not retrieve logic app runs in time, wait 1s and try again..."); + return _retryInterval; + }); + + PolicyResult> result = + await Policy.TimeoutAsync(_timeout) + .WrapAsync(retryPolicy) + .ExecuteAndCaptureAsync(GetLogicAppRunsAsync); + + if (result.Outcome == OutcomeType.Failure) + { + if (result.FinalException is null + || result.FinalException.GetType() == typeof(TimeoutRejectedException)) + { + string correlation = _hasCorrelationId + ? $"{Environment.NewLine} with correlation property equal '{_correlationId}'" + : String.Empty; + + string trackedProperty = _hasTrackedProperty + ? $" {Environment.NewLine} with tracked property [{_trackedPropertyName}] = {_trackedPropertyValue}" + : String.Empty; + + throw new TimeoutException( + $"Could not in the given timeout span ({_timeout:g}) retrieve the expected amount of logic app runs " + + $"{Environment.NewLine} with StartTime <= {_startTime:O}" + + correlation + + trackedProperty); + } + + throw result.FinalException; + } + + _logger.LogTrace("Polling finished successful with {LogicAppRunsCount} logic app runs", result.Result.Count()); + return result.Result; + } + + private async Task>> ExecutePolicy(RetryPolicy> retryPolicy) + { + PolicyResult> result = + await Policy.TimeoutAsync(_timeout) + .WrapAsync(retryPolicy) + .ExecuteAndCaptureAsync(GetLogicAppRunsAsync); + return result; + } + + private async Task> GetLogicAppRunsAsync() + { + using (LogicManagementClient managementClient = await _authentication.AuthenticateAsync()) + { + var odataQuery = new ODataQuery + { + Filter = $"StartTime ge {_startTime.UtcDateTime:O} and Status ne 'Running'" + }; + + if (_hasCorrelationId) + { + odataQuery.Filter += $" and ClientTrackingId eq '{_correlationId}'"; + } + + _logger.LogTrace( + "Query logic app runs for '{LogicAppName}' in resource group '{ResourceGroup}': {Query}", _logicAppName, _resourceGroup, odataQuery.Filter); + + IPage workFlowRuns = + await managementClient.WorkflowRuns.ListAsync(_resourceGroup, _logicAppName, odataQuery); + + _logger.LogTrace("Query returned {WorkFlowRunCount} workflow runs", workFlowRuns.Count()); + + var logicAppRuns = new Collection(); + foreach (WorkflowRun workFlowRun in workFlowRuns) + { + IEnumerable actions = + await FindLogicAppRunActionsAsync(managementClient, workFlowRun.Name); + + if (_hasTrackedProperty && actions.Any(action => HasTrackedProperty(action.TrackedProperties)) + || !_hasTrackedProperty) + { + var logicAppRun = LogicAppConverter.ToLogicAppRun(workFlowRun, actions); + logicAppRuns.Add(logicAppRun); + } + } + + _logger.LogTrace("Query resulted in {LogicAppRunCount} logic app runs", logicAppRuns.Count); + return logicAppRuns.AsEnumerable(); + } + } + + private async Task> FindLogicAppRunActionsAsync(ILogicManagementClient managementClient, string runName) + { + _logger.LogTrace("Find related logic app run actions..."); + IPage workflowRunActions = + await managementClient.WorkflowRunActions.ListAsync(_resourceGroup, _logicAppName, runName); + + var actions = new Collection(); + foreach (WorkflowRunAction workflowRunAction in workflowRunActions) + { + JToken input = await GetHttpJsonStringAsync(workflowRunAction.InputsLink?.Uri); + JToken output = await GetHttpJsonStringAsync(workflowRunAction.OutputsLink?.Uri); + + var action = LogicAppConverter.ToLogicAppAction(workflowRunAction, input, output); + actions.Add(action); + } + + _logger.LogTrace("Found {LogicAppActionsCount} logic app actions", actions.Count); + return actions.AsEnumerable(); + } + + private static async Task GetHttpJsonStringAsync(string uri) + { + if (uri != null) + { + string json = await HttpClient.GetStringAsync(uri); + return JToken.Parse(json); + } + + return null; + } + + private bool HasTrackedProperty(IDictionary properties) + { + if (properties is null || properties.Count <= 0) + { + return false; + } + + return properties.Any(property => + { + if (property.Key is null || property.Value is null) + { + return false; + } + + return property.Key.Equals(_trackedPropertyName, StringComparison.OrdinalIgnoreCase) + && property.Value.Equals(_trackedPropertyValue, StringComparison.OrdinalIgnoreCase); + }); + } + } +} \ No newline at end of file diff --git a/src/Invictus.Testing/LogicAuthentication.cs b/src/Invictus.Testing/LogicAuthentication.cs new file mode 100644 index 0000000..30390a4 --- /dev/null +++ b/src/Invictus.Testing/LogicAuthentication.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading.Tasks; +using GuardNet; +using Microsoft.Azure.Management.Logic; +using Microsoft.IdentityModel.Clients.ActiveDirectory; +using Microsoft.Rest; + +namespace Invictus.Testing +{ + /// + /// Authentication representation to authenticate with logic apps running on Azure. + /// + public class LogicAuthentication + { + private readonly Func> _authenticateAsync; + + private LogicAuthentication(Func> authenticateAsync) + { + Guard.NotNull(authenticateAsync, nameof(authenticateAsync)); + + _authenticateAsync = authenticateAsync; + } + + /// + /// Uses the service principal to authenticate with Azure. + /// + /// The ID where the resources are located on Azure. + /// The ID that identifies the subscription on Azure. + /// The ID of the client or application that has access to the logic apps running on Azure. + /// The secret of the client or application that has access to the logic apps running on Azure. + public static LogicAuthentication UsingServicePrincipal(string tenantId, string subscriptionId, string clientId, string clientSecret) + { + Guard.NotNullOrWhitespace(tenantId, nameof(tenantId)); + Guard.NotNullOrWhitespace(subscriptionId, nameof(subscriptionId)); + Guard.NotNullOrWhitespace(clientId, nameof(clientId)); + Guard.NotNullOrWhitespace(clientSecret, nameof(clientSecret)); + + return new LogicAuthentication( + () => AuthenticateLogicAppsManagementAsync(subscriptionId, tenantId, clientId, clientSecret)); + } + + /// + /// Authenticate with Azure with the previously chosen authentication mechanism. + /// + /// + /// The management client to interact with logic app resources running on Azure. + /// + public async Task AuthenticateAsync() + { + return await _authenticateAsync(); + } + + private static async Task AuthenticateLogicAppsManagementAsync(string subscriptionId, string tenantId, string clientId, string clientSecret) + { + string authority = $"https://login.windows.net/{tenantId}"; + + var authContext = new AuthenticationContext(authority); + var credential = new ClientCredential(clientId, clientSecret); + + AuthenticationResult token = await authContext.AcquireTokenAsync("https://management.azure.com/", credential); + + return new LogicManagementClient(new TokenCredentials(token.AccessToken)) + { + SubscriptionId = subscriptionId + }; + } + } +} \ No newline at end of file diff --git a/src/Invictus.Testing/Model/LogicApp.cs b/src/Invictus.Testing/Model/LogicApp.cs index 83d5384..df69c44 100644 --- a/src/Invictus.Testing/Model/LogicApp.cs +++ b/src/Invictus.Testing/Model/LogicApp.cs @@ -14,19 +14,5 @@ public class LogicApp public string Version { get; set; } public string AccessEndpoint { get; set; } public dynamic Definition { get; set; } - - public static explicit operator LogicApp(Workflow workflow) - { - return new LogicApp - { - Name = workflow.Name, - CreatedTime = workflow.CreatedTime, - ChangedTime = workflow.ChangedTime, - State = workflow.State, - Version = workflow.Version, - AccessEndpoint = workflow.AccessEndpoint, - Definition = workflow.Definition - }; - } } } diff --git a/src/Invictus.Testing/Model/LogicAppAction.cs b/src/Invictus.Testing/Model/LogicAppAction.cs index d05dd26..a1f6d96 100644 --- a/src/Invictus.Testing/Model/LogicAppAction.cs +++ b/src/Invictus.Testing/Model/LogicAppAction.cs @@ -16,20 +16,5 @@ public class LogicAppAction public dynamic Inputs { get; set; } public dynamic Outputs { get; set; } public Dictionary TrackedProperties { get; set; } - - public static explicit operator LogicAppAction(WorkflowRunAction workflowRunAction) - { - return new LogicAppAction - { - Name = workflowRunAction.Name, - StartTime = workflowRunAction.StartTime, - EndTime = workflowRunAction.EndTime, - Status = workflowRunAction.Status, - Error = workflowRunAction.Error, - TrackedProperties = workflowRunAction.TrackedProperties != null - ? JsonConvert.DeserializeObject>(workflowRunAction.TrackedProperties.ToString()) - : null - }; - } } } diff --git a/src/Invictus.Testing/Model/LogicAppRun.cs b/src/Invictus.Testing/Model/LogicAppRun.cs index 5b800dc..74214c0 100644 --- a/src/Invictus.Testing/Model/LogicAppRun.cs +++ b/src/Invictus.Testing/Model/LogicAppRun.cs @@ -14,21 +14,7 @@ public class LogicAppRun public object Error { get; set; } public string CorrelationId { get; set; } public LogicAppTrigger Trigger { get; set; } - public List Actions { get; set; } - public Dictionary TrackedProperties { get; set; } - - public static explicit operator LogicAppRun(WorkflowRun workFlowRun) - { - return new LogicAppRun - { - Id = workFlowRun.Name, - StartTime = workFlowRun.StartTime, - EndTime = workFlowRun.EndTime, - Status = workFlowRun.Status, - Error = workFlowRun.Error, - CorrelationId = workFlowRun.Correlation?.ClientTrackingId, - Trigger = (LogicAppTrigger)workFlowRun.Trigger - }; - } + public IEnumerable Actions { get; set; } + public IReadOnlyDictionary TrackedProperties { get; set; } } } diff --git a/src/Invictus.Testing/Model/LogicAppTrigger.cs b/src/Invictus.Testing/Model/LogicAppTrigger.cs index 74805b5..3721ca2 100644 --- a/src/Invictus.Testing/Model/LogicAppTrigger.cs +++ b/src/Invictus.Testing/Model/LogicAppTrigger.cs @@ -14,19 +14,5 @@ public class LogicAppTrigger public DateTimeOffset? EndTime { get; set; } public string Status { get; set; } public object Error { get; set; } - - public static explicit operator LogicAppTrigger(WorkflowRunTrigger workflowRunTrigger) - { - return new LogicAppTrigger - { - Name = workflowRunTrigger.Name, - Inputs = workflowRunTrigger.Inputs, - Outputs = workflowRunTrigger.Outputs, - StartTime = workflowRunTrigger.StartTime, - EndTime = workflowRunTrigger.EndTime, - Status = workflowRunTrigger.Status, - Error = workflowRunTrigger.Error, - }; - } } } diff --git a/src/Invictus.Testing/TimeoutTracker.cs b/src/Invictus.Testing/TimeoutTracker.cs deleted file mode 100644 index e949dae..0000000 --- a/src/Invictus.Testing/TimeoutTracker.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Invictus.Testing -{ - public struct TimeoutTracker - { - private int _total; - private int _start; - - public TimeoutTracker(TimeSpan timeout) - { - long ltm = (long)timeout.TotalMilliseconds; - if (ltm < -1 || ltm > (long)int.MaxValue) - throw new ArgumentOutOfRangeException(nameof(timeout)); - _total = (int)ltm; - if (_total != -1 && _total != 0) - _start = Environment.TickCount; - else - _start = 0; - } - - public TimeoutTracker(int millisecondsTimeout) - { - if (millisecondsTimeout < -1) - throw new ArgumentOutOfRangeException(nameof(millisecondsTimeout)); - _total = millisecondsTimeout; - if (_total != -1 && _total != 0) - _start = Environment.TickCount; - else - _start = 0; - } - - public int RemainingMilliseconds - { - get - { - if (_total == -1 || _total == 0) - return _total; - - int elapsed = Environment.TickCount - _start; - // elapsed may be negative if TickCount has overflowed by 2^31 milliseconds. - if (elapsed < 0 || elapsed >= _total) - return 0; - - return _total - elapsed; - } - } - - public bool IsExpired - { - get - { - return RemainingMilliseconds == 0; - } - } - } -}