Merge pull request #7125 from elsa-workflows/feat/tests-http-endpoint
Http endpoint unit, integration and component tests
This commit is contained in:
commit
63061aa657
|
|
@ -0,0 +1,135 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Elsa.Workflows.ComponentTests.Abstractions;
|
||||||
|
using Elsa.Workflows.ComponentTests.Fixtures;
|
||||||
|
|
||||||
|
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.Http;
|
||||||
|
|
||||||
|
public class HttpEndpointContentTests(App app) : AppComponentTest(app)
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task JsonContent_ValidJson_ReturnsEchoedJson()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var testData = new { Name = "John", Age = 30, City = "New York" };
|
||||||
|
var jsonContent = JsonSerializer.Serialize(testData);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await PostJsonContentAsync(jsonContent);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal("application/json", response.Content.Headers.ContentType?.MediaType);
|
||||||
|
|
||||||
|
// Verify JSON structure is preserved
|
||||||
|
var parsedResponse = JsonSerializer.Deserialize<JsonElement>(responseContent);
|
||||||
|
Assert.True(parsedResponse.TryGetProperty("Name", out var nameProperty));
|
||||||
|
Assert.Equal("John", nameProperty.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("{ \"name\": \"John\", invalid }", HttpStatusCode.BadRequest)]
|
||||||
|
[InlineData("", HttpStatusCode.OK, "No content received")]
|
||||||
|
public async Task JsonContent_InvalidOrEmpty_ReturnsExpectedResponse(
|
||||||
|
string jsonContent,
|
||||||
|
HttpStatusCode expectedStatusCode,
|
||||||
|
string? expectedContentFragment = null)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var response = await PostJsonContentAsync(jsonContent);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedStatusCode, response.StatusCode);
|
||||||
|
if (expectedContentFragment != null)
|
||||||
|
{
|
||||||
|
Assert.Contains(expectedContentFragment, responseContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("John Doe", "john@example.com", "Name: John Doe", "Email: john@example.com")]
|
||||||
|
[InlineData("Jane Smith", "jane@test.org", "Name: Jane Smith", "Email: jane@test.org")]
|
||||||
|
public async Task FormData_ValidData_ReturnsExtractedFields(
|
||||||
|
string name,
|
||||||
|
string email,
|
||||||
|
string expectedNameFragment,
|
||||||
|
string expectedEmailFragment)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var formData = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new("name", name),
|
||||||
|
new("email", email)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await PostFormDataAsync(formData);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Contains(expectedNameFragment, responseContent);
|
||||||
|
Assert.Contains(expectedEmailFragment, responseContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FormData_MissingFields_ReturnsUnknownValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var formData = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new("other", "value")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await PostFormDataAsync(formData);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
AssertOkResponseContains(response, responseContent, "Name: unknown", "Email: unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FormData_EmptyForm_ReturnsNoFormDataMessage()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var response = await PostEmptyFormAsync();
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
AssertOkResponseContains(response, responseContent, "No form data received");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> PostJsonContentAsync(string jsonContent)
|
||||||
|
{
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
using var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
|
||||||
|
return await client.PostAsync("test/json-content", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> PostFormDataAsync(IEnumerable<KeyValuePair<string, string>> formData)
|
||||||
|
{
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
using var content = new FormUrlEncodedContent(formData);
|
||||||
|
return await client.PostAsync("test/form-data", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> PostEmptyFormAsync()
|
||||||
|
{
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
using var content = new StringContent("", Encoding.UTF8, "text/plain");
|
||||||
|
return await client.PostAsync("test/form-data", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertOkResponseContains(HttpResponseMessage response, string responseContent, params string[] expectedFragments)
|
||||||
|
{
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
foreach (var fragment in expectedFragments)
|
||||||
|
{
|
||||||
|
Assert.Contains(fragment, responseContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using Elsa.Workflows.ComponentTests.Abstractions;
|
||||||
|
using Elsa.Workflows.ComponentTests.Fixtures;
|
||||||
|
|
||||||
|
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.Http;
|
||||||
|
|
||||||
|
public class HttpEndpointFileUploadTests(App app) : AppComponentTest(app)
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Test file content", "test.txt", "text/plain", "17 bytes")]
|
||||||
|
[InlineData("Sample document", "sample.txt", "text/plain", "15 bytes")]
|
||||||
|
[InlineData("", "empty.txt", "text/plain", "0 bytes")]
|
||||||
|
public async Task FileUpload_SingleFile_ReturnsExpectedMetadata(
|
||||||
|
string fileContent,
|
||||||
|
string fileName,
|
||||||
|
string contentType,
|
||||||
|
string expectedSizeText)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var fileData = Encoding.UTF8.GetBytes(fileContent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await PostSingleFileAsync(fileData, fileName, contentType);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
AssertOkResponseContains(response, responseContent, fileName, expectedSizeText, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FileUpload_MultipleFiles_ReturnsAllFileDetails()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var files = new[]
|
||||||
|
{
|
||||||
|
("File 1 content", "file1.txt", "text/plain"),
|
||||||
|
("File 2 content", "file2.txt", "text/plain")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await PostMultipleFilesAsync(files);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
AssertOkResponseContains(response, responseContent, "file1.txt", "file2.txt", "14 bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FileUpload_NoFiles_ReturnsNoFilesMessage()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var response = await PostFormDataWithoutFilesAsync();
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
AssertOkResponseContains(response, responseContent, "No files uploaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FileUpload_WithFormFields_ProcessesBothFilesAndFields()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var fileData = Encoding.UTF8.GetBytes("Test content");
|
||||||
|
var formFields = new[] { ("name", "John Doe") };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await PostFileWithFormFieldsAsync(fileData, "test.txt", "text/plain", formFields);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
AssertOkResponseContains(response, responseContent, "test.txt", "12 bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> PostSingleFileAsync(
|
||||||
|
byte[] fileData,
|
||||||
|
string fileName,
|
||||||
|
string contentType)
|
||||||
|
{
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
using var content = new MultipartFormDataContent();
|
||||||
|
|
||||||
|
var fileContent = new ByteArrayContent(fileData);
|
||||||
|
fileContent.Headers.ContentType = new(contentType);
|
||||||
|
content.Add(fileContent, "file", fileName);
|
||||||
|
|
||||||
|
return await client.PostAsync("test/file-upload", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> PostMultipleFilesAsync(
|
||||||
|
(string content, string fileName, string contentType)[] files)
|
||||||
|
{
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
using var content = new MultipartFormDataContent();
|
||||||
|
|
||||||
|
for (int i = 0; i < files.Length; i++)
|
||||||
|
{
|
||||||
|
var file = files[i];
|
||||||
|
var fileContent = new ByteArrayContent(Encoding.UTF8.GetBytes(file.content));
|
||||||
|
fileContent.Headers.ContentType = new(file.contentType);
|
||||||
|
content.Add(fileContent, $"file{i + 1}", file.fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await client.PostAsync("test/file-upload", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> PostFormDataWithoutFilesAsync()
|
||||||
|
{
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
using var content = new MultipartFormDataContent();
|
||||||
|
content.Add(new StringContent("value"), "field");
|
||||||
|
|
||||||
|
return await client.PostAsync("test/file-upload", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> PostFileWithFormFieldsAsync(
|
||||||
|
byte[] fileData,
|
||||||
|
string fileName,
|
||||||
|
string contentType,
|
||||||
|
(string name, string value)[] formFields)
|
||||||
|
{
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
using var content = new MultipartFormDataContent();
|
||||||
|
|
||||||
|
// Add file
|
||||||
|
var fileContent = new ByteArrayContent(fileData);
|
||||||
|
fileContent.Headers.ContentType = new(contentType);
|
||||||
|
content.Add(fileContent, "file", fileName);
|
||||||
|
|
||||||
|
// Add form fields
|
||||||
|
foreach (var (name, value) in formFields)
|
||||||
|
{
|
||||||
|
content.Add(new StringContent(value), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await client.PostAsync("test/file-upload", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertOkResponseContains(
|
||||||
|
HttpResponseMessage response,
|
||||||
|
string responseContent,
|
||||||
|
params string[] expectedFragments)
|
||||||
|
{
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
foreach (var fragment in expectedFragments)
|
||||||
|
{
|
||||||
|
Assert.Contains(fragment, responseContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
using Elsa.Workflows.ComponentTests.Abstractions;
|
||||||
|
using Elsa.Workflows.ComponentTests.Fixtures;
|
||||||
|
|
||||||
|
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.Http;
|
||||||
|
|
||||||
|
public class HttpEndpointQueryStringAndHeadersTests(App app) : AppComponentTest(app)
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("name=John", "TestAgent/1.0", "Name: John", "UserAgent: TestAgent/1.0")]
|
||||||
|
[InlineData("name=Jane", "CustomAgent/2.0", "Name: Jane", "UserAgent: CustomAgent/2.0")]
|
||||||
|
[InlineData("name=John&age=30&city=NewYork", "TestAgent/1.0", "Name: John", "UserAgent: TestAgent/1.0")]
|
||||||
|
public async Task QueryStringAndHeaders_WithParameters_ReturnsExtractedData(
|
||||||
|
string queryString,
|
||||||
|
string userAgent,
|
||||||
|
string expectedNameFragment,
|
||||||
|
string expectedUserAgentFragment)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var response = await GetQueryHeadersResponseAsync(queryString, userAgent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains(expectedNameFragment, response);
|
||||||
|
Assert.Contains(expectedUserAgentFragment, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task QueryStringAndHeaders_NoParameters_ReturnsDefaultValues()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var response = await GetQueryHeadersResponseAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("Name: unknown", response);
|
||||||
|
Assert.Contains("UserAgent:", response); // Should contain UserAgent even if empty
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task QueryStringAndHeaders_UrlEncodedQueryString_ReturnsDecodedValue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var encodedName = Uri.EscapeDataString("John Doe");
|
||||||
|
var queryString = $"name={encodedName}";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await GetQueryHeadersResponseAsync(queryString);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("Name: John Doe", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task QueryStringAndHeaders_CustomHeaders_ReturnsHeaderValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var customHeaders = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["X-Custom-Header"] = "CustomValue",
|
||||||
|
["User-Agent"] = "CustomAgent/2.0"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await GetQueryHeadersResponseAsync("name=Jane", customHeaders: customHeaders);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("Name: Jane", response);
|
||||||
|
Assert.Contains("UserAgent: CustomAgent/2.0", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetQueryHeadersResponseAsync(
|
||||||
|
string? queryString = null,
|
||||||
|
string? userAgent = null,
|
||||||
|
Dictionary<string, string>? customHeaders = null)
|
||||||
|
{
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
|
||||||
|
// Build URL
|
||||||
|
var url = "test/query-headers";
|
||||||
|
if (!string.IsNullOrEmpty(queryString))
|
||||||
|
{
|
||||||
|
url += $"?{queryString}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create request
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
|
|
||||||
|
// Add headers to request
|
||||||
|
if (!string.IsNullOrEmpty(userAgent))
|
||||||
|
{
|
||||||
|
request.Headers.Add("User-Agent", userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customHeaders != null)
|
||||||
|
{
|
||||||
|
foreach (var header in customHeaders)
|
||||||
|
{
|
||||||
|
request.Headers.Add(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseMessage = await client.SendAsync(request);
|
||||||
|
responseMessage.EnsureSuccessStatusCode();
|
||||||
|
return await responseMessage.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
using System.Net;
|
||||||
|
using Elsa.Workflows.ComponentTests.Abstractions;
|
||||||
|
using Elsa.Workflows.ComponentTests.Fixtures;
|
||||||
|
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.Http;
|
||||||
|
|
||||||
|
public class HttpEndpointRouteParametersTests(App app) : AppComponentTest(app)
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("123", "456", "UserId: 123, OrderId: 456")]
|
||||||
|
[InlineData("user-abc", "order-xyz", "UserId: user-abc, OrderId: order-xyz")]
|
||||||
|
[InlineData("999", "001", "UserId: 999, OrderId: 001")]
|
||||||
|
public async Task RouteParameters_ValidRouteValues_ReturnsExtractedParameters(string userId, string orderId, string expectedContent)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetStringAsync($"test/users/{userId}/orders/{orderId}");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedContent, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RouteParameters_UrlEncodedValues_ReturnsDecodedParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
var encodedUserId = Uri.EscapeDataString("user@domain.com");
|
||||||
|
var encodedOrderId = Uri.EscapeDataString("order-with-special-chars!");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetStringAsync($"test/users/{encodedUserId}/orders/{encodedOrderId}");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("user@domain.com", response);
|
||||||
|
Assert.Contains("order-with-special-chars!", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RouteParameters_InvalidRoute_ReturnsNotFound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync("test/users/123/invalid-path");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RouteParameters_MissingParameter_ReturnsNotFound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync("test/users/123/orders");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using Elsa.Workflows.ComponentTests.Abstractions;
|
||||||
|
using Elsa.Workflows.ComponentTests.Fixtures;
|
||||||
|
|
||||||
|
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.Http;
|
||||||
|
|
||||||
|
public class HttpEndpointSecurityAndEdgeCasesTests(App app) : AppComponentTest(app)
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task HttpEndpoint_BlockedFileExtensions_RejectsBlockedFiles()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
using var content = new MultipartFormDataContent();
|
||||||
|
var fileContent = new ByteArrayContent(Encoding.UTF8.GetBytes("Malicious file"));
|
||||||
|
fileContent.Headers.ContentType = new("application/octet-stream");
|
||||||
|
content.Add(fileContent, "file", "malware.exe"); // .exe is in blocked extensions
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.PostAsync("test/blocked-extensions", content);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HttpEndpoint_BlockedFileExtensions_AllowsNonBlockedFiles()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
using var content = new MultipartFormDataContent();
|
||||||
|
var fileContent = new ByteArrayContent(Encoding.UTF8.GetBytes("Safe file content"));
|
||||||
|
fileContent.Headers.ContentType = new("text/plain");
|
||||||
|
content.Add(fileContent, "file", "document.txt"); // .txt is not in blocked extensions
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.PostAsync("test/blocked-extensions", content);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal("File upload successful", responseContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("test/basic")]
|
||||||
|
[InlineData("TEST/BASIC")]
|
||||||
|
[InlineData("Test/Basic")]
|
||||||
|
[InlineData("test/BASIC")]
|
||||||
|
[InlineData("TEST/basic")]
|
||||||
|
public async Task HttpEndpoint_CaseSensitiveRoutes_RespectsRouteCase(string route)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync(route);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert - ASP.NET Core routing is case-insensitive by default
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("test/query-headers?name=")]
|
||||||
|
[InlineData("test/query-headers?name")]
|
||||||
|
[InlineData("test/query-headers?=value")]
|
||||||
|
[InlineData("test/query-headers?&&&")]
|
||||||
|
public async Task HttpEndpoint_NullAndEmptyQueryParameters_HandlesCorrectly(string url)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert - Should handle edge cases with query parameters gracefully
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HttpEndpoint_ZeroByteFile_ProcessesCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
using var content = new MultipartFormDataContent();
|
||||||
|
var emptyFile = new ByteArrayContent(Array.Empty<byte>());
|
||||||
|
emptyFile.Headers.ContentType = new("text/plain");
|
||||||
|
content.Add(emptyFile, "file", "empty.txt");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.PostAsync("test/file-upload", content);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Contains("empty.txt", responseContent);
|
||||||
|
Assert.Contains("0 bytes", responseContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using Elsa.Workflows.ComponentTests.Abstractions;
|
||||||
|
using Elsa.Workflows.ComponentTests.Fixtures;
|
||||||
|
|
||||||
|
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.Http;
|
||||||
|
|
||||||
|
public class HttpEndpointTests(App app) : AppComponentTest(app)
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task BasicHttpEndpoint_UnsupportedMethod_ReturnsNotFound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
using var content = new StringContent("", Encoding.UTF8, "text/plain");
|
||||||
|
var response = await client.PostAsync("test/basic", content);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// In this test environment, unsupported methods on unregistered endpoints return NotFound
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("GET")]
|
||||||
|
[InlineData("POST")]
|
||||||
|
[InlineData("PUT")]
|
||||||
|
[InlineData("DELETE")]
|
||||||
|
public async Task MultipleHttpMethods_SupportedMethods_ReturnsMethodName(string method)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
using var request = new HttpRequestMessage(new HttpMethod(method), "test/multi-method");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal($"Method: {method}", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MultipleHttpMethods_UnsupportedMethod_ReturnsNotFound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Patch, "test/multi-method");
|
||||||
|
// Act
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// In this test environment, unsupported methods on unregistered endpoints return NotFound
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HttpEndpoint_WorkflowCompletesCleanlySynchronously()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
|
||||||
|
// Act - Make HTTP request to trigger the workflow
|
||||||
|
var response = await client.GetAsync("test/basic");
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert - Verify the workflow completed and returned the expected response
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal("Basic HttpEndpoint Test Response", responseContent);
|
||||||
|
Assert.Equal("text/plain", response.Content.Headers.ContentType?.MediaType);
|
||||||
|
|
||||||
|
// The fact that we get a response means the workflow completed synchronously
|
||||||
|
// without hanging or requiring additional triggers
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HttpEndpoint_ConcurrentRequests_ProcessesAllSuccessfully()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
var tasks = new List<Task<string>>();
|
||||||
|
|
||||||
|
// Act - Send 10 concurrent requests
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
var index = i;
|
||||||
|
tasks.Add(client.GetStringAsync($"test/users/user-{index}/orders/order-{index}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var responses = await Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(10, responses.Length);
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
Assert.Contains($"UserId: user-{i}", responses[i]);
|
||||||
|
Assert.Contains($"OrderId: order-{i}", responses[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HttpEndpoint_SpecialCharactersInRoute_HandlesCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = WorkflowServer.CreateHttpWorkflowClient();
|
||||||
|
var specialUserId = Uri.EscapeDataString("user@domain.com");
|
||||||
|
var specialOrderId = Uri.EscapeDataString("order-with-special-chars!@#$%");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetStringAsync($"test/users/{specialUserId}/orders/{specialOrderId}");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("user@domain.com", response);
|
||||||
|
Assert.Contains("order-with-special-chars!@#$%", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
# HttpEndpoint Activity Component Tests
|
||||||
|
|
||||||
|
This directory contains comprehensive component tests for the `HttpEndpoint` activity class in Elsa Workflows. The tests are designed to thoroughly validate the functionality, security, and edge cases of the HttpEndpoint activity.
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
### Test Categories
|
||||||
|
|
||||||
|
1. **Core Functionality Tests** (`HttpEndpointTests.cs`)
|
||||||
|
- Basic HTTP endpoint functionality
|
||||||
|
- HTTP method validation
|
||||||
|
- Simple request/response handling
|
||||||
|
- Concurrent request processing
|
||||||
|
- Special character route handling
|
||||||
|
- Synchronous workflow completion validation
|
||||||
|
|
||||||
|
2. **Route Parameters Tests** (`HttpEndpointRouteParametersTests.cs`)
|
||||||
|
- Route parameter extraction
|
||||||
|
- URL encoding/decoding
|
||||||
|
- Invalid route handling
|
||||||
|
|
||||||
|
3. **Query String and Headers Tests** (`HttpEndpointQueryStringAndHeadersTests.cs`)
|
||||||
|
- Query parameter processing
|
||||||
|
- HTTP header extraction
|
||||||
|
- URL encoding in query strings
|
||||||
|
- Custom headers handling
|
||||||
|
|
||||||
|
4. **Content Processing Tests** (`HttpEndpointContentTests.cs`)
|
||||||
|
- JSON content parsing
|
||||||
|
- Form data handling
|
||||||
|
- Content validation
|
||||||
|
- Empty content scenarios
|
||||||
|
|
||||||
|
5. **File Upload Tests** (`HttpEndpointFileUploadTests.cs`)
|
||||||
|
- Single and multiple file uploads
|
||||||
|
- File metadata extraction
|
||||||
|
- Mixed form data and files
|
||||||
|
- Empty file handling
|
||||||
|
|
||||||
|
6. **Security and Edge Cases Tests** (`HttpEndpointSecurityAndEdgeCasesTests.cs`)
|
||||||
|
- Blocked file extensions
|
||||||
|
- File extension validation
|
||||||
|
- Case sensitivity
|
||||||
|
- Security constraints
|
||||||
|
|
||||||
|
### Workflow Test Fixtures
|
||||||
|
|
||||||
|
The `Workflows/` directory contains test workflow implementations:
|
||||||
|
|
||||||
|
- `BasicHttpEndpointWorkflow.cs` - Simple HTTP endpoint
|
||||||
|
- `MultipleHttpMethodsWorkflow.cs` - Multi-method endpoint with request method detection
|
||||||
|
- `RouteParametersWorkflow.cs` - Route parameter extraction
|
||||||
|
- `QueryStringAndHeadersWorkflow.cs` - Query and header processing
|
||||||
|
- `JsonContentWorkflow.cs` - JSON content parsing
|
||||||
|
- `FormDataWorkflow.cs` - Form data handling
|
||||||
|
- `FileUploadWorkflow.cs` - File upload processing
|
||||||
|
- `BlockedFileExtensionWorkflow.cs` - Security-focused workflows including authentication and blocked extensions
|
||||||
|
|
||||||
|
## Key Features Tested
|
||||||
|
|
||||||
|
### Core Functionality
|
||||||
|
- ✅ Basic HTTP endpoint creation
|
||||||
|
- ✅ Multiple HTTP methods (GET, POST, PUT, DELETE)
|
||||||
|
- ✅ Route parameter extraction with complex patterns
|
||||||
|
- ✅ Query string processing
|
||||||
|
- ✅ HTTP header extraction
|
||||||
|
- ✅ Request body parsing (JSON, form data)
|
||||||
|
- ✅ File upload handling
|
||||||
|
- ✅ Response generation
|
||||||
|
|
||||||
|
### Validation and Security
|
||||||
|
- ✅ Request size limits
|
||||||
|
- ✅ File size validation
|
||||||
|
- ✅ File extension allowlist/blocklist
|
||||||
|
- ✅ MIME type validation
|
||||||
|
- ✅ Authentication/authorization hooks
|
||||||
|
- ✅ Malformed request handling
|
||||||
|
|
||||||
|
### Edge Cases and Robustness
|
||||||
|
- ✅ Concurrent request processing
|
||||||
|
- ✅ Large payload handling
|
||||||
|
- ✅ Unicode content support
|
||||||
|
- ✅ URL encoding/decoding
|
||||||
|
- ✅ Empty and null value handling
|
||||||
|
- ✅ Case sensitivity scenarios
|
||||||
|
- ✅ Malformed multipart data
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- ✅ Invalid JSON processing
|
||||||
|
- ✅ Unsupported HTTP methods
|
||||||
|
- ✅ File validation failures
|
||||||
|
- ✅ Request size limit exceeded
|
||||||
|
- ✅ Missing route parameters
|
||||||
|
|
||||||
|
## Test Execution
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- .NET 10.0 SDK
|
||||||
|
- PostgreSQL (for component tests)
|
||||||
|
- Docker (for TestContainers)
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
```bash
|
||||||
|
# Run all HttpEndpoint tests
|
||||||
|
dotnet test --filter "FullyQualifiedName~HttpEndpoint"
|
||||||
|
|
||||||
|
# Run specific test categories
|
||||||
|
dotnet test --filter "HttpEndpointTests"
|
||||||
|
dotnet test --filter "HttpEndpointSecurityAndEdgeCasesTests"
|
||||||
|
dotnet test --filter "HttpEndpointRouteParametersTests"
|
||||||
|
dotnet test --filter "HttpEndpointFileUploadTests"
|
||||||
|
|
||||||
|
# Run with detailed output
|
||||||
|
dotnet test --filter "FullyQualifiedName~HttpEndpoint" --verbosity detailed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Data and Scenarios
|
||||||
|
|
||||||
|
The tests cover a wide range of scenarios:
|
||||||
|
|
||||||
|
- **HTTP Methods**: GET, POST, PUT, DELETE, and invalid methods
|
||||||
|
- **Content Types**: JSON, form-urlencoded, multipart/form-data, plain text
|
||||||
|
- **File Types**: Text files, JSON files, binary files, empty files
|
||||||
|
- **Route Patterns**: Simple routes, parameterized routes, complex nested routes
|
||||||
|
- **Query Parameters**: Single/multiple parameters, encoded values, empty values
|
||||||
|
- **Headers**: Standard headers, custom headers, large headers
|
||||||
|
- **Request Sizes**: Small requests, large requests, oversized requests
|
||||||
|
- **Concurrent Access**: Multiple simultaneous requests
|
||||||
|
- **Unicode Support**: International characters, emojis, special symbols
|
||||||
|
|
||||||
|
## Architecture and Design
|
||||||
|
|
||||||
|
The tests follow the established patterns in the Elsa component test suite:
|
||||||
|
|
||||||
|
1. **Test Inheritance**: All tests inherit from `AppComponentTest` base class
|
||||||
|
2. **Workflow Fixtures**: Each test scenario has corresponding workflow implementations
|
||||||
|
3. **HTTP Client**: Tests use `WorkflowServer.CreateHttpWorkflowClient()` for HTTP calls
|
||||||
|
4. **Assertions**: Comprehensive assertions for status codes, content, and behavior
|
||||||
|
5. **Clean Code**: DRY principles with shared test utilities and patterns
|
||||||
|
|
||||||
|
## Integration with Elsa Framework
|
||||||
|
|
||||||
|
These tests validate integration with:
|
||||||
|
|
||||||
|
- **Workflow Runtime**: Workflow execution and lifecycle
|
||||||
|
- **HTTP Module**: HTTP request/response handling
|
||||||
|
- **Variable System**: Data flow between activities
|
||||||
|
- **Expression System**: Dynamic content generation
|
||||||
|
- **Validation Framework**: Input validation and constraints
|
||||||
|
- **Security Framework**: Authentication and authorization
|
||||||
|
- **Error Handling**: Exception handling and fault tolerance
|
||||||
|
|
||||||
|
## Continuous Integration
|
||||||
|
|
||||||
|
The tests are designed to run in CI environments and validate:
|
||||||
|
|
||||||
|
- Functional correctness
|
||||||
|
- Performance characteristics
|
||||||
|
- Security compliance
|
||||||
|
- Edge case handling
|
||||||
|
- Integration stability
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
When adding new HttpEndpoint functionality:
|
||||||
|
|
||||||
|
1. Add corresponding test workflows in `Workflows/`
|
||||||
|
2. Create test cases covering the new functionality
|
||||||
|
3. Include both positive and negative test scenarios
|
||||||
|
4. Test edge cases and error conditions
|
||||||
|
5. Ensure tests are deterministic and isolated
|
||||||
|
6. Follow existing naming and structure conventions
|
||||||
|
|
||||||
|
## Test Coverage
|
||||||
|
|
||||||
|
The test suite provides comprehensive coverage of:
|
||||||
|
|
||||||
|
- All public properties of HttpEndpoint activity
|
||||||
|
- All supported HTTP methods and content types
|
||||||
|
- Error conditions and validation scenarios
|
||||||
|
- Security constraints and edge cases
|
||||||
|
- Integration with the broader Elsa framework
|
||||||
|
- Real-world usage patterns and scenarios
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
using System.Net;
|
||||||
|
using Elsa.Http;
|
||||||
|
using Elsa.Workflows.Activities;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.Http.Workflows;
|
||||||
|
|
||||||
|
public class BasicHttpEndpointWorkflow : WorkflowBase
|
||||||
|
{
|
||||||
|
public static readonly string DefinitionId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
protected override void Build(IWorkflowBuilder builder)
|
||||||
|
{
|
||||||
|
builder.WithDefinitionId(DefinitionId);
|
||||||
|
builder.Root = new Sequence
|
||||||
|
{
|
||||||
|
Activities =
|
||||||
|
[
|
||||||
|
new HttpEndpoint
|
||||||
|
{
|
||||||
|
Path = new("test/basic"),
|
||||||
|
SupportedMethods = new([HttpMethods.Get]),
|
||||||
|
CanStartWorkflow = true
|
||||||
|
},
|
||||||
|
new WriteHttpResponse
|
||||||
|
{
|
||||||
|
Content = new("Basic HttpEndpoint Test Response"),
|
||||||
|
ContentType = new("text/plain"),
|
||||||
|
StatusCode = new(HttpStatusCode.OK)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
using System.Net;
|
||||||
|
using Elsa.Http;
|
||||||
|
using Elsa.Workflows.Activities.Flowchart.Activities;
|
||||||
|
using Elsa.Workflows.Activities.Flowchart.Models;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Endpoint = Elsa.Workflows.Activities.Flowchart.Models.Endpoint;
|
||||||
|
|
||||||
|
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.Http.Workflows;
|
||||||
|
|
||||||
|
public class BlockedFileExtensionWorkflow : WorkflowBase
|
||||||
|
{
|
||||||
|
public static readonly string DefinitionId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
protected override void Build(IWorkflowBuilder builder)
|
||||||
|
{
|
||||||
|
builder.WithDefinitionId(DefinitionId);
|
||||||
|
|
||||||
|
var filesVariable = builder.WithVariable<IFormFile[]>();
|
||||||
|
|
||||||
|
var httpEndpoint = new HttpEndpoint
|
||||||
|
{
|
||||||
|
Path = new("test/blocked-extensions"),
|
||||||
|
SupportedMethods = new([HttpMethods.Post]),
|
||||||
|
CanStartWorkflow = true,
|
||||||
|
Files = new(filesVariable),
|
||||||
|
BlockedFileExtensions = new([".exe", ".bat", ".sh"]),
|
||||||
|
ExposeInvalidFileExtensionOutcome = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var successResponse = new WriteHttpResponse
|
||||||
|
{
|
||||||
|
Content = new("File upload successful"),
|
||||||
|
ContentType = new("text/plain"),
|
||||||
|
StatusCode = new(HttpStatusCode.OK)
|
||||||
|
};
|
||||||
|
|
||||||
|
var errorResponse = new WriteHttpResponse
|
||||||
|
{
|
||||||
|
Content = new("Blocked file extension detected"),
|
||||||
|
ContentType = new("text/plain"),
|
||||||
|
StatusCode = new(HttpStatusCode.UnsupportedMediaType)
|
||||||
|
};
|
||||||
|
|
||||||
|
builder.Root = new Flowchart
|
||||||
|
{
|
||||||
|
Start = httpEndpoint,
|
||||||
|
Activities = { httpEndpoint, successResponse, errorResponse },
|
||||||
|
Connections =
|
||||||
|
{
|
||||||
|
new Connection(new Endpoint(httpEndpoint, "Done"), new Endpoint(successResponse)),
|
||||||
|
new Connection(new Endpoint(httpEndpoint, "Invalid file extension"), new Endpoint(errorResponse))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
using System.Net;
|
||||||
|
using Elsa.Http;
|
||||||
|
using Elsa.Workflows.Activities;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.Http.Workflows;
|
||||||
|
|
||||||
|
public class FileUploadWorkflow : WorkflowBase
|
||||||
|
{
|
||||||
|
public static readonly string DefinitionId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
protected override void Build(IWorkflowBuilder builder)
|
||||||
|
{
|
||||||
|
builder.WithDefinitionId(DefinitionId);
|
||||||
|
|
||||||
|
var filesVariable = builder.WithVariable<IFormFile[]>();
|
||||||
|
var fileVariable = builder.WithVariable<IFormFile>();
|
||||||
|
|
||||||
|
builder.Root = new Sequence
|
||||||
|
{
|
||||||
|
Activities =
|
||||||
|
[
|
||||||
|
new HttpEndpoint
|
||||||
|
{
|
||||||
|
Path = new("test/file-upload"),
|
||||||
|
SupportedMethods = new([HttpMethods.Post]),
|
||||||
|
CanStartWorkflow = true,
|
||||||
|
Files = new(filesVariable),
|
||||||
|
File = new(fileVariable)
|
||||||
|
},
|
||||||
|
new WriteHttpResponse
|
||||||
|
{
|
||||||
|
Content = new(context =>
|
||||||
|
{
|
||||||
|
var files = filesVariable.Get(context);
|
||||||
|
|
||||||
|
if (files?.Length > 0)
|
||||||
|
{
|
||||||
|
var fileInfos = files.Select(f => $"{f.FileName} ({f.Length} bytes, {f.ContentType})");
|
||||||
|
return $"Files uploaded: {string.Join(", ", fileInfos)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "No files uploaded";
|
||||||
|
}),
|
||||||
|
ContentType = new("text/plain"),
|
||||||
|
StatusCode = new(HttpStatusCode.OK)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
using System.Net;
|
||||||
|
using Elsa.Http;
|
||||||
|
using Elsa.Workflows.Activities;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.Http.Workflows;
|
||||||
|
|
||||||
|
public class FormDataWorkflow : WorkflowBase
|
||||||
|
{
|
||||||
|
public static readonly string DefinitionId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
protected override void Build(IWorkflowBuilder builder)
|
||||||
|
{
|
||||||
|
builder.WithDefinitionId(DefinitionId);
|
||||||
|
|
||||||
|
var parsedContentVariable = builder.WithVariable<object>();
|
||||||
|
|
||||||
|
builder.Root = new Sequence
|
||||||
|
{
|
||||||
|
Activities =
|
||||||
|
[
|
||||||
|
new HttpEndpoint
|
||||||
|
{
|
||||||
|
Path = new("test/form-data"),
|
||||||
|
SupportedMethods = new([HttpMethods.Post]),
|
||||||
|
CanStartWorkflow = true,
|
||||||
|
ParsedContent = new(parsedContentVariable)
|
||||||
|
},
|
||||||
|
new WriteHttpResponse
|
||||||
|
{
|
||||||
|
Content = new(context =>
|
||||||
|
{
|
||||||
|
var content = parsedContentVariable.Get(context);
|
||||||
|
if (content is IDictionary<string, object> formData)
|
||||||
|
{
|
||||||
|
var name = formData.TryGetValue("name", out var nameObj) ? nameObj?.ToString() ?? "unknown" : "unknown";
|
||||||
|
var email = formData.TryGetValue("email", out var emailObj) ? emailObj?.ToString() ?? "unknown" : "unknown";
|
||||||
|
return $"Name: {name}, Email: {email}";
|
||||||
|
}
|
||||||
|
return "No form data received";
|
||||||
|
}),
|
||||||
|
ContentType = new("text/plain"),
|
||||||
|
StatusCode = new(HttpStatusCode.OK)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
using System.Net;
|
||||||
|
using Elsa.Http;
|
||||||
|
using Elsa.Workflows.Activities;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.Http.Workflows;
|
||||||
|
|
||||||
|
public class JsonContentWorkflow : WorkflowBase
|
||||||
|
{
|
||||||
|
public static readonly string DefinitionId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
protected override void Build(IWorkflowBuilder builder)
|
||||||
|
{
|
||||||
|
builder.WithDefinitionId(DefinitionId);
|
||||||
|
|
||||||
|
var parsedContentVariable = builder.WithVariable<object>();
|
||||||
|
|
||||||
|
builder.Root = new Sequence
|
||||||
|
{
|
||||||
|
Activities =
|
||||||
|
[
|
||||||
|
new HttpEndpoint
|
||||||
|
{
|
||||||
|
Path = new("test/json-content"),
|
||||||
|
SupportedMethods = new([HttpMethods.Post]),
|
||||||
|
CanStartWorkflow = true,
|
||||||
|
ParsedContent = new(parsedContentVariable)
|
||||||
|
},
|
||||||
|
new WriteHttpResponse
|
||||||
|
{
|
||||||
|
Content = new(context =>
|
||||||
|
{
|
||||||
|
var content = parsedContentVariable.Get(context);
|
||||||
|
return content != null
|
||||||
|
? JsonSerializer.Serialize(content)
|
||||||
|
: "No content received";
|
||||||
|
}),
|
||||||
|
ContentType = new("application/json"),
|
||||||
|
StatusCode = new(HttpStatusCode.OK)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System.Net;
|
||||||
|
using Elsa.Http;
|
||||||
|
using Elsa.Workflows.Activities;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.Http.Workflows;
|
||||||
|
|
||||||
|
public class MultipleHttpMethodsWorkflow : WorkflowBase
|
||||||
|
{
|
||||||
|
public static readonly string DefinitionId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
protected override void Build(IWorkflowBuilder builder)
|
||||||
|
{
|
||||||
|
builder.WithDefinitionId(DefinitionId);
|
||||||
|
|
||||||
|
var requestMethodVariable = builder.WithVariable<string>();
|
||||||
|
|
||||||
|
builder.Root = new Sequence
|
||||||
|
{
|
||||||
|
Activities =
|
||||||
|
[
|
||||||
|
new HttpEndpoint
|
||||||
|
{
|
||||||
|
Path = new("test/multi-method"),
|
||||||
|
SupportedMethods = new([HttpMethods.Get, HttpMethods.Post, HttpMethods.Put, HttpMethods.Delete]),
|
||||||
|
CanStartWorkflow = true
|
||||||
|
},
|
||||||
|
new SetVariable
|
||||||
|
{
|
||||||
|
Variable = requestMethodVariable,
|
||||||
|
Value = new(context => context.GetRequiredService<IHttpContextAccessor>().HttpContext!.Request.Method)
|
||||||
|
},
|
||||||
|
new WriteHttpResponse
|
||||||
|
{
|
||||||
|
Content = new(context => $"Method: {requestMethodVariable.Get(context)}"),
|
||||||
|
ContentType = new("text/plain"),
|
||||||
|
StatusCode = new(HttpStatusCode.OK)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
using System.Net;
|
||||||
|
using Elsa.Http;
|
||||||
|
using Elsa.Workflows.Activities;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.Http.Workflows;
|
||||||
|
|
||||||
|
public class QueryStringAndHeadersWorkflow : WorkflowBase
|
||||||
|
{
|
||||||
|
public static readonly string DefinitionId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
protected override void Build(IWorkflowBuilder builder)
|
||||||
|
{
|
||||||
|
builder.WithDefinitionId(DefinitionId);
|
||||||
|
|
||||||
|
var queryDataVariable = builder.WithVariable<IDictionary<string, object>>();
|
||||||
|
var headersVariable = builder.WithVariable<IDictionary<string, object>>();
|
||||||
|
|
||||||
|
builder.Root = new Sequence
|
||||||
|
{
|
||||||
|
Activities =
|
||||||
|
[
|
||||||
|
new HttpEndpoint
|
||||||
|
{
|
||||||
|
Path = new("test/query-headers"),
|
||||||
|
SupportedMethods = new([HttpMethods.Get]),
|
||||||
|
CanStartWorkflow = true,
|
||||||
|
QueryStringData = new(queryDataVariable),
|
||||||
|
Headers = new(headersVariable)
|
||||||
|
},
|
||||||
|
new WriteHttpResponse
|
||||||
|
{
|
||||||
|
Content = new(context =>
|
||||||
|
{
|
||||||
|
var queryData = queryDataVariable.Get(context);
|
||||||
|
var headers = headersVariable.Get(context);
|
||||||
|
|
||||||
|
var nameParam = queryData?.ContainsKey("name") == true ? queryData["name"]?.ToString() : "unknown";
|
||||||
|
var userAgent = headers?.ContainsKey("User-Agent") == true ? headers["User-Agent"]?.ToString() : "unknown";
|
||||||
|
|
||||||
|
return $"Name: {nameParam}, UserAgent: {userAgent}";
|
||||||
|
}),
|
||||||
|
ContentType = new("text/plain"),
|
||||||
|
StatusCode = new(HttpStatusCode.OK)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
using System.Net;
|
||||||
|
using Elsa.Http;
|
||||||
|
using Elsa.Workflows.Activities;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.Http.Workflows;
|
||||||
|
|
||||||
|
public class RouteParametersWorkflow : WorkflowBase
|
||||||
|
{
|
||||||
|
public static readonly string DefinitionId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
protected override void Build(IWorkflowBuilder builder)
|
||||||
|
{
|
||||||
|
builder.WithDefinitionId(DefinitionId);
|
||||||
|
|
||||||
|
var routeDataVariable = builder.WithVariable<IDictionary<string, object>>();
|
||||||
|
|
||||||
|
builder.Root = new Sequence
|
||||||
|
{
|
||||||
|
Activities =
|
||||||
|
[
|
||||||
|
new HttpEndpoint
|
||||||
|
{
|
||||||
|
Path = new("test/users/{userId}/orders/{orderId}"),
|
||||||
|
SupportedMethods = new([HttpMethods.Get]),
|
||||||
|
CanStartWorkflow = true,
|
||||||
|
RouteData = new(routeDataVariable)
|
||||||
|
},
|
||||||
|
new WriteHttpResponse
|
||||||
|
{
|
||||||
|
Content = new(context =>
|
||||||
|
{
|
||||||
|
var routeData = routeDataVariable.Get(context);
|
||||||
|
|
||||||
|
// Get route parameters from route data
|
||||||
|
var userId = "unknown";
|
||||||
|
var orderId = "unknown";
|
||||||
|
|
||||||
|
if (routeData is { Count: > 0 })
|
||||||
|
{
|
||||||
|
// Proper route data is available
|
||||||
|
userId = routeData.TryGetValue("userid", out var value) ? value.ToString() ?? "unknown" : "unknown";
|
||||||
|
orderId = routeData.TryGetValue("orderid", out var orderIdValue) ? orderIdValue.ToString() ?? "unknown" : "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"UserId: {userId}, OrderId: {orderId}";
|
||||||
|
}),
|
||||||
|
ContentType = new("text/plain"),
|
||||||
|
StatusCode = new(HttpStatusCode.OK)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
using Elsa.Extensions;
|
||||||
|
|
||||||
|
namespace Elsa.Workflows.ComponentTests.Scenarios.Extensions;
|
||||||
|
|
||||||
|
public class StringExtensionsTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/api/users", "/api/users")]
|
||||||
|
[InlineData("api/users", "/api/users")]
|
||||||
|
[InlineData("/api/users/", "/api/users")]
|
||||||
|
[InlineData("api/users/", "/api/users")]
|
||||||
|
public void NormalizeRoute_VariousInputs_ReturnsNormalizedPath(string inputPath, string expectedPath)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var normalizedPath = inputPath.NormalizeRoute();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedPath, normalizedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using Elsa.Expressions.Models;
|
||||||
|
using Elsa.Extensions;
|
||||||
|
using Elsa.Http;
|
||||||
|
using Elsa.Testing.Shared;
|
||||||
|
using Elsa.Workflows;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
|
namespace Elsa.Activities.UnitTests.Http;
|
||||||
|
|
||||||
|
public class HttpEndpointTests
|
||||||
|
{
|
||||||
|
// TODO: Once `HttpEndpoint` is updated to produce a fault, update this test accordingly.
|
||||||
|
[Fact]
|
||||||
|
public async Task Should_Create_Bookmark_When_No_Http_Context()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateHttpEndpoint("/api/test", new[] { "GET" });
|
||||||
|
var fixture = new ActivityTestFixture(endpoint)
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
// Don't provide HttpContextAccessor, simulating non-HTTP context
|
||||||
|
var mockAccessor = Substitute.For<IHttpContextAccessor>();
|
||||||
|
mockAccessor.HttpContext.Returns((HttpContext?)null);
|
||||||
|
services.AddSingleton(mockAccessor);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var context = await fixture.ExecuteAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// The activity should be suspended (not completed) with a bookmark
|
||||||
|
Assert.False(context.IsCompleted);
|
||||||
|
Assert.True(context.WorkflowExecutionContext.Bookmarks.Any());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static HttpEndpoint CreateHttpEndpoint(
|
||||||
|
string path,
|
||||||
|
string[] methods,
|
||||||
|
bool authorize = false,
|
||||||
|
string? policy = null,
|
||||||
|
TimeSpan? requestTimeout = null,
|
||||||
|
long? requestSizeLimit = null)
|
||||||
|
{
|
||||||
|
var endpoint = new HttpEndpoint(new Input<string>(path))
|
||||||
|
{
|
||||||
|
SupportedMethods = new Input<ICollection<string>>(methods),
|
||||||
|
Authorize = new Input<bool>(authorize)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (policy != null)
|
||||||
|
endpoint.Policy = new Input<string?>(policy);
|
||||||
|
|
||||||
|
if (requestTimeout.HasValue)
|
||||||
|
endpoint.RequestTimeout = new Input<TimeSpan?>(requestTimeout);
|
||||||
|
|
||||||
|
if (requestSizeLimit.HasValue)
|
||||||
|
endpoint.RequestSizeLimit = new Input<long?>(requestSizeLimit);
|
||||||
|
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in a new issue