Skip to content

Commit 5131ac0

Browse files
authored
Support for C# Inline Action (#35)
1 parent fac21e3 commit 5131ac0

File tree

10 files changed

+223
-12
lines changed

10 files changed

+223
-12
lines changed

src/LogicAppUnit.Samples.LogicApps.Tests/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public static class Constants
1414
public static readonly string FLUENT_REQUEST_MATCHING_WORKFLOW = "fluent-workflow";
1515
public static readonly string HTTP_WORKFLOW = "http-workflow";
1616
public static readonly string HTTP_ASYNC_WORKFLOW = "http-async-workflow";
17+
public static readonly string INLINE_SCRIPT_WORKFLOW = "inline-script-workflow";
1718
public static readonly string INVOKE_WORKFLOW = "invoke-workflow";
1819
public static readonly string LOOP_WORKFLOW = "loop-workflow";
1920
public static readonly string MANAGED_API_CONNECTOR_WORKFLOW = "managed-api-connector-workflow";
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using LogicAppUnit.Helper;
2+
using LogicAppUnit.Mocking;
3+
using Microsoft.VisualStudio.TestTools.UnitTesting;
4+
using Newtonsoft.Json.Linq;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Net;
9+
using System.Net.Http;
10+
11+
namespace LogicAppUnit.Samples.LogicApps.Tests.InlineScriptWorkflow
12+
{
13+
/// <summary>
14+
/// Test cases for the <i>http-workflow</i> workflow which uses a synchronous response for the HTTP trigger.
15+
/// </summary>
16+
[TestClass]
17+
public class InlineScriptWorkflowTest : WorkflowTestBase
18+
{
19+
[TestInitialize]
20+
public void TestInitialize()
21+
{
22+
Initialize(Constants.LOGIC_APP_TEST_EXAMPLE_BASE_PATH, Constants.INLINE_SCRIPT_WORKFLOW);
23+
}
24+
25+
[ClassCleanup]
26+
public static void CleanResources()
27+
{
28+
Close();
29+
}
30+
31+
/// <summary>
32+
/// Tests that the correct response is returned when the HTTP call to the Service Two API to update the customer details is successful.
33+
/// </summary>
34+
[TestMethod]
35+
public void InlineScriptWorkflowTest_When_Successful()
36+
{
37+
using (ITestRunner testRunner = CreateTestRunner())
38+
{
39+
// Run the workflow
40+
var workflowResponse = testRunner.TriggerWorkflow(
41+
GetRequest(),
42+
HttpMethod.Post);
43+
44+
// Check workflow run status
45+
Assert.AreEqual(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus);
46+
47+
// Check action result
48+
Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Execute_CSharp_Script_Code"));
49+
Assert.AreEqual(
50+
ContentHelper.FormatJson(ResourceHelper.GetAssemblyResourceAsString($"{GetType().Namespace}.MockData.Execute_CSharp_Script_Code_Output.json")),
51+
ContentHelper.FormatJson(testRunner.GetWorkflowActionOutput("Execute_CSharp_Script_Code").ToString()));
52+
}
53+
}
54+
55+
private static StringContent GetRequest()
56+
{
57+
return ContentHelper.CreateJsonStringContent(new {
58+
name = "Jane"
59+
});
60+
}
61+
}
62+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"body": {
3+
"message": "Hello Jane from CSharp action"
4+
}
5+
}

src/LogicAppUnit.Samples.LogicApps.Tests/LogicAppUnit.Samples.LogicApps.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
<EmbeddedResource Include="FluentWorkflow\MockData\Response.json" />
5151
<EmbeddedResource Include="FluentWorkflow\MockData\Response.txt" />
5252
<EmbeddedResource Include="HttpWorkflow\MockData\SystemTwo_Request.json" />
53+
<EmbeddedResource Include="InlineScriptWorkflow\MockData\Execute_CSharp_Script_Code_Output.json" />
5354
<EmbeddedResource Include="InvokeWorkflow\MockData\AddToPriorityQueueRequest.json" />
5455
<EmbeddedResource Include="InvokeWorkflow\MockData\InvokeWorkflowNotPriorityRequest.json" />
5556
<EmbeddedResource Include="InvokeWorkflow\MockData\InvokeWorkflowPriorityRequest.json" />
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
/// <summary>
11+
/// Executes the inline csharp code.
12+
/// </summary>
13+
/// <param name="context">The workflow context.</param>
14+
/// <remarks> This is the entry-point to your code. The function signature should remain unchanged.</remarks>
15+
public static async Task<Results> 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+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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/CsxTestInput.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
namespace LogicAppUnit
2+
{
3+
/// <summary>
4+
/// Defines a C# script that is to be tested.
5+
/// </summary>
6+
public class CsxTestInput
7+
{
8+
/// <summary>
9+
/// Gets the C# script content
10+
/// </summary>
11+
public string Script { init; get; }
12+
13+
/// <summary>
14+
/// Gets the C# script relative path.
15+
/// </summary>
16+
public string RelativePath { init; get; }
17+
18+
/// <summary>
19+
/// Gets the C# script filename.
20+
/// </summary>
21+
public string Filename { init; get; }
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="CsxTestInput"/> class.
25+
/// </summary>
26+
/// <param name="script">The script content.</param>
27+
/// <param name="relativePath">The script relative path.</param>
28+
/// <param name="filename">The script filename</param>
29+
public CsxTestInput(string script, string relativePath, string filename)
30+
{
31+
this.Script = script;
32+
this.RelativePath = relativePath;
33+
this.Filename = filename;
34+
}
35+
}
36+
}

src/LogicAppUnit/Hosting/WorkflowTestHost.cs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using LogicAppUnit;
2-
using LogicAppUnit.InternalHelper;
1+
using LogicAppUnit.InternalHelper;
32
using System;
43
using System.Collections.Generic;
54
using System.Diagnostics;
@@ -44,15 +43,15 @@ internal class WorkflowTestHost : IDisposable
4443
public WorkflowTestHost(
4544
WorkflowTestInput[] inputs = null,
4645
string localSettings = null, string parameters = null, string connectionDetails = null, string host = null,
47-
DirectoryInfo artifactsDirectory = null, DirectoryInfo customLibraryDirectory = null,
46+
CsxTestInput[] csxTestInputs = null, DirectoryInfo artifactsDirectory = null, DirectoryInfo customLibraryDirectory = null,
4847
bool writeFunctionRuntimeStartupLogsToConsole = false)
4948
{
5049
this.OutputData = new List<string>();
5150
this.ErrorData = new List<string>();
5251
this.WriteFunctionRuntimeStartupLogsToConsole = writeFunctionRuntimeStartupLogsToConsole;
5352

5453
this.WorkingDirectory = CreateWorkingFolder();
55-
CreateWorkingFilesRequiredForTest(inputs, localSettings, parameters, connectionDetails, host, artifactsDirectory, customLibraryDirectory);
54+
CreateWorkingFilesRequiredForTest(inputs, localSettings, parameters, connectionDetails, host, csxTestInputs, artifactsDirectory, customLibraryDirectory);
5655
StartFunctionRuntime();
5756
}
5857

@@ -61,7 +60,7 @@ public WorkflowTestHost(
6160
/// </summary>
6261
protected void CreateWorkingFilesRequiredForTest(WorkflowTestInput[] inputs,
6362
string localSettings, string parameters, string connectionDetails, string host,
64-
DirectoryInfo artifactsDirectory, DirectoryInfo customLibraryDirectory)
63+
CsxTestInput[] csxTestInputs, DirectoryInfo artifactsDirectory, DirectoryInfo customLibraryDirectory)
6564
{
6665
if (inputs != null && inputs.Length > 0)
6766
{
@@ -77,6 +76,7 @@ protected void CreateWorkingFilesRequiredForTest(WorkflowTestInput[] inputs,
7776
CreateWorkingFile(parameters, Constants.PARAMETERS);
7877
CreateWorkingFile(connectionDetails, Constants.CONNECTIONS);
7978

79+
WriteCsxFilesToWorkingFolder(csxTestInputs);
8080
CopySourceFolderToWorkingFolder(artifactsDirectory, Constants.ARTIFACTS_FOLDER);
8181
CopySourceFolderToWorkingFolder(customLibraryDirectory, Constants.CUSTOM_LIB_FOLDER);
8282
}
@@ -200,7 +200,7 @@ private static string GetEnvPathForFunctionTools()
200200
// The path to the 'func' executable can be in any of the environment variable scopes, depending on how the Functions Core Tools were installed.
201201
// If a DevOps build pipeline has updated the PATH environment variable for the 'Machine' or 'User' scopes, the 'Process' scope is not automatically updated to reflect the change.
202202
// So merge all three scopes to be sure!
203-
environmentPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process) + Path.PathSeparator +
203+
environmentPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process) + Path.PathSeparator +
204204
Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) + Path.PathSeparator +
205205
Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine);
206206
exeName = $"{FunctionsExecutableName}.exe";
@@ -304,6 +304,20 @@ private static void DeepCopyDirectory(DirectoryInfo source, DirectoryInfo destin
304304
}
305305
}
306306

307+
/// <summary>
308+
/// Writes the C# script files to the working folder
309+
/// </summary>
310+
/// <param name="csxTestInputs"></param>
311+
private void WriteCsxFilesToWorkingFolder(CsxTestInput[] csxTestInputs)
312+
{
313+
foreach (var csxTestInput in csxTestInputs)
314+
{
315+
var directory = Path.Combine(this.WorkingDirectory, csxTestInput.RelativePath);
316+
Directory.CreateDirectory(directory);
317+
CreateWorkingFile(csxTestInput.Script, Path.Combine(directory, csxTestInput.Filename), true);
318+
}
319+
}
320+
307321
/// <summary>
308322
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
309323
/// </summary>

src/LogicAppUnit/TestRunner.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ public string WorkflowTerminationMessage
149149
/// <param name="host">The contents of the host file.</param>
150150
/// <param name="parameters">The contents of the parameters file, or <c>null</c> if the file does not exist.</param>
151151
/// <param name="connections">The connections file, or <c>null</c> if the file does not exist.</param>
152+
/// <param name="csxTestInputs">A collection of C# script files or <c>null</c> if there are none.</param>
152153
/// <param name="artifactsDirectory">The (optional) artifacts directory containing maps and schemas that are used by the workflow being tested.</param>
153154
/// <param name="customLibraryDirectory">The (optional) custom library (lib/custom) directory containing custom components that are used by the workflow being tested.</param>
154155
internal TestRunner(
@@ -157,8 +158,13 @@ internal TestRunner(
157158
HttpClient client,
158159
List<MockResponse> mockResponsesFromBase,
159160
WorkflowDefinitionWrapper workflowDefinition,
160-
LocalSettingsWrapper localSettings, string host, string parameters = null, ConnectionsWrapper connections = null,
161-
DirectoryInfo artifactsDirectory = null, DirectoryInfo customLibraryDirectory = null)
161+
LocalSettingsWrapper localSettings,
162+
string host,
163+
string parameters = null,
164+
ConnectionsWrapper connections = null,
165+
CsxTestInput[] csxTestInputs = null,
166+
DirectoryInfo artifactsDirectory = null,
167+
DirectoryInfo customLibraryDirectory = null)
162168
{
163169
if (loggingConfig == null)
164170
throw new ArgumentNullException(nameof(loggingConfig));
@@ -185,7 +191,7 @@ internal TestRunner(
185191

186192
var workflowTestInput = new WorkflowTestInput[] { new WorkflowTestInput(workflowDefinition.WorkflowName, workflowDefinition.ToString()) };
187193
_workflowTestHost = new WorkflowTestHost(workflowTestInput, localSettings.ToString(), parameters, connections.ToString(), host,
188-
artifactsDirectory, customLibraryDirectory, loggingConfig.WriteFunctionRuntimeStartupLogs);
194+
csxTestInputs, artifactsDirectory, customLibraryDirectory, loggingConfig.WriteFunctionRuntimeStartupLogs);
189195
_apiHelper = new WorkflowApiHelper(client, workflowDefinition.WorkflowName);
190196

191197
// Create the mock definition and mock HTTP host

src/LogicAppUnit/WorkflowTestBase.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public abstract class WorkflowTestBase
3030

3131
private string _parameters;
3232
private string _host;
33-
33+
private CsxTestInput[] _csxTestInputs;
3434
private bool _workflowIsInitialised = false;
3535

3636
#region Lifetime management
@@ -158,6 +158,11 @@ protected void Initialize(string logicAppBasePath, string workflowName, string l
158158
_parameters = ReadFromPath(Path.Combine(logicAppBasePath, Constants.PARAMETERS), optional: true);
159159
_host = ReadFromPath(Path.Combine(logicAppBasePath, Constants.HOST));
160160

161+
_csxTestInputs = new DirectoryInfo(logicAppBasePath)
162+
.GetFiles("*.csx", SearchOption.AllDirectories)
163+
.Select(x => new CsxTestInput(File.ReadAllText(x.FullName), Path.GetRelativePath(logicAppBasePath, x.DirectoryName), x.Name))
164+
.ToArray();
165+
161166
// If this is a stateless workflow and the 'OperationOptions' is not 'WithStatelessRunHistory'...
162167
if (_workflowDefinition.WorkflowType == WorkflowType.Stateless && _localSettings.GetWorkflowOperationOptionsValue(_workflowDefinition.WorkflowName) != workflowOperationOptionsRunHistory)
163168
{
@@ -218,10 +223,18 @@ protected ITestRunner CreateTestRunner(Dictionary<string, string> localSettingsO
218223
}
219224

220225
return new TestRunner(
221-
_testConfig.Logging, _testConfig.Runner,
226+
_testConfig.Logging,
227+
_testConfig.Runner,
222228
_client,
223229
_mockResponses,
224-
_workflowDefinition, _localSettings, _host, _parameters, _connections, _artifactDirectory, _customLibraryDirectory);
230+
_workflowDefinition,
231+
_localSettings,
232+
_host,
233+
_parameters,
234+
_connections,
235+
_csxTestInputs,
236+
_artifactDirectory,
237+
_customLibraryDirectory);
225238
}
226239

227240
#endregion Create test runner

0 commit comments

Comments
 (0)