From ccc971a11ba24b417c9a8f283d97466e1ba0a83d Mon Sep 17 00:00:00 2001 From: Daniel Jonathan Date: Thu, 4 Dec 2025 17:45:53 +0100 Subject: [PATCH 1/2] feat: Add support for custom configuration files in test projects Add optional parameters to Initialize() method allowing tests to use custom-named configuration files (local.settings.json, parameters.json, connections.json) located in the test project instead of the Logic App project. Key Features: - Optional parameters: localSettingsFilename, parametersFilename, connectionsFilename - File resolution checks test project first, then falls back to Logic App project - Enables unified test suites across environments (DEV/QA/PROD) using the same test code with different configuration files - Maintains full backward compatibility with existing tests Changes: - Added Initialize() method overload with optional configuration parameters - Added ResolveConfigurationFilePath() helper method for file resolution - Updated ProcessLocalSettingsFile(), ProcessParametersFile(), and ProcessConnectionsFile() to accept custom filenames - Created example test classes demonstrating the feature - Updated project version to 1.13.0 Examples: - parameters-dev.json, parameters-qa.json, parameters-prod.json - local.settings-custom.json, connections-custom.json --- ChangeLog.md | 12 + .../HttpWorkflowCustomConfigTest.cs | 286 ++++++++++++++++++ ...ogicAppUnit.Samples.LogicApps.Tests.csproj | 9 + ...gedApiConnectorWorkflowCustomConfigTest.cs | 188 ++++++++++++ .../connections-custom.json | 66 ++++ .../local.settings-custom.json | 27 ++ .../parameters-custom.json | 38 +++ src/LogicAppUnit/LogicAppUnit.csproj | 2 +- src/LogicAppUnit/WorkflowTestBase.cs | 80 ++++- 9 files changed, 696 insertions(+), 12 deletions(-) create mode 100644 src/LogicAppUnit.Samples.LogicApps.Tests/HttpWorkflow/HttpWorkflowCustomConfigTest.cs create mode 100644 src/LogicAppUnit.Samples.LogicApps.Tests/ManagedApiConnectorWorkflow/ManagedApiConnectorWorkflowCustomConfigTest.cs create mode 100644 src/LogicAppUnit.Samples.LogicApps.Tests/connections-custom.json create mode 100644 src/LogicAppUnit.Samples.LogicApps.Tests/local.settings-custom.json create mode 100644 src/LogicAppUnit.Samples.LogicApps.Tests/parameters-custom.json diff --git a/ChangeLog.md b/ChangeLog.md index 8e3b25f..641c62d 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,15 @@ +# 1.13.0 (4th December 2025) + +LogicAppUnit Testing Framework: + +- Added support for custom configuration files in test projects. The `Initialize()` method now accepts optional parameters to specify custom filenames for `local.settings.json`, `parameters.json`, and `connections.json`. Configuration files are resolved by checking the test project directory first, then falling back to the Logic App project directory. This allows tests to use test-specific configurations that override the Logic App project defaults. This enables unified test suites across environments - use the same test code for DEV, QA, and PROD by simply swapping configuration files (e.g., `parameters-dev.json`, `parameters-qa.json`, `parameters-prod.json`). + +LogicAppUnit.Samples.LogicApps.Tests: + +- Added example custom configuration files (`local.settings-custom.json`, `parameters-custom.json`, `connections-custom.json`) in the test project. +- Added `HttpWorkflowCustomConfigTest`, `BuiltInConnectorWorkflowCustomConfigTest`, and `ManagedApiConnectorWorkflowCustomConfigTest` to demonstrate the custom configuration file feature. + + # 1.12.0 (18th July 2025) LogicAppUnit Testing Framework: diff --git a/src/LogicAppUnit.Samples.LogicApps.Tests/HttpWorkflow/HttpWorkflowCustomConfigTest.cs b/src/LogicAppUnit.Samples.LogicApps.Tests/HttpWorkflow/HttpWorkflowCustomConfigTest.cs new file mode 100644 index 0000000..f3c5089 --- /dev/null +++ b/src/LogicAppUnit.Samples.LogicApps.Tests/HttpWorkflow/HttpWorkflowCustomConfigTest.cs @@ -0,0 +1,286 @@ +using LogicAppUnit.Helper; +using LogicAppUnit.Mocking; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; + +namespace LogicAppUnit.Samples.LogicApps.Tests.HttpWorkflow +{ + /// + /// Test cases for the http-workflow workflow using custom configuration files. + /// This test class demonstrates the custom configuration file replacement feature. + /// All tests are identical to HttpWorkflowTest, but use custom parameters, connections, and local settings files. + /// + [TestClass] + public class HttpWorkflowCustomConfigTest : WorkflowTestBase + { + private const string _WebHookRequestApiKey = "serviceone-auth-webhook-apikey"; + + [TestInitialize] + public void TestInitialize() + { + // Using custom configuration files to test the replacement feature + // Files will be loaded from test project if they exist, otherwise from Logic App project + Initialize( + Constants.LOGIC_APP_TEST_EXAMPLE_BASE_PATH, + Constants.HTTP_WORKFLOW, + localSettingsFilename: "local.settings-custom.json", + parametersFilename: "parameters-custom.json" + ); + } + + [ClassCleanup] + public static void CleanResources() + { + Close(); + } + + /// + /// Tests that the correct response is returned when an incorrect value for the 'X-API-Key header' is used with the webhook request. + /// + [TestMethod] + public void HttpWorkflowCustomConfigTest_When_Wrong_API_Key_In_Request() + { + using (ITestRunner testRunner = CreateTestRunner()) + { + // Run the workflow + var workflowResponse = testRunner.TriggerWorkflow( + GetWebhookRequest(), + HttpMethod.Post, + new Dictionary { { "x-api-key", "wrong-key" } }); + + // Check workflow run status + Assert.AreEqual(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus); + + // Check workflow response + testRunner.ExceptionWrapper(() => Assert.AreEqual(HttpStatusCode.Unauthorized, workflowResponse.StatusCode)); + Assert.AreEqual("Invalid/No authorization header passed", workflowResponse.Content.ReadAsStringAsync().Result); + Assert.AreEqual("text/plain; charset=utf-8", workflowResponse.Content.Headers.ContentType.ToString()); + + // Check action result + Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Unauthorized_Response")); + Assert.AreEqual(ActionStatus.Skipped, testRunner.GetWorkflowActionStatus("Get_Customer_Details_from_Service_One")); + } + } + + /// + /// Tests that the correct response is returned when the HTTP call to the Service One API to get the customer details fails. + /// + [TestMethod] + public void HttpWorkflowCustomConfigTest_When_Get_Customer_Details_Fails() + { + using (ITestRunner testRunner = CreateTestRunner()) + { + // Configure mock responses + testRunner + .AddMockResponse( + MockRequestMatcher.Create() + .UsingGet() + .WithPath(PathMatchType.Exact, "/api/v1/customers/54617")) + .RespondWith( + MockResponseBuilder.Create() + .WithInternalServerError() + .WithContentAsPlainText("Internal server error detected in System One")); + + // Run the workflow + var workflowResponse = testRunner.TriggerWorkflow( + GetWebhookRequest(), + HttpMethod.Post, + new Dictionary { { "x-api-key", _WebHookRequestApiKey } }); + + // Check workflow run status + Assert.AreEqual(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus); + + // Check workflow response + testRunner.ExceptionWrapper(() => Assert.AreEqual(HttpStatusCode.InternalServerError, workflowResponse.StatusCode)); + Assert.AreEqual("Unable to get customer details: Internal server error detected in System One", workflowResponse.Content.ReadAsStringAsync().Result); + Assert.AreEqual("text/plain; charset=utf-8", workflowResponse.Content.Headers.ContentType.ToString()); + + // Check action result + Assert.AreEqual(ActionStatus.Skipped, testRunner.GetWorkflowActionStatus("Unauthorized_Response")); + Assert.AreEqual(ActionStatus.Failed, testRunner.GetWorkflowActionStatus("Get_Customer_Details_from_Service_One")); + Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Failed_Get_Response")); + Assert.AreEqual(ActionStatus.Skipped, testRunner.GetWorkflowActionStatus("Update_Customer_Details_in_Service_Two")); + } + } + + /// + /// Tests that the correct response is returned when the HTTP call to the Service Two API to update the customer details fails. + /// + [TestMethod] + public void HttpWorkflowCustomConfigTest_When_Update_Customer_Fails() + { + // Override one of the settings in the local settings file + var settingsToOverride = new Dictionary() { { "ServiceTwo-DefaultAddressType", "physical" } }; + + using (ITestRunner testRunner = CreateTestRunner(settingsToOverride)) + { + // Configure mock responses + testRunner + .AddMockResponse( + MockRequestMatcher.Create() + .UsingGet() + .WithPath(PathMatchType.Exact, "/api/v1/customers/54617")) + .RespondWith( + MockResponseBuilder.Create() + .WithSuccess() + .WithContent(GetCustomerResponse)); + testRunner + .AddMockResponse( + MockRequestMatcher.Create() + .UsingPut() + .WithPath(PathMatchType.Exact, "/api/v1.1/membership/customers/54617")) + .RespondWith( + MockResponseBuilder.Create() + .WithInternalServerError() + .WithContentAsPlainText("System Two has died")); + + // Run the workflow + var workflowResponse = testRunner.TriggerWorkflow( + GetWebhookRequest(), + HttpMethod.Post, + new Dictionary { { "x-api-key", _WebHookRequestApiKey } }); + + // Check workflow run status + Assert.AreEqual(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus); + + // Check workflow response + testRunner.ExceptionWrapper(() => Assert.AreEqual(HttpStatusCode.InternalServerError, workflowResponse.StatusCode)); + Assert.AreEqual("Unable to update customer details: System Two has died", workflowResponse.Content.ReadAsStringAsync().Result); + Assert.AreEqual("text/plain; charset=utf-8", workflowResponse.Content.Headers.ContentType.ToString()); + + // Check action result + Assert.AreEqual(ActionStatus.Skipped, testRunner.GetWorkflowActionStatus("Unauthorized_Response")); + Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Get_Customer_Details_from_Service_One")); + Assert.AreEqual(ActionStatus.Failed, testRunner.GetWorkflowActionStatus("Update_Customer_Details_in_Service_Two")); + Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Failed_Update_Response")); + + // Check request to System Two Membership API + var systemTwoRequest = testRunner.MockRequests.First(r => r.RequestUri.AbsolutePath == "/api/v1.1/membership/customers/54617"); + Assert.AreEqual(HttpMethod.Put, systemTwoRequest.Method); + Assert.AreEqual("application/json", systemTwoRequest.ContentHeaders["Content-Type"].First()); + Assert.AreEqual("ApiKey servicetwo-auth-apikey", systemTwoRequest.Headers["x-api-key"].First()); + Assert.AreEqual( + ContentHelper.FormatJson(ResourceHelper.GetAssemblyResourceAsString($"{GetType().Namespace}.MockData.SystemTwo_Request.json")), + ContentHelper.FormatJson(systemTwoRequest.Content)); + + JToken parseCustomerInput = testRunner.GetWorkflowActionInput("Parse_Customer"); + JToken parseCustomerOutput = testRunner.GetWorkflowActionOutput("Parse_Customer"); + Assert.IsNotNull(parseCustomerInput.ToString()); + Assert.IsNotNull(parseCustomerOutput.ToString()); + } + } + + /// + /// Tests that the correct response is returned when the HTTP call to the Service Two API to update the customer details is successful. + /// + [TestMethod] + public void HttpWorkflowCustomConfigTest_When_Successful() + { + // Override one of the settings in the local settings file + var settingsToOverride = new Dictionary() { { "ServiceTwo-DefaultAddressType", "physical" } }; + + using (ITestRunner testRunner = CreateTestRunner(settingsToOverride)) + { + // Configure mock responses + testRunner + .AddMockResponse( + MockRequestMatcher.Create() + .UsingGet() + .WithPath(PathMatchType.Exact, "/api/v1/customers/54617")) + .RespondWith( + MockResponseBuilder.Create() + .WithSuccess() + .WithContent(GetCustomerResponse)); + testRunner + .AddMockResponse( + MockRequestMatcher.Create() + .UsingPut() + .WithPath(PathMatchType.Exact, "/api/v1.1/membership/customers/54617")) + .RespondWith( + MockResponseBuilder.Create() + .WithSuccess() + .WithContentAsPlainText("success")); + + // Run the workflow + var workflowResponse = testRunner.TriggerWorkflow( + GetWebhookRequest(), + HttpMethod.Post, + new Dictionary { { "x-api-key", _WebHookRequestApiKey } }); + + // Check workflow run status + Assert.AreEqual(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus); + + // Check workflow response + testRunner.ExceptionWrapper(() => Assert.AreEqual(HttpStatusCode.OK, workflowResponse.StatusCode)); + Assert.AreEqual("Webhook processed successfully", workflowResponse.Content.ReadAsStringAsync().Result); + Assert.AreEqual("text/plain; charset=utf-8", workflowResponse.Content.Headers.ContentType.ToString()); + + // Check action result + Assert.AreEqual(ActionStatus.Skipped, testRunner.GetWorkflowActionStatus("Unauthorized_Response")); + Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Get_Customer_Details_from_Service_One")); + Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Update_Customer_Details_in_Service_Two")); + Assert.AreEqual(ActionStatus.Skipped, testRunner.GetWorkflowActionStatus("Failed_Update_Response")); + Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Success_Response")); + + // Check request to System Two Membership API + var systemTwoRequest = testRunner.MockRequests.First(r => r.RequestUri.AbsolutePath == "/api/v1.1/membership/customers/54617"); + Assert.AreEqual(HttpMethod.Put, systemTwoRequest.Method); + Assert.AreEqual("application/json", systemTwoRequest.ContentHeaders["Content-Type"].First()); + Assert.AreEqual("ApiKey servicetwo-auth-apikey", systemTwoRequest.Headers["x-api-key"].First()); + Assert.AreEqual( + ContentHelper.FormatJson(ResourceHelper.GetAssemblyResourceAsString($"{GetType().Namespace}.MockData.SystemTwo_Request.json")), + ContentHelper.FormatJson(systemTwoRequest.Content)); + + // Check tracked properties + var trackedProps = testRunner.GetWorkflowActionTrackedProperties("Get_Customer_Details_from_Service_One"); + Assert.AreEqual("customer", trackedProps["recordType"]); + Assert.AreEqual("54617", trackedProps["recordId"]); + Assert.AreEqual("c2ddb2f2-7bff-4cce-b724-ac2400b12760", trackedProps["correlationId"]); + } + } + + private static StringContent GetWebhookRequest() + { + return ContentHelper.CreateJsonStringContent(new + { + id = "71fbcb8e-f974-449a-bb14-ac2400b150aa", + correlationId = "c2ddb2f2-7bff-4cce-b724-ac2400b12760", + sourceSystem = "SystemOne", + timestamp = "2022-08-27T08:45:00.1493711Z", + type = "CustomerUpdated", + customerId = 54617, + resourceId = "54617", + resourceURI = "https://external-service-one.testing.net/api/v1/customer/54617" + }); + } + + private static StringContent GetCustomerResponse() + { + return ContentHelper.CreateJsonStringContent(new + { + id = 54624, + title = "Mr", + firstName = "Peter", + lastName = "Smith", + dateOfBirth = "1970-04-25", + languageCode = "en-GB", + address = new + { + line1 = "8 High Street", + line2 = (string)null, + line3 = (string)null, + town = "Luton", + county = "Bedfordshire", + postcode = "LT12 6TY", + countryCode = "UK", + countryName = "United Kingdom" + } + }); + } + } +} diff --git a/src/LogicAppUnit.Samples.LogicApps.Tests/LogicAppUnit.Samples.LogicApps.Tests.csproj b/src/LogicAppUnit.Samples.LogicApps.Tests/LogicAppUnit.Samples.LogicApps.Tests.csproj index 9c5d6cb..2869eeb 100644 --- a/src/LogicAppUnit.Samples.LogicApps.Tests/LogicAppUnit.Samples.LogicApps.Tests.csproj +++ b/src/LogicAppUnit.Samples.LogicApps.Tests/LogicAppUnit.Samples.LogicApps.Tests.csproj @@ -77,6 +77,15 @@ Always + + Always + + + Always + + + Always + diff --git a/src/LogicAppUnit.Samples.LogicApps.Tests/ManagedApiConnectorWorkflow/ManagedApiConnectorWorkflowCustomConfigTest.cs b/src/LogicAppUnit.Samples.LogicApps.Tests/ManagedApiConnectorWorkflow/ManagedApiConnectorWorkflowCustomConfigTest.cs new file mode 100644 index 0000000..713fc06 --- /dev/null +++ b/src/LogicAppUnit.Samples.LogicApps.Tests/ManagedApiConnectorWorkflow/ManagedApiConnectorWorkflowCustomConfigTest.cs @@ -0,0 +1,188 @@ +using LogicAppUnit.Helper; +using LogicAppUnit.Mocking; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; + +namespace LogicAppUnit.Samples.LogicApps.Tests.ManagedApiConnectorWorkflow +{ + /// + /// Test cases for the managed-api-connector-workflow workflow using custom configuration files. + /// This test class demonstrates the custom configuration file replacement feature. + /// All tests are identical to ManagedApiConnectorWorkflowTest, but use custom parameters, connections, and local settings files. + /// + [TestClass] + public class ManagedApiConnectorWorkflowCustomConfigTest : WorkflowTestBase + { + [TestInitialize] + public void TestInitialize() + { + // Using custom configuration files to test the replacement feature + // Files will be loaded from test project if they exist, otherwise from Logic App project + Initialize( + Constants.LOGIC_APP_TEST_EXAMPLE_BASE_PATH, + Constants.MANAGED_API_CONNECTOR_WORKFLOW, + localSettingsFilename: "local.settings-custom.json", + parametersFilename: "parameters-custom.json", + connectionsFilename: "connections-custom.json" + ); + } + + [ClassCleanup] + public static void CleanResources() + { + Close(); + } + + /// + /// Tests that the correct response is returned when the upsert in Salesforce is successful and the sending of the confirmation email is also successful. + /// + [TestMethod] + public void ManagedApiConnectorWorkflowCustomConfigTest_When_Successful() + { + // Override one of the settings in the local settings file + var settingsToOverride = new Dictionary() { { "Outlook-SubjectPrefix", "TEST ENVIRONMENT" } }; + + using (ITestRunner testRunner = CreateTestRunner(settingsToOverride)) + { + // Mock the Salesforce and Outlook actions (that use a Managed API connector) and customize responses + // For both types of actions, the URI in the request matches the 'connectionRuntimeUrl' in 'connections.json' and the 'path' configuration of the action + // It might be easier to use 'PathMatchType.EndsWith' since the URL can be quite long + testRunner + .AddMockResponse( + MockRequestMatcher.Create() + .UsingPatch() + .WithPath(PathMatchType.EndsWith, "/default/tables/Account_Staging__c/externalIdFields/External_Id__c/54624") + // We can match actions using managed API connections using the action name + .FromAction("Upsert_Customer")) + // No response content for Salesforce actions + .RespondWithDefault(); + testRunner + .AddMockResponse( + MockRequestMatcher.Create() + .UsingPost() + .WithPath(PathMatchType.EndsWith, "/v2/Mail") + // We can match actions using managed API connections using the action name + .FromAction("Send_a_confirmation_email")) + // No response content for Send Email actions + .RespondWithDefault(); + + // Run the workflow + var workflowResponse = testRunner.TriggerWorkflow(GetRequest(), HttpMethod.Post); + + // Check workflow run status + Assert.AreEqual(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus); + + // Check workflow response + testRunner.ExceptionWrapper(() => Assert.AreEqual(HttpStatusCode.OK, workflowResponse.StatusCode)); + Assert.AreEqual("Upsert is successful", workflowResponse.Content.ReadAsStringAsync().Result); + Assert.AreEqual("text/plain; charset=utf-8", workflowResponse.Content.Headers.ContentType.ToString()); + + // Check action result + Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Upsert_Customer")); + Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Send_a_confirmation_email")); + Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Success_Response")); + Assert.AreEqual(ActionStatus.Skipped, testRunner.GetWorkflowActionStatus("Failure_Response")); + + // Check message sent to Salesforce + var salesforceRequest = testRunner.MockRequests.First(r => r.RequestUri.AbsolutePath.EndsWith("/default/tables/Account_Staging__c/externalIdFields/External_Id__c/54624")); + Assert.AreEqual( + ContentHelper.FormatJson(ResourceHelper.GetAssemblyResourceAsString($"{GetType().Namespace}.MockData.Salesforce_Request.json")), + ContentHelper.FormatJson(salesforceRequest.Content)); + + // Check message sent to Outlook + var outlookRequest = testRunner.MockRequests.First(r => r.RequestUri.AbsolutePath.EndsWith("/v2/Mail")); + Assert.AreEqual( + ContentHelper.FormatJson(ResourceHelper.GetAssemblyResourceAsString($"{GetType().Namespace}.MockData.Outlook_Request.json")), + ContentHelper.FormatJson(outlookRequest.Content)); + } + } + + /// + /// Tests that the correct response is returned when the upsert in Salesforce is successful and the sending of the confirmation email fails. + /// + [TestMethod] + public void ManagedApiConnectorWorkflowCustomConfigTest_When_Send_Email_Fails() + { + // Override one of the settings in the local settings file + var settingsToOverride = new Dictionary() { { "Outlook-SubjectPrefix", "TEST ENVIRONMENT" } }; + + using (ITestRunner testRunner = CreateTestRunner(settingsToOverride)) + { + // Configure mock responses for the Salesforce and Outlook actions (that use a Managed API connector) + // For both types of actions, the URI in the request matches the 'connectionRuntimeUrl' in 'connections.json' and the 'path' configuration of the action + // It might be easier to use 'PathMatchType.EndsWith' since the URL can be quite long + testRunner + .AddMockResponse( + MockRequestMatcher.Create() + .UsingPatch() + .WithPath(PathMatchType.EndsWith, "/default/tables/Account_Staging__c/externalIdFields/External_Id__c/54624")) + // No response content for Salesforce actions + .RespondWith( + MockResponseBuilder.Create() + .WithSuccess()); + testRunner + .AddMockResponse( + MockRequestMatcher.Create() + .UsingPost() + .WithPath(PathMatchType.EndsWith, "/v2/Mail")) + // No response content for Send Email actions + .RespondWith( + MockResponseBuilder.Create() + .WithInternalServerError()); + + // Run the workflow + var workflowResponse = testRunner.TriggerWorkflow(GetRequest(), HttpMethod.Post); + + // Check workflow run status + Assert.AreEqual(WorkflowRunStatus.Failed, testRunner.WorkflowRunStatus); + + // Check workflow response + testRunner.ExceptionWrapper(() => Assert.AreEqual(HttpStatusCode.InternalServerError, workflowResponse.StatusCode)); + + // Check action result + Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Upsert_Customer")); + Assert.AreEqual(ActionStatus.Failed, testRunner.GetWorkflowActionStatus("Send_a_confirmation_email")); + Assert.AreEqual(ActionStatus.Skipped, testRunner.GetWorkflowActionStatus("Success_Response")); + Assert.AreEqual(ActionStatus.Succeeded, testRunner.GetWorkflowActionStatus("Failure_Response")); + + // Check message sent to Salesforce + var salesforceRequest = testRunner.MockRequests.First(r => r.RequestUri.AbsolutePath.EndsWith("/default/tables/Account_Staging__c/externalIdFields/External_Id__c/54624")); + Assert.AreEqual( + ContentHelper.FormatJson(ResourceHelper.GetAssemblyResourceAsString($"{GetType().Namespace}.MockData.Salesforce_Request.json")), + ContentHelper.FormatJson(salesforceRequest.Content)); + + // Check message sent to Outlook + var outlookRequest = testRunner.MockRequests.First(r => r.RequestUri.AbsolutePath.EndsWith("/v2/Mail")); + Assert.AreEqual( + ContentHelper.FormatJson(ResourceHelper.GetAssemblyResourceAsString($"{GetType().Namespace}.MockData.Outlook_Request.json")), + ContentHelper.FormatJson(outlookRequest.Content)); + } + } + + private static StringContent GetRequest() + { + return ContentHelper.CreateJsonStringContent(new + { + id = 54624, + title = "Mr", + firstName = "Peter", + lastName = "Smith", + dateOfBirth = "1970-04-25", + address = new + { + addressLine1 = "Blossoms Pasture", + addressLine2 = "High Street", + addressLine3 = "Tinyville", + town = "Luton", + county = "Bedfordshire", + postcode = "LT12 6TY", + countryCode = "UK", + countryName = "United Kingdom" + } + }); + } + } +} diff --git a/src/LogicAppUnit.Samples.LogicApps.Tests/connections-custom.json b/src/LogicAppUnit.Samples.LogicApps.Tests/connections-custom.json new file mode 100644 index 0000000..7195c09 --- /dev/null +++ b/src/LogicAppUnit.Samples.LogicApps.Tests/connections-custom.json @@ -0,0 +1,66 @@ +{ + "serviceProviderConnections": { + "serviceBus": { + "parameterValues": { + "connectionString": "@appsetting('ServiceBus_ConnectionString')" + }, + "serviceProvider": { + "id": "/serviceProviders/serviceBus" + }, + "displayName": "serviceBusConnection" + }, + "sql": { + "parameterValues": { + "connectionString": "@appsetting('Sql_ConnectionString')" + }, + "serviceProvider": { + "id": "/serviceProviders/sql" + }, + "displayName": "sqlConnection" + }, + "azureBlob": { + "parameterValues": { + "connectionString": "@appsetting('AzureBlob-ConnectionString')" + }, + "serviceProvider": { + "id": "/serviceProviders/AzureBlob" + }, + "displayName": "storageBlobConnection" + }, + "azureQueue": { + "parameterValues": { + "connectionString": "@appsetting('AzureQueue-ConnectionString')" + }, + "serviceProvider": { + "id": "/serviceProviders/azurequeues" + }, + "displayName": "storageQueueConnection" + } + }, + "managedApiConnections": { + "salesforce": { + "api": { + "id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/providers/Microsoft.Web/locations/@{appsetting('WORKFLOWS_LOCATION_NAME')}/managedApis/salesforce" + }, + "connection": { + "id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/resourceGroups/@{appsetting('WORKFLOWS_RESOURCE_GROUP_NAME')}/providers/Microsoft.Web/connections/salesforce01" + }, + "connectionRuntimeUrl": "@parameters('salesforce-ConnectionRuntimeUrl')", + "authentication": { + "type": "Raw", + "scheme": "Key", + "parameter": "@appsetting('Salesforce-ConnectionKey')" + } + }, + "outlook": { + "api": { + "id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/providers/Microsoft.Web/locations/@{appsetting('WORKFLOWS_LOCATION_NAME')}/managedApis/outlook" + }, + "connection": { + "id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/resourceGroups/@{appsetting('WORKFLOWS_RESOURCE_GROUP_NAME')}/providers/Microsoft.Web/connections/outlook01" + }, + "connectionRuntimeUrl": "@parameters('outlook-ConnectionRuntimeUrl')", + "authentication": "@parameters('outlook-Authentication')" + } + } +} diff --git a/src/LogicAppUnit.Samples.LogicApps.Tests/local.settings-custom.json b/src/LogicAppUnit.Samples.LogicApps.Tests/local.settings-custom.json new file mode 100644 index 0000000..e8cfd55 --- /dev/null +++ b/src/LogicAppUnit.Samples.LogicApps.Tests/local.settings-custom.json @@ -0,0 +1,27 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "APP_KIND": "workflowapp", + "FUNCTIONS_WORKER_RUNTIME": "node", + "WORKFLOWS_SUBSCRIPTION_ID": "c1661296-a732-44b9-8458-d1a0dd19815e", + "WORKFLOWS_LOCATION_NAME": "uksouth", + "WORKFLOWS_RESOURCE_GROUP_NAME": "rg-uks-01", + "AzureBlob-ConnectionString": "any-blob-connection-string", + "AzureQueue-ConnectionString": "any-queue-connection-string", + "Outlook-ConnectionKey": "any-outlook-connection-key", + "Outlook-SubjectPrefix": "INFORMATION", + "Outlook-ConnectionRuntimeUrl": "https://7606763fdc09952f.10.common.logic-uksouth.azure-apihub.net/apim/outlook/79a0bc680716416e90e17323b581695d/", + "Salesforce-ConnectionKey": "any-salesforce-connection-key", + "Salesforce-ConnectionRuntimeUrl": "https://7606763fdc09952f.10.common.logic-uksouth.azure-apihub.net/apim/salesforce/fba515601ef14f9193eee596a9dcfd1c/", + "ServiceOne-Url": "https://external-service-one.testing.net/api/v1", + "ServiceOne-Authentication-APIKey": "serviceone-auth-apikey", + "ServiceOne-Authentication-WebHook-APIKey": "serviceone-auth-webhook-apikey", + "ServiceTwo-Url": "https://external-service-two.testing.net/api/v1.1", + "ServiceTwo-Verison2Url": "https://external-service-two.testing.net/api/v2.0", + "ServiceTwo-Authentication-APIKey": "servicetwo-auth-apikey", + "ServiceTwo-DefaultAddressType": "business", + "Sql_ConnectionString": "any-sql-connection-string", + "ServiceBus_ConnectionString": "any-servicebus-connection-string" + } +} diff --git a/src/LogicAppUnit.Samples.LogicApps.Tests/parameters-custom.json b/src/LogicAppUnit.Samples.LogicApps.Tests/parameters-custom.json new file mode 100644 index 0000000..9499f25 --- /dev/null +++ b/src/LogicAppUnit.Samples.LogicApps.Tests/parameters-custom.json @@ -0,0 +1,38 @@ +{ + "ServiceOne-Url": { + "type": "String", + "value": "@appsetting('ServiceOne-Url')" + }, + "ServiceOne-Authentication-APIKey": { + "type": "String", + "value": "@appsetting('ServiceOne-Authentication-APIKey')" + }, + "ServiceOne-Authentication-WebHook-APIKey": { + "type": "String", + "value": "@appsetting('ServiceOne-Authentication-WebHook-APIKey')" + }, + "ServiceTwo-Url": { + "type": "String", + "value": "@appsetting('ServiceTwo-Url')" + }, + "ServiceTwo-Authentication-APIKey": { + "type": "String", + "value": "@appsetting('ServiceTwo-Authentication-APIKey')" + }, + "salesforce-ConnectionRuntimeUrl": { + "type": "String", + "value": "@appsetting('Salesforce-ConnectionRuntimeUrl')" + }, + "outlook-ConnectionRuntimeUrl": { + "type": "String", + "value": "@appsetting('Outlook-ConnectionRuntimeUrl')" + }, + "outlook-Authentication": { + "type": "Object", + "value": { + "type": "Raw", + "scheme": "Key", + "parameter": "@appsetting('Outlook-ConnectionKey')" + } + } +} diff --git a/src/LogicAppUnit/LogicAppUnit.csproj b/src/LogicAppUnit/LogicAppUnit.csproj index f2f707f..ce300bb 100644 --- a/src/LogicAppUnit/LogicAppUnit.csproj +++ b/src/LogicAppUnit/LogicAppUnit.csproj @@ -4,7 +4,7 @@ net6.0 LogicAppUnit true - 1.12.0 + 1.13.0 Logic App Unit Testing Framework Unit testing framework for Standard Logic Apps. https://github.com/LogicAppUnit/TestingFramework diff --git a/src/LogicAppUnit/WorkflowTestBase.cs b/src/LogicAppUnit/WorkflowTestBase.cs index e61fe48..7efc590 100644 --- a/src/LogicAppUnit/WorkflowTestBase.cs +++ b/src/LogicAppUnit/WorkflowTestBase.cs @@ -106,7 +106,7 @@ public IMockResponse AddMockResponse(string name, IMockRequestMatcher mockReques /// The name of the workflow. This matches the name of the folder that contains the workflow definition file. protected void Initialize(string logicAppBasePath, string workflowName) { - Initialize(logicAppBasePath, workflowName, null); + Initialize(logicAppBasePath, workflowName, null, null); } /// @@ -115,7 +115,10 @@ protected void Initialize(string logicAppBasePath, string workflowName) /// Path to the root folder containing the workflows. /// The name of the workflow. This matches the name of the folder that contains the workflow definition file. /// The name of the local settings file to be used, this can be used to override the default of local.settings.json. - protected void Initialize(string logicAppBasePath, string workflowName, string localSettingsFilename) + /// Path to the test project containing test-specific configuration files. If not specified, defaults to current directory. + /// The name of the parameters file to be used (e.g., 'parameterA.json'). If not specified, defaults to 'parameters.json'. + /// The name of the connections file to be used (e.g., 'connectionA.json'). If not specified, defaults to 'connections.json'. + protected void Initialize(string logicAppBasePath, string workflowName, string localSettingsFilename = null, string testProjectPath = null, string parametersFilename = null, string connectionsFilename = null) { if (string.IsNullOrEmpty(logicAppBasePath)) throw new ArgumentNullException(nameof(logicAppBasePath)); @@ -145,11 +148,18 @@ protected void Initialize(string logicAppBasePath, string workflowName, string l if (_testConfig.Azurite.EnableAzuritePortCheck && !AzuriteHelper.IsRunning(_testConfig.Azurite)) throw new TestException($"Azurite is not running on ports {_testConfig.Azurite.BlobServicePort} (Blob service), {_testConfig.Azurite.QueueServicePort} (Queue service) and {_testConfig.Azurite.TableServicePort} (Table service). Logic App workflows cannot run unless all three services are running in Azurite"); + // Default test project path to current directory if not specified + testProjectPath = testProjectPath ?? Directory.GetCurrentDirectory(); + + // Set default filenames if not specified + parametersFilename = parametersFilename ?? Constants.PARAMETERS; + connectionsFilename = connectionsFilename ?? Constants.CONNECTIONS; + // Process the workflow definition, local settings, parameters and connection files ProcessWorkflowDefinitionFile(logicAppBasePath, workflowName); - ProcessLocalSettingsFile(logicAppBasePath, localSettingsFilename); - ProcessParametersFile(logicAppBasePath); - ProcessConnectionsFile(logicAppBasePath); + ProcessLocalSettingsFile(logicAppBasePath, localSettingsFilename, testProjectPath); + ProcessParametersFile(logicAppBasePath, testProjectPath, parametersFilename); + ProcessConnectionsFile(logicAppBasePath, testProjectPath, connectionsFilename); // Set up the artifacts (schemas, maps) and custom library folders _artifactDirectory = SetSourceDirectory(logicAppBasePath, Constants.ARTIFACTS_FOLDER, "artifacts"); @@ -241,6 +251,38 @@ protected ITestRunner CreateTestRunner(Dictionary localSettingsO #endregion Create test runner + #region Helper methods + + /// + /// Resolves the configuration file path by checking the test project path first, then falling back to the Logic App path. + /// + /// Path to the root folder containing the workflows. + /// Path to the test project containing test-specific configuration files. + /// The name of the configuration file to locate. + /// The full path to the configuration file, or null if not found. + private string ResolveConfigurationFilePath(string logicAppBasePath, string testProjectPath, string fileName) + { + // Check test project first + var testProjectFilePath = Path.Combine(testProjectPath, fileName); + if (File.Exists(testProjectFilePath)) + { + Console.WriteLine($"Using configuration file from test project: {testProjectFilePath}"); + return testProjectFilePath; + } + + // Fall back to Logic App project + var logicAppFilePath = Path.Combine(logicAppBasePath, fileName); + if (File.Exists(logicAppFilePath)) + { + Console.WriteLine($"Using configuration file from Logic App project: {logicAppFilePath}"); + return logicAppFilePath; + } + + return null; + } + + #endregion Helper methods + #region Source file processing /// @@ -272,35 +314,51 @@ private void ProcessWorkflowDefinitionFile(string logicAppBasePath, string workf /// /// Process a workflow local settings file before the test is run. + /// Files in the test project path take precedence over files in the Logic App project. /// /// Path to the root folder containing the workflows. /// The name of the local settings file to be used, this can be used to override the default of local.settings.json. - private void ProcessLocalSettingsFile(string logicAppBasePath, string localSettingsFilename) + /// Path to the test project containing test-specific configuration files. + private void ProcessLocalSettingsFile(string logicAppBasePath, string localSettingsFilename, string testProjectPath) { // The name of the local setting file can be set in the test configuration - _localSettings = new LocalSettingsWrapper(ReadFromPath(Path.Combine(logicAppBasePath, SetLocalSettingsFile(localSettingsFilename)))); + var fileName = SetLocalSettingsFile(localSettingsFilename); + var filePath = ResolveConfigurationFilePath(logicAppBasePath, testProjectPath, fileName); + + if (filePath == null) + throw new TestException($"The local settings file '{fileName}' does not exist in test project '{testProjectPath}' or Logic App project '{logicAppBasePath}'"); + + _localSettings = new LocalSettingsWrapper(ReadFromPath(filePath)); _localSettings.ReplaceExternalUrlsWithMockServer(_testConfig.Workflow.ExternalApiUrlsToMock); } /// /// Process a workflow parameters file before the test is run. + /// Files in the test project path take precedence over files in the Logic App project. /// /// Path to the root folder containing the workflows. - private void ProcessParametersFile(string logicAppBasePath) + /// Path to the test project containing test-specific configuration files. + /// The name of the parameters file to be used. + private void ProcessParametersFile(string logicAppBasePath, string testProjectPath, string parametersFilename) { - _parameters = new ParametersWrapper(ReadFromPath(Path.Combine(logicAppBasePath, Constants.PARAMETERS), optional: true)); + var filePath = ResolveConfigurationFilePath(logicAppBasePath, testProjectPath, parametersFilename); + _parameters = new ParametersWrapper(ReadFromPath(filePath, optional: true)); } /// /// Process a workflow connections file before the test is run. + /// Files in the test project path take precedence over files in the Logic App project. /// /// Path to the root folder containing the workflows. - private void ProcessConnectionsFile(string logicAppBasePath) + /// Path to the test project containing test-specific configuration files. + /// The name of the connections file to be used. + private void ProcessConnectionsFile(string logicAppBasePath, string testProjectPath, string connectionsFilename) { const string invalidConnectionsMsg = "configured to use the 'ManagedServiceIdentity' authentication type. Only the 'Raw' and 'ActiveDirectoryOAuth' authentication types are allowed in a local developer environment"; - _connections = new ConnectionsWrapper(ReadFromPath(Path.Combine(logicAppBasePath, Constants.CONNECTIONS), optional: true), _localSettings, _parameters); + var filePath = ResolveConfigurationFilePath(logicAppBasePath, testProjectPath, connectionsFilename); + _connections = new ConnectionsWrapper(ReadFromPath(filePath, optional: true), _localSettings, _parameters); _connections.ReplaceManagedApiConnectionUrlsWithMockServer(_testConfig.Workflow.ManagedApisToMock); From 253eef33e87237be068a559b813c321a5214299a Mon Sep 17 00:00:00 2001 From: Daniel Jonathan Date: Thu, 4 Dec 2025 22:45:23 +0100 Subject: [PATCH 2/2] feat: Add support for custom configuration files in test projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add optional parameters to Initialize() for custom config filenames: * localSettingsFilename: Custom local settings file * testProjectPath: Path to test project directory * parametersFilename: Custom parameters file * connectionsFilename: Custom connections file - Configuration file resolution precedence: * Initialize() parameter → testConfiguration.json → default * Custom files search test project first, then Logic App project * Custom files are copied to standard names for Logic App runtime - Consolidate duplicate file processing logic: * Created ProcessConfigurationFile() helper method * Enhanced ResolveConfigurationFilePath() to return tuple with source info * Reduced code duplication by ~60% in file processing section - Fix testConfiguration.json structure: * Move localSettingsFilename to root level (not inside workflow section) * Ensures proper configuration loading for default test behavior - Copy behavior: * Custom files from test project are copied to testConfiguration.json --- ChangeLog.md | 32 ++--- .../HttpWorkflowCustomConfigTest.cs | 3 +- ...gedApiConnectorWorkflowCustomConfigTest.cs | 3 +- .../local.settings-custom.json | 6 +- .../testConfiguration.json | 8 +- .../local.settings.dev.json | 27 ++++ .../local.settings.json | 6 +- src/LogicAppUnit/WorkflowTestBase.cs | 116 +++++++++++++----- 8 files changed, 137 insertions(+), 64 deletions(-) create mode 100644 src/LogicAppUnit.Samples.LogicApps/local.settings.dev.json diff --git a/ChangeLog.md b/ChangeLog.md index 641c62d..ad2766b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -2,13 +2,12 @@ LogicAppUnit Testing Framework: -- Added support for custom configuration files in test projects. The `Initialize()` method now accepts optional parameters to specify custom filenames for `local.settings.json`, `parameters.json`, and `connections.json`. Configuration files are resolved by checking the test project directory first, then falling back to the Logic App project directory. This allows tests to use test-specific configurations that override the Logic App project defaults. This enables unified test suites across environments - use the same test code for DEV, QA, and PROD by simply swapping configuration files (e.g., `parameters-dev.json`, `parameters-qa.json`, `parameters-prod.json`). +- Added support for custom configuration files in test projects. The `Initialize()` method now accepts optional parameters to specify custom filenames for `local.settings.json`, `parameters.json`, and `connections.json`. Configuration files are resolved by checking the test project directory first, then falling back to the Logic App project directory. When custom-named configuration files are found in the test project, they are automatically copied to the Logic App directory with standard filenames (`parameters.json`, `connections.json`) to ensure the Logic App runtime uses the correct test-specific configurations. This allows tests to use test-specific configurations that override the Logic App project defaults. This enables unified test suites across environments - use the same test code for DEV, QA, and PROD by simply swapping configuration files (e.g., `parameters-dev.json`, `parameters-qa.json`, `parameters-prod.json`). LogicAppUnit.Samples.LogicApps.Tests: - Added example custom configuration files (`local.settings-custom.json`, `parameters-custom.json`, `connections-custom.json`) in the test project. -- Added `HttpWorkflowCustomConfigTest`, `BuiltInConnectorWorkflowCustomConfigTest`, and `ManagedApiConnectorWorkflowCustomConfigTest` to demonstrate the custom configuration file feature. - +- Added `HttpWorkflowCustomConfigTest` and `ManagedApiConnectorWorkflowCustomConfigTest` to demonstrate the custom configuration file feature. # 1.12.0 (18th July 2025) @@ -16,12 +15,10 @@ LogicAppUnit Testing Framework: - HTTP actions with the authentication type set to `ManagedServiceIdentity` are updated to use the `None` authentication type. [[Issue #49](https://github.com/LogicAppUnit/TestingFramework/issues/49)], [[Issue #50](https://github.com/LogicAppUnit/TestingFramework/issues/50)] and [[PR #51](https://github.com/LogicAppUnit/TestingFramework/pull/51), [@ronaldbosma ](https://github.com/ronaldbosma)] - LogicAppUnit.Samples.LogicApps.Tests: - Added a `http-with-managed-identity-workflow` workflow and unit test to test a workflow that includes a HTTP action with the authentication type set to `ManagedServiceIdentity`. - # 1.11.0 (11th April 2025) LogicAppUnit Testing Framework: @@ -32,7 +29,6 @@ LogicAppUnit Testing Framework: - Updated method `ContentHelper.FormatJson()` to use `JToken.Parse()` instead of `JObject.Parse()`. [[Issue #45](https://github.com/LogicAppUnit/TestingFramework/issues/45)] - Added new property `TestRunner.WorkflowTerminationCodeAsString` that returns the workflow termination code as a string value. The existing property `TestRunner.WorkflowTerminationCode` returns the code as an integer value, but the code is defined as a string data type in the workflow schema reference documentation. [[Issue #46](https://github.com/LogicAppUnit/TestingFramework/issues/46)] - # 1.10.0 (4th November 2024) LogicAppUnit Testing Framework: @@ -45,7 +41,6 @@ LogicAppUnit.Samples.LogicApps.Tests: - Added a `call-data-mapper-workflow` workflow and unit tests to test workflows that call the data mapper. - Added a `inline-script-workflow` workflow and unit tests to test workflows that call in-line C# script (.csx) files. - # 1.9.0 (23rd January 2024) LogicAppUnit Testing Framework: @@ -54,17 +49,16 @@ LogicAppUnit Testing Framework: - Added `IMockRequestMatcher.FromAction(string[] actionNames)` to allow a mock request matcher to match a request based on the name of the workflow action that created the request. This feature depends on the `x-ms-workflow-operation-name` header being present in the request. Refer to the [wiki](https://github.com/LogicAppUnit/TestingFramework/wiki) for details of when the Logic App runtime creates this header. - Retry policies for actions using a managed API connection are removed and replaced with a `none` policy. This is the same pre-processing that is applied to HTTP actions. Previous versions of the framework did not remove the retry policies for actions using a managed API connection which meant that tests could take a long time to complete if they were testing failure scenarios. - The framework checks the `connections.json` file and will fail a test if there are any managed API connections that are configured using the `ManagedServiceIdentity` authentication type. The Logic Apps runtime only supports the `Raw` - and `ActiveDirectoryOAuth` authentication types when running in a local developer environment. [[Issue #30](https://github.com/LogicAppUnit/TestingFramework/issues/30)] - - The `testConfiguration.json` file is now optional. If the file does not exist, or contains an empty JSON document (`{}`), the default values are used for all settings. Previous versions of the framework would fail a test if the configuration file did not exist. [[Issue #28](https://github.com/LogicAppUnit/TestingFramework/issues/28)] - - `Call a local function` actions are now mocked using HTTP actions. This means that the dependencies between a workflow and a .NET Framework function can be broken to enable better unit testing of the workflow. - - Added `IMockResponseBuilder.ThrowsException(Exception exceptionToThrow)` to simulate an exception being thrown by a local .NET Framework function. - - Fixed a typo in the name of the `logging.writeFunctionRuntimeStartupLogs` configuration setting. Previously the setting was named `logging.writeFunctionRuntineStartupLogs` (note the incorrect spelling `Runtine`). [[PR #29](https://github.com/LogicAppUnit/TestingFramework/pull/29), [@jeanpaulsmit](https://github.com/jeanpaulsmit)]
:warning: ***This is a breaking change. Any use of the `writeFunctionRuntineStartupLogs` setting in the `testConfiguration.json` file will need to be updated.*** + and `ActiveDirectoryOAuth` authentication types when running in a local developer environment. [[Issue #30](https://github.com/LogicAppUnit/TestingFramework/issues/30)] +- The `testConfiguration.json` file is now optional. If the file does not exist, or contains an empty JSON document (`{}`), the default values are used for all settings. Previous versions of the framework would fail a test if the configuration file did not exist. [[Issue #28](https://github.com/LogicAppUnit/TestingFramework/issues/28)] +- `Call a local function` actions are now mocked using HTTP actions. This means that the dependencies between a workflow and a .NET Framework function can be broken to enable better unit testing of the workflow. +- Added `IMockResponseBuilder.ThrowsException(Exception exceptionToThrow)` to simulate an exception being thrown by a local .NET Framework function. +- Fixed a typo in the name of the `logging.writeFunctionRuntimeStartupLogs` configuration setting. Previously the setting was named `logging.writeFunctionRuntineStartupLogs` (note the incorrect spelling `Runtine`). [[PR #29](https://github.com/LogicAppUnit/TestingFramework/pull/29), [@jeanpaulsmit](https://github.com/jeanpaulsmit)]
:warning: **_This is a breaking change. Any use of the `writeFunctionRuntineStartupLogs` setting in the `testConfiguration.json` file will need to be updated._** LogicAppUnit.Samples.LogicApps.Tests: - Added a `call-local-function-workflow` workflow and unit tests to demonstrate the mocking of a local .NET Framework function. - # 1.8.0 (24th October 2023) LogicAppUnit Testing Framework: @@ -75,12 +69,11 @@ LogicAppUnit Testing Framework: - Added a new feature to remove the chunking configuration for HTTP actions (`runtimeConfiguration.contentTransfer.transferMode`). This feature is enabled/disabled in the `testConfiguration.json` file using the `workflow.removeHttpChunkingConfiguration` option. The default value for this option is `true`. [[Issue #24](https://github.com/LogicAppUnit/TestingFramework/issues/24)] - Added `IMockResponseBuilder.WithAccepted()` as a short-cut when creating a response with a HTTP 202 (Accepted) status code. - # 1.7.0 (27th July 2023) LogicAppUnit Testing Framework: -- Mock responses can be configured using `ITestRunner.AddMockResponse()` and a fluent API, this includes the definition of the request matching conditions and the response. +- Mock responses can be configured using `ITestRunner.AddMockResponse()` and a fluent API, this includes the definition of the request matching conditions and the response. - Removed public methods `ContentHelper.SerializeObject()`, `ContentHelper.DeserializeObject()` and `ContentHelper.JClone()`, these were for internal use only and are now obsolete. - Include the LogicAppUnit version at the end of the test log. - The maximum execution time for a workflow can now be set in the `testConfiguration.json` file using the `runner.maxWorkflowExecutionDuration` option. Previously this duration was hard-coded to 5 minutes. The default value for this option is 300 seconds (5 minutes). @@ -90,7 +83,6 @@ LogicAppUnit.Samples.LogicApps.Tests: - Added a `fluent-workflow` workflow and unit tests to demonstrate the use of the fluent API. - # 1.6.0 (5th June 2023) LogicAppUnit Testing Framework: @@ -101,13 +93,12 @@ LogicAppUnit Testing Framework: - Added support for workflows using a HTTP trigger where the response action is not the last action in the workflow. Previous versions of the framework assumed that the workflow was complete once the response was received. Now the framework polls the workflow status to ensure that the workflow has completed. [[PR #15](https://github.com/LogicAppUnit/TestingFramework/pull/15), [@atthevergeof](https://github.com/atthevergeof)] - Fixed a bug in `TestRunner.TriggerWorkflow()` where the return value was being incorrectly set to the (disposed) workflow run history API response. The response is now correctly set to the workflow trigger API response. This bug only occurred for workflows that have a non-HTTP trigger (which is then replaced by a HTTP trigger by the framework).
- :warning: ***This is a breaking change. Previously the status code for the response would have been HTTP 200 (OK), now it will be HTTP 202 (Accepted).*** + :warning: **_This is a breaking change. Previously the status code for the response would have been HTTP 200 (OK), now it will be HTTP 202 (Accepted)._** LogicAppUnit.Samples.LogicApps.Tests: - Added a `http-async-workflow` workflow and unit tests to demonstrate the use of the testing framework with HTTP triggers and asynchronous responses. - # 1.5.0 (14th April 2023) LogicAppUnit Testing Framework: @@ -121,14 +112,12 @@ LogicAppUnit.Samples.LogicApps.Tests: - Added an `invoke-workflow` workflow and unit tests to demonstrate the use of the testing framework with child workflows that are invoked from a parent workflow. - # 1.4.0 (21st February 2023) LogicAppUnit Testing Framework: - Changed the logic that updates the `connectionRuntimeUrl` for Managed API connectors so that it works with URL values that include `@appsetting()` references. [[Issue #9](https://github.com/LogicAppUnit/TestingFramework/issues/9)] - # 1.3.0 (1st February 2023) LogicAppUnit Testing Framework: @@ -141,7 +130,6 @@ LogicAppUnit.Samples.LogicApps.Tests: - Updated the `http-workflow` workflow and unit tests to include tracked properties. - # 1.2.0 (9th January 2023) LogicAppUnit Testing Framework: @@ -155,7 +143,6 @@ LogicAppUnit.Samples.LogicApps.Tests: - Added a `loop-workflow` workflow and unit tests to demonstrate the use of the testing framework with a workflow containing actions in an `Until` loop and a `ForEach` loop. - # 1.1.0 (16th December 2022) LogicAppUnit Testing Framework: @@ -174,7 +161,6 @@ LogicAppUnit.Samples.LogicApps.Tests: - Added a `stateless-workflow` workflow and unit tests to demonstrate the use of the testing framework with a stateless workflow, a custom client tracking id and a relative path configured in the HTTP trigger. - # 1.0.0 (9th December 2022) - Initial version. diff --git a/src/LogicAppUnit.Samples.LogicApps.Tests/HttpWorkflow/HttpWorkflowCustomConfigTest.cs b/src/LogicAppUnit.Samples.LogicApps.Tests/HttpWorkflow/HttpWorkflowCustomConfigTest.cs index f3c5089..02cebcf 100644 --- a/src/LogicAppUnit.Samples.LogicApps.Tests/HttpWorkflow/HttpWorkflowCustomConfigTest.cs +++ b/src/LogicAppUnit.Samples.LogicApps.Tests/HttpWorkflow/HttpWorkflowCustomConfigTest.cs @@ -29,7 +29,8 @@ public void TestInitialize() Constants.LOGIC_APP_TEST_EXAMPLE_BASE_PATH, Constants.HTTP_WORKFLOW, localSettingsFilename: "local.settings-custom.json", - parametersFilename: "parameters-custom.json" + parametersFilename: "parameters-custom.json", + connectionsFilename: "connections-custom.json" ); } diff --git a/src/LogicAppUnit.Samples.LogicApps.Tests/ManagedApiConnectorWorkflow/ManagedApiConnectorWorkflowCustomConfigTest.cs b/src/LogicAppUnit.Samples.LogicApps.Tests/ManagedApiConnectorWorkflow/ManagedApiConnectorWorkflowCustomConfigTest.cs index 713fc06..90a7a69 100644 --- a/src/LogicAppUnit.Samples.LogicApps.Tests/ManagedApiConnectorWorkflow/ManagedApiConnectorWorkflowCustomConfigTest.cs +++ b/src/LogicAppUnit.Samples.LogicApps.Tests/ManagedApiConnectorWorkflow/ManagedApiConnectorWorkflowCustomConfigTest.cs @@ -25,8 +25,7 @@ public void TestInitialize() Constants.LOGIC_APP_TEST_EXAMPLE_BASE_PATH, Constants.MANAGED_API_CONNECTOR_WORKFLOW, localSettingsFilename: "local.settings-custom.json", - parametersFilename: "parameters-custom.json", - connectionsFilename: "connections-custom.json" + parametersFilename: "parameters-custom.json" ); } diff --git a/src/LogicAppUnit.Samples.LogicApps.Tests/local.settings-custom.json b/src/LogicAppUnit.Samples.LogicApps.Tests/local.settings-custom.json index e8cfd55..83c6571 100644 --- a/src/LogicAppUnit.Samples.LogicApps.Tests/local.settings-custom.json +++ b/src/LogicAppUnit.Samples.LogicApps.Tests/local.settings-custom.json @@ -14,14 +14,14 @@ "Outlook-ConnectionRuntimeUrl": "https://7606763fdc09952f.10.common.logic-uksouth.azure-apihub.net/apim/outlook/79a0bc680716416e90e17323b581695d/", "Salesforce-ConnectionKey": "any-salesforce-connection-key", "Salesforce-ConnectionRuntimeUrl": "https://7606763fdc09952f.10.common.logic-uksouth.azure-apihub.net/apim/salesforce/fba515601ef14f9193eee596a9dcfd1c/", - "ServiceOne-Url": "https://external-service-one.testing.net/api/v1", + "ServiceOne-Url": "https://external-service-one1.testing.net/api/v1", "ServiceOne-Authentication-APIKey": "serviceone-auth-apikey", "ServiceOne-Authentication-WebHook-APIKey": "serviceone-auth-webhook-apikey", "ServiceTwo-Url": "https://external-service-two.testing.net/api/v1.1", "ServiceTwo-Verison2Url": "https://external-service-two.testing.net/api/v2.0", "ServiceTwo-Authentication-APIKey": "servicetwo-auth-apikey", "ServiceTwo-DefaultAddressType": "business", - "Sql_ConnectionString": "any-sql-connection-string", - "ServiceBus_ConnectionString": "any-servicebus-connection-string" + "Sql_ConnectionString": "any-sql-connection-string-custom", + "ServiceBus_ConnectionString": "any-servicebus-connection-custom-string" } } diff --git a/src/LogicAppUnit.Samples.LogicApps.Tests/testConfiguration.json b/src/LogicAppUnit.Samples.LogicApps.Tests/testConfiguration.json index a23753c..9e840bb 100644 --- a/src/LogicAppUnit.Samples.LogicApps.Tests/testConfiguration.json +++ b/src/LogicAppUnit.Samples.LogicApps.Tests/testConfiguration.json @@ -7,7 +7,9 @@ "workflow": { "externalApiUrlsToMock": [ "https://external-service-one.testing.net", - "https://external-service-two.testing.net" + "https://external-service-two.testing.net", + "https://external-service-one1.testing.net" + ], "builtInConnectorsToMock": [ "executeQuery", @@ -18,5 +20,7 @@ "CreateOrUpdateDocument" ], "autoConfigureWithStatelessRunHistory": true - } + }, + + "localSettingsFilename": "local.settings.dev.json" } \ No newline at end of file diff --git a/src/LogicAppUnit.Samples.LogicApps/local.settings.dev.json b/src/LogicAppUnit.Samples.LogicApps/local.settings.dev.json new file mode 100644 index 0000000..83c6571 --- /dev/null +++ b/src/LogicAppUnit.Samples.LogicApps/local.settings.dev.json @@ -0,0 +1,27 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "APP_KIND": "workflowapp", + "FUNCTIONS_WORKER_RUNTIME": "node", + "WORKFLOWS_SUBSCRIPTION_ID": "c1661296-a732-44b9-8458-d1a0dd19815e", + "WORKFLOWS_LOCATION_NAME": "uksouth", + "WORKFLOWS_RESOURCE_GROUP_NAME": "rg-uks-01", + "AzureBlob-ConnectionString": "any-blob-connection-string", + "AzureQueue-ConnectionString": "any-queue-connection-string", + "Outlook-ConnectionKey": "any-outlook-connection-key", + "Outlook-SubjectPrefix": "INFORMATION", + "Outlook-ConnectionRuntimeUrl": "https://7606763fdc09952f.10.common.logic-uksouth.azure-apihub.net/apim/outlook/79a0bc680716416e90e17323b581695d/", + "Salesforce-ConnectionKey": "any-salesforce-connection-key", + "Salesforce-ConnectionRuntimeUrl": "https://7606763fdc09952f.10.common.logic-uksouth.azure-apihub.net/apim/salesforce/fba515601ef14f9193eee596a9dcfd1c/", + "ServiceOne-Url": "https://external-service-one1.testing.net/api/v1", + "ServiceOne-Authentication-APIKey": "serviceone-auth-apikey", + "ServiceOne-Authentication-WebHook-APIKey": "serviceone-auth-webhook-apikey", + "ServiceTwo-Url": "https://external-service-two.testing.net/api/v1.1", + "ServiceTwo-Verison2Url": "https://external-service-two.testing.net/api/v2.0", + "ServiceTwo-Authentication-APIKey": "servicetwo-auth-apikey", + "ServiceTwo-DefaultAddressType": "business", + "Sql_ConnectionString": "any-sql-connection-string-custom", + "ServiceBus_ConnectionString": "any-servicebus-connection-custom-string" + } +} diff --git a/src/LogicAppUnit.Samples.LogicApps/local.settings.json b/src/LogicAppUnit.Samples.LogicApps/local.settings.json index e8cfd55..83c6571 100644 --- a/src/LogicAppUnit.Samples.LogicApps/local.settings.json +++ b/src/LogicAppUnit.Samples.LogicApps/local.settings.json @@ -14,14 +14,14 @@ "Outlook-ConnectionRuntimeUrl": "https://7606763fdc09952f.10.common.logic-uksouth.azure-apihub.net/apim/outlook/79a0bc680716416e90e17323b581695d/", "Salesforce-ConnectionKey": "any-salesforce-connection-key", "Salesforce-ConnectionRuntimeUrl": "https://7606763fdc09952f.10.common.logic-uksouth.azure-apihub.net/apim/salesforce/fba515601ef14f9193eee596a9dcfd1c/", - "ServiceOne-Url": "https://external-service-one.testing.net/api/v1", + "ServiceOne-Url": "https://external-service-one1.testing.net/api/v1", "ServiceOne-Authentication-APIKey": "serviceone-auth-apikey", "ServiceOne-Authentication-WebHook-APIKey": "serviceone-auth-webhook-apikey", "ServiceTwo-Url": "https://external-service-two.testing.net/api/v1.1", "ServiceTwo-Verison2Url": "https://external-service-two.testing.net/api/v2.0", "ServiceTwo-Authentication-APIKey": "servicetwo-auth-apikey", "ServiceTwo-DefaultAddressType": "business", - "Sql_ConnectionString": "any-sql-connection-string", - "ServiceBus_ConnectionString": "any-servicebus-connection-string" + "Sql_ConnectionString": "any-sql-connection-string-custom", + "ServiceBus_ConnectionString": "any-servicebus-connection-custom-string" } } diff --git a/src/LogicAppUnit/WorkflowTestBase.cs b/src/LogicAppUnit/WorkflowTestBase.cs index 7efc590..d4686e2 100644 --- a/src/LogicAppUnit/WorkflowTestBase.cs +++ b/src/LogicAppUnit/WorkflowTestBase.cs @@ -110,14 +110,14 @@ protected void Initialize(string logicAppBasePath, string workflowName) } /// - /// Initializes all the workflow specific variables which will be used throughout the test executions, including the local settings file. + /// Initializes all the workflow specific variables which will be used throughout the test executions, with optional custom configuration files. /// /// Path to the root folder containing the workflows. /// The name of the workflow. This matches the name of the folder that contains the workflow definition file. - /// The name of the local settings file to be used, this can be used to override the default of local.settings.json. + /// Optional custom local settings filename (e.g., 'local.settings-custom.json'). If specified, searches test project first. If not specified, uses testConfiguration.json setting or default 'local.settings.json' from Logic App project. /// Path to the test project containing test-specific configuration files. If not specified, defaults to current directory. - /// The name of the parameters file to be used (e.g., 'parameterA.json'). If not specified, defaults to 'parameters.json'. - /// The name of the connections file to be used (e.g., 'connectionA.json'). If not specified, defaults to 'connections.json'. + /// Optional custom parameters filename (e.g., 'parameters-custom.json'). If specified, searches test project first. If not specified, uses default 'parameters.json' from Logic App project. + /// Optional custom connections filename (e.g., 'connections-custom.json'). If specified, searches test project first. If not specified, uses default 'connections.json' from Logic App project. protected void Initialize(string logicAppBasePath, string workflowName, string localSettingsFilename = null, string testProjectPath = null, string parametersFilename = null, string connectionsFilename = null) { if (string.IsNullOrEmpty(logicAppBasePath)) @@ -148,6 +148,12 @@ protected void Initialize(string logicAppBasePath, string workflowName, string l if (_testConfig.Azurite.EnableAzuritePortCheck && !AzuriteHelper.IsRunning(_testConfig.Azurite)) throw new TestException($"Azurite is not running on ports {_testConfig.Azurite.BlobServicePort} (Blob service), {_testConfig.Azurite.QueueServicePort} (Queue service) and {_testConfig.Azurite.TableServicePort} (Table service). Logic App workflows cannot run unless all three services are running in Azurite"); + // Track if custom filenames were provided before applying defaults + // Note: testConfiguration.json is NOT considered custom - it just specifies which file in Logic App project to use + var hasCustomLocalSettings = !string.IsNullOrEmpty(localSettingsFilename); + var hasCustomParametersFile = !string.IsNullOrEmpty(parametersFilename); + var hasCustomConnectionsFile = !string.IsNullOrEmpty(connectionsFilename); + // Default test project path to current directory if not specified testProjectPath = testProjectPath ?? Directory.GetCurrentDirectory(); @@ -157,9 +163,9 @@ protected void Initialize(string logicAppBasePath, string workflowName, string l // Process the workflow definition, local settings, parameters and connection files ProcessWorkflowDefinitionFile(logicAppBasePath, workflowName); - ProcessLocalSettingsFile(logicAppBasePath, localSettingsFilename, testProjectPath); - ProcessParametersFile(logicAppBasePath, testProjectPath, parametersFilename); - ProcessConnectionsFile(logicAppBasePath, testProjectPath, connectionsFilename); + ProcessLocalSettingsFile(logicAppBasePath, localSettingsFilename, testProjectPath, hasCustomLocalSettings); + ProcessParametersFile(logicAppBasePath, testProjectPath, parametersFilename, hasCustomParametersFile); + ProcessConnectionsFile(logicAppBasePath, testProjectPath, connectionsFilename, hasCustomConnectionsFile); // Set up the artifacts (schemas, maps) and custom library folders _artifactDirectory = SetSourceDirectory(logicAppBasePath, Constants.ARTIFACTS_FOLDER, "artifacts"); @@ -254,20 +260,24 @@ protected ITestRunner CreateTestRunner(Dictionary localSettingsO #region Helper methods /// - /// Resolves the configuration file path by checking the test project path first, then falling back to the Logic App path. + /// Resolves the configuration file path by checking the test project path first (if searching), then falling back to the Logic App path. /// /// Path to the root folder containing the workflows. /// Path to the test project containing test-specific configuration files. /// The name of the configuration file to locate. - /// The full path to the configuration file, or null if not found. - private string ResolveConfigurationFilePath(string logicAppBasePath, string testProjectPath, string fileName) + /// If true, search test project first; otherwise only search Logic App project. + /// A tuple containing the full path to the configuration file (or null if not found) and a flag indicating if it's from the test project. + private (string filePath, bool isFromTestProject) ResolveConfigurationFilePath(string logicAppBasePath, string testProjectPath, string fileName, bool searchTestProject) { - // Check test project first - var testProjectFilePath = Path.Combine(testProjectPath, fileName); - if (File.Exists(testProjectFilePath)) + // Check test project first (if searchTestProject is true) + if (searchTestProject) { - Console.WriteLine($"Using configuration file from test project: {testProjectFilePath}"); - return testProjectFilePath; + var testProjectFilePath = Path.Combine(testProjectPath, fileName); + if (File.Exists(testProjectFilePath)) + { + Console.WriteLine($"Using configuration file from test project: {testProjectFilePath}"); + return (testProjectFilePath, true); + } } // Fall back to Logic App project @@ -275,10 +285,44 @@ private string ResolveConfigurationFilePath(string logicAppBasePath, string test if (File.Exists(logicAppFilePath)) { Console.WriteLine($"Using configuration file from Logic App project: {logicAppFilePath}"); - return logicAppFilePath; + return (logicAppFilePath, false); + } + + return (null, false); + } + + /// + /// Generic helper to process configuration files with optional custom naming and test project support. + /// + /// Path to the root folder containing the workflows. + /// Path to the test project containing test-specific configuration files. + /// The name of the configuration file to locate. + /// True if a custom filename was explicitly provided by the user. + /// The standard filename to copy to if file is from test project (e.g., 'local.settings.json'). + /// True if the file is optional. + /// The final file path to use, or null if optional and not found. + private string ProcessConfigurationFile(string logicAppBasePath, string testProjectPath, string fileName, bool hasCustomFile, string standardFileName, bool optional = false) + { + // Resolve the file location + var (filePath, isFromTestProject) = ResolveConfigurationFilePath(logicAppBasePath, testProjectPath, fileName, hasCustomFile); + + if (filePath == null) + { + if (optional) + return null; + throw new TestException($"The configuration file '{fileName}' does not exist in test project '{testProjectPath}' or Logic App project '{logicAppBasePath}'"); + } + + // If file is from test project and has custom name, copy to standard location for Logic App runtime + if (isFromTestProject && hasCustomFile && fileName != standardFileName) + { + var targetPath = Path.Combine(logicAppBasePath, standardFileName); + File.Copy(filePath, targetPath, overwrite: true); + Console.WriteLine($"Copied {Path.GetFileName(standardFileName).Replace(".json", "")} file '{fileName}' from test project to '{standardFileName}' for Logic App runtime"); + return targetPath; } - return null; + return filePath; } #endregion Helper methods @@ -314,50 +358,62 @@ private void ProcessWorkflowDefinitionFile(string logicAppBasePath, string workf /// /// Process a workflow local settings file before the test is run. - /// Files in the test project path take precedence over files in the Logic App project. + /// The filename is determined by: Initialize() parameter → testConfiguration.json → default (local.settings.json). + /// If a custom filename was specified via Initialize() parameter, the test project is searched first, then the Logic App project. + /// If the file is found in the test project, it will be copied to the standard location (from testConfiguration.json or default) for the Logic App runtime. /// /// Path to the root folder containing the workflows. - /// The name of the local settings file to be used, this can be used to override the default of local.settings.json. + /// The name of the local settings file passed to Initialize(), or null. /// Path to the test project containing test-specific configuration files. - private void ProcessLocalSettingsFile(string logicAppBasePath, string localSettingsFilename, string testProjectPath) + /// True if a custom filename was explicitly provided via Initialize() parameter. + private void ProcessLocalSettingsFile(string logicAppBasePath, string localSettingsFilename, string testProjectPath, bool hasCustomFile) { - // The name of the local setting file can be set in the test configuration + // Determine the filename: Initialize param → testConfig → default var fileName = SetLocalSettingsFile(localSettingsFilename); - var filePath = ResolveConfigurationFilePath(logicAppBasePath, testProjectPath, fileName); - - if (filePath == null) - throw new TestException($"The local settings file '{fileName}' does not exist in test project '{testProjectPath}' or Logic App project '{logicAppBasePath}'"); - _localSettings = new LocalSettingsWrapper(ReadFromPath(filePath)); + // Determine the standard filename (from testConfig or default) for copying custom files to + var standardFileName = SetLocalSettingsFile(null); + + // Process the file using generic helper + var filePath = ProcessConfigurationFile(logicAppBasePath, testProjectPath, fileName, hasCustomFile, standardFileName); + _localSettings = new LocalSettingsWrapper(ReadFromPath(filePath)); _localSettings.ReplaceExternalUrlsWithMockServer(_testConfig.Workflow.ExternalApiUrlsToMock); } /// /// Process a workflow parameters file before the test is run. /// Files in the test project path take precedence over files in the Logic App project. + /// If a custom-named file is provided from the test project, it will be copied to the standard parameters.json location. /// /// Path to the root folder containing the workflows. /// Path to the test project containing test-specific configuration files. /// The name of the parameters file to be used. - private void ProcessParametersFile(string logicAppBasePath, string testProjectPath, string parametersFilename) + /// True if a custom filename was explicitly provided by the user. + private void ProcessParametersFile(string logicAppBasePath, string testProjectPath, string parametersFilename, bool hasCustomFile) { - var filePath = ResolveConfigurationFilePath(logicAppBasePath, testProjectPath, parametersFilename); + // Process the file using generic helper + var filePath = ProcessConfigurationFile(logicAppBasePath, testProjectPath, parametersFilename, hasCustomFile, Constants.PARAMETERS, optional: true); + _parameters = new ParametersWrapper(ReadFromPath(filePath, optional: true)); } /// /// Process a workflow connections file before the test is run. /// Files in the test project path take precedence over files in the Logic App project. + /// If a custom-named file is provided from the test project, it will be copied to the standard connections.json location. /// /// Path to the root folder containing the workflows. /// Path to the test project containing test-specific configuration files. /// The name of the connections file to be used. - private void ProcessConnectionsFile(string logicAppBasePath, string testProjectPath, string connectionsFilename) + /// True if a custom filename was explicitly provided by the user. + private void ProcessConnectionsFile(string logicAppBasePath, string testProjectPath, string connectionsFilename, bool hasCustomFile) { const string invalidConnectionsMsg = "configured to use the 'ManagedServiceIdentity' authentication type. Only the 'Raw' and 'ActiveDirectoryOAuth' authentication types are allowed in a local developer environment"; - var filePath = ResolveConfigurationFilePath(logicAppBasePath, testProjectPath, connectionsFilename); + // Process the file using generic helper + var filePath = ProcessConfigurationFile(logicAppBasePath, testProjectPath, connectionsFilename, hasCustomFile, Constants.CONNECTIONS, optional: true); + _connections = new ConnectionsWrapper(ReadFromPath(filePath, optional: true), _localSettings, _parameters); _connections.ReplaceManagedApiConnectionUrlsWithMockServer(_testConfig.Workflow.ManagedApisToMock);