diff --git a/.azure-devops/pipelines/build.yml b/.azure-devops/pipelines/build.yml index 48d32a3..337a651 100644 --- a/.azure-devops/pipelines/build.yml +++ b/.azure-devops/pipelines/build.yml @@ -24,9 +24,9 @@ jobs: Windows: imageName: 'windows-latest' matrixName: Windows - #Mac: - # imageName: 'macOS-latest' - # matrixName: Mac + Mac: + imageName: 'macOS-latest' + matrixName: Mac pool: vmImage: $(imageName) @@ -35,13 +35,6 @@ jobs: # Build .NET solution - - task: UseDotNet@2 - displayName: 'Setup .NET' - inputs: - packageType: sdk - version: '6.x' - installationPath: $(Agent.ToolsDirectory)/dotnet - - task: DotNetCoreCLI@2 displayName: 'Restore dependencies' inputs: @@ -58,26 +51,16 @@ jobs: # Install and configure Logic Apps runtime environment - - task: NodeTool@0 - displayName: 'Setup node' - inputs: - versionSpec: '18.x' - - task: FuncToolsInstaller@0 displayName: 'Install Functions core tools' inputs: version: 'latest' - - task: CmdLine@2 - displayName: 'Check Functions Core tools installation' - inputs: - script: func - - task: Npm@1 displayName: 'Install Azurite' inputs: command: 'custom' - customCommand: 'install -g azurite@3.33.0' + customCommand: 'install -g azurite@3.34.0' - task: CmdLine@2 displayName: 'Start Azurite services (not Windows)' @@ -91,6 +74,23 @@ jobs: inputs: script: 'start /b azurite' + # Check software versions + + - task: CmdLine@2 + displayName: 'Check dotnet SDK installation' + inputs: + script: dotnet --info + + - task: CmdLine@2 + displayName: 'Check node installation' + inputs: + script: node --version + + - task: CmdLine@2 + displayName: 'Check Functions Core tools installation' + inputs: + script: func --version + # Run tests and publish test results to Azure DevOps - task: DotNetCoreCLI@2 @@ -104,16 +104,16 @@ jobs: publishTestResults: true testRunTitle: 'Tests ($(matrixName))' - - task: DotNetCoreCLI@2 - displayName: 'Run tests (Windows)' - condition: and(succeeded(), eq(variables.matrixName, 'Windows')) - continueOnError: true - inputs: - command: test - arguments: '--no-restore --verbosity normal --configuration ${{ parameters.buildConfiguration }}' - projects: '$(System.DefaultWorkingDirectory)/src/LogicAppUnit.sln' - publishTestResults: true - testRunTitle: 'Tests ($(matrixName))' + # - task: DotNetCoreCLI@2 # there is an issue with Azurite ports being blocked when running tests on Windows build servers + # displayName: 'Run tests (Windows)' + # condition: and(succeeded(), eq(variables.matrixName, 'Windows')) + # continueOnError: true + # inputs: + # command: test + # arguments: '--no-restore --verbosity normal --configuration ${{ parameters.buildConfiguration }}' + # projects: '$(System.DefaultWorkingDirectory)/src/LogicAppUnit.sln' + # publishTestResults: true + # testRunTitle: 'Tests ($(matrixName))' # Publish NuGet package diff --git a/.editorconfig b/.editorconfig index 0d652c5..bff08ce 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,12 +1,13 @@ ; This file is for unifying the coding style for different editors and IDEs. ; More information at http://EditorConfig.org -# top-most EditorConfig file root = true -# Don't use tabs for indentation +# All files [*] indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true # Code files [*.{cs,csx}] @@ -38,3 +39,13 @@ dotnet_diagnostic.CA1310.severity = none # Underscores are acceptable for unit test names. [*.cs] dotnet_diagnostic.CA1707.severity = none + +# CA1852: Seal internal types +# Sealing classes provides minimal performance improvements and is not a concern. +[*.cs] +dotnet_diagnostic.CA1852.severity = none + +# CA1866: Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char +# Implementing this causes the unit tests to break on macOS and Linux platforms! +[*.cs] +dotnet_diagnostic.CA1866.severity = none diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b25ce27..748b8c4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,11 +25,6 @@ jobs: uses: actions/checkout@v3 # Build .NET solution - - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 6.0.x - name: Restore dependencies run: dotnet restore ${{ github.workspace }}/src/LogicAppUnit.sln @@ -39,11 +34,6 @@ jobs: # Install and configure Logic Apps runtime environment - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: 18 - - name: Install Functions Core tools run: 'npm install -g azure-functions-core-tools@4 --unsafe-perm true' @@ -52,40 +42,48 @@ jobs: run: 'setx /m Path "C:\npm\prefix\node_modules\azure-functions-core-tools\bin;%Path%"' shell: cmd - - name: Check Functions Core tools installation - run: 'func' - - name: Install Azurite - run: 'npm install -g azurite@3.33.0' + run: 'npm install -g azurite@3.34.0' - name: Start Azurite services run: 'azurite &' shell: bash + # Check software versions + + - name: Check dotnet SDK installation + run: 'dotnet --info' + + - name: Check node installation + run: 'node --version' + + - name: Check Functions Core tools installation + run: 'func --version' + # Run tests - # - name: Run tests - # if: success() && matrix.os != 'windows-latest' - # run: dotnet test ${{ github.workspace }}/src/LogicAppUnit.sln --no-restore --verbosity normal --logger "trx" --filter TestCategory!="WindowsOnly" + - name: Run tests + if: success() && matrix.os != 'windows-latest' + run: dotnet test ${{ github.workspace }}/src/LogicAppUnit.sln --no-restore --verbosity normal --logger "trx" --filter TestCategory!="WindowsOnly" - # - name: Run tests - # if: success() && matrix.os == 'windows-latest' - # run: dotnet test ${{ github.workspace }}/src/LogicAppUnit.sln --no-restore --verbosity normal --logger "trx" + - name: Run tests + if: success() && matrix.os == 'windows-latest' + run: dotnet test ${{ github.workspace }}/src/LogicAppUnit.sln --no-restore --verbosity normal --logger "trx" # Publish artefacts and test results - # - name: Publish test log - # uses: actions/upload-artifact@v3 - # if: success() || failure() - # with: - # name: test-results.${{ matrix.os }} - # path: ${{ github.workspace }}/src/LogicAppUnit.Samples.LogicApps.Tests/TestResults/*.trx - - # - name: Publish test results - # if: (success() || failure()) && github.event_name != 'pull_request' - # uses: dorny/test-reporter@v1 - # with: - # name: Test Results (${{ matrix.os }}) - # path: ${{ github.workspace }}/src/LogicAppUnit.Samples.LogicApps.Tests/TestResults/*.trx - # path-replace-backslashes: true - # reporter: dotnet-trx + - name: Publish test log + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: test-results.${{ matrix.os }} + path: ${{ github.workspace }}/src/LogicAppUnit.Samples.LogicApps.Tests/TestResults/*.trx + + - name: Publish test results + if: (success() || failure()) && github.event_name != 'pull_request' + uses: dorny/test-reporter@v2 + with: + name: Test Results (${{ matrix.os }}) + path: ${{ github.workspace }}/src/LogicAppUnit.Samples.LogicApps.Tests/TestResults/*.trx + path-replace-backslashes: true + reporter: dotnet-trx diff --git a/ChangeLog.md b/ChangeLog.md index f218c85..e661288 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,14 @@ +# 1.11.0 (11th April 2025) + +LogicAppUnit Testing Framework: + +- The framework now supports parameterised connections that are created by the Standard Logic App extension. [[Issue #42](https://github.com/LogicAppUnit/TestingFramework/issues/42)] +- Bumped versions of NuGet packages to remove critical vulnerabilities in some of the transitive packages. +- Added configuration for NuGet Audit so that any future vulnerabilities are logged as build warnings and do not break the LogicAppUnit build. [[Issue #40](https://github.com/LogicAppUnit/TestingFramework/issues/40)] +- Updated method `ContentHelper.FormatJson()` to use `JToken.Parse()` instead of `JObject.Parse()`. [[Issue #45](https://github.com/LogicAppUnit/TestingFramework/issues/45)] +- Added new property `TestRunner.WorkflowTerminationCodeAsString` that returns the workflow termination code as a string value. The existing property `TestRunner.WorkflowTerminationCode` returns the code as an integer value, but the code is defined as a string data type in the workflow schema reference documentation. [[Issue #46](https://github.com/LogicAppUnit/TestingFramework/issues/46)] + + # 1.10.0 (4th November 2024) LogicAppUnit Testing Framework: diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..8aec736 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,20 @@ + + + + + + false + True + latest-recommended + + + true + all + low + + + true + NU1901;NU1902;NU1903;NU1904 + + + diff --git a/src/LogicAppUnit.Samples.LogicApps.Tests/BuiltInConnectorWorkflow/BuiltInConnectorWorkflowTest.cs b/src/LogicAppUnit.Samples.LogicApps.Tests/BuiltInConnectorWorkflow/BuiltInConnectorWorkflowTest.cs index 7f1afaf..fa5f8df 100644 --- a/src/LogicAppUnit.Samples.LogicApps.Tests/BuiltInConnectorWorkflow/BuiltInConnectorWorkflowTest.cs +++ b/src/LogicAppUnit.Samples.LogicApps.Tests/BuiltInConnectorWorkflow/BuiltInConnectorWorkflowTest.cs @@ -189,7 +189,7 @@ public void BuiltInConnectorWorkflowTest_When_Invalid_Language_Code() } } - private static HttpContent GetServiceBusMessageForTriggerNoLanguageCode() + private static StringContent GetServiceBusMessageForTriggerNoLanguageCode() { return ContentHelper.CreateJsonStringContent(new { @@ -228,7 +228,7 @@ private static HttpContent GetServiceBusMessageForTriggerNoLanguageCode() }); } - private static HttpContent GetServiceBusMessageForTriggerWithValidLanguageCode() + private static StringContent GetServiceBusMessageForTriggerWithValidLanguageCode() { return ContentHelper.CreateJsonStringContent(new { @@ -268,7 +268,7 @@ private static HttpContent GetServiceBusMessageForTriggerWithValidLanguageCode() }); } - private static HttpContent GetServiceBusMessageForTriggerWithInvalidLanguageCode() + private static StringContent GetServiceBusMessageForTriggerWithInvalidLanguageCode() { return ContentHelper.CreateJsonStringContent(new { @@ -308,7 +308,7 @@ private static HttpContent GetServiceBusMessageForTriggerWithInvalidLanguageCode }); } - private static object GetSQLExecuteResponse() + private static object[] GetSQLExecuteResponse() { return new object[] { @@ -323,11 +323,9 @@ private static object GetSQLExecuteResponse() }; } - private static object GetSQLExecuteResponseNoRecords() + private static object[] GetSQLExecuteResponseNoRecords() { - return new object[] - { - }; + return System.Array.Empty(); } } } \ No newline at end of file diff --git a/src/LogicAppUnit.Samples.LogicApps.Tests/LogicAppUnit.Samples.LogicApps.Tests.csproj b/src/LogicAppUnit.Samples.LogicApps.Tests/LogicAppUnit.Samples.LogicApps.Tests.csproj index 437211b..9c5d6cb 100644 --- a/src/LogicAppUnit.Samples.LogicApps.Tests/LogicAppUnit.Samples.LogicApps.Tests.csproj +++ b/src/LogicAppUnit.Samples.LogicApps.Tests/LogicAppUnit.Samples.LogicApps.Tests.csproj @@ -1,14 +1,9 @@  - net6.0 + net8.0 LogicAppUnit.Samples.LogicApps.Tests false - - true - false - True - latest-recommended @@ -68,7 +63,7 @@ - + diff --git a/src/LogicAppUnit.Samples.LogicApps.Tests/ManagedApiConnectorWorkflow/ManagedApiConnectorWorkflowTest.cs b/src/LogicAppUnit.Samples.LogicApps.Tests/ManagedApiConnectorWorkflow/ManagedApiConnectorWorkflowTest.cs index 05c91ad..ffa702b 100644 --- a/src/LogicAppUnit.Samples.LogicApps.Tests/ManagedApiConnectorWorkflow/ManagedApiConnectorWorkflowTest.cs +++ b/src/LogicAppUnit.Samples.LogicApps.Tests/ManagedApiConnectorWorkflow/ManagedApiConnectorWorkflowTest.cs @@ -152,7 +152,7 @@ public void ManagedApiConnectorWorkflowTest_When_Send_Email_Fails() } } - private static HttpContent GetRequest() + private static StringContent GetRequest() { return ContentHelper.CreateJsonStringContent(new { diff --git a/src/LogicAppUnit.Samples.LogicApps/connections.json b/src/LogicAppUnit.Samples.LogicApps/connections.json index 32a9de5..7195c09 100644 --- a/src/LogicAppUnit.Samples.LogicApps/connections.json +++ b/src/LogicAppUnit.Samples.LogicApps/connections.json @@ -45,7 +45,7 @@ "connection": { "id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/resourceGroups/@{appsetting('WORKFLOWS_RESOURCE_GROUP_NAME')}/providers/Microsoft.Web/connections/salesforce01" }, - "connectionRuntimeUrl": "https://7606763fdc09952f.10.common.logic-uksouth.azure-apihub.net/apim/salesforce/fba515601ef14f9193eee596a9dcfd1c/", + "connectionRuntimeUrl": "@parameters('salesforce-ConnectionRuntimeUrl')", "authentication": { "type": "Raw", "scheme": "Key", @@ -59,12 +59,8 @@ "connection": { "id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/resourceGroups/@{appsetting('WORKFLOWS_RESOURCE_GROUP_NAME')}/providers/Microsoft.Web/connections/outlook01" }, - "connectionRuntimeUrl": "@appsetting('Outlook-ManagedConnectionRuntimeUrl')", - "authentication": { - "type": "Raw", - "scheme": "Key", - "parameter": "@appsetting('Outlook-ConnectionKey')" - } + "connectionRuntimeUrl": "@parameters('outlook-ConnectionRuntimeUrl')", + "authentication": "@parameters('outlook-Authentication')" } } } diff --git a/src/LogicAppUnit.Samples.LogicApps/local.settings.json b/src/LogicAppUnit.Samples.LogicApps/local.settings.json index 406cfe0..e8cfd55 100644 --- a/src/LogicAppUnit.Samples.LogicApps/local.settings.json +++ b/src/LogicAppUnit.Samples.LogicApps/local.settings.json @@ -11,8 +11,9 @@ "AzureQueue-ConnectionString": "any-queue-connection-string", "Outlook-ConnectionKey": "any-outlook-connection-key", "Outlook-SubjectPrefix": "INFORMATION", - "Outlook-ManagedConnectionRuntimeUrl": "https://7606763fdc09952f.10.common.logic-uksouth.azure-apihub.net/apim/outlook/79a0bc680716416e90e17323b581695d/", + "Outlook-ConnectionRuntimeUrl": "https://7606763fdc09952f.10.common.logic-uksouth.azure-apihub.net/apim/outlook/79a0bc680716416e90e17323b581695d/", "Salesforce-ConnectionKey": "any-salesforce-connection-key", + "Salesforce-ConnectionRuntimeUrl": "https://7606763fdc09952f.10.common.logic-uksouth.azure-apihub.net/apim/salesforce/fba515601ef14f9193eee596a9dcfd1c/", "ServiceOne-Url": "https://external-service-one.testing.net/api/v1", "ServiceOne-Authentication-APIKey": "serviceone-auth-apikey", "ServiceOne-Authentication-WebHook-APIKey": "serviceone-auth-webhook-apikey", diff --git a/src/LogicAppUnit.Samples.LogicApps/parameters.json b/src/LogicAppUnit.Samples.LogicApps/parameters.json index e70ae83..9499f25 100644 --- a/src/LogicAppUnit.Samples.LogicApps/parameters.json +++ b/src/LogicAppUnit.Samples.LogicApps/parameters.json @@ -18,5 +18,21 @@ "ServiceTwo-Authentication-APIKey": { "type": "String", "value": "@appsetting('ServiceTwo-Authentication-APIKey')" + }, + "salesforce-ConnectionRuntimeUrl": { + "type": "String", + "value": "@appsetting('Salesforce-ConnectionRuntimeUrl')" + }, + "outlook-ConnectionRuntimeUrl": { + "type": "String", + "value": "@appsetting('Outlook-ConnectionRuntimeUrl')" + }, + "outlook-Authentication": { + "type": "Object", + "value": { + "type": "Raw", + "scheme": "Key", + "parameter": "@appsetting('Outlook-ConnectionKey')" + } } } diff --git a/src/LogicAppUnit/Helper/ContentHelper.cs b/src/LogicAppUnit/Helper/ContentHelper.cs index de8d2f6..38ed20b 100644 --- a/src/LogicAppUnit/Helper/ContentHelper.cs +++ b/src/LogicAppUnit/Helper/ContentHelper.cs @@ -29,8 +29,7 @@ public static class ContentHelper /// The HTTP content. public static StreamContent CreateStreamContent(Stream stream, string contentType) { - if (stream == null) - throw new ArgumentNullException(nameof(stream)); + ArgumentNullException.ThrowIfNull(stream); if (string.IsNullOrEmpty(contentType)) throw new ArgumentNullException(nameof(contentType)); @@ -72,8 +71,7 @@ public static StreamContent CreatePlainStreamContent(Stream stream) /// The HTTP content. public static StringContent CreateStringContent(string value, string contentType, Encoding encoding = null) { - if (value == null) - throw new ArgumentNullException(nameof(value)); + ArgumentNullException.ThrowIfNull(value); if (string.IsNullOrEmpty(contentType)) throw new ArgumentNullException(nameof(contentType)); @@ -87,8 +85,7 @@ public static StringContent CreateStringContent(string value, string contentType /// The HTTP content. public static StringContent CreateJsonStringContent(string jsonString) { - if (jsonString == null) - throw new ArgumentNullException(nameof(jsonString)); + ArgumentNullException.ThrowIfNull(jsonString); return new StringContent(jsonString, Encoding.UTF8, JsonContentType); } @@ -123,8 +120,7 @@ public static StringContent CreateXmlStringContent(string xmlString) public static StringContent CreateJsonStringContent(object jsonObject) { // The name of this method is inconsistent - if (jsonObject == null) - throw new ArgumentNullException(nameof(jsonObject)); + ArgumentNullException.ThrowIfNull(jsonObject); var json = JsonConvert.SerializeObject(jsonObject); return new StringContent(json, Encoding.UTF8, JsonContentType); @@ -138,8 +134,7 @@ public static StringContent CreateJsonStringContent(object jsonObject) public static StringContent CreateXmlStringContent(XmlDocument xmlDoc) { // The name of this method is inconsistent - if (xmlDoc == null) - throw new ArgumentNullException(nameof(xmlDoc)); + ArgumentNullException.ThrowIfNull(xmlDoc); return new StringContent(xmlDoc.ToString(), Encoding.UTF8, XmlContentType); } @@ -153,8 +148,7 @@ public static StringContent CreateXmlStringContent(XmlDocument xmlDoc) /// The stream content as a . public static string ConvertStreamToString(Stream input) { - if (input == null) - throw new ArgumentNullException(nameof(input)); + ArgumentNullException.ThrowIfNull(input); string convertedValue = string.Empty; using (var sr = new StreamReader(input)) @@ -172,8 +166,7 @@ public static string ConvertStreamToString(Stream input) /// The stream representation. public static Stream ConvertStringToStream(string input) { - if (input == null) - throw new ArgumentNullException(nameof(input)); + ArgumentNullException.ThrowIfNull(input); byte[] byteArray = Encoding.ASCII.GetBytes(input); return new MemoryStream(byteArray); @@ -201,9 +194,9 @@ public static string FormatJson(string json) CommentHandling = CommentHandling.Ignore }; - // Format the JSON by loading into a JObject and then extracting it as a string. + // Format the JSON by loading into a JToken and then extracting it as a string. // Perhaps a little heavy-handed, but it does the trick. - var obj = JObject.Parse(json, settings); + var obj = JToken.Parse(json, settings); return obj.ToString(); } @@ -217,8 +210,7 @@ public static string FormatJson(string json) /// public static string FormatXml(Stream xmlStream) { - if (xmlStream == null) - throw new ArgumentNullException(nameof(xmlStream)); + ArgumentNullException.ThrowIfNull(xmlStream); XmlDocument xmlDoc = new XmlDocument(); xmlDoc.Load(xmlStream); @@ -236,8 +228,7 @@ public static string FormatXml(Stream xmlStream) /// public static string FormatXml(string xml) { - if (xml == null) - throw new ArgumentNullException(nameof(xml)); + ArgumentNullException.ThrowIfNull(xml); XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(xml); @@ -252,8 +243,7 @@ public static string FormatXml(string xml) /// The formatted XML. private static string FormatXml(XmlDocument xmlDoc) { - if (xmlDoc == null) - throw new ArgumentNullException(nameof(xmlDoc)); + ArgumentNullException.ThrowIfNull(xmlDoc); XmlDsigC14NTransform xmlTransform = new XmlDsigC14NTransform(); xmlTransform.LoadInput(xmlDoc); diff --git a/src/LogicAppUnit/Helper/ResourceHelper.cs b/src/LogicAppUnit/Helper/ResourceHelper.cs index f526d08..bbd0f27 100644 --- a/src/LogicAppUnit/Helper/ResourceHelper.cs +++ b/src/LogicAppUnit/Helper/ResourceHelper.cs @@ -27,10 +27,8 @@ public static Stream GetAssemblyResourceAsStream(string resourceName) /// The resource data. public static Stream GetAssemblyResourceAsStream(string resourceName, Assembly containingAssembly) { - if (resourceName == null) - throw new ArgumentNullException(nameof(resourceName)); - if (containingAssembly == null) - throw new ArgumentNullException(nameof(containingAssembly)); + ArgumentNullException.ThrowIfNull(resourceName); + ArgumentNullException.ThrowIfNull(containingAssembly); Stream resourceData = containingAssembly.GetManifestResourceStream(resourceName); if (resourceData == null) diff --git a/src/LogicAppUnit/ITestRunner.cs b/src/LogicAppUnit/ITestRunner.cs index 19d5cb5..3fba928 100644 --- a/src/LogicAppUnit/ITestRunner.cs +++ b/src/LogicAppUnit/ITestRunner.cs @@ -72,11 +72,17 @@ public interface ITestRunner : IDisposable bool WorkflowWasTerminated { get; } /// - /// Gets the workflow termination code. This only applies when a workflow was terminated with failure. + /// Gets the workflow termination code as an integer value. This only applies when a workflow was terminated with failure. /// /// The workflow termination code, or null if the workflow was not terminated, or terminated with a status that was not failed. int? WorkflowTerminationCode { get; } + /// + /// Gets the workflow termination code as a string value. This only applies when a workflow was terminated with failure. + /// + /// The workflow termination code, or null if the workflow was not terminated, or terminated with a status that was not failed. + string WorkflowTerminationCodeAsString { get; } + /// /// Gets the workflow termination message. This only applies when a workflow was terminated with failure. /// diff --git a/src/LogicAppUnit/InternalHelper/AzuriteHelper.cs b/src/LogicAppUnit/InternalHelper/AzuriteHelper.cs index c2bb9fe..a95daf4 100644 --- a/src/LogicAppUnit/InternalHelper/AzuriteHelper.cs +++ b/src/LogicAppUnit/InternalHelper/AzuriteHelper.cs @@ -21,8 +21,7 @@ internal static class AzuriteHelper /// internal static bool IsRunning(TestConfigurationAzurite config) { - if (config == null) - throw new ArgumentNullException(nameof(config)); + ArgumentNullException.ThrowIfNull(config); // If Azurite is running, it will run on localhost (127.0.0.1) IPAddress expectedIp = new IPAddress(new byte[] { 127, 0, 0, 1 }); diff --git a/src/LogicAppUnit/InternalHelper/WorkflowApiHelper.cs b/src/LogicAppUnit/InternalHelper/WorkflowApiHelper.cs index 7a15426..99b4cd2 100644 --- a/src/LogicAppUnit/InternalHelper/WorkflowApiHelper.cs +++ b/src/LogicAppUnit/InternalHelper/WorkflowApiHelper.cs @@ -31,8 +31,7 @@ internal class WorkflowApiHelper /// The name of the workflow being tested. public WorkflowApiHelper(HttpClient client, string workflowName) { - if (client == null) - throw new ArgumentNullException(nameof(client)); + ArgumentNullException.ThrowIfNull(client); if (string.IsNullOrEmpty(workflowName)) throw new ArgumentNullException(nameof(workflowName)); diff --git a/src/LogicAppUnit/LogicAppUnit.csproj b/src/LogicAppUnit/LogicAppUnit.csproj index 69a4fda..2c1850d 100644 --- a/src/LogicAppUnit/LogicAppUnit.csproj +++ b/src/LogicAppUnit/LogicAppUnit.csproj @@ -3,12 +3,8 @@ net6.0 LogicAppUnit - true - false true - True - latest-recommended - 1.10.0 + 1.11.0 Logic App Unit Testing Framework Unit testing framework for Standard Logic Apps. https://github.com/LogicAppUnit/TestingFramework @@ -29,12 +25,13 @@ - + - + - - + + + diff --git a/src/LogicAppUnit/Mocking/IMockRequestMatcher.cs b/src/LogicAppUnit/Mocking/IMockRequestMatcher.cs index 68d7d1c..54630b4 100644 --- a/src/LogicAppUnit/Mocking/IMockRequestMatcher.cs +++ b/src/LogicAppUnit/Mocking/IMockRequestMatcher.cs @@ -109,7 +109,7 @@ public interface IMockRequestMatcher /// /// The match count numbers. /// The . - /// This match is the logical inverse of . + /// This match is the logical inverse of . IMockRequestMatcher WithMatchCount(params int[] matchCounts); /// @@ -117,7 +117,7 @@ public interface IMockRequestMatcher /// /// The match count numbers. /// The . - /// This match is the logical inverse of . + /// This match is the logical inverse of . IMockRequestMatcher WithNotMatchCount(params int[] matchCounts); /// diff --git a/src/LogicAppUnit/Mocking/MockDefinition.cs b/src/LogicAppUnit/Mocking/MockDefinition.cs index e1ff5ae..d5866fd 100644 --- a/src/LogicAppUnit/Mocking/MockDefinition.cs +++ b/src/LogicAppUnit/Mocking/MockDefinition.cs @@ -155,8 +155,7 @@ public void TestRunComplete() /// The HTTP response message. public async Task MatchRequestAndBuildResponseAsync(HttpRequestMessage request) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + ArgumentNullException.ThrowIfNull(request); // Cache the mock request to enable test assertions // Include anything that might be useful to the test author to validate the workflow diff --git a/src/LogicAppUnit/Mocking/MockRequestMatcher.cs b/src/LogicAppUnit/Mocking/MockRequestMatcher.cs index cbd151c..bb5f765 100644 --- a/src/LogicAppUnit/Mocking/MockRequestMatcher.cs +++ b/src/LogicAppUnit/Mocking/MockRequestMatcher.cs @@ -25,7 +25,7 @@ public class MockRequestMatcher : IMockRequestMatcher private Func _requestContentStringMatcherDelegate; private Func _requestContentJsonMatcherDelegate; - private int _requestMatchCounter = 0; + private int _requestMatchCounter; // default is 0 /// /// Initializes a new instance of the class. @@ -231,8 +231,7 @@ public IMockRequestMatcher WithNotMatchCount(params int[] matchCounts) /// public IMockRequestMatcher WithContentAsString(Func requestContentMatch) { - if (requestContentMatch == null) - throw new ArgumentNullException(nameof(requestContentMatch)); + ArgumentNullException.ThrowIfNull(requestContentMatch); _requestContentStringMatcherDelegate = requestContentMatch; return this; @@ -241,8 +240,7 @@ public IMockRequestMatcher WithContentAsString(Func requestContent /// public IMockRequestMatcher WithContentAsJson(Func requestContentMatch) { - if (requestContentMatch == null) - throw new ArgumentNullException(nameof(requestContentMatch)); + ArgumentNullException.ThrowIfNull(requestContentMatch); _requestContentJsonMatcherDelegate = requestContentMatch; return this; diff --git a/src/LogicAppUnit/Mocking/MockResponse.cs b/src/LogicAppUnit/Mocking/MockResponse.cs index 9250ffe..383124c 100644 --- a/src/LogicAppUnit/Mocking/MockResponse.cs +++ b/src/LogicAppUnit/Mocking/MockResponse.cs @@ -26,8 +26,7 @@ internal string MockName /// The request matcher. internal MockResponse(string name, IMockRequestMatcher mockRequestMatcher) { - if (mockRequestMatcher == null) - throw new ArgumentNullException(nameof(mockRequestMatcher)); + ArgumentNullException.ThrowIfNull(mockRequestMatcher); _mockName = name; _mockRequestMatcher = (MockRequestMatcher)mockRequestMatcher; @@ -36,8 +35,7 @@ internal MockResponse(string name, IMockRequestMatcher mockRequestMatcher) /// public void RespondWith(IMockResponseBuilder mockResponseBuilder) { - if (mockResponseBuilder == null) - throw new ArgumentNullException(nameof(mockResponseBuilder)); + ArgumentNullException.ThrowIfNull(mockResponseBuilder); _mockResponseBuilder = (MockResponseBuilder)mockResponseBuilder; } @@ -57,10 +55,8 @@ public void RespondWithDefault() /// The response for the matching request, or null if there was no match. internal async Task MatchRequestAndCreateResponseAsync(HttpRequestMessage request, MockRequestCache requestCache, List requestMatchingLog) { - if (request == null) - throw new ArgumentNullException(nameof(request)); - if (requestCache == null) - throw new ArgumentNullException(nameof(requestCache)); + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(requestCache); if (_mockRequestMatcher == null) throw new TestException("A request matcher has not been configured"); diff --git a/src/LogicAppUnit/Mocking/MockResponseBuilder.cs b/src/LogicAppUnit/Mocking/MockResponseBuilder.cs index b16ce41..ab45ab7 100644 --- a/src/LogicAppUnit/Mocking/MockResponseBuilder.cs +++ b/src/LogicAppUnit/Mocking/MockResponseBuilder.cs @@ -135,8 +135,7 @@ public IMockResponseBuilder AfterDelay(TimeSpan min, TimeSpan max) /// public IMockResponseBuilder WithContent(Func content) { - if (content == null) - throw new ArgumentNullException(nameof(content)); + ArgumentNullException.ThrowIfNull(content); _contentDelegate = content; return this; @@ -203,12 +202,11 @@ public IMockResponseBuilder WithContent(string resourceName, Assembly containing } /// - public IMockResponseBuilder ThrowsException(Exception excpetionToThrow) + public IMockResponseBuilder ThrowsException(Exception exceptionToThrow) { - if (excpetionToThrow == null) - throw new ArgumentNullException(nameof(excpetionToThrow)); + ArgumentNullException.ThrowIfNull(exceptionToThrow); - _excpetionToThrow = excpetionToThrow; + _excpetionToThrow = exceptionToThrow; return this; } @@ -223,8 +221,7 @@ public IMockResponseBuilder ThrowsException(Exception excpetionToThrow) /// The . internal IMockResponseBuilder AfterDelay(Func delay) { - if (delay == null) - throw new ArgumentNullException(nameof(delay)); + ArgumentNullException.ThrowIfNull(delay); _delayDelegate = delay; return this; diff --git a/src/LogicAppUnit/TestConfiguration.cs b/src/LogicAppUnit/TestConfiguration.cs index fc3bc56..36c85b5 100644 --- a/src/LogicAppUnit/TestConfiguration.cs +++ b/src/LogicAppUnit/TestConfiguration.cs @@ -55,7 +55,7 @@ public class TestConfigurationLogging /// /// Default value is false. /// - public bool WriteFunctionRuntimeStartupLogs { get; set; } = false; + public bool WriteFunctionRuntimeStartupLogs { get; set; } // default is false /// /// true if the mock request matching logs are to be written to the test execution logs, otherwise false. @@ -63,7 +63,7 @@ public class TestConfigurationLogging /// /// Default value is false. /// - public bool WriteMockRequestMatchingLogs { get; set; } = false; + public bool WriteMockRequestMatchingLogs { get; set; } // default is false } /// diff --git a/src/LogicAppUnit/TestRunner.cs b/src/LogicAppUnit/TestRunner.cs index b09cbea..f841d64 100644 --- a/src/LogicAppUnit/TestRunner.cs +++ b/src/LogicAppUnit/TestRunner.cs @@ -102,7 +102,7 @@ public WorkflowRunStatus WorkflowRunStatus { get { - return (WorkflowRunStatus)Enum.Parse(typeof(WorkflowRunStatus), _apiHelper.WorkflowRunContent()["properties"]["status"].ToString()); + return Enum.Parse(_apiHelper.WorkflowRunContent()["properties"]["status"].ToString()); } } @@ -124,6 +124,15 @@ public int? WorkflowTerminationCode } } + /// + public string WorkflowTerminationCodeAsString + { + get + { + return _apiHelper.WorkflowRunContent()["properties"]["error"]?["code"]?.ToObject(); + } + } + /// public string WorkflowTerminationMessage { @@ -143,12 +152,12 @@ public string WorkflowTerminationMessage /// The logging configuration for the test execution. /// The test runner configuration for the test execution. /// The HTTP client. + /// The contents of the host file. /// Mock responses that have been configured in the test base class. /// The workflow definition file. - /// The local settings file. - /// The contents of the host file. - /// The contents of the parameters file, or null if the file does not exist. - /// The connections file, or null if the file does not exist. + /// The local settings file wrapper. + /// The parameters file wrapper. + /// The connections file wrapper. /// A collection of C# script files or null if there are none. /// The (optional) artifacts directory containing maps and schemas that are used by the workflow being tested. /// The (optional) custom library (lib/custom) directory containing custom components that are used by the workflow being tested. @@ -156,28 +165,22 @@ internal TestRunner( TestConfigurationLogging loggingConfig, TestConfigurationRunner runnerConfig, HttpClient client, + string host, List mockResponsesFromBase, WorkflowDefinitionWrapper workflowDefinition, LocalSettingsWrapper localSettings, - string host, - string parameters = null, - ConnectionsWrapper connections = null, + ParametersWrapper parameters, + ConnectionsWrapper connections, CsxWrapper[] csxTestInputs = null, DirectoryInfo artifactsDirectory = null, DirectoryInfo customLibraryDirectory = null) { - if (loggingConfig == null) - throw new ArgumentNullException(nameof(loggingConfig)); - if (runnerConfig == null) - throw new ArgumentNullException(nameof(runnerConfig)); - if (client == null) - throw new ArgumentNullException(nameof(client)); - if (mockResponsesFromBase == null) - throw new ArgumentNullException(nameof(mockResponsesFromBase)); - if (workflowDefinition == null) - throw new ArgumentNullException(nameof(workflowDefinition)); - if (localSettings == null) - throw new ArgumentNullException(nameof(localSettings)); + ArgumentNullException.ThrowIfNull(loggingConfig); + ArgumentNullException.ThrowIfNull(runnerConfig); + ArgumentNullException.ThrowIfNull(client); + ArgumentNullException.ThrowIfNull(mockResponsesFromBase); + ArgumentNullException.ThrowIfNull(workflowDefinition); + ArgumentNullException.ThrowIfNull(localSettings); LoggingHelper.LogBanner("Starting test runner"); //Console.WriteLine($"Max workflow duration: {runnerConfig.MaxWorkflowExecutionDuration} seconds"); @@ -190,8 +193,11 @@ internal TestRunner( _runnerConfig = runnerConfig; var workflowTestInput = new WorkflowTestInput[] { new WorkflowTestInput(workflowDefinition.WorkflowName, workflowDefinition.ToString()) }; - _workflowTestHost = new WorkflowTestHost(workflowTestInput, localSettings.ToString(), parameters, connections.ToString(), host, - csxTestInputs, artifactsDirectory, customLibraryDirectory, loggingConfig.WriteFunctionRuntimeStartupLogs); + _workflowTestHost = new WorkflowTestHost(workflowTestInput, + localSettings.ToString(), parameters.ToString(), connections.ToString(), + host, + csxTestInputs, artifactsDirectory, customLibraryDirectory, + loggingConfig.WriteFunctionRuntimeStartupLogs); _apiHelper = new WorkflowApiHelper(client, workflowDefinition.WorkflowName); // Create the mock definition and mock HTTP host @@ -218,7 +224,7 @@ public JToken GetWorkflowAction(string actionName) if (string.IsNullOrEmpty(actionName)) throw new ArgumentNullException(nameof(actionName)); - JToken getActionFromRunHistory = _apiHelper.ActionsContent(WorkflowRunId).Where(actionResult => actionResult["name"].ToString().Equals(actionName)).FirstOrDefault(); + JToken getActionFromRunHistory = _apiHelper.ActionsContent(WorkflowRunId).Where(actionResult => actionResult["name"].ToString().Equals(actionName, StringComparison.Ordinal)).FirstOrDefault(); if (getActionFromRunHistory == null) throw new TestException($"Action '{actionName}' was not found in the workflow run history."); @@ -230,7 +236,7 @@ public JToken GetWorkflowAction(string actionName) public ActionStatus GetWorkflowActionStatus(string actionName) { JToken actionRunProperties = GetWorkflowAction(actionName); - return (ActionStatus)Enum.Parse(typeof(ActionStatus), actionRunProperties["status"].ToString()); + return Enum.Parse(actionRunProperties["status"].ToString()); } /// @@ -307,7 +313,7 @@ public JToken GetWorkflowActionRepetition(string actionName, int repetitionNumbe public ActionStatus GetWorkflowActionStatus(string actionName, int repetitionNumber) { JToken actionRunRepetitionProperties = GetWorkflowActionRepetition(actionName, repetitionNumber); - return (ActionStatus)Enum.Parse(typeof(ActionStatus), actionRunRepetitionProperties["status"].ToString()); + return Enum.Parse(actionRunRepetitionProperties["status"].ToString()); } /// @@ -442,7 +448,8 @@ public void ExceptionWrapper(Action assertion) { // Include a list of the failed workflow actions to help with the investigations List failedActions = _apiHelper.ActionsContent(WorkflowRunId).Where(actionResult => - actionResult["properties"]["status"].ToString().Equals("Failed") || actionResult["properties"]["status"].ToString().Equals("Running")).ToList(); + actionResult["properties"]["status"].ToString().Equals("Failed", StringComparison.Ordinal) || + actionResult["properties"]["status"].ToString().Equals("Running", StringComparison.Ordinal)).ToList(); if (failedActions.Count > 0) { diff --git a/src/LogicAppUnit/WorkflowTestBase.cs b/src/LogicAppUnit/WorkflowTestBase.cs index ab3997c..41a7df0 100644 --- a/src/LogicAppUnit/WorkflowTestBase.cs +++ b/src/LogicAppUnit/WorkflowTestBase.cs @@ -26,12 +26,12 @@ public abstract class WorkflowTestBase private WorkflowDefinitionWrapper _workflowDefinition; private LocalSettingsWrapper _localSettings; + private ParametersWrapper _parameters; private ConnectionsWrapper _connections; private CsxWrapper[] _csxTestInputs; - private string _parameters; private string _host; - private bool _workflowIsInitialised = false; + private bool _workflowIsInitialised; // default = false #region Lifetime management @@ -145,17 +145,17 @@ protected void Initialize(string logicAppBasePath, string workflowName, string l if (_testConfig.Azurite.EnableAzuritePortCheck && !AzuriteHelper.IsRunning(_testConfig.Azurite)) throw new TestException($"Azurite is not running on ports {_testConfig.Azurite.BlobServicePort} (Blob service), {_testConfig.Azurite.QueueServicePort} (Queue service) and {_testConfig.Azurite.TableServicePort} (Table service). Logic App workflows cannot run unless all three services are running in Azurite"); - // Process the workflow definition, local settings ans connection files + // Process the workflow definition, local settings, parameters and connection files ProcessWorkflowDefinitionFile(logicAppBasePath, workflowName); ProcessLocalSettingsFile(logicAppBasePath, localSettingsFilename); + ProcessParametersFile(logicAppBasePath); ProcessConnectionsFile(logicAppBasePath); // Set up the artifacts (schemas, maps) and custom library folders _artifactDirectory = SetSourceDirectory(logicAppBasePath, Constants.ARTIFACTS_FOLDER, "artifacts"); _customLibraryDirectory = SetSourceDirectory(logicAppBasePath, Constants.CUSTOM_LIB_FOLDER, "custom library"); - // Other files needed to test the workflow, but we don't need to update these - _parameters = ReadFromPath(Path.Combine(logicAppBasePath, Constants.PARAMETERS), optional: true); + // Other files needed to test the workflow, but we don't need to read or modify these _host = ReadFromPath(Path.Combine(logicAppBasePath, Constants.HOST)); // Find all of the csx files that are used by the Logic App @@ -228,10 +228,10 @@ protected ITestRunner CreateTestRunner(Dictionary localSettingsO _testConfig.Logging, _testConfig.Runner, _client, + _host, _mockResponses, _workflowDefinition, _localSettings, - _host, _parameters, _connections, _csxTestInputs, @@ -282,6 +282,15 @@ private void ProcessLocalSettingsFile(string logicAppBasePath, string localSetti _localSettings.ReplaceExternalUrlsWithMockServer(_testConfig.Workflow.ExternalApiUrlsToMock); } + /// + /// Process a workflow parameters file before the test is run. + /// + /// Path to the root folder containing the workflows. + private void ProcessParametersFile(string logicAppBasePath) + { + _parameters = new ParametersWrapper(ReadFromPath(Path.Combine(logicAppBasePath, Constants.PARAMETERS), optional: true)); + } + /// /// Process a workflow connections file before the test is run. /// @@ -290,7 +299,7 @@ private void ProcessConnectionsFile(string logicAppBasePath) { const string invalidConnectionsMsg = "configured to use the 'ManagedServiceIdentity' authentication type. Only the 'Raw' and 'ActiveDirectoryOAuth' authentication types are allowed in a local developer environment"; - _connections = new ConnectionsWrapper(ReadFromPath(Path.Combine(logicAppBasePath, Constants.CONNECTIONS), optional: true), _localSettings); + _connections = new ConnectionsWrapper(ReadFromPath(Path.Combine(logicAppBasePath, Constants.CONNECTIONS), optional: true), _localSettings, _parameters); _connections.ReplaceManagedApiConnectionUrlsWithMockServer(_testConfig.Workflow.ManagedApisToMock); diff --git a/src/LogicAppUnit/Wrapper/ConnectionsWrapper.cs b/src/LogicAppUnit/Wrapper/ConnectionsWrapper.cs index a77ba36..a83102e 100644 --- a/src/LogicAppUnit/Wrapper/ConnectionsWrapper.cs +++ b/src/LogicAppUnit/Wrapper/ConnectionsWrapper.cs @@ -13,16 +13,17 @@ internal class ConnectionsWrapper { private readonly JObject _jObjectConnection; private readonly LocalSettingsWrapper _localSettings; + private readonly ParametersWrapper _parameters; /// /// Initializes a new instance of the class. /// /// The contents of the connections file, or null if the file does not exist. - /// The settings helper that is used to manage the local application settings. - public ConnectionsWrapper(string connectionsContent, LocalSettingsWrapper localSettings) + /// The local settings wrapper that is used to manage the local application settings. + /// The parameters wrapper that is used to manage the parameters. + public ConnectionsWrapper(string connectionsContent, LocalSettingsWrapper localSettings, ParametersWrapper parameters) { - if (localSettings == null) - throw new ArgumentNullException(nameof(localSettings)); + ArgumentNullException.ThrowIfNull(localSettings); if (!string.IsNullOrEmpty(connectionsContent)) { @@ -30,6 +31,7 @@ public ConnectionsWrapper(string connectionsContent, LocalSettingsWrapper localS } _localSettings = localSettings; + _parameters = parameters; } /// @@ -70,9 +72,9 @@ public void ReplaceManagedApiConnectionUrlsWithMockServer(List managedAp string connectionUrl = connection.Value["connectionRuntimeUrl"].Value(); Uri validatedConnectionUri; - if (!connectionUrl.Contains("@appsetting")) + if (!connectionUrl.Contains("@appsetting") && !connectionUrl.Contains("@parameters")) { - // This connection runtime URL must be a valid URL since it is not using any appsetting substitution + // This connection runtime URL must be a valid URL since it is not using any substitution var isValidUrl = Uri.TryCreate(connectionUrl, UriKind.Absolute, out validatedConnectionUri); if (!isValidUrl) throw new TestException($"The connection runtime URL for managed connection '{connection.Name}' is not a valid URL. The URL is '{connectionUrl}'"); @@ -80,10 +82,11 @@ public void ReplaceManagedApiConnectionUrlsWithMockServer(List managedAp else { // Check that the expanded connection runtime URL is a valid URL - string expandedConnectionUrl = _localSettings.ExpandAppSettingsValues(connectionUrl); + // Expand parameters first because parameters can reference app settings + string expandedConnectionUrl = _localSettings.ExpandAppSettingsValues(_parameters.ExpandParametersAsString(connectionUrl)); var isValidUrl = Uri.TryCreate(expandedConnectionUrl, UriKind.Absolute, out validatedConnectionUri); if (!isValidUrl) - throw new TestException($"The connection runtime URL for managed connection '{connection.Name}' is not a valid URL, even when the app settings have been expanded. The expanded URL is '{expandedConnectionUrl}'"); + throw new TestException($"The connection runtime URL for managed connection '{connection.Name}' is not a valid URL, even when the parameters and app settings have been expanded. The expanded URL is '{expandedConnectionUrl}'"); } // Replace the host with the mock URL @@ -105,8 +108,32 @@ public IEnumerable ListManagedApiConnectionsUsingManagedServiceIdentity( if (_jObjectConnection == null) return null; - var managedApiConnectionsUsingMsi = _jObjectConnection.SelectTokens("managedApiConnections.*").Where(x => x["authentication"]["type"].ToString() == "ManagedServiceIdentity").ToList(); - return managedApiConnectionsUsingMsi.Select(x => ((JProperty)x.Parent).Name); + List returnValue = new(); + var managedApiConnections = _jObjectConnection.SelectToken("managedApiConnections").Children().ToList(); + + managedApiConnections.ForEach((connection) => + { + JObject connAuthTypeObject = null; + JToken connAuth = ((JObject)connection.Value)["authentication"]; + + switch (connAuth.Type) + { + case JTokenType.String: + // Connection object structure is parameterised + connAuthTypeObject = _parameters.ExpandParameterAsObject(connAuth.Value()); + break; + + case JTokenType.Object: + // Connection object structure is not parameterised + connAuthTypeObject = connAuth.Value(); + break; + } + + if (connAuthTypeObject["type"].Value() == "ManagedServiceIdentity") + returnValue.Add(connection.Name); + }); + + return returnValue; } } } diff --git a/src/LogicAppUnit/Wrapper/LocalSettingsWrapper.cs b/src/LogicAppUnit/Wrapper/LocalSettingsWrapper.cs index 0bea3a2..cc7ed81 100644 --- a/src/LogicAppUnit/Wrapper/LocalSettingsWrapper.cs +++ b/src/LogicAppUnit/Wrapper/LocalSettingsWrapper.cs @@ -76,8 +76,7 @@ public void ReplaceExternalUrlsWithMockServer(List externalApiUrls) /// The settings to be updated. public void ReplaceSettingOverrides(Dictionary settingsToUpdate) { - if (settingsToUpdate == null) - throw new ArgumentNullException(nameof(settingsToUpdate)); + ArgumentNullException.ThrowIfNull(settingsToUpdate); Console.WriteLine($"Updating local settings file with test overrides:"); @@ -105,8 +104,7 @@ public void ReplaceSettingOverrides(Dictionary settingsToUpdate) /// The value of the setting, or null if the setting does not exist. public string GetSettingValue(string settingName) { - if (settingName == null) - throw new ArgumentNullException(nameof(settingName)); + ArgumentNullException.ThrowIfNull(settingName); var setting = _jObjectSettings.SelectToken("Values").Children().Where(x => x.Name == settingName).FirstOrDefault(); @@ -120,8 +118,7 @@ public string GetSettingValue(string settingName) /// The value of the setting, or null if the setting does not exist. public string GetWorkflowOperationOptionsValue(string workflowName) { - if (workflowName == null) - throw new ArgumentNullException(nameof(workflowName)); + ArgumentNullException.ThrowIfNull(workflowName); return GetSettingValue($"Workflows.{workflowName}.OperationOptions"); } @@ -134,10 +131,8 @@ public string GetWorkflowOperationOptionsValue(string workflowName) /// The setting that has been created. public string SetWorkflowOperationOptionsValue(string workflowName, string value) { - if (workflowName == null) - throw new ArgumentNullException(nameof(workflowName)); - if (value == null) - throw new ArgumentNullException(nameof(value)); + ArgumentNullException.ThrowIfNull(workflowName); + ArgumentNullException.ThrowIfNull(value); string settingName = $"Workflows.{workflowName}.OperationOptions"; _jObjectSettings["Values"][settingName] = value; diff --git a/src/LogicAppUnit/Wrapper/ParametersWrapper.cs b/src/LogicAppUnit/Wrapper/ParametersWrapper.cs new file mode 100644 index 0000000..661980c --- /dev/null +++ b/src/LogicAppUnit/Wrapper/ParametersWrapper.cs @@ -0,0 +1,97 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace LogicAppUnit.Wrapper +{ + /// + /// Wrapper class to manage the parameters.json file. + /// + internal class ParametersWrapper + { + private readonly JObject _jObjectParameters; + + /// + /// Initializes a new instance of the class. + /// + /// The contents of the parameters file, or null if the file does not exist. + public ParametersWrapper(string parametersContent) + { + if (!string.IsNullOrEmpty(parametersContent)) + { + _jObjectParameters = JObject.Parse(parametersContent); + } + } + + /// + /// Returns the parameters content. + /// + /// The parameters content. + public override string ToString() + { + if (_jObjectParameters == null) + return null; + + return _jObjectParameters.ToString(); + } + + /// + /// Get the value for a parameter. + /// + /// The name of the parameter. + /// The type of the parameter. + /// The value of the parameter, or null if the parameter does not exist. + public T GetParameterValue(string parameterName) + { + ArgumentNullException.ThrowIfNull(parameterName); + + var param = _jObjectParameters.Children().Where(x => x.Name == parameterName).FirstOrDefault(); + if (param == null) + return default; + + return ((JObject)param.Value)["value"].Value(); + } + + /// + /// Expand the parameters in as a string value. + /// + /// The string value containing the parameters to be expanded. + /// Expanded parameter value. + public string ExpandParametersAsString(string value) + { + // If there is no parameters file then the value is not replaced + if (_jObjectParameters == null) + return value; + + const string parametersPattern = @"@parameters\('[\w.:-]*'\)"; + string expandedValue = value; + + MatchCollection matches = Regex.Matches(value, parametersPattern, RegexOptions.IgnoreCase); + foreach (Match match in matches) + { + string parameterName = match.Value[13..^2]; + expandedValue = expandedValue.Replace(match.Value, GetParameterValue(parameterName)); + } + + return expandedValue; + } + + /// + /// Expand the parameter in as an object. + /// + /// The string value containing the parameter to be expanded. + /// Expanded parameter value. + public JObject ExpandParameterAsObject(string value) + { + // If there is no parameters file then the value is not replaced + if (_jObjectParameters == null) + return null; + + const string parametersPattern = @"^@parameters\('[\w.:-]*'\)$"; + + MatchCollection matches = Regex.Matches(value, parametersPattern, RegexOptions.IgnoreCase); + return GetParameterValue(matches[0].Value[13..^2]); + } + } +} \ No newline at end of file diff --git a/src/LogicAppUnit/Wrapper/WorkflowDefinitionWrapper.cs b/src/LogicAppUnit/Wrapper/WorkflowDefinitionWrapper.cs index e1fc4d2..f4f27a5 100644 --- a/src/LogicAppUnit/Wrapper/WorkflowDefinitionWrapper.cs +++ b/src/LogicAppUnit/Wrapper/WorkflowDefinitionWrapper.cs @@ -61,7 +61,7 @@ public WorkflowType WorkflowType { get { - return (WorkflowType)Enum.Parse(typeof(WorkflowType), _jObjectWorkflow["kind"].ToString()); + return Enum.Parse(_jObjectWorkflow["kind"].ToString()); } }