elsa-core/test/unit/Elsa.Activities.UnitTests/Http/SendHttpRequestTests.cs
Sipke Schoorstra 2e712d367a
Add OpenTelemetry workflow instrumentation (#7514)
* Add OpenTelemetry workflow instrumentation

* Fix workflow telemetry metric tags

* Tighten telemetry test listeners

* Refine workflow telemetry boundaries

* Address telemetry review feedback

* Complete workflow telemetry coverage

* Address telemetry instrumentation review feedback

* Handle cancelled workflow telemetry

* Refine workflow activity telemetry tags

* Address telemetry review feedback

* Document OpenTelemetry extension coexistence

Agent-Logs-Url: https://github.com/elsa-workflows/elsa-core/sessions/33211c71-c3c9-424c-b7eb-a13ebd4713a3

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Address telemetry PR review comments

* Address telemetry review follow-ups

* Preserve workflow executing status transition order

* Address telemetry review feedback

* Refine workflow telemetry review fixes

* Address telemetry review feedback

* Address workflow instrumentation review feedback

* Fix faulted workflow telemetry tags

* Restrict workflow exception mutation

* Fix canceled activity telemetry status

* Clarify workflow exception access

* Cover HTTP trace context propagation

* Report cancelled workflow telemetry consistently

* Refine telemetry cancellation classification

* Record thrown workflow exceptions on context

* Tighten workflow telemetry exception handling

* Preserve first workflow exception

* Handle workflow cancellation separately

* Clarify workflow telemetry enum references

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
2026-05-22 01:17:05 +02:00

300 lines
12 KiB
C#

using System.Net;
using Elsa.Activities.UnitTests.Http.Helpers;
using Elsa.Extensions;
using Elsa.Http;
using Elsa.Testing.Shared;
using Elsa.Workflows;
using NSubstitute;
namespace Elsa.Activities.UnitTests.Http;
public class SendHttpRequestTests
{
[Theory]
[InlineData("GET", "https://api.example.com/data", "{\"result\": \"success\"}", 200)]
[InlineData("POST", "https://api.example.com/create", "{\"id\": 123}", 201)]
[InlineData("PUT", "https://api.example.com/update", "{\"updated\": true}", 200)]
public async Task Should_Send_Request_And_Handle_Success_Response(string method, string url, string jsonResponse, int expectedStatusCode)
{
// Arrange
var expectedUrl = new Uri(url);
var expectedMethod = new HttpMethod(method);
var expectedHttpStatusCode = (HttpStatusCode)expectedStatusCode;
var requestCapture = new RequestCapture();
var responseHandler = CreateResponseHandler(expectedHttpStatusCode, jsonResponse, requestCapture);
var sendHttpRequest = CreateSendHttpRequest(expectedUrl, method);
// Act
var context = await ExecuteActivityAsync(sendHttpRequest, responseHandler);
// Assert
Assert.NotNull(requestCapture.CapturedRequest);
Assert.Equal(expectedMethod, requestCapture.CapturedRequest.Method);
Assert.Equal(expectedUrl, requestCapture.CapturedRequest.RequestUri);
var statusCodeOutput = context.GetActivityOutput(() => sendHttpRequest.StatusCode);
Assert.Equal(expectedStatusCode, statusCodeOutput);
}
[Theory]
[InlineData("Bearer token123")]
[InlineData("Basic YWRtaW46cGFzcw==")]
[InlineData("ApiKey abc123")]
public async Task Should_Add_Authorization_Header(string authorizationHeader)
{
// Arrange
var expectedUrl = new Uri("https://api.example.com/secure");
var requestCapture = new RequestCapture();
var responseHandler = CreateResponseHandler(HttpStatusCode.OK, null, requestCapture);
var sendHttpRequest = CreateSendHttpRequest(expectedUrl, authorization: authorizationHeader);
// Act
await ExecuteActivityAsync(sendHttpRequest, responseHandler);
// Assert
Assert.NotNull(requestCapture.CapturedRequest);
Assert.NotNull(requestCapture.CapturedRequest.Headers.Authorization);
Assert.Equal(authorizationHeader, requestCapture.CapturedRequest.Headers.Authorization.ToString());
}
[Fact]
public async Task Should_Propagate_Current_Trace_Context()
{
await SendHttpRequestTestHelpers.AssertPropagatesCurrentTraceContextAsync(async (url, responseHandler) =>
{
var sendHttpRequest = CreateSendHttpRequest(url);
await ExecuteActivityAsync(sendHttpRequest, responseHandler);
});
}
[Theory]
[InlineData(new[]{200, 404}, new[]{"mockActivity200", "mockActivity404"}, "mockUnmatchedActivity", HttpStatusCode.NotFound, "mockActivity404")]
[InlineData(new[]{200, 404}, new[]{"mockActivity200", "mockActivity404"}, "mockUnmatchedActivity", HttpStatusCode.InternalServerError, "mockUnmatchedActivity")]
public async Task Should_Schedule_Activity_According_To_Handlers(int[] statusCodes, string[] activityNames, string handler, HttpStatusCode expectedStatusCode, string expectedScheduledActivityName)
{
// Arrange
var (sendHttpRequest, childActivities) = CreateSendHttpRequestWithStatusHandlers(
expectedStatusCodes: [(statusCodes[0], activityNames[0]), (statusCodes[1], activityNames[1])],
unmatchedHandler: handler
);
var responseHandler = CreateResponseHandler(expectedStatusCode);
// Act
var context = await ExecuteActivityAsync(sendHttpRequest, responseHandler);
// Assert that the correct activity was scheduled.
var expectedActivity = childActivities[expectedScheduledActivityName];
var hasScheduledActivity = context.HasScheduledActivity(expectedActivity);
Assert.True(hasScheduledActivity);
}
[Fact]
public async Task Should_Schedule_FailedToConnect_Activity_On_HttpRequestException()
{
// Arrange
var (sendHttpRequest, childActivities) = CreateSendHttpRequestWithErrorHandlers(
failedToConnect: "mockFailedToConnect"
);
var responseHandler = CreateExceptionHandler<HttpRequestException>("Connection failed");
// Act
var context = await ExecuteActivityAsync(sendHttpRequest, responseHandler);
// Assert
var expectedScheduledActivity = childActivities["mockFailedToConnect"];
var hasScheduledExpectedActivity = context.HasScheduledActivity(expectedScheduledActivity);
Assert.True(hasScheduledExpectedActivity);
}
[Fact]
public async Task Should_Schedule_Timeout_Activity_On_TaskCanceledException()
{
// Arrange
var (sendHttpRequest, childActivities) = CreateSendHttpRequestWithErrorHandlers(
timeout: "mockTimeout"
);
var responseHandler = CreateExceptionHandler<TaskCanceledException>("Request timed out");
// Act
var context = await ExecuteActivityAsync(sendHttpRequest, responseHandler);
// Assert
var expectedScheduledActivity = childActivities["mockTimeout"];
var hasScheduledExpectedActivity = context.HasScheduledActivity(expectedScheduledActivity);
Assert.True(hasScheduledExpectedActivity);
}
[Fact]
public async Task Should_Schedule_No_Activity_When_No_Status_Code_Cases_Match_And_No_Unmatched_Handler()
{
// Arrange
var (configured, _) = CreateSendHttpRequestWithStatusHandlers([(200, "handler200")], unmatchedHandler: null);
var responseHandler = CreateResponseHandler(HttpStatusCode.InternalServerError); // 500 - no match
// Act
var context = await ExecuteActivityAsync(configured, responseHandler);
// Assert
var allScheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList();
Assert.Empty(allScheduledActivities);
}
[Fact]
public async Task Should_Set_Response_Headers_Output()
{
// Arrange
var expectedHeaders = new Dictionary<string, string>
{
{ "Custom-Header", "CustomValue" },
{ "X-Rate-Limit", "100" }
};
var responseHandler = CreateResponseHandler(HttpStatusCode.OK, additionalHeaders: expectedHeaders);
var sendHttpRequest = CreateSendHttpRequest(new("https://api.example.com/headers"));
// Act
var context = await ExecuteActivityAsync(sendHttpRequest, responseHandler);
// Assert
var responseHeadersObj = context.GetActivityOutput(() => sendHttpRequest.ResponseHeaders);
var responseHeaders = responseHeadersObj as HttpHeaders;
Assert.NotNull(responseHeaders);
Assert.True(responseHeaders.ContainsKey("Custom-Header"));
Assert.True(responseHeaders.ContainsKey("X-Rate-Limit"));
}
[Fact]
public void Should_Have_Correct_Activity_Attributes()
{
var fixture = new ActivityTestFixture(new SendHttpRequest());
fixture.AssertActivityAttributes(
expectedNamespace: "Elsa",
expectedCategory: "HTTP",
expectedDisplayName: "HTTP Request",
expectedDescription: "Send an HTTP request.",
expectedKind: Elsa.Workflows.ActivityKind.Task
);
}
// Private helper methods placed after all public members
private static Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> CreateResponseHandler(
HttpStatusCode statusCode,
string? content = null,
RequestCapture? requestCapture = null,
Dictionary<string, string>? additionalHeaders = null)
{
return (request, _) =>
{
if (requestCapture != null)
requestCapture.CapturedRequest = request;
return Task.FromResult(ActivityTestFixtureHttpExtensions.CreateHttpResponse(statusCode, content, additionalHeaders));
};
}
private sealed class RequestCapture
{
public HttpRequestMessage? CapturedRequest { get; set; }
}
private static SendHttpRequest CreateSendHttpRequest(
Uri url,
string method = "GET",
object? content = null,
string? contentType = null,
string? authorization = null)
{
return new()
{
Url = new(url),
Method = new(method),
Content = content != null ? new Input<object?>(content) : null!,
ContentType = contentType != null ? new Input<string?>(contentType) : null!,
Authorization = authorization != null ? new Input<string?>(authorization) : null!,
ExpectedStatusCodes = new List<HttpStatusCodeCase>()
};
}
private static Task<ActivityExecutionContext> ExecuteActivityAsync(
SendHttpRequest sendHttpRequest,
Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> responseHandler)
{
return new ActivityTestFixture(sendHttpRequest).WithHttpServices(responseHandler).ExecuteAsync();
}
private static (SendHttpRequest sendHttpRequest, Dictionary<string, IActivity> childActivities) CreateSendHttpRequestWithStatusHandlers(
(int statusCode, string activityName)[] expectedStatusCodes,
string? unmatchedHandler)
{
var childActivities = new Dictionary<string, IActivity>();
// Create mock activities for expected status codes
var expectedStatusCodeCases = expectedStatusCodes.Select(x =>
{
var mockActivity = Substitute.For<IActivity>();
childActivities[x.activityName] = mockActivity;
return new HttpStatusCodeCase(x.statusCode, mockActivity);
}).ToList();
// Create mock activity for unmatched handler
var unmatchedActivity = Substitute.For<IActivity>();
if (unmatchedHandler is not null)
{
childActivities[unmatchedHandler] = unmatchedActivity;
}
var sendHttpRequest = new SendHttpRequest
{
Url = new(new Uri("https://api.example.com/test")),
Method = new("GET"),
ExpectedStatusCodes = expectedStatusCodeCases,
UnmatchedStatusCode = unmatchedHandler is not null ? unmatchedActivity : null
};
return (sendHttpRequest, childActivities);
}
private static (SendHttpRequest sendHttpRequest, Dictionary<string, IActivity> childActivities) CreateSendHttpRequestWithErrorHandlers(
string? failedToConnect = null,
string? timeout = null)
{
var childActivities = new Dictionary<string, IActivity>();
IActivity? failedToConnectActivity = null;
IActivity? timeoutActivity = null;
if (failedToConnect != null)
{
failedToConnectActivity = Substitute.For<IActivity>();
childActivities[failedToConnect] = failedToConnectActivity;
}
if (timeout != null)
{
timeoutActivity = Substitute.For<IActivity>();
childActivities[timeout] = timeoutActivity;
}
var sendHttpRequest = new SendHttpRequest
{
Url = new(new Uri("https://api.example.com/error")),
Method = new("GET"),
ExpectedStatusCodes = new List<HttpStatusCodeCase>(),
FailedToConnect = failedToConnectActivity,
Timeout = timeoutActivity
};
return (sendHttpRequest, childActivities);
}
private static Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> CreateExceptionHandler<TException>(string message)
where TException : Exception
{
return (_, _) => throw ((TException)Activator.CreateInstance(typeof(TException), message)!);
}
}