├── .azure-devops └── pipelines │ └── build.yml ├── .editorconfig ├── .github └── workflows │ └── build.yml ├── .gitignore ├── ChangeLog.md ├── License.md ├── LogicAppUnit.png ├── README.md └── src ├── Directory.Build.props ├── LogicAppUnit.Samples.Functions ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── LogicAppUnit.Samples.Functions.csproj └── WeatherForecast.cs ├── LogicAppUnit.Samples.LogicApps.Tests ├── BuiltInConnectorWorkflow │ ├── BuiltInConnectorWorkflowTest.cs │ └── MockData │ │ ├── SQL_Request_en_GB.json │ │ ├── SQL_Request_xx_GB.json │ │ ├── ServiceBus_Request_LanguageCode.json │ │ └── ServiceBus_Request_NoLanguageCode.json ├── CallDataMapperWorkflow │ ├── CallDataMapperWorkflowTest.cs │ └── MockData │ │ ├── WorkflowRequest.xml │ │ └── WorkflowResponse.xml ├── CallLocalFunctionWorkflow │ └── CallLocalFunctionWorkflowTest.cs ├── Constants.cs ├── FluentWorkflow │ ├── FluentWorkflowRequestMatcherTest.cs │ ├── FluentWorkflowResponseBuilderTest.cs │ ├── FluentWorkflowResponseBuilderWithBaseTest.cs │ └── MockData │ │ ├── Response.ClueXml.xml │ │ ├── Response.json │ │ └── Response.txt ├── HttpAsyncWorkflow │ └── HttpAsyncWorkflowTest.cs ├── HttpChunkingWorkflow │ └── HttpChunkingWorkflow.cs ├── HttpWorkflow │ ├── HttpWorkflowTest.cs │ └── MockData │ │ └── SystemTwo_Request.json ├── InlineScriptWorkflow │ ├── InlineScriptWorkflowTest.cs │ └── MockData │ │ └── Execute_CSharp_Script_Code_Output.json ├── InvokeWorkflow │ ├── InvokeWorkflowTest.cs │ └── MockData │ │ ├── AddToPriorityQueueRequest.json │ │ ├── InvokeWorkflowNotPriorityRequest.json │ │ ├── InvokeWorkflowPriorityRequest.json │ │ └── WorkflowRequest.json ├── LogicAppUnit.Samples.LogicApps.Tests.csproj ├── LogicAppUnit.Samples.LogicApps.Tests.sln ├── LoopWorkflow │ ├── LoopWorkflowTest.cs │ └── MockData │ │ └── Response.json ├── ManagedApiConnectorWorkflow │ ├── ManagedApiConnectorWorkflowTest.cs │ └── MockData │ │ ├── Outlook_Request.json │ │ └── Salesforce_Request.json ├── StatelessWorkflow │ ├── MockData │ │ ├── UploadBlobRequest.json │ │ ├── UploadBlobResponseFailed.json │ │ └── WorkflowRequest.json │ └── StatelessWorkflowTest.cs └── testConfiguration.json ├── LogicAppUnit.Samples.LogicApps ├── .funcignore ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── Artifacts │ ├── MapDefinitions │ │ └── CustomerCampaignToCampaignRequest.lml │ ├── Maps │ │ └── CustomerCampaignToCampaignRequest.xslt │ └── Schemas │ │ ├── CampaignRequest.xsd │ │ └── CustomerCampaign.xsd ├── built-in-connector-workflow │ └── workflow.json ├── call-data-mapper-workflow │ └── workflow.json ├── call-local-function-workflow │ └── workflow.json ├── connections.json ├── fluent-workflow │ └── workflow.json ├── host.json ├── http-async-workflow │ └── workflow.json ├── http-chunking-workflow │ └── workflow.json ├── http-workflow │ └── workflow.json ├── inline-script-workflow │ ├── execute_csharp_script_code.csx │ └── workflow.json ├── invoke-workflow │ └── workflow.json ├── local.settings.json ├── loop-workflow │ └── workflow.json ├── managed-api-connector-workflow │ └── workflow.json ├── parameters.json ├── stateless-workflow │ └── workflow.json └── workflow-designtime │ ├── host.json │ └── local.settings.json ├── LogicAppUnit.code-workspace ├── LogicAppUnit.sln ├── LogicAppUnit ├── ActionStatus.cs ├── Constants.cs ├── Helper │ ├── ContentHelper.cs │ └── ResourceHelper.cs ├── Hosting │ ├── CallbackUrlDefinition.cs │ ├── HttpRequestMessageFeature.cs │ ├── MockHttpHost.cs │ ├── TestEnvironment.cs │ └── WorkflowTestHost.cs ├── ITestRunner.cs ├── InternalHelper │ ├── AzuriteHelper.cs │ ├── LoggingHelper.cs │ └── WorkflowApiHelper.cs ├── LogicAppUnit.csproj ├── Mocking │ ├── IMockRequestMatcher.cs │ ├── IMockResponse.cs │ ├── IMockResponseBuilder.cs │ ├── MockDefinition.cs │ ├── MockRequest.cs │ ├── MockRequestCache.cs │ ├── MockRequestLog.cs │ ├── MockRequestMatchResult.cs │ ├── MockRequestMatcher.cs │ ├── MockRequestPath.cs │ ├── MockResponse.cs │ ├── MockResponseBuilder.cs │ └── PathMatchType.cs ├── TestConfiguration.cs ├── TestException.cs ├── TestRunner.cs ├── WorkflowRunStatus.cs ├── WorkflowTestBase.cs ├── WorkflowTestInput.cs ├── WorkflowType.cs └── Wrapper │ ├── ConnectionsWrapper.cs │ ├── CsxWrapper.cs │ ├── LocalSettingsWrapper.cs │ ├── ParametersWrapper.cs │ └── WorkflowDefinitionWrapper.cs └── nuget.config /.azure-devops/pipelines/build.yml: -------------------------------------------------------------------------------- 1 | # Manual trigger only 2 | trigger: none 3 | pr: none 4 | 5 | parameters: 6 | - name: buildConfiguration 7 | displayName: 'Build configuration' 8 | type: string 9 | default: Debug 10 | values: 11 | - Debug 12 | - Release 13 | 14 | jobs: 15 | - job: build 16 | displayName: 'Build' 17 | timeoutInMinutes: 30 18 | 19 | strategy: 20 | matrix: 21 | Linux: 22 | imageName: 'ubuntu-latest' 23 | matrixName: Linux 24 | Windows: 25 | imageName: 'windows-latest' 26 | matrixName: Windows 27 | Mac: 28 | imageName: 'macOS-latest' 29 | matrixName: Mac 30 | 31 | pool: 32 | vmImage: $(imageName) 33 | 34 | steps: 35 | 36 | # Build .NET solution 37 | 38 | - task: DotNetCoreCLI@2 39 | displayName: 'Restore dependencies' 40 | inputs: 41 | command: restore 42 | verbosityRestore: Normal 43 | projects: '$(System.DefaultWorkingDirectory)/src/LogicAppUnit.sln' 44 | 45 | - task: DotNetCoreCLI@2 46 | displayName: 'Build (${{ parameters.buildConfiguration }})' 47 | inputs: 48 | command: build 49 | arguments: '--no-restore --configuration ${{ parameters.buildConfiguration }}' 50 | projects: '$(System.DefaultWorkingDirectory)/src/LogicAppUnit.sln' 51 | 52 | # Install and configure Logic Apps runtime environment 53 | 54 | - task: FuncToolsInstaller@0 55 | displayName: 'Install Functions core tools' 56 | inputs: 57 | version: 'latest' 58 | 59 | - task: Npm@1 60 | displayName: 'Install Azurite' 61 | inputs: 62 | command: 'custom' 63 | customCommand: 'install -g azurite@3.34.0' 64 | 65 | - task: CmdLine@2 66 | displayName: 'Start Azurite services (not Windows)' 67 | condition: and(succeeded(), ne(variables.matrixName, 'Windows')) 68 | inputs: 69 | script: 'azurite &' 70 | 71 | - task: CmdLine@2 72 | displayName: 'Start Azurite services (Windows)' 73 | condition: and(succeeded(), eq(variables.matrixName, 'Windows')) 74 | inputs: 75 | script: 'start /b azurite' 76 | 77 | # Check software versions 78 | 79 | - task: CmdLine@2 80 | displayName: 'Check dotnet SDK installation' 81 | inputs: 82 | script: dotnet --info 83 | 84 | - task: CmdLine@2 85 | displayName: 'Check node installation' 86 | inputs: 87 | script: node --version 88 | 89 | - task: CmdLine@2 90 | displayName: 'Check Functions Core tools installation' 91 | inputs: 92 | script: func --version 93 | 94 | # Run tests and publish test results to Azure DevOps 95 | 96 | - task: DotNetCoreCLI@2 97 | displayName: 'Run tests (not Windows)' 98 | condition: and(succeeded(), ne(variables.matrixName, 'Windows')) 99 | continueOnError: true 100 | inputs: 101 | command: test 102 | arguments: '--no-restore --verbosity normal --configuration ${{ parameters.buildConfiguration }} --filter TestCategory!="WindowsOnly"' 103 | projects: '$(System.DefaultWorkingDirectory)/src/LogicAppUnit.sln' 104 | publishTestResults: true 105 | testRunTitle: 'Tests ($(matrixName))' 106 | 107 | # - task: DotNetCoreCLI@2 # there is an issue with Azurite ports being blocked when running tests on Windows build servers 108 | # displayName: 'Run tests (Windows)' 109 | # condition: and(succeeded(), eq(variables.matrixName, 'Windows')) 110 | # continueOnError: true 111 | # inputs: 112 | # command: test 113 | # arguments: '--no-restore --verbosity normal --configuration ${{ parameters.buildConfiguration }}' 114 | # projects: '$(System.DefaultWorkingDirectory)/src/LogicAppUnit.sln' 115 | # publishTestResults: true 116 | # testRunTitle: 'Tests ($(matrixName))' 117 | 118 | # Publish NuGet package 119 | 120 | - task: PublishPipelineArtifact@1 121 | displayName: 'Publish NuGet package (Release build only)' 122 | condition: and(succeeded(), eq('${{ parameters.buildConfiguration }}', 'Release')) 123 | inputs: 124 | targetPath: '$(System.DefaultWorkingDirectory)/src/LogicAppUnit/bin/${{ parameters.buildConfiguration }}' 125 | artifact: 'NuGetPackage-$(matrixName)' 126 | publishLocation: 'pipeline' -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://EditorConfig.org 3 | 4 | root = true 5 | 6 | # All files 7 | [*] 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # Code files 13 | [*.{cs,csx}] 14 | indent_size = 4 15 | 16 | # XML project files 17 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,msbuildproj,props,targets}] 18 | indent_size = 2 19 | 20 | # YAML files 21 | [*.{yaml,yml}] 22 | indent_size = 2 23 | 24 | # JSON files 25 | [*.json] 26 | indent_size = 2 27 | 28 | # CA1307: Specify StringComparison for clarity 29 | # Clarity of intent is not required, this is non-localizable code. 30 | [*.cs] 31 | dotnet_diagnostic.CA1307.severity = none 32 | 33 | # CA1310: Specify StringComparison for correctness 34 | # User locales are not a concern. 35 | [*.cs] 36 | dotnet_diagnostic.CA1310.severity = none 37 | 38 | # CA1707: Identifiers should not contain underscores 39 | # Underscores are acceptable for unit test names. 40 | [*.cs] 41 | dotnet_diagnostic.CA1707.severity = none 42 | 43 | # CA1852: Seal internal types 44 | # Sealing classes provides minimal performance improvements and is not a concern. 45 | [*.cs] 46 | dotnet_diagnostic.CA1852.severity = none 47 | 48 | # CA1866: Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char 49 | # Implementing this causes the unit tests to break on macOS and Linux platforms! 50 | [*.cs] 51 | dotnet_diagnostic.CA1866.severity = none 52 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: LogicAppUnit-Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'features/**' 8 | pull_request: 9 | branches: 10 | - main 11 | workflow_dispatch: 12 | 13 | jobs: 14 | 15 | Build: 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [ubuntu-latest, windows-latest] # macos-latest 20 | 21 | runs-on: ${{ matrix.os }} 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | 27 | # Build .NET solution 28 | 29 | - name: Restore dependencies 30 | run: dotnet restore ${{ github.workspace }}/src/LogicAppUnit.sln 31 | 32 | - name: Build 33 | run: dotnet build ${{ github.workspace }}/src/LogicAppUnit.sln --no-restore 34 | 35 | # Install and configure Logic Apps runtime environment 36 | 37 | - name: Install Functions Core tools 38 | run: 'npm install -g azure-functions-core-tools@4 --unsafe-perm true' 39 | 40 | - name: Set Functions Core tools path (Windows only) 41 | if: matrix.os == 'windows-latest' 42 | run: 'setx /m Path "C:\npm\prefix\node_modules\azure-functions-core-tools\bin;%Path%"' 43 | shell: cmd 44 | 45 | - name: Install Azurite 46 | run: 'npm install -g azurite@3.34.0' 47 | 48 | - name: Start Azurite services 49 | run: 'azurite &' 50 | shell: bash 51 | 52 | # Check software versions 53 | 54 | - name: Check dotnet SDK installation 55 | run: 'dotnet --info' 56 | 57 | - name: Check node installation 58 | run: 'node --version' 59 | 60 | - name: Check Functions Core tools installation 61 | run: 'func --version' 62 | 63 | # Run tests 64 | 65 | - name: Run tests 66 | if: success() && matrix.os != 'windows-latest' 67 | run: dotnet test ${{ github.workspace }}/src/LogicAppUnit.sln --no-restore --verbosity normal --logger "trx" --filter TestCategory!="WindowsOnly" 68 | 69 | - name: Run tests 70 | if: success() && matrix.os == 'windows-latest' 71 | run: dotnet test ${{ github.workspace }}/src/LogicAppUnit.sln --no-restore --verbosity normal --logger "trx" 72 | 73 | # Publish artefacts and test results 74 | 75 | - name: Publish test log 76 | uses: actions/upload-artifact@v4 77 | if: success() || failure() 78 | with: 79 | name: test-results.${{ matrix.os }} 80 | path: ${{ github.workspace }}/src/LogicAppUnit.Samples.LogicApps.Tests/TestResults/*.trx 81 | 82 | - name: Publish test results 83 | if: (success() || failure()) && github.event_name != 'pull_request' 84 | uses: dorny/test-reporter@v2 85 | with: 86 | name: Test Results (${{ matrix.os }}) 87 | path: ${{ github.workspace }}/src/LogicAppUnit.Samples.LogicApps.Tests/TestResults/*.trx 88 | path-replace-backslashes: true 89 | reporter: dotnet-trx 90 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 LogicAppUnit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LogicAppUnit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LogicAppUnit/TestingFramework/aa3f53ae621411e873f79cac7254d88666be0d1b/LogicAppUnit.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LogicAppUnit Testing Framework 2 | 3 | 4 | 5 | LogicAppUnit is a testing framework that simplifies the creation of automated unit tests for Standard Logic Apps running in a *local development environment*, or in a *build server as part of a DevOps pipeline*. Standard Logic Apps do not include an out-of-the-box testing capability and this framework has been designed to fill this gap. The framework is based on the [Logic Apps Sample Test Framework](https://techcommunity.microsoft.com/t5/integrations-on-azure-blog/automated-testing-with-logic-apps-standard/ba-p/2960623) that was developed by Henry Liu, and includes additional functionality to make it easier to author and run tests and validate (assert) the results. 6 | 7 | The framework does not support the testing of: 8 | 9 | - Consumption Logic App workflows. 10 | - Standard Logic App workflows that have been deployed to Azure. 11 | 12 | The testing framework has been designed to make it easier to perform isolated unit testing of a workflow. The framework does this by modifying a copy of the workflow definition to remove the dependencies on external services and APIs, without affecting the functionality or behaviour of the workflow. This means that workflows can be easily tested in a developer's local environment, and by a DevOps pipeline running on a build server, where there is no access to Azure services or any other workflow dependencies. 13 | 14 | ## Key Features 15 | 16 | - Replace non-HTTP triggers with HTTP triggers to enable automated testing of every workflow, irrespective of the trigger type. 17 | - Remove external service dependencies for built-in service provider connectors by replacing these actions with HTTP actions and a mock HTTP server that is managed by the framework. 18 | - Remove external service dependencies for managed API connectors by automatically re-configuring managed API connections to use a mock HTTP server that is managed by the framework. 19 | - Remove dependencies on invoked workflows and called local functions by replacing the Invoke Workflow and Call Local Function actions with HTTP actions and a mock HTTP server that is managed by the framework. 20 | - Remove all retry policies to ensure that tests exercising failure scenarios do not take a long time to execute. 21 | - A fluent API to configure request matching and the creation of responses for the mock HTTP server. 22 | - Detailed test execution logging to help with workflow test authoring and debugging. 23 | - Programmatic access to the workflow run history to enable assertion of workflow run status, response status, action status, input and output messages and more. This includes support for action repetitions inside a loop. 24 | - Programmatic access to the requests sent to the mock HTTP server to enable assertion of the data sent from the workflow to external services and APIs. 25 | - Override specific local settings for a test case to enable more testing scenarios (e.g. feature flags). 26 | 27 | 28 | ## Projects 29 | 30 | This code repository includes four projects: 31 | 32 | | Name | Description | 33 | |:-----|:------------| 34 | | LogicAppUnit | The testing framework. | 35 | | LogicAppUnit.Samples.LogicApps.Tests | Test project that demonstrates the features of the testing framework. 36 | | LogicAppUnit.Samples.LogicApps | Workflows that are tested by the sample test project. | 37 | | LogicAppUnit.Samples.Functions | Local .NET Framework functions that are called by workflows. | 38 | 39 | 40 | ## Packages 41 | 42 | Download the *LogicAppUnit* testing framework package from nuget: https://www.nuget.org/packages/LogicAppUnit/ 43 | 44 | [![NuGet Version Badge](https://img.shields.io/nuget/v/LogicAppUnit)](https://www.nuget.org/packages/LogicAppUnit) [![NuGet Download Badge](https://img.shields.io/nuget/dt/LogicAppUnit)](https://www.nuget.org/packages/LogicAppUnit) 45 | 46 | 47 | ## Compatibility 48 | 49 | The framework has been tested with these environments: 50 | 51 | - Windows 52 | - Linux (Ubuntu) 53 | - MacOS 54 | 55 | 56 | ## Give a Star :star: 57 | If you like or are using this project please give it a star. Thanks. 58 | 59 | 60 | ## Main Contributors 61 | 62 | - [Mark Abrams](https://github.com/mark-abrams) 63 | - [Sanket Borhade](https://github.com/sanket-borhade) 64 | - [Shadhaj Kumar](https://github.com/shadhajSH) 65 | 66 | 67 | ## Documentation 68 | 69 | The best way to understand how the framework works and how to write tests using it is to read the [wiki](https://github.com/LogicAppUnit/TestingFramework/wiki) and look at the example tests in the *LogicAppUnit.Samples.LogicApps.Tests* project. 70 | 71 | 72 | ## Future Improvements and Changes 73 | 74 | This is a list of possible future improvements and changes for the framework. Please create a [new issue](https://github.com/LogicAppUnit/TestingFramework/issues) if there are other features that you would like to see. 75 | 76 | - Add more features to the fluent API for request matching and the creation of mock responses. 77 | - Add a `Verifiable()` feature to the fluent API so that a test case can assert that a test execution did send a request to the mock HTTP server that was successfully matched. This would work in a simialar way to the `Verifiable()` feature in the [moq](https://github.com/devlooped/moq) unit testing framework. 78 | - Auto-generate C# test cases based on a workflow's run history in the local development environment. -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | false 7 | True 8 | latest-recommended 9 | 10 | 11 | true 12 | all 13 | low 14 | 15 | 16 | true 17 | NU1901;NU1902;NU1903;NU1904 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.Functions/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-dotnettools.csharp" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.Functions/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to .NET Functions", 6 | "type": "clr", 7 | "request": "attach", 8 | "processName": "Microsoft.Azure.Workflows.Functions.CustomCodeNetFxWorker.exe" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.Functions/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": "bin/Release/net472/publish", 3 | "azureFunctions.projectLanguage": "C#", 4 | "azureFunctions.projectRuntime": "~4", 5 | "debug.internalConsoleOptions": "neverOpen", 6 | "azureFunctions.preDeployTask": "publish (functions)", 7 | "azureFunctions.templateFilter": "Core", 8 | "azureFunctions.showTargetFrameworkWarning": false, 9 | "azureFunctions.projectSubpath": "bin\\Release\\net472\\publish" 10 | } 11 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.Functions/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}" 11 | ], 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.Functions/LogicAppUnit.Samples.Functions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net472 4 | Library 5 | x64 6 | LogicAppUnit.Samples.Functions 7 | v4 8 | false 9 | 10 | true 11 | false 12 | True 13 | latest-recommended 14 | 15 | LogicAppUnit.Samples.LogicApps 16 | Always 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.Functions/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------ 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | //------------------------------------------------------------ 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | using Microsoft.Azure.Functions.Extensions.Workflows; 9 | using Microsoft.Azure.WebJobs; 10 | 11 | namespace LogicAppUnit.Samples.Functions 12 | { 13 | /// 14 | /// Function to generate a weather forecast. 15 | /// 16 | public static class WeatherForecast 17 | { 18 | /// 19 | /// Executes the function. 20 | /// 21 | /// The zip code. 22 | /// The temperature scale (e.g., Celsius or Fahrenheit). 23 | [FunctionName("WeatherForecast")] 24 | public static Task Run([WorkflowActionTrigger] int zipCode, string temperatureScale) 25 | { 26 | // Generate random temperature within a range based on the temperature scale 27 | Random rnd = new Random(); 28 | var currentTemp = temperatureScale == "Celsius" ? rnd.Next(1, 30) : rnd.Next(40, 90); 29 | var lowTemp = currentTemp - 10; 30 | var highTemp = currentTemp + 10; 31 | 32 | // Create a Weather object with the temperature information 33 | var weather = new Weather() 34 | { 35 | ZipCode = zipCode, 36 | CurrentWeather = $"The current weather is {currentTemp} {temperatureScale}", 37 | DayLow = $"The low for the day is {lowTemp} {temperatureScale}", 38 | DayHigh = $"The high for the day is {highTemp} {temperatureScale}" 39 | }; 40 | 41 | //throw new InvalidOperationException("Something went bang!"); 42 | return Task.FromResult(weather); 43 | } 44 | 45 | /// 46 | /// Represents the weather information. 47 | /// 48 | public class Weather 49 | { 50 | /// 51 | /// Gets or sets the zip code. 52 | /// 53 | public int ZipCode { get; set; } 54 | 55 | /// 56 | /// Gets or sets the current weather. 57 | /// 58 | public string CurrentWeather { get; set; } 59 | 60 | /// 61 | /// Gets or sets the low temperature for the day. 62 | /// 63 | public string DayLow { get; set; } 64 | 65 | /// 66 | /// Gets or sets the high temperature for the day. 67 | /// 68 | public string DayHigh { get; set; } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/BuiltInConnectorWorkflow/MockData/SQL_Request_en_GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "query": "SELECT LanguageName, LanguageCode FROM config.Languages WITH (NOLOCK) WHERE LanguageCode = @LanguageCode", 3 | "queryParameters": { 4 | "LanguageCode": "en-GB" 5 | } 6 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/BuiltInConnectorWorkflow/MockData/SQL_Request_xx_GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "query": "SELECT LanguageName, LanguageCode FROM config.Languages WITH (NOLOCK) WHERE LanguageCode = @LanguageCode", 3 | "queryParameters": { 4 | "LanguageCode": "xx-GB" 5 | } 6 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/BuiltInConnectorWorkflow/MockData/ServiceBus_Request_LanguageCode.json: -------------------------------------------------------------------------------- 1 | { 2 | "entityName": "customer-topic", 3 | "message": { 4 | "contentData": { 5 | "id": 54624, 6 | "title": "Mr", 7 | "firstName": "Peter", 8 | "lastName": "Smith", 9 | "dateOfBirth": "1970-04-25", 10 | "language": { 11 | "code": "en-GB", 12 | "name": "English (United Kingdom)" 13 | } 14 | }, 15 | "contentType": "application/json", 16 | "sessionId": "54624", 17 | "userProperties": { 18 | "entityId": 54624, 19 | "entityType": "customer" 20 | }, 21 | "messageId": "ff421d65-5be6-4084-b748-af490100c9a5" 22 | } 23 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/BuiltInConnectorWorkflow/MockData/ServiceBus_Request_NoLanguageCode.json: -------------------------------------------------------------------------------- 1 | { 2 | "entityName": "customer-topic", 3 | "message": { 4 | "contentData": { 5 | "id": 54624, 6 | "title": "Mr", 7 | "firstName": "Peter", 8 | "lastName": "Smith", 9 | "dateOfBirth": "1970-04-25", 10 | "language": { 11 | "code": null, 12 | "name": "" 13 | } 14 | }, 15 | "contentType": "application/json", 16 | "sessionId": "54624", 17 | "userProperties": { 18 | "entityId": 54624, 19 | "entityType": "customer" 20 | }, 21 | "messageId": "ff421d65-5be6-4084-b748-af490100c9a5" 22 | } 23 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/CallDataMapperWorkflow/CallDataMapperWorkflowTest.cs: -------------------------------------------------------------------------------- 1 | using LogicAppUnit.Helper; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Net; 4 | using System.Net.Http; 5 | 6 | namespace LogicAppUnit.Samples.LogicApps.Tests.CallDataMapperWorkflow 7 | { 8 | /// 9 | /// Test cases for the call-data-mapper-workflow workflow which calls a XSLT map that exists in the same Logic App. 10 | /// 11 | [TestClass] 12 | public class CallDataMapperWorkflowTest : WorkflowTestBase 13 | { 14 | private const string XmlContentType = "application/xml"; 15 | 16 | [TestInitialize] 17 | public void TestInitialize() 18 | { 19 | Initialize(Constants.LOGIC_APP_TEST_EXAMPLE_BASE_PATH, Constants.CALL_DATA_MAPPER_WORKFLOW); 20 | } 21 | 22 | [ClassCleanup] 23 | public static void CleanResources() 24 | { 25 | Close(); 26 | } 27 | 28 | /// 29 | /// Tests the workflow when the calling of the map is successful. 30 | /// This test can only be run on Windows: 31 | /// https://learn.microsoft.com/en-us/azure/logic-apps/create-maps-data-transformation-visual-studio-code#limitations-and-known-issues 32 | /// 33 | [TestMethod] 34 | [TestCategory("WindowsOnly")] 35 | public void CallDataMapperWorkflowTest_When_Successful() 36 | { 37 | using (ITestRunner testRunner = CreateTestRunner()) 38 | { 39 | // Run the workflow 40 | var workflowResponse = testRunner.TriggerWorkflow( 41 | ContentHelper.CreateStreamContent(ResourceHelper.GetAssemblyResourceAsStream($"{GetType().Namespace}.MockData.WorkflowRequest.xml"), XmlContentType), 42 | HttpMethod.Post); 43 | 44 | // Check workflow run status 45 | Assert.AreEqual(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus); 46 | 47 | // Check workflow response 48 | Assert.AreEqual(HttpStatusCode.OK, workflowResponse.StatusCode); 49 | Assert.AreEqual(XmlContentType, workflowResponse.Content.Headers.ContentType.MediaType); 50 | Assert.AreEqual( 51 | ContentHelper.FormatXml(ResourceHelper.GetAssemblyResourceAsStream($"{GetType().Namespace}.MockData.WorkflowResponse.xml")), 52 | ContentHelper.FormatXml(workflowResponse.Content.ReadAsStreamAsync().Result)); 53 | 54 | // Check action result 55 | Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Transform_using_Data_Mapper")); 56 | Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Response_Success")); 57 | Assert.AreEqual(ActionStatus.Skipped, testRunner.GetWorkflowActionStatus("Response_Failure")); 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/CallDataMapperWorkflow/MockData/WorkflowRequest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7612 4 | This is a really long campaign name that will be shortened 5 | 100567 6 | Peter 7 | Smith 8 | peter.smith@example.com 9 | 45 10 | 982 11 | 12 | 13 | 7613 14 | Another campaign 15 | 100845 16 | John 17 | Jackson 18 | jjackson@example.com 19 | 29 20 | 1908 21 | 22 | 23 | 7614 24 | Another campaign 25 | 104009 26 | Simon 27 | Atkins 28 | simon.a@example.com 29 | 50 30 | 1908 31 | 32 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/CallDataMapperWorkflow/MockData/WorkflowResponse.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7612 7 | This is a really lo 8 | 9 | 10 | 100567 11 | Peter 12 | Smith 13 | peter.smith@example.com 14 | 45 15 | 16 | 982 17 | 18 | 19 | 20 | 7613 21 | Another campaign 22 | 23 | 24 | 100845 25 | John 26 | Jackson 27 | jjackson@example.com 28 | 29 29 | 30 | 1908 31 | 32 | 33 | 34 | 7614 35 | Another campaign 36 | 37 | 38 | 104009 39 | Simon 40 | Atkins 41 | simon.a@example.com 42 | 50 43 | 44 | 1908 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/CallLocalFunctionWorkflow/CallLocalFunctionWorkflowTest.cs: -------------------------------------------------------------------------------- 1 | using LogicAppUnit.Mocking; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using Newtonsoft.Json.Linq; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Net; 7 | using System.Net.Http; 8 | 9 | namespace LogicAppUnit.Samples.LogicApps.Tests.CallLocalFunctionWorkflow 10 | { 11 | /// 12 | /// Test cases for the call-local-function-workflow workflow which calls a local function that exists in the same Logic App. 13 | /// 14 | [TestClass] 15 | public class CallLocalFunctionWorkflowTest : WorkflowTestBase 16 | { 17 | [TestInitialize] 18 | public void TestInitialize() 19 | { 20 | Initialize(Constants.LOGIC_APP_TEST_EXAMPLE_BASE_PATH, Constants.CALL_LOCAL_FUNCTION_WORKFLOW); 21 | } 22 | 23 | [ClassCleanup] 24 | public static void CleanResources() 25 | { 26 | Close(); 27 | } 28 | 29 | /// 30 | /// Tests the workflow when the calling of the local function is successful. 31 | /// This test can only be run on Windows because it calls a local function targetting the .NET Framework. 32 | /// 33 | [TestMethod] 34 | [TestCategory("WindowsOnly")] 35 | public void CallLocalFunctionWorkflowTest_When_Successful() 36 | { 37 | const string zipCode = "13579"; 38 | const string tempScale = "Fahrenheit"; 39 | 40 | using (ITestRunner testRunner = CreateTestRunner()) 41 | { 42 | // Configure mock responses 43 | testRunner 44 | .AddMockResponse( 45 | MockRequestMatcher.Create() 46 | .FromAction("Get_Weather_Forecast")) 47 | .RespondWith( 48 | MockResponseBuilder.Create() 49 | .WithSuccess() 50 | .WithContentAsJson(new { 51 | ZipCode = zipCode, 52 | CurrentWeather = "The current weather is 41 Fahrenheit", 53 | DayLow = "The low for the day is 31 Fahrenheit", 54 | DayHigh = "The high for the day is 51 Fahrenheit" 55 | })); 56 | 57 | // Run the workflow 58 | Dictionary queryParams = new() 59 | { 60 | { "zipCode", zipCode }, 61 | { "tempScale", tempScale } 62 | }; 63 | var workflowResponse = testRunner.TriggerWorkflow(queryParams, HttpMethod.Get); 64 | 65 | // Check workflow run status 66 | Assert.AreEqual(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus); 67 | 68 | // Check workflow response 69 | Assert.AreEqual(HttpStatusCode.OK, workflowResponse.StatusCode); 70 | JObject responseContent = workflowResponse.Content.ReadAsAsync().Result; 71 | Assert.AreEqual(zipCode, responseContent["ZipCode"].Value()); 72 | Assert.IsTrue(responseContent["CurrentWeather"].Value().Contains(tempScale)); 73 | Assert.IsTrue(responseContent["DayLow"].Value().Contains(tempScale)); 74 | Assert.IsTrue(responseContent["DayHigh"].Value().Contains(tempScale)); 75 | 76 | // Check the "Call a local Function" action 77 | Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Get_Weather_Forecast")); 78 | 79 | // Check the "Call a local Function" action input 80 | JToken getWeatherForecastInput = testRunner.GetWorkflowActionInput("Get_Weather_Forecast"); 81 | Assert.AreEqual("WeatherForecast", getWeatherForecastInput["body"]["functionName"].Value()); 82 | Assert.AreEqual(zipCode, getWeatherForecastInput["body"]["parameters"]["zipCode"].Value()); 83 | Assert.AreEqual(tempScale, getWeatherForecastInput["body"]["parameters"]["temperatureScale"].Value()); 84 | 85 | // Check the "Call a local Function" action output 86 | JToken getWeatherForecastOutput = testRunner.GetWorkflowActionOutput("Get_Weather_Forecast"); 87 | Assert.AreEqual(zipCode, getWeatherForecastOutput["body"]["ZipCode"].Value()); 88 | Assert.IsTrue(getWeatherForecastOutput["body"]["CurrentWeather"].Value().Contains(tempScale)); 89 | Assert.IsTrue(getWeatherForecastOutput["body"]["DayLow"].Value().Contains(tempScale)); 90 | Assert.IsTrue(getWeatherForecastOutput["body"]["DayHigh"].Value().Contains(tempScale)); 91 | } 92 | } 93 | 94 | /// 95 | /// Tests the workflow when the calling of the local function fails with an exception 96 | /// This test can only be run on Windows because it calls a local function targetting the .NET Framework. 97 | /// 98 | [TestMethod] 99 | [TestCategory("WindowsOnly")] 100 | public void CallLocalFunctionWorkflowTest_When_Exception() 101 | { 102 | const string zipCode = "54321"; 103 | const string tempScale = "Celsius"; 104 | 105 | using (ITestRunner testRunner = CreateTestRunner()) 106 | { 107 | // Configure mock responses 108 | testRunner 109 | .AddMockResponse( 110 | MockRequestMatcher.Create() 111 | .FromAction("Get_Weather_Forecast")) 112 | .RespondWith( 113 | MockResponseBuilder.Create() 114 | .ThrowsException(new InvalidOperationException("Something went bang!"))); 115 | 116 | // Run the workflow 117 | Dictionary queryParams = new() 118 | { 119 | { "zipCode", zipCode }, 120 | { "tempScale", tempScale } 121 | }; 122 | var workflowResponse = testRunner.TriggerWorkflow(queryParams, HttpMethod.Get); 123 | 124 | // Check workflow run status 125 | Assert.AreEqual(WorkflowRunStatus.Failed, testRunner.WorkflowRunStatus); 126 | 127 | // Check workflow response 128 | Assert.AreEqual(HttpStatusCode.InternalServerError, workflowResponse.StatusCode); 129 | 130 | // Check the "Call a local Function" action 131 | Assert.AreEqual(ActionStatus.Failed, testRunner.GetWorkflowActionStatus("Get_Weather_Forecast")); 132 | 133 | // Check the "Call a local Function" action input 134 | JToken getWeatherForecastInput = testRunner.GetWorkflowActionInput("Get_Weather_Forecast"); 135 | Assert.AreEqual("WeatherForecast", getWeatherForecastInput["body"]["functionName"].Value()); 136 | Assert.AreEqual(zipCode, getWeatherForecastInput["body"]["parameters"]["zipCode"].Value()); 137 | Assert.AreEqual(tempScale, getWeatherForecastInput["body"]["parameters"]["temperatureScale"].Value()); 138 | 139 | // The throwing of an exception in a local function does not generate an action output in the workflow. 140 | // Therefore we shouldn't be validating the action output in a test. We can only validate the action status (failed). 141 | // JToken getWeatherForecastOutput = testRunner.GetWorkflowActionOutput("Get_Weather_Forecast"); 142 | } 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace LogicAppUnit.Samples.LogicApps.Tests 2 | { 3 | /// 4 | /// Commonly used hardcoded strings for the example workflow tests. 5 | /// 6 | public static class Constants 7 | { 8 | // Base path 9 | public static readonly string LOGIC_APP_TEST_EXAMPLE_BASE_PATH = "../../../../LogicAppUnit.Samples.LogicApps"; 10 | 11 | // Workflows 12 | public static readonly string BUILT_IN_CONNECTOR_WORKFLOW = "built-in-connector-workflow"; 13 | public static readonly string CALL_DATA_MAPPER_WORKFLOW = "call-data-mapper-workflow"; 14 | public static readonly string CALL_LOCAL_FUNCTION_WORKFLOW = "call-local-function-workflow"; 15 | public static readonly string FLUENT_REQUEST_MATCHING_WORKFLOW = "fluent-workflow"; 16 | public static readonly string HTTP_WORKFLOW = "http-workflow"; 17 | public static readonly string HTTP_ASYNC_WORKFLOW = "http-async-workflow"; 18 | public static readonly string INLINE_SCRIPT_WORKFLOW = "inline-script-workflow"; 19 | public static readonly string INVOKE_WORKFLOW = "invoke-workflow"; 20 | public static readonly string LOOP_WORKFLOW = "loop-workflow"; 21 | public static readonly string MANAGED_API_CONNECTOR_WORKFLOW = "managed-api-connector-workflow"; 22 | public static readonly string STATELESS_WORKFLOW = "stateless-workflow"; 23 | } 24 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/FluentWorkflow/FluentWorkflowResponseBuilderWithBaseTest.cs: -------------------------------------------------------------------------------- 1 | using LogicAppUnit.Helper; 2 | using LogicAppUnit.Mocking; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using System.Net; 5 | using System.Net.Http; 6 | 7 | namespace LogicAppUnit.Samples.LogicApps.Tests.FluentWorkflow 8 | { 9 | /// 10 | /// Test cases for the fluent-workflow workflow and the Response Builder features, when the test base class defines a mock response. 11 | /// 12 | [TestClass] 13 | public class FluentWorkflowResponseBuilderWithBaseTest : WorkflowTestBase 14 | { 15 | [TestInitialize] 16 | public void TestInitialize() 17 | { 18 | Initialize(Constants.LOGIC_APP_TEST_EXAMPLE_BASE_PATH, Constants.FLUENT_REQUEST_MATCHING_WORKFLOW); 19 | 20 | // Configure mock responses for all tests 21 | // The request matcher will match all requests because there are no match criteria 22 | AddMockResponse("DefinedInTestClass", 23 | MockRequestMatcher.Create()) 24 | .RespondWith( 25 | MockResponseBuilder.Create() 26 | .WithNoContent()); 27 | } 28 | 29 | [ClassCleanup] 30 | public static void CleanResources() 31 | { 32 | Close(); 33 | } 34 | 35 | /// 36 | /// Tests the response builder when no mock response is configured in the test and therefore the mock response in the test class is matched. 37 | /// 38 | [TestMethod] 39 | public void FluentWorkflowTest_ResponseBuilder_NoTestCaseMock() 40 | { 41 | using (ITestRunner testRunner = CreateTestRunner()) 42 | { 43 | // Do not configure mock responses, the test base mock response should match 44 | 45 | // Run the workflow 46 | var workflowResponse = testRunner.TriggerWorkflow( 47 | GetRequest(), 48 | HttpMethod.Post); 49 | 50 | // Check workflow run status 51 | Assert.AreEqual(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus); 52 | 53 | // Check workflow response 54 | Assert.AreEqual(HttpStatusCode.NoContent, workflowResponse.StatusCode); 55 | Assert.AreEqual(string.Empty, workflowResponse.Content.ReadAsStringAsync().Result); 56 | } 57 | } 58 | 59 | /// 60 | /// Tests the response builder when a mock response is configured in the test and matches, therefore the mock response in the test class is not used. 61 | /// 62 | [TestMethod] 63 | public void FluentWorkflowTest_ResponseBuilder_WithTestCaseMockThatMatches() 64 | { 65 | using (ITestRunner testRunner = CreateTestRunner()) 66 | { 67 | // Configure mock responses 68 | testRunner 69 | .AddMockResponse("DefinedInTestCase", 70 | MockRequestMatcher.Create()) 71 | .RespondWith( 72 | MockResponseBuilder.Create() 73 | .WithStatusCode(HttpStatusCode.Accepted) 74 | .WithContentAsPlainText("Your request has been queued for processing")); 75 | 76 | // Run the workflow 77 | var workflowResponse = testRunner.TriggerWorkflow( 78 | GetRequest(), 79 | HttpMethod.Post); 80 | 81 | // Check workflow run status 82 | Assert.AreEqual(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus); 83 | 84 | // Check workflow response 85 | Assert.AreEqual(HttpStatusCode.Accepted, workflowResponse.StatusCode); 86 | Assert.AreEqual("Your request has been queued for processing", workflowResponse.Content.ReadAsStringAsync().Result); 87 | } 88 | } 89 | 90 | /// 91 | /// Tests the response builder when a mock response is configured in the test and does not match, therefore the mock response in the test class is matched. 92 | /// 93 | [TestMethod] 94 | public void FluentWorkflowTest_ResponseBuilder_TestCaseMockNotMatched() 95 | { 96 | using (ITestRunner testRunner = CreateTestRunner()) 97 | { 98 | // Configure mock responses 99 | testRunner 100 | .AddMockResponse("DefinedInTestCase", 101 | MockRequestMatcher.Create() 102 | .WithPath(PathMatchType.Contains, "HelloWorld")) 103 | .RespondWith( 104 | MockResponseBuilder.Create() 105 | .WithStatusCode(HttpStatusCode.InternalServerError) 106 | .WithContentAsPlainText("It all went wrong!")); 107 | 108 | // Run the workflow 109 | var workflowResponse = testRunner.TriggerWorkflow( 110 | GetRequest(), 111 | HttpMethod.Post); 112 | 113 | // Check workflow run status 114 | Assert.AreEqual(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus); 115 | 116 | // Check workflow response 117 | Assert.AreEqual(HttpStatusCode.NoContent, workflowResponse.StatusCode); 118 | Assert.AreEqual(string.Empty, workflowResponse.Content.ReadAsStringAsync().Result); 119 | } 120 | } 121 | 122 | private static StringContent GetRequest() 123 | { 124 | return ContentHelper.CreateJsonStringContent(new 125 | { 126 | name = "", 127 | manufacturer = "Virgin Orbit" 128 | }); 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/FluentWorkflow/MockData/Response.ClueXml.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | CS1 11 | 12 | 13 | 14 | 0.0 15 | 0.0 16 | 10.0 17 | 18 | 19 | 0.0 20 | 1.0 21 | 10.0 22 | 23 | 24 | 25 | true 26 | EG1 27 | main audio from the room 28 | 29 | 1 30 | it 31 | static 32 | room 33 | 34 | alice 35 | bob 36 | ciccio 37 | 38 | 39 | 43 | CS1 44 | 45 | 46 | 47 | -2.0 48 | 0.0 49 | 10.0 50 | 51 | 52 | 53 | 54 | -3.0 55 | 20.0 56 | 9.0 57 | 58 | 59 | -1.0 60 | 20.0 61 | 9.0 62 | 63 | 64 | -3.0 65 | 20.0 66 | 11.0 67 | 68 | 69 | -1.0 70 | 20.0 71 | 11.0 72 | 73 | 74 | 75 | true 76 | EG0 77 | left camera video capture 78 | 79 | 1 80 | it 81 | static 82 | individual 83 | 84 | ciccio 85 | 86 | 87 | 88 | 89 | 90 | 600000 91 | 92 | ENC1 93 | ENC2 94 | ENC3 95 | 96 | 97 | 98 | 300000 99 | 100 | ENC4 101 | ENC5 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | VC0 111 | VC1 112 | VC2 113 | 114 | 115 | 116 | 117 | VC3 118 | 119 | 120 | 121 | 122 | VC4 123 | 124 | 125 | 126 | 127 | AC0 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | VC3 136 | SE1 137 | 138 | 139 | VC0 140 | VC2 141 | VC4 142 | 143 | 144 | 145 | 146 | 147 | 148 | Bob 149 | 150 | 151 | minute taker 152 | 153 | 154 | 155 | 156 | Alice 157 | 158 | 159 | presenter 160 | 161 | 162 | 163 | 164 | Ciccio 165 | 166 | 167 | chairman 168 | timekeeper 169 | 170 | 171 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/FluentWorkflow/MockData/Response.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Starship", 3 | "manufacturer": "SpaceX", 4 | "diameter": 9, 5 | "height": 120, 6 | "massToLeo": 150, 7 | "volumeToLeo": 1000 8 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/FluentWorkflow/MockData/Response.txt: -------------------------------------------------------------------------------- 1 | In computing, plain text is a loose term for data (e.g. file contents) that represent only characters of readable material but not its graphical representation nor other objects (floating-point numbers, images, etc.). It may also include a limited number of "whitespace" characters that affect simple arrangement of text, such as spaces, line breaks, or tabulation characters. Plain text is different from formatted text, where style information is included; from structured text, where structural parts of the document such as paragraphs, sections, and the like are identified; and from binary files in which some portions must be interpreted as binary objects (encoded integers, real numbers, images, etc.). 2 | 3 | The term is sometimes used quite loosely, to mean files that contain only "readable" content (or just files with nothing that the speaker doesn't prefer). For example, that could exclude any indication of fonts or layout (such as markup, markdown, or even tabs); characters such as curly quotes, non-breaking spaces, soft hyphens, em dashes, and/or ligatures; or other things. 4 | 5 | In principle, plain text can be in any encoding, but occasionally the term is taken to imply ASCII. As Unicode-based encodings such as UTF-8 and UTF-16 become more common, that usage may be shrinking. 6 | 7 | Plain text is also sometimes used only to exclude "binary" files: those in which at least some parts of the file cannot be correctly interpreted via the character encoding in effect. For example, a file or string consisting of "hello" (in any encoding), following by 4 bytes that express a binary integer that is not a character, is a binary file. Converting a plain text file to a different character encoding does not change the meaning of the text, as long as the correct character encoding is used. However, converting a binary file to a different format may alter the interpretation of the non-textual data. -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/HttpChunkingWorkflow/HttpChunkingWorkflow.cs: -------------------------------------------------------------------------------- 1 | using LogicAppUnit.Mocking; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Net; 4 | using System.Net.Http; 5 | 6 | namespace LogicAppUnit.Samples.LogicApps.Tests.HttpChunkingWorkflow 7 | { 8 | /// 9 | /// Test cases for the http-chunking-workflow workflow which uses a chunked transfer mode within HTTP action. 10 | /// 11 | [TestClass] 12 | public class HttpChunkingWorkflowTest : WorkflowTestBase 13 | { 14 | [TestInitialize] 15 | public void TestInitialize() 16 | { 17 | Initialize(Constants.LOGIC_APP_TEST_EXAMPLE_BASE_PATH, "http-chunking-workflow"); 18 | } 19 | 20 | [ClassCleanup] 21 | public static void CleanResources() 22 | { 23 | Close(); 24 | } 25 | 26 | [TestMethod] 27 | public void ChunkedTransferWorkflow_Success() 28 | { 29 | using (ITestRunner testRunner = CreateTestRunner()) 30 | { 31 | // Configure mock responses 32 | testRunner 33 | .AddMockResponse( 34 | MockRequestMatcher.Create() 35 | .UsingGet() 36 | .WithPath(PathMatchType.Exact, "/api/v1/data")) 37 | .RespondWith( 38 | MockResponseBuilder.Create() 39 | .WithSuccess() 40 | .WithContentAsJson(GetDataResponse())); 41 | testRunner 42 | .AddMockResponse( 43 | MockRequestMatcher.Create() 44 | .UsingPost() 45 | .WithPath(PathMatchType.Exact, "/api/v1.1/upload")) 46 | .RespondWithDefault(); 47 | 48 | // Run the workflow 49 | var workflowResponse = testRunner.TriggerWorkflow(HttpMethod.Post); 50 | 51 | // Check workflow run status 52 | Assert.AreEqual(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus); 53 | 54 | // Check workflow response 55 | Assert.AreEqual(HttpStatusCode.Accepted, workflowResponse.StatusCode); 56 | 57 | // Check action result 58 | Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Get_Action")); 59 | Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Post_Action")); 60 | } 61 | } 62 | 63 | private static dynamic GetDataResponse() 64 | { 65 | return new 66 | { 67 | id = 54624, 68 | title = "Mr", 69 | firstName = "Peter", 70 | lastName = "Smith", 71 | dateOfBirth = "1970-04-25", 72 | languageCode = "en-GB", 73 | address = new 74 | { 75 | line1 = "8 High Street", 76 | line2 = (string)null, 77 | line3 = (string)null, 78 | town = "Luton", 79 | county = "Bedfordshire", 80 | postcode = "LT12 6TY", 81 | countryCode = "UK", 82 | countryName = "United Kingdom" 83 | }, 84 | extra = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer et nisl in tellus sodales aliquet in id sem. Suspendisse cursus mollis erat eu ullamcorper. Nulla congue id odio at facilisis. Sed ultrices dolor nisi, sit amet cursus leo pellentesque eget. Praesent sagittis ligula leo. Vestibulum varius eros posuere tortor tristique eleifend. Praesent ornare accumsan nisi sed auctor. Fusce ullamcorper nisi nec mi euismod, in efficitur quam volutpat.Vestibulum at iaculis felis. Fusce augue sem, efficitur ut vulputate quis, cursus nec mi. Nulla sagittis posuere ornare. Morbi lectus eros, luctus non condimentum eget, pretium eget sem. Aliquam convallis sed sem accumsan ultricies. Quisque commodo at odio sit amet iaculis. Curabitur nec lectus vel leo tristique aliquam et a ipsum. Duis tortor augue, gravida sed dui ac, feugiat pulvinar ex. Integer luctus urna at mauris feugiat, nec mattis elit mattis. Fusce dictum odio quis semper blandit. Pellentesque nunc augue, elementum sit amet nunc et." 85 | }; 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/HttpWorkflow/MockData/SystemTwo_Request.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "correlationId": "71fbcb8e-f974-449a-bb14-ac2400b150aa", 4 | "dateUpdated": "2022-08-27T08:45:00.1493711Z" 5 | }, 6 | "customerType": "individual", 7 | "title": "Mr", 8 | "name": "Peter Smith", 9 | "addresses": [ 10 | { 11 | "addressType": "physical", 12 | "addressLine1": "8 High Street", 13 | "addressLine2": null, 14 | "addressLine3": null, 15 | "town": "Luton", 16 | "county": "Bedfordshire", 17 | "postalCode": "LT12 6TY" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/InlineScriptWorkflow/InlineScriptWorkflowTest.cs: -------------------------------------------------------------------------------- 1 | using LogicAppUnit.Helper; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Net.Http; 4 | 5 | namespace LogicAppUnit.Samples.LogicApps.Tests.InlineScriptWorkflow 6 | { 7 | /// 8 | /// Test cases for the inline-script-workflow workflow which calls a C# script (.csx). 9 | /// 10 | [TestClass] 11 | public class InlineScriptWorkflowTest : WorkflowTestBase 12 | { 13 | [TestInitialize] 14 | public void TestInitialize() 15 | { 16 | Initialize(Constants.LOGIC_APP_TEST_EXAMPLE_BASE_PATH, Constants.INLINE_SCRIPT_WORKFLOW); 17 | } 18 | 19 | [ClassCleanup] 20 | public static void CleanResources() 21 | { 22 | Close(); 23 | } 24 | 25 | /// 26 | /// Tests that the correct response is returned when the call to the C# script (.csx) is successful. 27 | /// 28 | [TestMethod] 29 | public void InlineScriptWorkflowTest_When_Successful() 30 | { 31 | using (ITestRunner testRunner = CreateTestRunner()) 32 | { 33 | // Run the workflow 34 | var workflowResponse = testRunner.TriggerWorkflow( 35 | GetRequest(), 36 | HttpMethod.Post); 37 | 38 | // Check workflow run status 39 | Assert.AreEqual(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus); 40 | 41 | // Check action result 42 | Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Execute_CSharp_Script_Code")); 43 | Assert.AreEqual( 44 | ContentHelper.FormatJson(ResourceHelper.GetAssemblyResourceAsString($"{GetType().Namespace}.MockData.Execute_CSharp_Script_Code_Output.json")), 45 | ContentHelper.FormatJson(testRunner.GetWorkflowActionOutput("Execute_CSharp_Script_Code").ToString())); 46 | } 47 | } 48 | 49 | private static StringContent GetRequest() 50 | { 51 | return ContentHelper.CreateJsonStringContent(new { 52 | name = "Jane" 53 | }); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/InlineScriptWorkflow/MockData/Execute_CSharp_Script_Code_Output.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": { 3 | "message": "Hello Jane from CSharp action" 4 | } 5 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/InvokeWorkflow/InvokeWorkflowTest.cs: -------------------------------------------------------------------------------- 1 | using LogicAppUnit.Helper; 2 | using LogicAppUnit.Mocking; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Newtonsoft.Json.Linq; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Net.Http; 8 | 9 | namespace LogicAppUnit.Samples.LogicApps.Tests.InvokeWorkflow 10 | { 11 | /// 12 | /// Test cases for the invoke-workflow workflow. 13 | /// 14 | [TestClass] 15 | public class InvokeWorkflowTest : WorkflowTestBase 16 | { 17 | [TestInitialize] 18 | public void TestInitialize() 19 | { 20 | Initialize(Constants.LOGIC_APP_TEST_EXAMPLE_BASE_PATH, Constants.INVOKE_WORKFLOW); 21 | 22 | // Configure mock responses for all tests 23 | AddMockResponse("DeleteBlob", 24 | MockRequestMatcher.Create() 25 | .UsingPost() 26 | .WithPath(PathMatchType.Exact, "/Delete_blob")) 27 | .RespondWithDefault(); 28 | } 29 | 30 | [ClassCleanup] 31 | public static void CleanResources() 32 | { 33 | Close(); 34 | } 35 | 36 | /// 37 | /// Tests that a standard customer message is processed correctly. 38 | /// 39 | [TestMethod] 40 | public void InvokeWorkflowTest_When_Not_Priority_Successful() 41 | { 42 | using (ITestRunner testRunner = CreateTestRunner()) 43 | { 44 | // Configure mock responses 45 | testRunner 46 | .AddMockResponse("Invoke-Not Priority", 47 | MockRequestMatcher.Create() 48 | .UsingPost() 49 | .WithPath(PathMatchType.Exact, "/Invoke_a_workflow_(not_Priority)")) 50 | .RespondWith( 51 | MockResponseBuilder.Create(). 52 | WithContentAsPlainText("Upsert is successful")); 53 | 54 | // Create request 55 | JObject x = JObject.Parse(ResourceHelper.GetAssemblyResourceAsString($"{GetType().Namespace}.MockData.WorkflowRequest.json")); 56 | ((JValue)x["name"]).Value = "Standard customer.json"; 57 | 58 | // Run the workflow 59 | var workflowResponse = testRunner.TriggerWorkflow( 60 | ContentHelper.CreateJsonStringContent(x.ToString()), 61 | HttpMethod.Post); 62 | 63 | // Check workflow run status 64 | Assert.AreEqual(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus); 65 | 66 | // Check workflow response 67 | // The workflow does not have a 'Response' action, so no content to validate 68 | Assert.AreEqual(HttpStatusCode.Accepted, workflowResponse.StatusCode); 69 | 70 | // Check action result 71 | Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Invoke_a_workflow_(not_Priority)")); 72 | Assert.AreEqual(ActionStatus.Skipped, testRunner.GetWorkflowActionStatus("Invoke_a_workflow_(Priority)")); 73 | Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Delete_blob")); 74 | 75 | // Check request to Invoke Workflow 76 | var invokeWorkflowRequest = testRunner.MockRequests.First(r => r.RequestUri.AbsolutePath == "/Invoke_a_workflow_(not_Priority)"); 77 | Assert.AreEqual(HttpMethod.Post, invokeWorkflowRequest.Method); 78 | Assert.AreEqual( 79 | ContentHelper.FormatJson(ResourceHelper.GetAssemblyResourceAsString($"{GetType().Namespace}.MockData.InvokeWorkflowNotPriorityRequest.json")), 80 | ContentHelper.FormatJson(invokeWorkflowRequest.Content)); 81 | } 82 | } 83 | 84 | /// 85 | /// Tests that a priority customer message is processed correctly. 86 | /// 87 | [TestMethod] 88 | public void InvokeWorkflowTest_When_Priority_Successful() 89 | { 90 | using (ITestRunner testRunner = CreateTestRunner()) 91 | { 92 | // Mock the HTTP calls and customize responses 93 | testRunner.AddApiMocks = (request) => 94 | { 95 | HttpResponseMessage mockedResponse = new HttpResponseMessage(); 96 | if (request.RequestUri.AbsolutePath == "/Add_customer_to_Priority_queue" && request.Method == HttpMethod.Post) 97 | { 98 | mockedResponse.RequestMessage = request; 99 | mockedResponse.StatusCode = HttpStatusCode.OK; 100 | } 101 | return mockedResponse; 102 | }; 103 | 104 | // Create request 105 | JObject x = JObject.Parse(ResourceHelper.GetAssemblyResourceAsString($"{GetType().Namespace}.MockData.WorkflowRequest.json")); 106 | ((JValue)x["name"]).Value = "Priority customer.json"; 107 | 108 | // Run the workflow 109 | var workflowResponse = testRunner.TriggerWorkflow( 110 | ContentHelper.CreateJsonStringContent(x.ToString()), 111 | HttpMethod.Post); 112 | 113 | // Check workflow run status 114 | Assert.AreEqual(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus); 115 | 116 | // Check workflow response 117 | // The workflow does not have a 'Response' action, so no content to validate 118 | Assert.AreEqual(HttpStatusCode.Accepted, workflowResponse.StatusCode); 119 | 120 | // Check action result 121 | Assert.AreEqual(ActionStatus.Skipped, testRunner.GetWorkflowActionStatus("Invoke_a_workflow_(not_Priority)")); 122 | Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Invoke_a_workflow_(Priority)")); 123 | Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Add_customer_to_Priority_queue")); 124 | Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Delete_blob")); 125 | 126 | // Check request to Invoke Workflow 127 | var invokeWorkflowRequest = testRunner.MockRequests.First(r => r.RequestUri.AbsolutePath == "/Invoke_a_workflow_(Priority)"); 128 | Assert.AreEqual(HttpMethod.Post, invokeWorkflowRequest.Method); 129 | Assert.AreEqual( 130 | ContentHelper.FormatJson(ResourceHelper.GetAssemblyResourceAsString($"{GetType().Namespace}.MockData.InvokeWorkflowPriorityRequest.json")), 131 | ContentHelper.FormatJson(invokeWorkflowRequest.Content)); 132 | 133 | // Check request to Add Customer to the storage queue 134 | var addToQueueRequest = testRunner.MockRequests.First(r => r.RequestUri.AbsolutePath == "/Add_customer_to_Priority_queue"); 135 | Assert.AreEqual(HttpMethod.Post, addToQueueRequest.Method); 136 | Assert.AreEqual( 137 | ContentHelper.FormatJson(ResourceHelper.GetAssemblyResourceAsString($"{GetType().Namespace}.MockData.AddToPriorityQueueRequest.json")), 138 | ContentHelper.FormatJson(addToQueueRequest.Content)); 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/InvokeWorkflow/MockData/AddToPriorityQueueRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "queueName": "customers-priority-queue", 3 | "message": "{\n \"blobName\": \"Priority customer.json\",\n \"blobContent\": {\"id\":54624,\"title\":\"Mr\",\"firstName\":\"Peter\",\"lastName\":\"Smith\",\"dateOfBirth\":\"1970-04-25\",\"address\":{\"addressLine1\":\"Blossoms Pasture\",\"addressLine2\":\"High Street\",\"addressLine3\":\"Tinyville\",\"town\":\"Luton\",\"county\":\"Bedfordshire\",\"postcode\":\"LT12 6TY\",\"countryCode\":\"UK\",\"countryName\":\"United Kingdom\"}}\n}" 4 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/InvokeWorkflow/MockData/InvokeWorkflowNotPriorityRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": { 3 | "workflow": { 4 | "id": "managed-api-connector-test-workflow" 5 | } 6 | }, 7 | "headers": { 8 | "Content-Type": "application/json", 9 | "DataSource": "customers", 10 | "Priority": false 11 | }, 12 | "body": { 13 | "id": 54624, 14 | "title": "Mr", 15 | "firstName": "Peter", 16 | "lastName": "Smith", 17 | "dateOfBirth": "1970-04-25", 18 | "address": { 19 | "addressLine1": "Blossoms Pasture", 20 | "addressLine2": "High Street", 21 | "addressLine3": "Tinyville", 22 | "town": "Luton", 23 | "county": "Bedfordshire", 24 | "postcode": "LT12 6TY", 25 | "countryCode": "UK", 26 | "countryName": "United Kingdom" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/InvokeWorkflow/MockData/InvokeWorkflowPriorityRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": { 3 | "workflow": { 4 | "id": "managed-api-connector-test-workflow" 5 | } 6 | }, 7 | "headers": { 8 | "Content-Type": "application/json", 9 | "DataSource": "customers", 10 | "Priority": true 11 | }, 12 | "body": { 13 | "id": 54624, 14 | "title": "Mr", 15 | "firstName": "Peter", 16 | "lastName": "Smith", 17 | "dateOfBirth": "1970-04-25", 18 | "address": { 19 | "addressLine1": "Blossoms Pasture", 20 | "addressLine2": "High Street", 21 | "addressLine3": "Tinyville", 22 | "town": "Luton", 23 | "county": "Bedfordshire", 24 | "postcode": "LT12 6TY", 25 | "countryCode": "UK", 26 | "countryName": "United Kingdom" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/InvokeWorkflow/MockData/WorkflowRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": { 3 | "id": 54624, 4 | "title": "Mr", 5 | "firstName": "Peter", 6 | "lastName": "Smith", 7 | "dateOfBirth": "1970-04-25", 8 | "address": { 9 | "addressLine1": "Blossoms Pasture", 10 | "addressLine2": "High Street", 11 | "addressLine3": "Tinyville", 12 | "town": "Luton", 13 | "county": "Bedfordshire", 14 | "postcode": "LT12 6TY", 15 | "countryCode": "UK", 16 | "countryName": "United Kingdom" 17 | } 18 | }, 19 | "containerInfo": { 20 | "name": "customers", 21 | "properties": { 22 | "lastModified": "2023-01-27T12:01:06+00:00", 23 | "leaseStatus": "Unlocked", 24 | "leaseState": "Available", 25 | "leaseDuration": "Infinite", 26 | "hasImmutabilityPolicy": false, 27 | "hasLegalHold": false, 28 | "defaultEncryptionScope": "$account-encryption-key", 29 | "preventEncryptionScopeOverride": false, 30 | "eTag": {}, 31 | "metadata": {}, 32 | "hasImmutableStorageWithVersioning": false 33 | } 34 | }, 35 | "name": "Priority Customer.json", 36 | "properties": { 37 | "appendBlobCommittedBlockCount": 0, 38 | "blobTierInferred": true, 39 | "blobTierLastModifiedTime": "2023-01-27T12:08:02+00:00", 40 | "blobType": "Block", 41 | "contentMD5": "(?SP}\u001a?????1??V", 42 | "contentType": "application/json", 43 | "created": "2023-01-27T12:08:02+00:00", 44 | "creationTime": "2023-01-27T12:08:02+00:00", 45 | "eTag": "\"0x8DB005F23D6CBDD\"", 46 | "isIncrementalCopy": false, 47 | "isServerEncrypted": true, 48 | "lastModified": "2023-01-27T12:08:02+00:00", 49 | "leaseDuration": "Infinite", 50 | "leaseState": "Available", 51 | "leaseStatus": "Unlocked", 52 | "length": 442, 53 | "pageBlobSequenceNumber": 0, 54 | "premiumPageBlobTier": "Hot", 55 | "standardBlobTier": "Hot" 56 | }, 57 | "metadata": {} 58 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/LogicAppUnit.Samples.LogicApps.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | LogicAppUnit.Samples.LogicApps.Tests 6 | false 7 | 8 | 9 | 10 | True 11 | 12 | 13 | 14 | True 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | Always 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/LogicAppUnit.Samples.LogicApps.Tests.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogicAppUnit.Samples.LogicApps.Tests", "LogicAppUnit.Samples.LogicApps.Tests.csproj", "{5BD5CF75-DDF9-4BFF-B22D-8FDC9DD2243B}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {5BD5CF75-DDF9-4BFF-B22D-8FDC9DD2243B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {5BD5CF75-DDF9-4BFF-B22D-8FDC9DD2243B}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {5BD5CF75-DDF9-4BFF-B22D-8FDC9DD2243B}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {5BD5CF75-DDF9-4BFF-B22D-8FDC9DD2243B}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {E4EC391D-9710-436F-B942-B9AA054163DC} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/LoopWorkflow/MockData/Response.json: -------------------------------------------------------------------------------- 1 | { 2 | "loopCounter": 5, 3 | "serviceOneResponses": [ 4 | { 5 | "message": "All working in System One" 6 | }, 7 | { 8 | "message": "All working in System One" 9 | }, 10 | { 11 | "message": "All working in System One" 12 | }, 13 | { 14 | "message": "Internal server error detected in System One" 15 | }, 16 | { 17 | "message": "All working in System One" 18 | } 19 | ], 20 | "serviceTwoResponses": [ 21 | { 22 | "message": "All working in System Two" 23 | }, 24 | { 25 | "message": "Bad request received by System Two" 26 | }, 27 | { 28 | "message": "Bad request received by System Two" 29 | }, 30 | { 31 | "message": "All working in System Two" 32 | }, 33 | { 34 | "message": "All working in System Two" 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/ManagedApiConnectorWorkflow/MockData/Outlook_Request.json: -------------------------------------------------------------------------------- 1 | { 2 | "To": "update-notification@test-example.net", 3 | "Subject": "TEST ENVIRONMENT: Customer 54624 (Peter Smith) has been updated", 4 | "Body": "

Notification
\n
\nCustomer 54624 has been updated.

", 5 | "From": "integration@test-example.net", 6 | "Importance": "Normal" 7 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/ManagedApiConnectorWorkflow/MockData/Salesforce_Request.json: -------------------------------------------------------------------------------- 1 | { 2 | "Title__c": "Mr", 3 | "FirstName__c": "Peter", 4 | "LastName__c": "Smith", 5 | "Address_Line_1__c": "Blossoms Pasture", 6 | "Address_Line_2__c": "High Street, Tinyville", 7 | "Town__c": "Luton", 8 | "County__c": "Bedfordshire", 9 | "Country__c": "United Kingdom", 10 | "Post_Code__c": "LT12 6TY", 11 | "Status__c": "Active" 12 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/StatelessWorkflow/MockData/UploadBlobRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "containerName": "thisIsMyContainer", 3 | "blobName": "thisIsMyBlob", 4 | "content": { 5 | "customerType": "individual", 6 | "title": "Mr", 7 | "name": "Peter Smith", 8 | "addresses": [ 9 | { 10 | "addressType": "physical", 11 | "addressLine1": "8 High Street", 12 | "addressLine2": null, 13 | "addressLine3": null, 14 | "town": "Luton", 15 | "county": "Bedfordshire", 16 | "postalCode": "LT12 6TY" 17 | } 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/StatelessWorkflow/MockData/UploadBlobResponseFailed.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": "ServiceProviderActionFailed", 3 | "message": "The service provider action failed with error code 'BadRequest' and error message 'The specified blob named 'thisIsMyBlob' in container 'thisIsMyContainer' already exists.'." 4 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/StatelessWorkflow/MockData/WorkflowRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "customerType": "individual", 3 | "title": "Mr", 4 | "name": "Peter Smith", 5 | "addresses": [ 6 | { 7 | "addressType": "physical", 8 | "addressLine1": "8 High Street", 9 | "addressLine2": null, 10 | "addressLine3": null, 11 | "town": "Luton", 12 | "county": "Bedfordshire", 13 | "postalCode": "LT12 6TY" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps.Tests/testConfiguration.json: -------------------------------------------------------------------------------- 1 | { 2 | "logging": { 3 | "writeFunctionRuntimeStartupLogs": false, 4 | "WriteMockRequestMatchingLogs": true 5 | }, 6 | 7 | "workflow": { 8 | "externalApiUrlsToMock": [ 9 | "https://external-service-one.testing.net", 10 | "https://external-service-two.testing.net" 11 | ], 12 | "builtInConnectorsToMock": [ 13 | "executeQuery", 14 | "sendMessage", 15 | "uploadBlob", 16 | "deleteBlob", 17 | "putMessage", 18 | "CreateOrUpdateDocument" 19 | ], 20 | "autoConfigureWithStatelessRunHistory": true 21 | } 22 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/.funcignore: -------------------------------------------------------------------------------- 1 | .debug 2 | .git* 3 | .vscode 4 | local.settings.json 5 | test 6 | workflow-designtime/ -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Azure Functions artifacts 3 | bin 4 | obj 5 | appsettings.json 6 | # local.settings.json 7 | 8 | # Logic App debug symbols 9 | .debug 10 | 11 | # Build output from local Function project 12 | lib/custom 13 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Logic App", 6 | "type": "coreclr", 7 | "request": "attach", 8 | "processId": "${command:azureLogicAppsStandard.pickProcess}" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureLogicAppsStandard.projectLanguage": "JavaScript", 3 | "azureLogicAppsStandard.projectRuntime": "~4", 4 | "debug.internalConsoleOptions": "neverOpen", 5 | "azureFunctions.suppressProject": true 6 | } 7 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "generateDebugSymbols", 6 | "command": "dotnet", 7 | "args": [ 8 | "${input:getDebugSymbolDll}" 9 | ], 10 | "type": "process", 11 | "problemMatcher": "$msCompile" 12 | }, 13 | { 14 | "type": "func", 15 | "command": "host start", 16 | "problemMatcher": "$func-watch", 17 | "isBackground": true, 18 | "label": "func: host start", 19 | "group": { 20 | "kind": "build", 21 | "isDefault": true 22 | } 23 | } 24 | ], 25 | "inputs": [ 26 | { 27 | "id": "getDebugSymbolDll", 28 | "type": "command", 29 | "command": "azureLogicAppsStandard.getDebugSymbolDll" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/Artifacts/MapDefinitions/CustomerCampaignToCampaignRequest.lml: -------------------------------------------------------------------------------- 1 | $version: 1 2 | $input: XML 3 | $output: XML 4 | $sourceSchema: CustomerCampaign.xsd 5 | $targetSchema: CampaignRequest.xsd 6 | $sourceNamespaces: 7 | ns0: http://schemas.logicappunit.net/CustomerCampaign/v1 8 | xs: http://www.w3.org/2001/XMLSchema 9 | $targetNamespaces: 10 | tns: http://schemas.logicappunit.net/CampaignRequest 11 | xs: http://www.w3.org/2001/XMLSchema 12 | tns:campaignRequest: 13 | $@numberOfCampaigns: count(/ns0:CustomerCampaigns/ns0:Campaign) 14 | $for(/ns0:CustomerCampaigns/ns0:Campaign): 15 | tns:campaign: 16 | tns:campaignDetails: 17 | tns:id: ns0:CampaignId 18 | tns:name: substring(ns0:CampaignName, 0, 20) 19 | tns:customer: 20 | tns:id: ns0:CustomerId 21 | tns:forename: substring(ns0:FirstName, 0, 40) 22 | tns:surname: substring(ns0:LastName, 0, 40) 23 | tns:email: ns0:Email 24 | tns:age: ns0:Age 25 | tns:premisesid: ns0:SiteCode 26 | 27 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/Artifacts/Maps/CustomerCampaignToCampaignRequest.xslt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {count(/ns0:CustomerCampaigns/ns0:Campaign)} 9 | 10 | 11 | 12 | {ns0:CampaignId} 13 | {substring(ns0:CampaignName, 0, 20)} 14 | 15 | 16 | {ns0:CustomerId} 17 | {substring(ns0:FirstName, 0, 40)} 18 | {substring(ns0:LastName, 0, 40)} 19 | {ns0:Email} 20 | {ns0:Age} 21 | 22 | {ns0:SiteCode} 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/Artifacts/Schemas/CampaignRequest.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/Artifacts/Schemas/CustomerCampaign.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/call-data-mapper-workflow/workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "definition": { 3 | "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", 4 | "actions": { 5 | "Transform_using_Data_Mapper": { 6 | "type": "Xslt", 7 | "kind": "DataMapper", 8 | "inputs": { 9 | "content": "@triggerBody()", 10 | "map": { 11 | "source": "LogicApp", 12 | "name": "CustomerCampaignToCampaignRequest.xslt" 13 | } 14 | }, 15 | "runAfter": {} 16 | }, 17 | "Response_Success": { 18 | "type": "Response", 19 | "kind": "Http", 20 | "inputs": { 21 | "statusCode": 200, 22 | "body": "@body('Transform_using_Data_Mapper')" 23 | }, 24 | "runAfter": { 25 | "Transform_using_Data_Mapper": [ 26 | "SUCCEEDED" 27 | ] 28 | } 29 | }, 30 | "Response_Failure": { 31 | "type": "Response", 32 | "kind": "Http", 33 | "inputs": { 34 | "statusCode": 500 35 | }, 36 | "runAfter": { 37 | "Transform_using_Data_Mapper": [ 38 | "TIMEDOUT", 39 | "FAILED" 40 | ] 41 | } 42 | } 43 | }, 44 | "contentVersion": "1.0.0.0", 45 | "outputs": {}, 46 | "triggers": { 47 | "When_a_HTTP_request_is_received": { 48 | "type": "Request", 49 | "kind": "Http" 50 | } 51 | } 52 | }, 53 | "kind": "Stateful" 54 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/call-local-function-workflow/workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "definition": { 3 | "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", 4 | "actions": { 5 | "Get_Weather_Forecast": { 6 | "type": "InvokeFunction", 7 | "inputs": { 8 | "functionName": "WeatherForecast", 9 | "parameters": { 10 | "zipCode": "@triggerOutputs()['queries']['zipCode']", 11 | "temperatureScale": "@{triggerOutputs()['queries']['tempScale']}" 12 | } 13 | }, 14 | "runAfter": {} 15 | }, 16 | "Response_Success": { 17 | "type": "Response", 18 | "kind": "Http", 19 | "inputs": { 20 | "statusCode": 200, 21 | "body": "@body('Get_Weather_Forecast')" 22 | }, 23 | "runAfter": { 24 | "Get_Weather_Forecast": [ 25 | "SUCCEEDED" 26 | ] 27 | } 28 | }, 29 | "Response_Failure": { 30 | "type": "Response", 31 | "kind": "Http", 32 | "inputs": { 33 | "statusCode": "@coalesce(outputs('Get_Weather_Forecast')?['statusCode'], 500)", 34 | "body": "@body('Get_Weather_Forecast')" 35 | }, 36 | "runAfter": { 37 | "Get_Weather_Forecast": [ 38 | "FAILED", 39 | "TIMEDOUT" 40 | ] 41 | } 42 | } 43 | }, 44 | "contentVersion": "1.0.0.0", 45 | "outputs": {}, 46 | "triggers": { 47 | "When_a_HTTP_request_is_received": { 48 | "type": "Request", 49 | "kind": "Http", 50 | "inputs": { 51 | "method": "GET" 52 | } 53 | } 54 | } 55 | }, 56 | "kind": "Stateful" 57 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/connections.json: -------------------------------------------------------------------------------- 1 | { 2 | "serviceProviderConnections": { 3 | "serviceBus": { 4 | "parameterValues": { 5 | "connectionString": "@appsetting('ServiceBus_ConnectionString')" 6 | }, 7 | "serviceProvider": { 8 | "id": "/serviceProviders/serviceBus" 9 | }, 10 | "displayName": "serviceBusConnection" 11 | }, 12 | "sql": { 13 | "parameterValues": { 14 | "connectionString": "@appsetting('Sql_ConnectionString')" 15 | }, 16 | "serviceProvider": { 17 | "id": "/serviceProviders/sql" 18 | }, 19 | "displayName": "sqlConnection" 20 | }, 21 | "azureBlob": { 22 | "parameterValues": { 23 | "connectionString": "@appsetting('AzureBlob-ConnectionString')" 24 | }, 25 | "serviceProvider": { 26 | "id": "/serviceProviders/AzureBlob" 27 | }, 28 | "displayName": "storageBlobConnection" 29 | }, 30 | "azureQueue": { 31 | "parameterValues": { 32 | "connectionString": "@appsetting('AzureQueue-ConnectionString')" 33 | }, 34 | "serviceProvider": { 35 | "id": "/serviceProviders/azurequeues" 36 | }, 37 | "displayName": "storageQueueConnection" 38 | } 39 | }, 40 | "managedApiConnections": { 41 | "salesforce": { 42 | "api": { 43 | "id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/providers/Microsoft.Web/locations/@{appsetting('WORKFLOWS_LOCATION_NAME')}/managedApis/salesforce" 44 | }, 45 | "connection": { 46 | "id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/resourceGroups/@{appsetting('WORKFLOWS_RESOURCE_GROUP_NAME')}/providers/Microsoft.Web/connections/salesforce01" 47 | }, 48 | "connectionRuntimeUrl": "@parameters('salesforce-ConnectionRuntimeUrl')", 49 | "authentication": { 50 | "type": "Raw", 51 | "scheme": "Key", 52 | "parameter": "@appsetting('Salesforce-ConnectionKey')" 53 | } 54 | }, 55 | "outlook": { 56 | "api": { 57 | "id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/providers/Microsoft.Web/locations/@{appsetting('WORKFLOWS_LOCATION_NAME')}/managedApis/outlook" 58 | }, 59 | "connection": { 60 | "id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/resourceGroups/@{appsetting('WORKFLOWS_RESOURCE_GROUP_NAME')}/providers/Microsoft.Web/connections/outlook01" 61 | }, 62 | "connectionRuntimeUrl": "@parameters('outlook-ConnectionRuntimeUrl')", 63 | "authentication": "@parameters('outlook-Authentication')" 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/fluent-workflow/workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "definition": { 3 | "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", 4 | "actions": { 5 | "Call_Service_One": { 6 | "type": "Http", 7 | "inputs": { 8 | "uri": "@{parameters('ServiceOne-Url')}/service", 9 | "method": "POST", 10 | "headers": { 11 | "Accept": "application/json", 12 | "Expect": "application/json", 13 | "UserAgent": "LogicAppUnit", 14 | "MyCustomHeader": "MyValue" 15 | }, 16 | "queries": { 17 | "one": "oneValue", 18 | "two": "twoValue", 19 | "three": "", 20 | "four": "fourValue", 21 | "five": "55555" 22 | }, 23 | "body": "@triggerBody()" 24 | }, 25 | "runAfter": {} 26 | }, 27 | "Response_Success": { 28 | "type": "Response", 29 | "kind": "Http", 30 | "inputs": { 31 | "statusCode": "@outputs('Call_Service_One')?['statusCode']", 32 | "headers": { 33 | "oneHeader": "@{outputs('Call_Service_One')?['headers']?['oneHeader']}", 34 | "twoHeader": "@{outputs('Call_Service_One')?['headers']?['twoHeader']}", 35 | "threeHeader": "@{outputs('Call_Service_One')?['headers']?['threeHeader']}" 36 | }, 37 | "body": "@body('Call_Service_One')" 38 | }, 39 | "runAfter": { 40 | "Call_Service_One": [ 41 | "SUCCEEDED" 42 | ] 43 | } 44 | }, 45 | "Response_Failure": { 46 | "type": "Response", 47 | "kind": "Http", 48 | "inputs": { 49 | "statusCode": "@outputs('Call_Service_One')?['statusCode']", 50 | "body": "@body('Call_Service_One')" 51 | }, 52 | "runAfter": { 53 | "Call_Service_One": [ 54 | "TIMEDOUT", 55 | "FAILED" 56 | ] 57 | } 58 | } 59 | }, 60 | "contentVersion": "1.0.0.0", 61 | "outputs": {}, 62 | "triggers": { 63 | "When_a_HTTP_request_is_received": { 64 | "type": "Request", 65 | "kind": "Http", 66 | "inputs": { 67 | "method": "POST" 68 | } 69 | } 70 | } 71 | }, 72 | "kind": "Stateful" 73 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensionBundle": { 4 | "id": "Microsoft.Azure.Functions.ExtensionBundle.Workflows", 5 | "version": "[1.*, 2.0.0)" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/http-chunking-workflow/workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "definition": { 3 | "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", 4 | "actions": { 5 | "Get_Action": { 6 | "type": "Http", 7 | "inputs": { 8 | "uri": "@{parameters('ServiceOne-Url')}/data", 9 | "method": "GET" 10 | }, 11 | "runAfter": {}, 12 | "runtimeConfiguration": { 13 | "contentTransfer": { 14 | "transferMode": "Chunked" 15 | } 16 | } 17 | }, 18 | "Post_Action": { 19 | "type": "Http", 20 | "inputs": { 21 | "uri": "@{parameters('ServiceTwo-Url')}/upload", 22 | "method": "POST", 23 | "body": "@body('Get_Action')" 24 | }, 25 | "runAfter": { 26 | "Get_Action": [ 27 | "SUCCEEDED" 28 | ] 29 | }, 30 | "runtimeConfiguration": { 31 | "contentTransfer": { 32 | "transferMode": "Chunked" 33 | } 34 | } 35 | } 36 | }, 37 | "triggers": { 38 | "Recurrence": { 39 | "type": "Recurrence", 40 | "recurrence": { 41 | "frequency": "Day", 42 | "interval": 1, 43 | "schedule": { 44 | "hours": [ 45 | "8" 46 | ] 47 | } 48 | } 49 | } 50 | }, 51 | "parameters": { 52 | "ServiceOne-Url": { 53 | "type": "String", 54 | "value": "@appsetting('ServiceOne-Url')", 55 | "defaultValue": "@appsetting('ServiceOne-Url')" 56 | }, 57 | "ServiceOne-Authentication-APIKey": { 58 | "type": "String", 59 | "value": "@appsetting('ServiceOne-Authentication-APIKey')", 60 | "defaultValue": "@appsetting('ServiceOne-Authentication-APIKey')" 61 | }, 62 | "ServiceOne-Authentication-WebHook-APIKey": { 63 | "type": "String", 64 | "value": "@appsetting('ServiceOne-Authentication-WebHook-APIKey')", 65 | "defaultValue": "@appsetting('ServiceOne-Authentication-WebHook-APIKey')" 66 | }, 67 | "ServiceTwo-Url": { 68 | "type": "String", 69 | "value": "@appsetting('ServiceTwo-Url')", 70 | "defaultValue": "@appsetting('ServiceTwo-Url')" 71 | }, 72 | "ServiceTwo-Authentication-APIKey": { 73 | "type": "String", 74 | "value": "@appsetting('ServiceTwo-Authentication-APIKey')", 75 | "defaultValue": "@appsetting('ServiceTwo-Authentication-APIKey')" 76 | } 77 | } 78 | }, 79 | "kind": "Stateful" 80 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/inline-script-workflow/execute_csharp_script_code.csx: -------------------------------------------------------------------------------- 1 | // Add the required libraries 2 | #r "Newtonsoft.Json" 3 | #r "Microsoft.Azure.Workflows.Scripting" 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Primitives; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Azure.Workflows.Scripting; 8 | using Newtonsoft.Json.Linq; 9 | 10 | /// 11 | /// Executes the inline csharp code. 12 | /// 13 | /// The workflow context. 14 | /// This is the entry-point to your code. The function signature should remain unchanged. 15 | public static async Task Run(WorkflowContext context, ILogger log) 16 | { 17 | var triggerOutputs = (await context.GetTriggerResults().ConfigureAwait(false)).Outputs; 18 | 19 | ////the following dereferences the 'name' property from trigger payload. 20 | var name = triggerOutputs?["body"]?["name"]?.ToString(); 21 | 22 | ////the following can be used to get the action outputs from a prior action 23 | //var actionOutputs = (await context.GetActionResults("Compose").ConfigureAwait(false)).Outputs; 24 | 25 | ////these logs will show-up in Application Insight traces table 26 | //log.LogInformation("Outputting results."); 27 | 28 | //var name = null; 29 | 30 | return new Results 31 | { 32 | Message = !string.IsNullOrEmpty(name) ? $"Hello {name} from CSharp action" : "Hello from CSharp action." 33 | }; 34 | } 35 | 36 | public class Results 37 | { 38 | public string Message {get; set;} 39 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/inline-script-workflow/workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "definition": { 3 | "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", 4 | "actions": { 5 | "Execute_CSharp_Script_Code": { 6 | "type": "CSharpScriptCode", 7 | "inputs": { 8 | "CodeFile": "execute_csharp_script_code.csx" 9 | }, 10 | "runAfter": {} 11 | } 12 | }, 13 | "contentVersion": "1.0.0.0", 14 | "outputs": {}, 15 | "triggers": { 16 | "manual": { 17 | "type": "Request", 18 | "kind": "Http", 19 | "inputs": { 20 | "method": "POST", 21 | "schema": { 22 | "properties": { 23 | "name": { 24 | "type": "string" 25 | } 26 | }, 27 | "type": "object" 28 | } 29 | } 30 | } 31 | } 32 | }, 33 | "kind": "Stateful" 34 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/invoke-workflow/workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "definition": { 3 | "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", 4 | "actions": { 5 | "Is_this_a_Priority_blob": { 6 | "type": "If", 7 | "description": "This Decision is a bit pointless, just trying to demonstrate a workflow that includes two \"Invoke Workflow\" actions. ", 8 | "expression": { 9 | "and": [ 10 | { 11 | "contains": [ 12 | "@triggerBody()?['name']", 13 | "Priority" 14 | ] 15 | } 16 | ] 17 | }, 18 | "actions": { 19 | "Invoke_a_workflow_(Priority)": { 20 | "type": "Workflow", 21 | "inputs": { 22 | "host": { 23 | "workflow": { 24 | "id": "managed-api-connector-test-workflow" 25 | } 26 | }, 27 | "headers": { 28 | "Content-Type": "@triggerOutputs()?['body']?['properties']?['contentType']", 29 | "DataSource": "@triggerOutputs()?['body']?['containerInfo']?['name']", 30 | "Priority": true 31 | }, 32 | "body": "@triggerOutputs()?['body']?['content']" 33 | } 34 | }, 35 | "Add_customer_to_Priority_queue": { 36 | "type": "ServiceProvider", 37 | "inputs": { 38 | "parameters": { 39 | "queueName": "customers-priority-queue", 40 | "message": "{\n \"blobName\": \"@{triggerOutputs()?['body']?['name']}\",\n \"blobContent\": @{triggerOutputs()?['body']?['content']}\n}" 41 | }, 42 | "serviceProviderConfiguration": { 43 | "connectionName": "azureQueue", 44 | "operationId": "putMessage", 45 | "serviceProviderId": "/serviceProviders/azurequeues" 46 | } 47 | }, 48 | "runAfter": { 49 | "Invoke_a_workflow_(Priority)": [ 50 | "SUCCEEDED", 51 | "TIMEDOUT", 52 | "FAILED" 53 | ] 54 | } 55 | } 56 | }, 57 | "else": { 58 | "actions": { 59 | "Invoke_a_workflow_(not_Priority)": { 60 | "type": "Workflow", 61 | "inputs": { 62 | "host": { 63 | "workflow": { 64 | "id": "managed-api-connector-test-workflow" 65 | } 66 | }, 67 | "headers": { 68 | "Content-Type": "@triggerOutputs()?['body']?['properties']?['contentType']", 69 | "DataSource": "@triggerOutputs()?['body']?['containerInfo']?['name']", 70 | "Priority": false 71 | }, 72 | "body": "@triggerOutputs()?['body']?['content']" 73 | } 74 | } 75 | } 76 | }, 77 | "runAfter": {} 78 | }, 79 | "Delete_blob": { 80 | "type": "ServiceProvider", 81 | "inputs": { 82 | "parameters": { 83 | "containerName": "customers", 84 | "blobName": "@triggerOutputs()?['body']?['name']" 85 | }, 86 | "serviceProviderConfiguration": { 87 | "connectionName": "azureBlob", 88 | "operationId": "deleteBlob", 89 | "serviceProviderId": "/serviceProviders/AzureBlob" 90 | } 91 | }, 92 | "runAfter": { 93 | "Is_this_a_Priority_blob": [ 94 | "SUCCEEDED", 95 | "FAILED", 96 | "TIMEDOUT" 97 | ] 98 | } 99 | } 100 | }, 101 | "contentVersion": "1.0.0.0", 102 | "outputs": {}, 103 | "triggers": { 104 | "When_a_blob_is_added_or_updated": { 105 | "type": "ServiceProvider", 106 | "inputs": { 107 | "parameters": { 108 | "path": "customers" 109 | }, 110 | "serviceProviderConfiguration": { 111 | "connectionName": "azureBlob", 112 | "operationId": "whenABlobIsAddedOrModified", 113 | "serviceProviderId": "/serviceProviders/AzureBlob" 114 | } 115 | } 116 | } 117 | } 118 | }, 119 | "kind": "Stateful" 120 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "APP_KIND": "workflowapp", 6 | "FUNCTIONS_WORKER_RUNTIME": "node", 7 | "WORKFLOWS_SUBSCRIPTION_ID": "c1661296-a732-44b9-8458-d1a0dd19815e", 8 | "WORKFLOWS_LOCATION_NAME": "uksouth", 9 | "WORKFLOWS_RESOURCE_GROUP_NAME": "rg-uks-01", 10 | "AzureBlob-ConnectionString": "any-blob-connection-string", 11 | "AzureQueue-ConnectionString": "any-queue-connection-string", 12 | "Outlook-ConnectionKey": "any-outlook-connection-key", 13 | "Outlook-SubjectPrefix": "INFORMATION", 14 | "Outlook-ConnectionRuntimeUrl": "https://7606763fdc09952f.10.common.logic-uksouth.azure-apihub.net/apim/outlook/79a0bc680716416e90e17323b581695d/", 15 | "Salesforce-ConnectionKey": "any-salesforce-connection-key", 16 | "Salesforce-ConnectionRuntimeUrl": "https://7606763fdc09952f.10.common.logic-uksouth.azure-apihub.net/apim/salesforce/fba515601ef14f9193eee596a9dcfd1c/", 17 | "ServiceOne-Url": "https://external-service-one.testing.net/api/v1", 18 | "ServiceOne-Authentication-APIKey": "serviceone-auth-apikey", 19 | "ServiceOne-Authentication-WebHook-APIKey": "serviceone-auth-webhook-apikey", 20 | "ServiceTwo-Url": "https://external-service-two.testing.net/api/v1.1", 21 | "ServiceTwo-Verison2Url": "https://external-service-two.testing.net/api/v2.0", 22 | "ServiceTwo-Authentication-APIKey": "servicetwo-auth-apikey", 23 | "ServiceTwo-DefaultAddressType": "business", 24 | "Sql_ConnectionString": "any-sql-connection-string", 25 | "ServiceBus_ConnectionString": "any-servicebus-connection-string" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/loop-workflow/workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "definition": { 3 | "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", 4 | "actions": { 5 | "Until_Loop": { 6 | "type": "Until", 7 | "expression": "@equals(variables('loopCounter'), triggerBody()?['numberOfIterations'])", 8 | "limit": { 9 | "count": 60, 10 | "timeout": "PT1H" 11 | }, 12 | "actions": { 13 | "Call_Service_One": { 14 | "type": "Http", 15 | "inputs": { 16 | "uri": "@{parameters('ServiceOne-Url')}/doSomethingInsideUntilLoop", 17 | "method": "POST", 18 | "headers": { 19 | "Content-Type": "application/json" 20 | }, 21 | "body": { 22 | "iterationNumber": "@variables('loopCounter')" 23 | } 24 | }, 25 | "runAfter": { 26 | "Increment_variable": [ 27 | "Succeeded" 28 | ] 29 | }, 30 | "operationOptions": "DisableAsyncPattern" 31 | }, 32 | "Append_response_to_systemOneResponses": { 33 | "type": "AppendToArrayVariable", 34 | "inputs": { 35 | "name": "systemOneResponses", 36 | "value": "@body('Call_Service_One')" 37 | }, 38 | "runAfter": { 39 | "Call_Service_One": [ 40 | "Succeeded", 41 | "FAILED" 42 | ] 43 | } 44 | }, 45 | "Increment_variable": { 46 | "type": "IncrementVariable", 47 | "inputs": { 48 | "name": "loopCounter", 49 | "value": 1 50 | } 51 | } 52 | }, 53 | "runAfter": { 54 | "Initialize_systemTwoResponses": [ 55 | "Succeeded" 56 | ] 57 | } 58 | }, 59 | "Initialize_systemOneResponses": { 60 | "type": "InitializeVariable", 61 | "inputs": { 62 | "variables": [ 63 | { 64 | "name": "systemOneResponses", 65 | "type": "array" 66 | } 67 | ] 68 | }, 69 | "runAfter": { 70 | "Initialize_loopCounter": [ 71 | "Succeeded" 72 | ] 73 | } 74 | }, 75 | "Response": { 76 | "type": "Response", 77 | "kind": "http", 78 | "inputs": { 79 | "statusCode": 200, 80 | "headers": { 81 | "Content-Type": "application/json" 82 | }, 83 | "body": { 84 | "loopCounter": "@variables('loopCounter')", 85 | "serviceOneResponses": "@variables('systemOneResponses')", 86 | "serviceTwoResponses": "@variables('systemTwoResponses')" 87 | } 88 | }, 89 | "runAfter": { 90 | "For_Each_Loop": [ 91 | "Succeeded" 92 | ] 93 | } 94 | }, 95 | "For_Each_Loop": { 96 | "type": "Foreach", 97 | "foreach": "@variables('systemOneResponses')", 98 | "actions": { 99 | "Call_Service_Two": { 100 | "type": "Http", 101 | "inputs": { 102 | "uri": "@{parameters('ServiceTwo-Url')}/doSomethingInsideForEachLoop", 103 | "method": "POST", 104 | "headers": { 105 | "Content-Type": "application/json" 106 | }, 107 | "body": "@items('For_Each_Loop')" 108 | } 109 | }, 110 | "Append_response_to_systemTwoResponses": { 111 | "type": "AppendToArrayVariable", 112 | "inputs": { 113 | "name": "systemTwoResponses", 114 | "value": "@body('Call_Service_Two')" 115 | }, 116 | "runAfter": { 117 | "Call_Service_Two": [ 118 | "Succeeded", 119 | "FAILED" 120 | ] 121 | } 122 | } 123 | }, 124 | "runAfter": { 125 | "Until_Loop": [ 126 | "Succeeded" 127 | ] 128 | }, 129 | "runtimeConfiguration": { 130 | "concurrency": { 131 | "repetitions": 1 132 | } 133 | } 134 | }, 135 | "Initialize_systemTwoResponses": { 136 | "type": "InitializeVariable", 137 | "inputs": { 138 | "variables": [ 139 | { 140 | "name": "systemTwoResponses", 141 | "type": "array" 142 | } 143 | ] 144 | }, 145 | "runAfter": { 146 | "Initialize_systemOneResponses": [ 147 | "Succeeded" 148 | ] 149 | } 150 | }, 151 | "Initialize_loopCounter": { 152 | "type": "InitializeVariable", 153 | "inputs": { 154 | "variables": [ 155 | { 156 | "name": "loopCounter", 157 | "type": "integer", 158 | "value": 0 159 | } 160 | ] 161 | }, 162 | "runAfter": {} 163 | } 164 | }, 165 | "contentVersion": "1.0.0.0", 166 | "outputs": {}, 167 | "triggers": { 168 | "manual": { 169 | "type": "Request", 170 | "kind": "Http", 171 | "inputs": { 172 | "schema": { 173 | "properties": { 174 | "numberOfIterations": { 175 | "type": "integer" 176 | } 177 | }, 178 | "type": "object" 179 | }, 180 | "method": "POST" 181 | } 182 | } 183 | } 184 | }, 185 | "kind": "Stateful" 186 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServiceOne-Url": { 3 | "type": "String", 4 | "value": "@appsetting('ServiceOne-Url')" 5 | }, 6 | "ServiceOne-Authentication-APIKey": { 7 | "type": "String", 8 | "value": "@appsetting('ServiceOne-Authentication-APIKey')" 9 | }, 10 | "ServiceOne-Authentication-WebHook-APIKey": { 11 | "type": "String", 12 | "value": "@appsetting('ServiceOne-Authentication-WebHook-APIKey')" 13 | }, 14 | "ServiceTwo-Url": { 15 | "type": "String", 16 | "value": "@appsetting('ServiceTwo-Url')" 17 | }, 18 | "ServiceTwo-Authentication-APIKey": { 19 | "type": "String", 20 | "value": "@appsetting('ServiceTwo-Authentication-APIKey')" 21 | }, 22 | "salesforce-ConnectionRuntimeUrl": { 23 | "type": "String", 24 | "value": "@appsetting('Salesforce-ConnectionRuntimeUrl')" 25 | }, 26 | "outlook-ConnectionRuntimeUrl": { 27 | "type": "String", 28 | "value": "@appsetting('Outlook-ConnectionRuntimeUrl')" 29 | }, 30 | "outlook-Authentication": { 31 | "type": "Object", 32 | "value": { 33 | "type": "Raw", 34 | "scheme": "Key", 35 | "parameter": "@appsetting('Outlook-ConnectionKey')" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/stateless-workflow/workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "definition": { 3 | "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", 4 | "actions": { 5 | "Failed_Response": { 6 | "type": "Response", 7 | "kind": "http", 8 | "inputs": { 9 | "statusCode": 500, 10 | "body": "Blob '@{triggerOutputs()['relativePathParameters']['blobName']}' failed to upload to storage container '@{triggerOutputs()['relativePathParameters']['containerName']}'" 11 | }, 12 | "runAfter": { 13 | "Upload_Blob": [ 14 | "FAILED", 15 | "TIMEDOUT" 16 | ] 17 | } 18 | }, 19 | "Success_Response": { 20 | "type": "Response", 21 | "kind": "http", 22 | "inputs": { 23 | "statusCode": 200, 24 | "body": "Blob '@{triggerOutputs()['relativePathParameters']['blobName']}' has been uploaded to storage container '@{triggerOutputs()['relativePathParameters']['containerName']}'" 25 | }, 26 | "runAfter": { 27 | "Upload_Blob": [ 28 | "Succeeded" 29 | ] 30 | } 31 | }, 32 | "Upload_Blob": { 33 | "type": "ServiceProvider", 34 | "inputs": { 35 | "parameters": { 36 | "containerName": "@triggerOutputs()['relativePathParameters']['containerName']", 37 | "blobName": "@triggerOutputs()['relativePathParameters']['blobName']", 38 | "content": "@triggerBody()" 39 | }, 40 | "serviceProviderConfiguration": { 41 | "connectionName": "azureBlob", 42 | "operationId": "uploadBlob", 43 | "serviceProviderId": "/serviceProviders/AzureBlob" 44 | } 45 | }, 46 | "runAfter": {}, 47 | "trackedProperties": { 48 | "blobName": "@{triggerOutputs()['relativePathParameters']['blobName']}", 49 | "containerName": "@{triggerOutputs()['relativePathParameters']['containerName']}" 50 | } 51 | } 52 | }, 53 | "contentVersion": "1.0.0.0", 54 | "outputs": {}, 55 | "triggers": { 56 | "manual": { 57 | "type": "Request", 58 | "kind": "Http", 59 | "inputs": { 60 | "schema": {}, 61 | "method": "POST", 62 | "relativePath": "{containerName}/{blobName}" 63 | }, 64 | "correlation": { 65 | "clientTrackingId": "@concat(triggerOutputs()['relativePathParameters']['containerName'], '-', triggerOutputs()['relativePathParameters']['blobName'])" 66 | }, 67 | "operationOptions": "SuppressWorkflowHeadersOnResponse" 68 | } 69 | } 70 | }, 71 | "kind": "Stateless" 72 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/workflow-designtime/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensionBundle": { 4 | "id": "Microsoft.Azure.Functions.ExtensionBundle.Workflows", 5 | "version": "[1.*, 2.0.0)" 6 | }, 7 | "extensions": { 8 | "workflow": { 9 | "settings": { 10 | "Runtime.WorkflowOperationDiscoveryHostMode": "true" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/LogicAppUnit.Samples.LogicApps/workflow-designtime/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsSecretStorageType": "Files", 5 | "FUNCTIONS_WORKER_RUNTIME": "node" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/LogicAppUnit.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "LogicAppUnit" 5 | }, 6 | { 7 | "path": "LogicAppUnit.Samples.LogicApps" 8 | }, 9 | { 10 | "path": "LogicAppUnit.Samples.LogicApps.Tests" 11 | }, 12 | { 13 | "path": "LogicAppUnit.Samples.Functions" 14 | } 15 | ], 16 | "settings": {} 17 | } -------------------------------------------------------------------------------- /src/LogicAppUnit.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33103.184 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogicAppUnit", "LogicAppUnit\LogicAppUnit.csproj", "{44ABDA22-F220-4C17-A7D0-B1D641884EEC}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogicAppUnit.Samples.Functions", "LogicAppUnit.Samples.Functions\LogicAppUnit.Samples.Functions.csproj", "{8BA56858-023A-4D84-A13E-ADA88FD9558E}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogicAppUnit.Samples.LogicApps.Tests", "LogicAppUnit.Samples.LogicApps.Tests\LogicAppUnit.Samples.LogicApps.Tests.csproj", "{00E21DB1-8738-4B4E-A613-38D7F564577C}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{19845D13-8B58-49AF-8EA5-9107B6CF57D3}" 13 | ProjectSection(SolutionItems) = preProject 14 | nuget.config = nuget.config 15 | EndProjectSection 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {44ABDA22-F220-4C17-A7D0-B1D641884EEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {44ABDA22-F220-4C17-A7D0-B1D641884EEC}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {44ABDA22-F220-4C17-A7D0-B1D641884EEC}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {44ABDA22-F220-4C17-A7D0-B1D641884EEC}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {8BA56858-023A-4D84-A13E-ADA88FD9558E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {8BA56858-023A-4D84-A13E-ADA88FD9558E}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {8BA56858-023A-4D84-A13E-ADA88FD9558E}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {8BA56858-023A-4D84-A13E-ADA88FD9558E}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {00E21DB1-8738-4B4E-A613-38D7F564577C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {00E21DB1-8738-4B4E-A613-38D7F564577C}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {00E21DB1-8738-4B4E-A613-38D7F564577C}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {00E21DB1-8738-4B4E-A613-38D7F564577C}.Release|Any CPU.Build.0 = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(SolutionProperties) = preSolution 37 | HideSolutionNode = FALSE 38 | EndGlobalSection 39 | GlobalSection(ExtensibilityGlobals) = postSolution 40 | SolutionGuid = {088BAC01-EB70-440C-950B-2B887C066FCC} 41 | EndGlobalSection 42 | EndGlobal 43 | -------------------------------------------------------------------------------- /src/LogicAppUnit/ActionStatus.cs: -------------------------------------------------------------------------------- 1 | namespace LogicAppUnit 2 | { 3 | /// 4 | /// Possible statuses for a workflow action. 5 | /// 6 | public enum ActionStatus 7 | { 8 | /// 9 | /// The action stopped or didn't finish due to external problems, for example, a system outage. 10 | /// 11 | Aborted, 12 | 13 | /// 14 | /// The action was running but received a cancel request. 15 | /// 16 | Cancelled, 17 | 18 | /// 19 | /// The action failed. 20 | /// 21 | Failed, 22 | 23 | /// 24 | /// The action is currently running. 25 | /// 26 | Running, 27 | 28 | /// 29 | /// The action was skipped because its runAfter conditions weren't met, for example, a preceding action failed. 30 | /// 31 | Skipped, 32 | 33 | /// 34 | /// The action succeeded. 35 | /// 36 | Succeeded, 37 | 38 | /// 39 | /// The action stopped due to the timeout limit specified by that action's settings. 40 | /// 41 | TimedOut, 42 | 43 | /// 44 | /// The action is waiting for an inbound request from a caller. 45 | /// 46 | Waiting 47 | } 48 | } -------------------------------------------------------------------------------- /src/LogicAppUnit/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace LogicAppUnit 2 | { 3 | /// 4 | /// Commonly used hardcoded strings. 5 | /// 6 | /// 7 | /// This class and its members are internal because they are only intended for use within the test framework, not for use by the test classes. 8 | /// 9 | internal static class Constants 10 | { 11 | // Logic App files 12 | internal static readonly string WORKFLOW = "workflow.json"; 13 | internal static readonly string LOCAL_SETTINGS = "local.settings.json"; 14 | internal static readonly string PARAMETERS = "parameters.json"; 15 | internal static readonly string CONNECTIONS = "connections.json"; 16 | internal static readonly string HOST = "host.json"; 17 | 18 | // Logic App folders 19 | internal static readonly string ARTIFACTS_FOLDER = "Artifacts"; 20 | internal static readonly string LIB_FOLDER = "lib"; 21 | internal static readonly string CUSTOM_FOLDER = "custom"; 22 | internal static readonly string CUSTOM_LIB_FOLDER = System.IO.Path.Combine(LIB_FOLDER, CUSTOM_FOLDER); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Helper/ResourceHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | 5 | namespace LogicAppUnit.Helper 6 | { 7 | /// 8 | /// Helper class to read embedded resources from an assembly. 9 | /// 10 | public static class ResourceHelper 11 | { 12 | /// 13 | /// Get an assembly resource from the calling assembly as a . 14 | /// 15 | /// The fully-qualified name of the resource. 16 | /// The resource data. 17 | public static Stream GetAssemblyResourceAsStream(string resourceName) 18 | { 19 | return GetAssemblyResourceAsStream(resourceName, Assembly.GetCallingAssembly()); 20 | } 21 | 22 | /// 23 | /// Get an assembly resource as a . 24 | /// 25 | /// The fully-qualified name of the resource. 26 | /// The assembly containing the resource. 27 | /// The resource data. 28 | public static Stream GetAssemblyResourceAsStream(string resourceName, Assembly containingAssembly) 29 | { 30 | ArgumentNullException.ThrowIfNull(resourceName); 31 | ArgumentNullException.ThrowIfNull(containingAssembly); 32 | 33 | Stream resourceData = containingAssembly.GetManifestResourceStream(resourceName); 34 | if (resourceData == null) 35 | throw new TestException($"The resource '{resourceName}' could not be found in assembly '{containingAssembly.GetName().Name}'. Make sure that the resource name is a fully qualified name (including the .NET namespace), that the correct assembly is referenced and the resource is built as an Embedded Resource."); 36 | 37 | return resourceData; 38 | } 39 | 40 | /// 41 | /// Get an assembly resource from the calling assembly as a value. 42 | /// 43 | /// The fully-qualified name of the resource. 44 | /// The resource data. 45 | public static string GetAssemblyResourceAsString(string resourceName) 46 | { 47 | return ContentHelper.ConvertStreamToString(GetAssemblyResourceAsStream(resourceName, Assembly.GetCallingAssembly())); 48 | } 49 | 50 | /// 51 | /// Get an assembly resource as a value. 52 | /// 53 | /// The fully-qualified name of the resource. 54 | /// The assembly containing the resource. 55 | /// The resource data. 56 | public static string GetAssemblyResourceAsString(string resourceName, Assembly containingAssembly) 57 | { 58 | return ContentHelper.ConvertStreamToString(GetAssemblyResourceAsStream(resourceName, containingAssembly)); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Hosting/CallbackUrlDefinition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using Newtonsoft.Json; 6 | 7 | namespace LogicAppUnit.Hosting 8 | { 9 | /// 10 | /// Workflow callback URL definition. 11 | /// 12 | internal class CallbackUrlDefinition 13 | { 14 | /// 15 | /// Gets or sets the value, without any relative path component. 16 | /// 17 | [JsonProperty] 18 | public Uri Value { get; set; } 19 | 20 | /// 21 | /// Gets or sets the method. 22 | /// 23 | [JsonProperty] 24 | public string Method { get; set; } 25 | 26 | /// 27 | /// Gets or sets the base path. 28 | /// 29 | [JsonProperty] 30 | public Uri BasePath { get; set; } 31 | 32 | /// 33 | /// Gets or sets the relative path. 34 | /// 35 | [JsonProperty] 36 | public string RelativePath { get; set; } 37 | 38 | /// 39 | /// Gets or sets relative path parameters. 40 | /// 41 | [JsonProperty] 42 | public List RelativePathParameters { get; set; } 43 | 44 | /// 45 | /// Gets or sets queries. 46 | /// 47 | [JsonProperty] 48 | public Dictionary Queries { get; set; } 49 | 50 | /// 51 | /// Gets the queries as a query string, without a leading question mark. 52 | /// 53 | public string QueryString 54 | { 55 | get 56 | { 57 | return string.Join("&", Queries.Select(q => $"{WebUtility.UrlEncode(q.Key)}={WebUtility.UrlEncode(q.Value)}")); 58 | } 59 | } 60 | 61 | /// 62 | /// Gets the value, with a relative path and any query parameters. 63 | /// 64 | /// The relative path to be used in the trigger. The path must already be URL-encoded. 65 | /// The query parameters to be passed to the workflow. 66 | public Uri ValueWithRelativePathAndQueryParams(string relativePath, Dictionary queryParams) 67 | { 68 | // If there is no relative path and no query parameters, use the 'Value' 69 | if (string.IsNullOrEmpty(relativePath) && queryParams == null) 70 | return Value; 71 | 72 | // If there is a relative path, remove the preceding "/" 73 | // Relative path should not have a preceding "/"; 74 | // See Remark under https://learn.microsoft.com/en-us/dotnet/api/system.uri.-ctor?view=net-7.0#system-uri-ctor(system-uri-system-string) 75 | if (!string.IsNullOrEmpty(relativePath)) 76 | relativePath = relativePath.TrimStart('/'); 77 | 78 | // If there are query parameters, add them to the Queries property 79 | if (queryParams != null) 80 | foreach (var pair in queryParams) 81 | Queries.Add(pair.Key, pair.Value); 82 | 83 | // Make sure the base path has a trailing slash to preserve the relative path in 'Value' 84 | string basePathAsString = BasePath.ToString(); 85 | var baseUri = new Uri(basePathAsString + (basePathAsString.EndsWith("/") ? "" : "/")); 86 | 87 | return new UriBuilder(new Uri(baseUri, relativePath)) 88 | { 89 | Query = QueryString 90 | }.Uri; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Hosting/HttpRequestMessageFeature.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace LogicAppUnit.Hosting 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Net.Http; 9 | using System.Threading; 10 | using Microsoft.AspNetCore.Http; 11 | 12 | /// 13 | /// Http request message feature. 14 | /// 15 | internal class HttpRequestMessageFeature 16 | { 17 | /// 18 | /// The request message. 19 | /// 20 | private HttpRequestMessage httpRequestMessage; 21 | 22 | /// 23 | /// Gets or sets the http context. 24 | /// 25 | private HttpContext HttpContext { get; set; } 26 | 27 | /// 28 | /// Gets or sets the http request message. 29 | /// 30 | public HttpRequestMessage HttpRequestMessage 31 | { 32 | get => this.httpRequestMessage ?? Interlocked.CompareExchange(ref this.httpRequestMessage, HttpRequestMessageFeature.CreateHttpRequestMessage(this.HttpContext), null) ?? this.httpRequestMessage; 33 | 34 | set 35 | { 36 | var oldValue = this.httpRequestMessage; 37 | if (Interlocked.Exchange(ref this.httpRequestMessage, value) != oldValue) 38 | { 39 | oldValue?.Dispose(); 40 | } 41 | } 42 | } 43 | 44 | /// 45 | /// Initializes a new instance of the class. 46 | /// 47 | /// The http request message feature. 48 | public HttpRequestMessageFeature(HttpContext httpContext) 49 | { 50 | this.HttpContext = httpContext; 51 | } 52 | 53 | /// 54 | /// Creates the http request message. 55 | /// 56 | /// The http context. 57 | private static HttpRequestMessage CreateHttpRequestMessage(HttpContext httpContext) 58 | { 59 | HttpRequestMessage message = null; 60 | try 61 | { 62 | var httpRequest = httpContext.Request; 63 | var uriString = 64 | httpRequest.Scheme + "://" + 65 | httpRequest.Host + 66 | httpRequest.PathBase + 67 | httpRequest.Path + 68 | httpRequest.QueryString; 69 | 70 | message = new HttpRequestMessage(new HttpMethod(httpRequest.Method), uriString); 71 | 72 | // This allows us to pass the message through APIs defined in legacy code and then operate on the HttpContext inside. 73 | message.Options.Set(new HttpRequestOptionsKey(nameof(HttpContext)), httpContext); 74 | 75 | message.Content = new StreamContent(httpRequest.Body); 76 | 77 | foreach (var header in httpRequest.Headers) 78 | { 79 | // Every header should be able to fit into one of the two header collections. 80 | // Try message.Headers first since that accepts more of them. 81 | if (!message.Headers.TryAddWithoutValidation(header.Key, (IEnumerable)header.Value)) 82 | { 83 | var added = message.Content.Headers.TryAddWithoutValidation(header.Key, (IEnumerable)header.Value); 84 | } 85 | } 86 | 87 | return message; 88 | } 89 | catch (Exception) 90 | { 91 | message?.Dispose(); 92 | throw; 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Hosting/MockHttpHost.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace LogicAppUnit.Hosting 5 | { 6 | using System; 7 | using System.Linq; 8 | using System.Net.Http; 9 | using LogicAppUnit.Mocking; 10 | using Microsoft.AspNetCore; 11 | using Microsoft.AspNetCore.Builder; 12 | using Microsoft.AspNetCore.Hosting; 13 | using Microsoft.AspNetCore.Http; 14 | using Microsoft.AspNetCore.Http.Features; 15 | using Microsoft.AspNetCore.ResponseCompression; 16 | using Microsoft.Extensions.DependencyInjection; 17 | using Microsoft.Extensions.Hosting; 18 | using Microsoft.Extensions.Logging; 19 | using Microsoft.Extensions.Primitives; 20 | 21 | /// 22 | /// The mock HTTP host. 23 | /// 24 | internal class MockHttpHost : IDisposable 25 | { 26 | private readonly MockDefinition _mockDefinition; 27 | 28 | /// 29 | /// The web host. 30 | /// 31 | public IWebHost Host { get; set; } 32 | 33 | /// 34 | /// Initializes a new instance of the class. 35 | /// The definition of the requests and responses to be mocked. 36 | /// URL for the mock host to listen on. 37 | /// 38 | public MockHttpHost(MockDefinition mockDefinition, string url = null) 39 | { 40 | _mockDefinition = mockDefinition; 41 | 42 | this.Host = WebHost 43 | .CreateDefaultBuilder() 44 | .UseSetting(key: WebHostDefaults.SuppressStatusMessagesKey, value: "true") 45 | .ConfigureLogging(config => config.ClearProviders()) 46 | .ConfigureServices(services => 47 | { 48 | services.AddSingleton(this); 49 | }) 50 | .UseStartup() 51 | .UseUrls(url ?? TestEnvironment.FlowV2MockTestHostUri) 52 | .Build(); 53 | 54 | this.Host.Start(); 55 | } 56 | 57 | /// 58 | /// Disposes the resources. 59 | /// 60 | public void Dispose() 61 | { 62 | this.Host.StopAsync().Wait(); 63 | } 64 | 65 | private class Startup 66 | { 67 | /// 68 | /// Gets or sets the request pipeline manager. 69 | /// 70 | private MockHttpHost Host { get; set; } 71 | 72 | public Startup(MockHttpHost host) 73 | { 74 | this.Host = host; 75 | } 76 | 77 | /// 78 | /// Configure the services. 79 | /// 80 | /// The services. 81 | public void ConfigureServices(IServiceCollection services) 82 | { 83 | services 84 | .Configure(options => 85 | { 86 | options.AllowSynchronousIO = true; 87 | }) 88 | .AddResponseCompression(options => 89 | { 90 | options.EnableForHttps = true; 91 | options.Providers.Add(); 92 | }) 93 | .AddMvc(options => 94 | { 95 | options.EnableEndpointRouting = true; 96 | }); 97 | } 98 | 99 | /// 100 | /// Configures the application. 101 | /// 102 | /// The application. 103 | public void Configure(IApplicationBuilder app) 104 | { 105 | app.UseResponseCompression(); 106 | 107 | app.Run(async (context) => 108 | { 109 | var syncIOFeature = context.Features.Get(); 110 | if (syncIOFeature != null) 111 | { 112 | syncIOFeature.AllowSynchronousIO = true; 113 | } 114 | 115 | using (var request = GetHttpRequestMessage(context)) 116 | using (var responseMessage = await this.Host._mockDefinition.MatchRequestAndBuildResponseAsync(request)) 117 | { 118 | var response = context.Response; 119 | 120 | response.StatusCode = (int)responseMessage.StatusCode; 121 | 122 | var responseHeaders = responseMessage.Headers; 123 | 124 | // Ignore the Transfer-Encoding header if it is just "chunked". 125 | // We let the host decide about whether the response should be chunked or not. 126 | if (responseHeaders.TransferEncodingChunked == true && 127 | responseHeaders.TransferEncoding.Count == 1) 128 | { 129 | responseHeaders.TransferEncoding.Clear(); 130 | } 131 | 132 | foreach (var header in responseHeaders) 133 | { 134 | response.Headers.Append(header.Key, header.Value.ToArray()); 135 | } 136 | 137 | if (responseMessage.Content != null) 138 | { 139 | var contentHeaders = responseMessage.Content.Headers; 140 | 141 | // Copy the response content headers only after ensuring they are complete. 142 | // We ask for Content-Length first because HttpContent lazily computes this and only afterwards writes the value into the content headers. 143 | var unused = contentHeaders.ContentLength; 144 | 145 | foreach (var header in contentHeaders) 146 | { 147 | response.Headers.Append(header.Key, header.Value.ToArray()); 148 | } 149 | 150 | await responseMessage.Content.CopyToAsync(response.Body).ConfigureAwait(false); 151 | } 152 | } 153 | }); 154 | } 155 | } 156 | 157 | /// 158 | /// Gets the http request message. 159 | /// 160 | /// The HTTP context. 161 | public static HttpRequestMessage GetHttpRequestMessage(HttpContext httpContext) 162 | { 163 | var feature = httpContext.Features.Get(); 164 | if (feature == null) 165 | { 166 | feature = new HttpRequestMessageFeature(httpContext); 167 | httpContext.Features.Set(feature); 168 | } 169 | 170 | return feature.HttpRequestMessage; 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Hosting/TestEnvironment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace LogicAppUnit.Hosting 5 | { 6 | /// 7 | /// Defines the URLs for the workflow management API operations. 8 | /// 9 | internal class TestEnvironment 10 | { 11 | /// 12 | /// The Edge Preview API version (2019-10-01-edge-preview). 13 | /// 14 | public static readonly string EdgePreview20191001ApiVersion = "2019-10-01-edge-preview"; 15 | 16 | /// 17 | /// The Preview API version (2020-05-01-preview). 18 | /// 19 | public static readonly string EdgePreview20200501ApiVersion = "2020-05-01-preview"; 20 | 21 | /// 22 | /// The local machine name. 23 | /// 24 | public static readonly string MachineHostName = OperatingSystem.IsWindows() ? Environment.MachineName : "localhost"; 25 | 26 | /// 27 | /// Workflow runtime webhook extension URI base path. 28 | /// 29 | public static readonly string WorkflowExtensionBasePath = "/runtime/webhooks/workflow"; 30 | 31 | /// 32 | /// Workflow runtime webhook extension URI management base path. 33 | /// 34 | public static readonly string FlowExtensionManagementBasePath = $"{TestEnvironment.WorkflowExtensionBasePath}/api/management"; 35 | 36 | /// 37 | /// Workflow runtime webhook extension URI workflow management base path. 38 | /// 39 | public static readonly string FlowExtensionWorkflowManagementBasePath = $"{TestEnvironment.FlowExtensionManagementBasePath}/workflows"; 40 | 41 | /// 42 | /// The test host URI. 43 | /// 44 | public static readonly string FlowV2TestHostUri = new UriBuilder(Uri.UriSchemeHttp, TestEnvironment.MachineHostName, 7071).Uri.ToString().TrimEnd('/'); 45 | 46 | /// 47 | /// The mock test host URI. 48 | /// 49 | public static readonly string FlowV2MockTestHostUri = new UriBuilder(Uri.UriSchemeHttp, TestEnvironment.MachineHostName, 7075).Uri.ToString().TrimEnd('/'); 50 | 51 | /// 52 | /// Workflow runtime webhook extension URI workflow management base path. 53 | /// 54 | public static readonly string ManagementWorkflowBaseUrl = TestEnvironment.FlowV2TestHostUri + FlowExtensionWorkflowManagementBasePath; 55 | 56 | /// 57 | /// Gets the workflow trigger callback URI. 58 | /// 59 | /// The workflow name. 60 | /// The trigger name. 61 | public static string GetTriggerCallbackRequestUri(string workflowName, string triggerName) 62 | { 63 | return string.Format( 64 | CultureInfo.InvariantCulture, 65 | "{0}/{1}/triggers/{2}/listCallbackUrl?api-version={3}", 66 | TestEnvironment.ManagementWorkflowBaseUrl, 67 | workflowName, 68 | triggerName, 69 | TestEnvironment.EdgePreview20191001ApiVersion); 70 | } 71 | 72 | /// 73 | /// Gets the request URI for the 'List Workflow Runs' operation. 74 | /// 75 | /// The workflow name. 76 | /// The maximum number of records to return. 77 | public static string GetListWorkflowRunsRequestUri(string workflowName, int? top = null) 78 | { 79 | return top != null 80 | ? string.Format( 81 | CultureInfo.InvariantCulture, 82 | "{0}/{1}/runs?api-version={2}&$top={3}", 83 | TestEnvironment.ManagementWorkflowBaseUrl, 84 | workflowName, 85 | TestEnvironment.EdgePreview20191001ApiVersion, 86 | top.Value) 87 | : string.Format( 88 | CultureInfo.InvariantCulture, 89 | "{0}/{1}/runs?api-version={2}", 90 | TestEnvironment.ManagementWorkflowBaseUrl, 91 | workflowName, 92 | TestEnvironment.EdgePreview20191001ApiVersion); 93 | } 94 | 95 | /// 96 | /// Gets the request URI for the 'Get Workflow Run' operation. 97 | /// 98 | /// The workflow name. 99 | /// The run id. 100 | public static string GetGetWorkflowRunRequestUri(string workflowName, string runId) 101 | { 102 | return string.Format( 103 | CultureInfo.InvariantCulture, 104 | "{0}/{1}/runs/{2}?api-version={3}", 105 | TestEnvironment.ManagementWorkflowBaseUrl, 106 | workflowName, 107 | runId, 108 | TestEnvironment.EdgePreview20191001ApiVersion); 109 | } 110 | 111 | /// 112 | /// Gets the request URI for the 'List Workflow Run Actions' operation. 113 | /// 114 | /// The workflow name. 115 | /// The run id. 116 | public static string GetListWorkflowRunActionsRequestUri(string workflowName, string runId) 117 | { 118 | return string.Format( 119 | CultureInfo.InvariantCulture, 120 | "{0}/{1}/runs/{2}/actions?api-version={3}", 121 | TestEnvironment.ManagementWorkflowBaseUrl, 122 | workflowName, 123 | runId, 124 | TestEnvironment.EdgePreview20191001ApiVersion); 125 | } 126 | 127 | /// 128 | /// Gets the request URI for the 'List Workflow Run Action Repetitions' operation. 129 | /// 130 | /// The workflow name. 131 | /// The run id. 132 | /// The action name. 133 | public static string GetListWorkflowRunActionRepetitionsRequestUri(string workflowName, string runId, string actionName) 134 | { 135 | return string.Format( 136 | CultureInfo.InvariantCulture, 137 | "{0}/{1}/runs/{2}/actions/{3}/repetitions?api-version={4}", 138 | TestEnvironment.ManagementWorkflowBaseUrl, 139 | workflowName, 140 | runId, 141 | actionName, 142 | TestEnvironment.EdgePreview20191001ApiVersion); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/LogicAppUnit/InternalHelper/AzuriteHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net.NetworkInformation; 3 | using System.Net; 4 | using System.Collections.Generic; 5 | using System; 6 | 7 | namespace LogicAppUnit.InternalHelper 8 | { 9 | /// 10 | /// Helper class for the Azurite storage emulator. 11 | /// 12 | internal static class AzuriteHelper 13 | { 14 | /// 15 | /// Determine if Azurite is running. Without Azurite running we can't run any workflows. 16 | /// 17 | /// True if Azurite is running, otherwise false. 18 | /// 19 | /// Testing if Azurite is running is tricky because there are so many ways that Azurite can be installed and run. For example, you can run it within Visual Studio, or within 20 | /// Visual Studio Code, or as a stand-alone node application. The most robust way to determine if Azurite is running is to see if anything is listening on the Azurite ports. 21 | /// 22 | internal static bool IsRunning(TestConfigurationAzurite config) 23 | { 24 | ArgumentNullException.ThrowIfNull(config); 25 | 26 | // If Azurite is running, it will run on localhost (127.0.0.1) 27 | IPAddress expectedIp = new IPAddress(new byte[] { 127, 0, 0, 1 }); 28 | var expectedPorts = new[] 29 | { 30 | config.BlobServicePort, 31 | config.QueueServicePort, 32 | config.TableServicePort 33 | }; 34 | 35 | // Get the active TCP listeners and filter for the Azurite ports 36 | IPEndPoint[] activeTcpListeners = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners(); 37 | List relevantListeners = activeTcpListeners.Where(t => expectedPorts.Contains(t.Port) && t.Address.Equals(expectedIp)).ToList(); 38 | 39 | if (relevantListeners.Count == expectedPorts.Length) 40 | { 41 | Console.WriteLine($"Azurite is listening on ports {config.BlobServicePort} (Blob service), {config.QueueServicePort} (Queue service) and {config.TableServicePort} (Table service)."); 42 | return true; 43 | } 44 | else 45 | { 46 | return false; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/LogicAppUnit/InternalHelper/LoggingHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LogicAppUnit.InternalHelper 4 | { 5 | /// 6 | /// Helper class for writing to the test execution log. 7 | /// 8 | internal class LoggingHelper 9 | { 10 | /// 11 | /// Write a banner to the test execution log. 12 | /// 13 | /// The text to be shown in the banner. 14 | internal static void LogBanner(string bannerText) 15 | { 16 | const int bannerSize = 80; 17 | const int bannerPaddingOnEachSide = 2; 18 | 19 | if (string.IsNullOrEmpty(bannerText)) 20 | throw new ArgumentNullException(nameof(bannerText)); 21 | if (bannerText.Length > bannerSize - (bannerPaddingOnEachSide * 2)) 22 | throw new ArgumentException($"The size of the banner text cannot be more than {bannerSize - (bannerPaddingOnEachSide * 2)} characters."); 23 | 24 | int paddingStart = (bannerSize - (bannerPaddingOnEachSide * 2) - bannerText.Length) / 2; 25 | int paddingEnd = bannerSize - (bannerPaddingOnEachSide * 2) - bannerText.Length - paddingStart; 26 | 27 | Console.WriteLine(); 28 | Console.WriteLine(new string('-', bannerSize)); 29 | Console.WriteLine($"- {new string(' ', paddingStart)}{bannerText.ToUpperInvariant()}{new string(' ', paddingEnd)} -"); 30 | Console.WriteLine(new string('-', bannerSize)); 31 | Console.WriteLine(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/LogicAppUnit/LogicAppUnit.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | LogicAppUnit 6 | true 7 | 1.11.0 8 | Logic App Unit Testing Framework 9 | Unit testing framework for Standard Logic Apps. 10 | https://github.com/LogicAppUnit/TestingFramework 11 | git 12 | $(AssemblyName) 13 | https://github.com/LogicAppUnit/TestingFramework 14 | azure logic-apps unit-testing integration-testing 15 | MIT 16 | LogicAppUnit.png 17 | 18 | 19 | 20 | true 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Mocking/IMockRequestMatcher.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | using System.Net.Http; 4 | 5 | namespace LogicAppUnit.Mocking 6 | { 7 | /// 8 | /// Request Matcher that is used to build the request match conditions for mocking. 9 | /// 10 | public interface IMockRequestMatcher 11 | { 12 | /// 13 | /// Configure request matching using any HTTP method. 14 | /// 15 | /// The . 16 | IMockRequestMatcher UsingAnyMethod(); 17 | 18 | /// 19 | /// Configure request matching using HTTP GET. 20 | /// 21 | /// The . 22 | IMockRequestMatcher UsingGet(); 23 | 24 | /// 25 | /// Configure request matching using HTTP POST. 26 | /// 27 | /// The . 28 | IMockRequestMatcher UsingPost(); 29 | 30 | /// 31 | /// Configure request matching using HTTP PUT. 32 | /// 33 | /// The . 34 | IMockRequestMatcher UsingPut(); 35 | 36 | /// 37 | /// Configure request matching using HTTP PATCH. 38 | /// 39 | /// The . 40 | IMockRequestMatcher UsingPatch(); 41 | 42 | /// 43 | /// Configure request matching using HTTP DELETE. 44 | /// 45 | /// The . 46 | IMockRequestMatcher UsingDelete(); 47 | 48 | /// 49 | /// Configure request matching using one or more HTTP methods. 50 | /// 51 | /// The HTTP methods to match. 52 | /// The . 53 | IMockRequestMatcher UsingMethod(params HttpMethod[] methods); 54 | 55 | /// 56 | /// Configure request matching based on the names of one or more workflow actions that sent the request. 57 | /// 58 | /// The action names to match. 59 | /// The . 60 | IMockRequestMatcher FromAction(params string[] actionNames); 61 | 62 | /// 63 | /// Configure request matching using one or more URL absolute paths. 64 | /// 65 | /// The type of match to be used when matching the absolute paths. 66 | /// The absolute paths to match. 67 | /// The . 68 | IMockRequestMatcher WithPath(PathMatchType matchType, params string[] paths); 69 | 70 | /// 71 | /// Configure request matching based on the existance of a HTTP header. The value of the header is not considered in the match. 72 | /// 73 | /// The header name. 74 | /// The . 75 | IMockRequestMatcher WithHeader(string name); 76 | 77 | /// 78 | /// Configure request matching using a HTTP header and its value. 79 | /// 80 | /// The header name. 81 | /// The header value. 82 | /// The . 83 | IMockRequestMatcher WithHeader(string name, string value); 84 | 85 | /// 86 | /// Configure request matching using one or more content types, for example application/json or application/xml; charset=utf-8. 87 | /// 88 | /// The content types to match. This must be an exact match, including any media type and encoding. 89 | /// The . 90 | IMockRequestMatcher WithContentType(params string[] contentTypes); 91 | 92 | /// 93 | /// Configure request matching based on the existance of a query parameter. The value of the parameter is not considered in the match. 94 | /// 95 | /// The query parameter name. 96 | /// The . 97 | IMockRequestMatcher WithQueryParam(string name); 98 | 99 | /// 100 | /// Configure request matching based on a query parameter and its value. 101 | /// 102 | /// The query parameter name. 103 | /// The query parameter value. 104 | /// The . 105 | IMockRequestMatcher WithQueryParam(string name, string value); 106 | 107 | /// 108 | /// Configure request matching using the request match count number, where the number of times that the request has been matched during the test execution matches ). 109 | /// 110 | /// The match count numbers. 111 | /// The . 112 | /// This match is the logical inverse of . 113 | IMockRequestMatcher WithMatchCount(params int[] matchCounts); 114 | 115 | /// 116 | /// Configure request matching using the request match count number, where the number of times that the request has been matched during the test execution does not match ). 117 | /// 118 | /// The match count numbers. 119 | /// The . 120 | /// This match is the logical inverse of . 121 | IMockRequestMatcher WithNotMatchCount(params int[] matchCounts); 122 | 123 | /// 124 | /// Configure request matching based on the request content (as a ) and a delegate function that determines if the request is matched. 125 | /// 126 | /// Delegate function that returns true if the content is matched, otherwise false. 127 | /// The . 128 | IMockRequestMatcher WithContentAsString(Func requestContentMatch); 129 | 130 | /// 131 | /// Configure request matching based on the JSON request content (as a ) and a delegate function that determines if the request is matched. 132 | /// 133 | /// Delegate function that returns true if the content is matched, otherwise false. 134 | /// The . 135 | IMockRequestMatcher WithContentAsJson(Func requestContentMatch); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Mocking/IMockResponse.cs: -------------------------------------------------------------------------------- 1 | namespace LogicAppUnit.Mocking 2 | { 3 | /// 4 | /// A Mocked response consisting of a request matcher and a corresponding response builder. 5 | /// 6 | public interface IMockResponse 7 | { 8 | /// 9 | /// Configure the mocked response when the request is matched. 10 | /// 11 | /// The mocked response. 12 | void RespondWith(IMockResponseBuilder mockResponseBuilder); 13 | 14 | /// 15 | /// Configure the default mocked response using a status code of 200 (OK), no response content and no additional response headers. 16 | /// 17 | void RespondWithDefault(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Mocking/MockRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | 5 | namespace LogicAppUnit 6 | { 7 | /// 8 | /// Represents a request that was sent from a workflow and received by the mock test server. 9 | /// 10 | public class MockRequest 11 | { 12 | /// 13 | /// The timestamp for the request, in local time. 14 | /// 15 | /// 16 | /// Use local time because (i) this is more meaningful to a developer when they are not in UTC, and (ii) this value is only going to be used in the content of the test execution 17 | /// which lasts no more than a few minutes at most. 18 | /// 19 | public DateTime Timestamp { get; set; } = DateTime.Now; 20 | 21 | /// 22 | /// The name of the request, this is based on the name of the API that was called. 23 | /// 24 | /// 25 | /// The request URI will not be unique in the collection of mock requests when the same API endpoint is called multiple times. 26 | /// 27 | public Uri RequestUri { get; set; } 28 | 29 | /// 30 | /// The HTTP method for the request. 31 | /// 32 | public HttpMethod Method { get; set; } 33 | 34 | /// 35 | /// The set of headers for the request. 36 | /// 37 | public Dictionary> Headers { get; set; } 38 | 39 | /// 40 | /// The content of the request, as a value. 41 | /// 42 | public string Content { get; set; } 43 | 44 | /// 45 | /// The set of content headers for the request. 46 | /// 47 | public Dictionary> ContentHeaders { get; set; } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Mocking/MockRequestCache.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | 5 | namespace LogicAppUnit.Mocking 6 | { 7 | /// 8 | /// A cache for parts of the request for performance and efficiency. 9 | /// 10 | internal class MockRequestCache 11 | { 12 | private readonly HttpRequestMessage _request; 13 | private string _contentAsString; 14 | private JToken _contentAsJson; 15 | 16 | /// 17 | /// Get the cached context as . 18 | /// 19 | public async Task ContentAsStringAsync() 20 | { 21 | if (string.IsNullOrEmpty(_contentAsString)) 22 | { 23 | _contentAsString = await _request.Content.ReadAsStringAsync(); 24 | } 25 | 26 | return _contentAsString; 27 | } 28 | 29 | /// 30 | /// Gets the cached JSON context as a . 31 | /// 32 | public async Task ContentAsJsonAsync() 33 | { 34 | _contentAsJson ??= await _request.Content.ReadAsAsync(); 35 | 36 | return _contentAsJson; 37 | } 38 | 39 | /// 40 | /// Initializes a new instance of the class. 41 | /// 42 | /// The HTTP request to be matched. 43 | public MockRequestCache(HttpRequestMessage request) 44 | { 45 | _request = request; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Mocking/MockRequestLog.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace LogicAppUnit.Mocking 4 | { 5 | internal class MockRequestLog : MockRequest 6 | { 7 | /// 8 | /// A log for request matching, this can be used to understand how requests are being matched, or not being matched! 9 | /// 10 | public List Log { get; set; } 11 | 12 | /// 13 | /// Initializes a new instance of the class. 14 | /// 15 | public MockRequestLog() 16 | { 17 | Log = new List(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Mocking/MockRequestMatchResult.cs: -------------------------------------------------------------------------------- 1 | namespace LogicAppUnit.Mocking 2 | { 3 | internal class MockRequestMatchResult 4 | { 5 | /// 6 | /// Gets the match result, true if the request was matched, otherwise false. 7 | /// 8 | public bool IsMatch { init; get; } 9 | 10 | /// 11 | /// Gets the match log, indicating why a request was not matched. 12 | /// 13 | public string MatchLog { init; get; } 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// true if the request was matched, otherwise false. 19 | public MockRequestMatchResult(bool isMatch) : this(isMatch, string.Empty) 20 | { 21 | } 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// true if the request was matched, otherwise false. 27 | /// Match log, indicating why a request was not matched. 28 | public MockRequestMatchResult(bool isMatch, string matchLog) 29 | { 30 | IsMatch = isMatch; 31 | MatchLog = matchLog; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Mocking/MockRequestPath.cs: -------------------------------------------------------------------------------- 1 | namespace LogicAppUnit.Mocking 2 | { 3 | /// 4 | /// A path for mock request matching. 5 | /// 6 | internal class MockRequestPath 7 | { 8 | /// 9 | /// Gets the path to be matched. 10 | /// 11 | public string Path { init; get; } 12 | 13 | /// 14 | /// Gets the type of matching to be used. 15 | /// 16 | public PathMatchType MatchType { init; get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Mocking/MockResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | 6 | namespace LogicAppUnit.Mocking 7 | { 8 | /// 9 | /// A Mocked response consisting of a request matcher and a corresponding response builder. 10 | /// 11 | public class MockResponse : IMockResponse 12 | { 13 | private readonly string _mockName; 14 | private readonly MockRequestMatcher _mockRequestMatcher; 15 | private MockResponseBuilder _mockResponseBuilder; 16 | 17 | internal string MockName 18 | { 19 | get => _mockName; 20 | } 21 | 22 | /// 23 | /// Initializes a new instance of the class using a request matcher. 24 | /// 25 | /// The name of the mock, or null if it does not have a name. 26 | /// The request matcher. 27 | internal MockResponse(string name, IMockRequestMatcher mockRequestMatcher) 28 | { 29 | ArgumentNullException.ThrowIfNull(mockRequestMatcher); 30 | 31 | _mockName = name; 32 | _mockRequestMatcher = (MockRequestMatcher)mockRequestMatcher; 33 | } 34 | 35 | /// 36 | public void RespondWith(IMockResponseBuilder mockResponseBuilder) 37 | { 38 | ArgumentNullException.ThrowIfNull(mockResponseBuilder); 39 | 40 | _mockResponseBuilder = (MockResponseBuilder)mockResponseBuilder; 41 | } 42 | 43 | /// 44 | public void RespondWithDefault() 45 | { 46 | _mockResponseBuilder = (MockResponseBuilder)MockResponseBuilder.Create(); 47 | } 48 | 49 | /// 50 | /// Match a HTTP request with a request matcher and create a response if there is a match. 51 | /// 52 | /// The HTTP request to be matched. 53 | /// Cache for parts of the request for performance and efficiency. 54 | /// Request matching log. 55 | /// The response for the matching request, or null if there was no match. 56 | internal async Task MatchRequestAndCreateResponseAsync(HttpRequestMessage request, MockRequestCache requestCache, List requestMatchingLog) 57 | { 58 | ArgumentNullException.ThrowIfNull(request); 59 | ArgumentNullException.ThrowIfNull(requestCache); 60 | 61 | if (_mockRequestMatcher == null) 62 | throw new TestException("A request matcher has not been configured"); 63 | if (_mockResponseBuilder == null) 64 | throw new TestException("A response builder has not been configured - use RespondWith() to create a response, or RespondWithDefault() to create a default response using a status code of 200 (OK) and no content"); 65 | 66 | MockRequestMatchResult matchResult = await _mockRequestMatcher.MatchRequestAsync(request, requestCache); 67 | if (matchResult.IsMatch) 68 | { 69 | requestMatchingLog.Add(" Matched"); 70 | await _mockResponseBuilder.ExecuteDelayAsync(requestMatchingLog); 71 | return _mockResponseBuilder.BuildResponse(request); 72 | } 73 | else 74 | { 75 | requestMatchingLog.Add($" Not matched - {matchResult.MatchLog}"); 76 | return null; 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Mocking/PathMatchType.cs: -------------------------------------------------------------------------------- 1 | namespace LogicAppUnit.Mocking 2 | { 3 | /// 4 | /// Path match type. 5 | /// 6 | public enum PathMatchType 7 | { 8 | /// 9 | /// Value is an exact match for the path, e.g. '\api\v1\this-service\this-operation'. 10 | /// 11 | Exact, 12 | 13 | /// 14 | /// Value is contained within the path, e.g. 'v1\this-service'. 15 | /// 16 | Contains, 17 | 18 | /// 19 | /// Value matches the end of the path, e.g. 'this-operation'. 20 | /// 21 | EndsWith 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/LogicAppUnit/TestConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace LogicAppUnit 4 | { 5 | /// 6 | /// Configuration that can be set by a test project to configure how tests are set up and executed. 7 | /// 8 | public class TestConfiguration 9 | { 10 | /// 11 | /// Name of the local settings JSON file. This is optional and is only used when a non-standard filename is used. 12 | /// 13 | public string LocalSettingsFilename { get; set; } 14 | 15 | /// 16 | /// Azurite configuration. Azurite is a dependency for running Logic App workflows. 17 | /// 18 | public TestConfigurationAzurite Azurite { get; set; } 19 | 20 | /// 21 | /// Logging configuration for test execution. 22 | /// 23 | public TestConfigurationLogging Logging { get; set; } 24 | 25 | /// 26 | /// Test runner configuration for test execution. 27 | /// 28 | public TestConfigurationRunner Runner { get; set; } 29 | 30 | /// 31 | /// Workflow configuration, controls how the workflow definition is modified to enable mocking. 32 | /// 33 | public TestConfigurationWorkflow Workflow { get; set; } 34 | 35 | /// 36 | /// Initializes a new instance of the class. 37 | /// 38 | public TestConfiguration() 39 | { 40 | Azurite = new TestConfigurationAzurite(); 41 | Logging = new TestConfigurationLogging(); 42 | Runner = new TestConfigurationRunner(); 43 | Workflow = new TestConfigurationWorkflow(); 44 | } 45 | } 46 | 47 | /// 48 | /// Configuration for test execution logging. 49 | /// 50 | public class TestConfigurationLogging 51 | { 52 | /// 53 | /// true if the Functions runtime start-up logs are to be written to the test execution logs, otherwise false. 54 | /// 55 | /// 56 | /// Default value is false. 57 | /// 58 | public bool WriteFunctionRuntimeStartupLogs { get; set; } // default is false 59 | 60 | /// 61 | /// true if the mock request matching logs are to be written to the test execution logs, otherwise false. 62 | /// 63 | /// 64 | /// Default value is false. 65 | /// 66 | public bool WriteMockRequestMatchingLogs { get; set; } // default is false 67 | } 68 | 69 | /// 70 | /// Configuration for the Test Runner. 71 | /// 72 | public class TestConfigurationRunner 73 | { 74 | /// 75 | /// Maximum time (in seconds) to poll for the workflow result. The Test Runner will fail any test where the workflow execution is longer than this value. 76 | /// 77 | /// 78 | /// Default value is 300 seconds (5 minutes). 79 | /// 80 | public int MaxWorkflowExecutionDuration { get; set; } = 300; 81 | 82 | /// 83 | /// The HTTP status code for the default mock response, used when no mock Request Matchers are matched and when the mock response delegate function is not set, or returns null. 84 | /// 85 | /// 86 | /// Default value is HTTP 200 (OK). 87 | /// 88 | public int DefaultHttpResponseStatusCode { get; set; } = 200; 89 | } 90 | 91 | /// 92 | /// Configuration for Azurite. Azurite is a dependency for running Logic App workflows. 93 | /// 94 | public class TestConfigurationAzurite 95 | { 96 | /// 97 | /// true if the test framework checks that Azurite is running and listening on the required ports, otherwise false. 98 | /// 99 | /// 100 | /// Default value is true. 101 | /// 102 | public bool EnableAzuritePortCheck { get; set; } = true; 103 | 104 | /// 105 | /// Port number used by the Blob service. 106 | /// 107 | public int BlobServicePort { get; set; } = 10000; 108 | 109 | /// 110 | /// Port number used by the Queue service. 111 | /// 112 | public int QueueServicePort { get; set; } = 10001; 113 | 114 | /// 115 | /// Port number used by the Table service. 116 | /// 117 | public int TableServicePort { get; set; } = 10002; 118 | } 119 | 120 | /// 121 | /// Configuration for the workflow, controls how the workflow definition is modified to enable mocking. 122 | /// 123 | public class TestConfigurationWorkflow 124 | { 125 | /// 126 | /// List of external API URLs that are to be replaced with the test mock server. 127 | /// 128 | public List ExternalApiUrlsToMock { get; set; } = new List(); 129 | 130 | /// 131 | /// List of built-in connectors where the actions are to be replaced with HTTP actions referencing the test mock server. 132 | /// 133 | public List BuiltInConnectorsToMock { get; set; } = new List(); 134 | 135 | 136 | /// 137 | /// List of managed api connectors where the actions are to be replaced with HTTP actions referencing the test mock server. 138 | /// 139 | public List ManagedApisToMock { get; set; } = new List(); 140 | 141 | 142 | /// 143 | /// true if the test framework automatically configures the OperationOptions setting to WithStatelessRunHistory for a stateless workflow, otherwise false. 144 | /// 145 | /// 146 | /// Default value is true. 147 | /// 148 | public bool AutoConfigureWithStatelessRunHistory { get; set; } = true; 149 | 150 | /// 151 | /// true if the retry configuration in HTTP actions is to be removed, otherwise false. 152 | /// 153 | /// 154 | /// Default value is true. 155 | /// 156 | public bool RemoveHttpRetryConfiguration { get; set; } = true; 157 | 158 | /// 159 | /// true if the chunking configuration in HTTP actions is to be removed, otherwise false. 160 | /// 161 | /// 162 | /// Default value is true. 163 | /// 164 | public bool RemoveHttpChunkingConfiguration { get; set; } = true; 165 | 166 | /// 167 | /// true if the retry configuration in actions using managed API connections is to be removed, otherwise false. 168 | /// 169 | /// 170 | /// Default value is true. 171 | /// 172 | public bool RemoveManagedApiConnectionRetryConfiguration { get; set; } = true; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/LogicAppUnit/TestException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LogicAppUnit 4 | { 5 | /// 6 | /// Represents errors that occur within the testing framework. 7 | /// 8 | public class TestException : Exception 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | public TestException() : base() 14 | { 15 | } 16 | 17 | /// 18 | /// Initializes a new instance of the class with a specified error message. 19 | /// 20 | /// The error message. 21 | public TestException(string message) : base(message) 22 | { 23 | } 24 | 25 | /// 26 | /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of the exception. 27 | /// 28 | /// The error message. 29 | /// The inner exception. 30 | public TestException(string message, Exception inner) : base(message, inner) 31 | { 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/LogicAppUnit/WorkflowRunStatus.cs: -------------------------------------------------------------------------------- 1 | namespace LogicAppUnit 2 | { 3 | /// 4 | /// Possible statuses for a workflow run. 5 | /// 6 | public enum WorkflowRunStatus 7 | { 8 | /// 9 | /// The workflow has not been triggered. 10 | /// 11 | NotTriggered, 12 | 13 | /// 14 | /// The workflow run stopped or didn't finish due to external problems, for example, a system outage. 15 | /// 16 | Aborted, 17 | 18 | /// 19 | /// The workflow run was triggered and started but received a cancel request. 20 | /// 21 | Cancelled, 22 | 23 | /// 24 | /// At least one action in the workflow run failed. No subsequent actions in the workflow were set up to handle the failure. 25 | /// 26 | Failed, 27 | 28 | /// 29 | /// The run was triggered and is in progress, or the run is throttled due to action limits or the current pricing plan. 30 | /// 31 | Running, 32 | 33 | /// 34 | /// The workflow run succeeded. If any action failed, a subsequent action in the workflow handled that failure. 35 | /// 36 | Succeeded, 37 | 38 | /// 39 | /// The workflow run timed out because the current duration exceeded the workflow run duration limit. 40 | /// 41 | TimedOut, 42 | 43 | /// 44 | /// The workflow run hasn't started or is paused, for example, due to an earlier workflow instance that's still running. 45 | /// 46 | Waiting 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/LogicAppUnit/WorkflowTestInput.cs: -------------------------------------------------------------------------------- 1 | namespace LogicAppUnit 2 | { 3 | /// 4 | /// Defines a workflow that is to be tested. 5 | /// 6 | public class WorkflowTestInput 7 | { 8 | /// 9 | /// Gets the workflow name. 10 | /// 11 | public string WorkflowName { init; get; } 12 | 13 | /// 14 | /// Gets the workflow definition. 15 | /// 16 | public string WorkflowDefinition { init; get; } 17 | 18 | /// 19 | /// Gets the workflow filename. 20 | /// 21 | public string WorkflowFilename { init; get; } 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// The workflow name. 27 | /// The workflow definition. 28 | /// The workflow filename. 29 | public WorkflowTestInput(string workflowName, string workflowDefinition, string workflowFilename = null) 30 | { 31 | this.WorkflowName = workflowName; 32 | this.WorkflowDefinition = workflowDefinition; 33 | this.WorkflowFilename = workflowFilename ?? Constants.WORKFLOW; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/LogicAppUnit/WorkflowType.cs: -------------------------------------------------------------------------------- 1 | namespace LogicAppUnit 2 | { 3 | /// 4 | /// Possible types for a workflow. 5 | /// 6 | public enum WorkflowType 7 | { 8 | /// 9 | /// Stateless. 10 | /// 11 | Stateless, 12 | 13 | /// 14 | /// Stateful. 15 | /// 16 | Stateful 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Wrapper/ConnectionsWrapper.cs: -------------------------------------------------------------------------------- 1 | using LogicAppUnit.Hosting; 2 | using Newtonsoft.Json.Linq; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace LogicAppUnit.Wrapper 8 | { 9 | /// 10 | /// Wrapper class to manage the connections.json file. 11 | /// 12 | internal class ConnectionsWrapper 13 | { 14 | private readonly JObject _jObjectConnection; 15 | private readonly LocalSettingsWrapper _localSettings; 16 | private readonly ParametersWrapper _parameters; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The contents of the connections file, or null if the file does not exist. 22 | /// The local settings wrapper that is used to manage the local application settings. 23 | /// The parameters wrapper that is used to manage the parameters. 24 | public ConnectionsWrapper(string connectionsContent, LocalSettingsWrapper localSettings, ParametersWrapper parameters) 25 | { 26 | ArgumentNullException.ThrowIfNull(localSettings); 27 | 28 | if (!string.IsNullOrEmpty(connectionsContent)) 29 | { 30 | _jObjectConnection = JObject.Parse(connectionsContent); 31 | } 32 | 33 | _localSettings = localSettings; 34 | _parameters = parameters; 35 | } 36 | 37 | /// 38 | /// Returns the connections content. 39 | /// 40 | /// The connections content. 41 | public override string ToString() 42 | { 43 | if (_jObjectConnection == null) 44 | return null; 45 | 46 | return _jObjectConnection.ToString(); 47 | } 48 | 49 | /// 50 | /// Update the connections by replacing all URL references to managed API connectors with the URL reference for the mock test server. 51 | /// The list of managed API connections to mock, or null if all connectors are to be mocked. 52 | /// 53 | 54 | public void ReplaceManagedApiConnectionUrlsWithMockServer(List managedApisToMock) 55 | { 56 | if (_jObjectConnection == null) 57 | return; 58 | 59 | var managedApiConnections = _jObjectConnection.SelectToken("managedApiConnections").Children().ToList(); 60 | 61 | // If no managed apis are specified then all managed apis are mocked 62 | if (managedApisToMock != null && managedApisToMock.Count > 0) 63 | managedApiConnections = managedApiConnections.Where(con => managedApisToMock.Contains(con.Name)).ToList(); 64 | 65 | if (managedApiConnections.Count > 0) 66 | { 67 | Console.WriteLine("Updating connections file for managed API connectors:"); 68 | 69 | managedApiConnections.ForEach((connection) => 70 | { 71 | // Get the original connection URL that points to the Microsoft-hosted API connection 72 | string connectionUrl = connection.Value["connectionRuntimeUrl"].Value(); 73 | 74 | Uri validatedConnectionUri; 75 | if (!connectionUrl.Contains("@appsetting") && !connectionUrl.Contains("@parameters")) 76 | { 77 | // This connection runtime URL must be a valid URL since it is not using any substitution 78 | var isValidUrl = Uri.TryCreate(connectionUrl, UriKind.Absolute, out validatedConnectionUri); 79 | if (!isValidUrl) 80 | throw new TestException($"The connection runtime URL for managed connection '{connection.Name}' is not a valid URL. The URL is '{connectionUrl}'"); 81 | } 82 | else 83 | { 84 | // Check that the expanded connection runtime URL is a valid URL 85 | // Expand parameters first because parameters can reference app settings 86 | string expandedConnectionUrl = _localSettings.ExpandAppSettingsValues(_parameters.ExpandParametersAsString(connectionUrl)); 87 | var isValidUrl = Uri.TryCreate(expandedConnectionUrl, UriKind.Absolute, out validatedConnectionUri); 88 | if (!isValidUrl) 89 | 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}'"); 90 | } 91 | 92 | // Replace the host with the mock URL 93 | Uri newConnectionUrl = new Uri(new Uri(TestEnvironment.FlowV2MockTestHostUri), validatedConnectionUri.AbsolutePath); 94 | connection.Value["connectionRuntimeUrl"] = newConnectionUrl; 95 | 96 | Console.WriteLine($" {connection.Name}:"); 97 | Console.WriteLine($" {connectionUrl} ->"); 98 | Console.WriteLine($" {newConnectionUrl}"); 99 | }); 100 | } 101 | } 102 | 103 | /// 104 | /// List all connections that are using the ManagedServiceIdentity authentication type. 105 | /// 106 | public IEnumerable ListManagedApiConnectionsUsingManagedServiceIdentity() 107 | { 108 | if (_jObjectConnection == null) 109 | return null; 110 | 111 | List returnValue = new(); 112 | var managedApiConnections = _jObjectConnection.SelectToken("managedApiConnections").Children().ToList(); 113 | 114 | managedApiConnections.ForEach((connection) => 115 | { 116 | JObject connAuthTypeObject = null; 117 | JToken connAuth = ((JObject)connection.Value)["authentication"]; 118 | 119 | switch (connAuth.Type) 120 | { 121 | case JTokenType.String: 122 | // Connection object structure is parameterised 123 | connAuthTypeObject = _parameters.ExpandParameterAsObject(connAuth.Value()); 124 | break; 125 | 126 | case JTokenType.Object: 127 | // Connection object structure is not parameterised 128 | connAuthTypeObject = connAuth.Value(); 129 | break; 130 | } 131 | 132 | if (connAuthTypeObject["type"].Value() == "ManagedServiceIdentity") 133 | returnValue.Add(connection.Name); 134 | }); 135 | 136 | return returnValue; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Wrapper/CsxWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace LogicAppUnit.Wrapper 2 | { 3 | /// 4 | /// Wrapper class to manage the C# scripts that are used by a workflow. 5 | /// 6 | public class CsxWrapper 7 | { 8 | /// 9 | /// Gets the C# script content. 10 | /// 11 | public string Script { init; get; } 12 | 13 | /// 14 | /// Gets the C# script relative path. 15 | /// 16 | public string RelativePath { init; get; } 17 | 18 | /// 19 | /// Gets the C# script filename. 20 | /// 21 | public string Filename { init; get; } 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// The script content. 27 | /// The script relative path. 28 | /// The script filename 29 | public CsxWrapper(string script, string relativePath, string filename) 30 | { 31 | this.Script = script; 32 | this.RelativePath = relativePath; 33 | this.Filename = filename; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Wrapper/LocalSettingsWrapper.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using LogicAppUnit.Hosting; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace LogicAppUnit.Wrapper 9 | { 10 | /// 11 | /// Wrapper class to manage the local.settings.json file. 12 | /// 13 | internal class LocalSettingsWrapper 14 | { 15 | private readonly JObject _jObjectSettings; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The contents of the settings file. 21 | public LocalSettingsWrapper(string settingsContent) 22 | { 23 | if (string.IsNullOrEmpty(settingsContent)) 24 | throw new ArgumentNullException(nameof(settingsContent)); 25 | 26 | _jObjectSettings = JObject.Parse(settingsContent); 27 | } 28 | 29 | /// 30 | /// Returns the settings content. 31 | /// 32 | /// The settings content. 33 | public override string ToString() 34 | { 35 | return _jObjectSettings.ToString(); 36 | } 37 | 38 | /// 39 | /// Update the local settings by replacing all URL references to external systems with the URL reference for the mock test server. 40 | /// 41 | /// List of external API host names to be replaced. 42 | public void ReplaceExternalUrlsWithMockServer(List externalApiUrls) 43 | { 44 | // It is acceptable for a test project not to define any external API URLs if there are no external API dependencies in the workflows 45 | if (externalApiUrls.Count == 0) 46 | return; 47 | 48 | foreach (string apiUrl in externalApiUrls) 49 | { 50 | // Get all of the settings that start with the external API URL 51 | var settings = _jObjectSettings.SelectToken("Values").Children().Where(x => x.Value.ToString().StartsWith(apiUrl)).ToList(); 52 | if (settings.Count > 0) 53 | { 54 | Console.WriteLine($"Updating local settings file for '{apiUrl}':"); 55 | 56 | settings.ForEach((setting) => 57 | { 58 | // Get the original URL that points to the external endpoint 59 | Uri externalUrl = new Uri(setting.Value.ToString()); 60 | 61 | // Replace the host with the mock URL 62 | Uri newExternalUrl = new Uri(new Uri(TestEnvironment.FlowV2MockTestHostUri), externalUrl.AbsolutePath); 63 | setting.Value = newExternalUrl; 64 | 65 | Console.WriteLine($" {setting.Name}:"); 66 | Console.WriteLine($" {externalUrl} ->"); 67 | Console.WriteLine($" {newExternalUrl}"); 68 | }); 69 | } 70 | } 71 | } 72 | 73 | /// 74 | /// Update the local settings by replacing values as defined in the dictionary. 75 | /// 76 | /// The settings to be updated. 77 | public void ReplaceSettingOverrides(Dictionary settingsToUpdate) 78 | { 79 | ArgumentNullException.ThrowIfNull(settingsToUpdate); 80 | 81 | Console.WriteLine($"Updating local settings file with test overrides:"); 82 | 83 | foreach (KeyValuePair setting in settingsToUpdate) 84 | { 85 | var settingToUpdate = _jObjectSettings.SelectToken("Values").Children().Where(x => x.Name == setting.Key).FirstOrDefault(); 86 | Console.WriteLine($" {setting.Key}"); 87 | 88 | if (settingToUpdate != null) 89 | { 90 | Console.WriteLine($" Updated value to: {setting.Value}"); 91 | settingToUpdate.Value = setting.Value; 92 | } 93 | else 94 | { 95 | Console.WriteLine($" WARNING: Setting does not exist"); 96 | } 97 | } 98 | } 99 | 100 | /// 101 | /// Get the value for a setting. 102 | /// 103 | /// The name of the setting. 104 | /// The value of the setting, or null if the setting does not exist. 105 | public string GetSettingValue(string settingName) 106 | { 107 | ArgumentNullException.ThrowIfNull(settingName); 108 | 109 | var setting = _jObjectSettings.SelectToken("Values").Children().Where(x => x.Name == settingName).FirstOrDefault(); 110 | 111 | return setting?.Value.ToString(); 112 | } 113 | 114 | /// 115 | /// Get the value of the OperationOptions setting for a workflow. 116 | /// 117 | /// The name of the workflow. 118 | /// The value of the setting, or null if the setting does not exist. 119 | public string GetWorkflowOperationOptionsValue(string workflowName) 120 | { 121 | ArgumentNullException.ThrowIfNull(workflowName); 122 | 123 | return GetSettingValue($"Workflows.{workflowName}.OperationOptions"); 124 | } 125 | 126 | /// 127 | /// Set the value of the OperationOptions setting for a workflow. 128 | /// 129 | /// The name of the workflow. 130 | /// The value to be set. 131 | /// The setting that has been created. 132 | public string SetWorkflowOperationOptionsValue(string workflowName, string value) 133 | { 134 | ArgumentNullException.ThrowIfNull(workflowName); 135 | ArgumentNullException.ThrowIfNull(value); 136 | 137 | string settingName = $"Workflows.{workflowName}.OperationOptions"; 138 | _jObjectSettings["Values"][settingName] = value; 139 | 140 | return $"{settingName} = {value}"; 141 | } 142 | 143 | /// 144 | /// Expand the app settings values in . 145 | /// 146 | /// The string value containing the app settings to be expanded. 147 | /// Expanded string. 148 | public string ExpandAppSettingsValues(string value) 149 | { 150 | const string appSettingsPattern = @"@appsetting\('[\w.:-]*'\)"; 151 | string expandedValue = value; 152 | 153 | MatchCollection matches = Regex.Matches(value, appSettingsPattern, RegexOptions.IgnoreCase); 154 | foreach (Match match in matches) 155 | { 156 | string appSettingName = match.Value[13..^2]; 157 | expandedValue = expandedValue.Replace(match.Value, GetSettingValue(appSettingName)); 158 | } 159 | 160 | return expandedValue; 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/LogicAppUnit/Wrapper/ParametersWrapper.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace LogicAppUnit.Wrapper 7 | { 8 | /// 9 | /// Wrapper class to manage the parameters.json file. 10 | /// 11 | internal class ParametersWrapper 12 | { 13 | private readonly JObject _jObjectParameters; 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// The contents of the parameters file, or null if the file does not exist. 19 | public ParametersWrapper(string parametersContent) 20 | { 21 | if (!string.IsNullOrEmpty(parametersContent)) 22 | { 23 | _jObjectParameters = JObject.Parse(parametersContent); 24 | } 25 | } 26 | 27 | /// 28 | /// Returns the parameters content. 29 | /// 30 | /// The parameters content. 31 | public override string ToString() 32 | { 33 | if (_jObjectParameters == null) 34 | return null; 35 | 36 | return _jObjectParameters.ToString(); 37 | } 38 | 39 | /// 40 | /// Get the value for a parameter. 41 | /// 42 | /// The name of the parameter. 43 | /// The type of the parameter. 44 | /// The value of the parameter, or null if the parameter does not exist. 45 | public T GetParameterValue(string parameterName) 46 | { 47 | ArgumentNullException.ThrowIfNull(parameterName); 48 | 49 | var param = _jObjectParameters.Children().Where(x => x.Name == parameterName).FirstOrDefault(); 50 | if (param == null) 51 | return default; 52 | 53 | return ((JObject)param.Value)["value"].Value(); 54 | } 55 | 56 | /// 57 | /// Expand the parameters in as a string value. 58 | /// 59 | /// The string value containing the parameters to be expanded. 60 | /// Expanded parameter value. 61 | public string ExpandParametersAsString(string value) 62 | { 63 | // If there is no parameters file then the value is not replaced 64 | if (_jObjectParameters == null) 65 | return value; 66 | 67 | const string parametersPattern = @"@parameters\('[\w.:-]*'\)"; 68 | string expandedValue = value; 69 | 70 | MatchCollection matches = Regex.Matches(value, parametersPattern, RegexOptions.IgnoreCase); 71 | foreach (Match match in matches) 72 | { 73 | string parameterName = match.Value[13..^2]; 74 | expandedValue = expandedValue.Replace(match.Value, GetParameterValue(parameterName)); 75 | } 76 | 77 | return expandedValue; 78 | } 79 | 80 | /// 81 | /// Expand the parameter in as an object. 82 | /// 83 | /// The string value containing the parameter to be expanded. 84 | /// Expanded parameter value. 85 | public JObject ExpandParameterAsObject(string value) 86 | { 87 | // If there is no parameters file then the value is not replaced 88 | if (_jObjectParameters == null) 89 | return null; 90 | 91 | const string parametersPattern = @"^@parameters\('[\w.:-]*'\)$"; 92 | 93 | MatchCollection matches = Regex.Matches(value, parametersPattern, RegexOptions.IgnoreCase); 94 | return GetParameterValue(matches[0].Value[13..^2]); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | --------------------------------------------------------------------------------