());
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 |
--------------------------------------------------------------------------------