diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml
index b9801c2f0..760b28067 100644
--- a/.github/workflows/packages.yml
+++ b/.github/workflows/packages.yml
@@ -36,6 +36,7 @@ jobs:
dotnet-version: |
8.0.x
9.0.x
+ 10.0.x
- name: Cache NuGet packages
uses: actions/cache@v4
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 23eb221dd..01970e991 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,191 +1,174 @@
-
- true
- false
-
-
- 10.0.0
- 10.0.0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build/_build.csproj b/build/_build.csproj
index c004aa8d9..bd32549c9 100644
--- a/build/_build.csproj
+++ b/build/_build.csproj
@@ -2,7 +2,7 @@
Exe
- net9.0
+ net10.0
CS0649;CS0169;CA1050;CA1822;CA2211;IDE1006
..
@@ -19,12 +19,7 @@
-
-
-
-
-
-
+
diff --git a/test/integration/Elsa.Activities.IntegrationTests/Elsa.Activities.IntegrationTests.csproj b/test/integration/Elsa.Activities.IntegrationTests/Elsa.Activities.IntegrationTests.csproj
index 511e3c7ff..5f9082087 100644
--- a/test/integration/Elsa.Activities.IntegrationTests/Elsa.Activities.IntegrationTests.csproj
+++ b/test/integration/Elsa.Activities.IntegrationTests/Elsa.Activities.IntegrationTests.csproj
@@ -8,7 +8,9 @@
+
+
-
+
diff --git a/test/integration/Elsa.Activities.IntegrationTests/Http/DownloadHttpFileTests.cs b/test/integration/Elsa.Activities.IntegrationTests/Http/DownloadHttpFileTests.cs
new file mode 100644
index 000000000..5c5fc3cfb
--- /dev/null
+++ b/test/integration/Elsa.Activities.IntegrationTests/Http/DownloadHttpFileTests.cs
@@ -0,0 +1,224 @@
+using System.Net;
+using Elsa.Activities.UnitTests.Http.Helpers;
+using Elsa.Extensions;
+using Elsa.Http;
+using Elsa.Testing.Shared;
+using Elsa.Workflows;
+using Elsa.Workflows.Models;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit.Abstractions;
+
+namespace Elsa.Activities.IntegrationTests.Http;
+
+public class DownloadHttpFileTests(ITestOutputHelper testOutputHelper)
+{
+ [Fact(DisplayName = "DownloadHttpFile downloads file successfully")]
+ public async Task DownloadsFile_Successfully()
+ {
+ // Arrange
+ var fileContent = "Test file content"u8.ToArray();
+ var handler = CreateFileResponseHandler(fileContent, "document.pdf", "application/pdf");
+
+ // Act
+ var (workflowResult, activity) = await RunActivityAsync("https://example.com/file.pdf", handler);
+
+ // Assert
+ var file = workflowResult.GetActivityOutput(activity);
+ Assert.NotNull(file);
+ Assert.Equal("document.pdf", file.Filename);
+ Assert.Equal("application/pdf", file.ContentType);
+
+ var stream = workflowResult.GetActivityOutput(activity, nameof(DownloadHttpFile.ResponseContentStream));
+ Assert.NotNull(stream);
+ using var reader = new StreamReader(stream);
+ var content = await reader.ReadToEndAsync();
+ Assert.Equal("Test file content", content);
+ }
+
+ [Fact(DisplayName = "DownloadHttpFile handles POST requests")]
+ public async Task HandlesPostRequest()
+ {
+ // Arrange
+ var fileContent = "Response content"u8.ToArray();
+ var handler = CreateFileResponseHandler(fileContent);
+
+ // Act
+ var (workflowResult, activity) = await RunActivityAsync(
+ "https://api.example.com/download",
+ handler,
+ method: "POST",
+ requestContent: new { id = 123 },
+ requestContentType: "application/json");
+
+ // Assert
+ var statusCode = workflowResult.GetActivityOutput(activity, nameof(DownloadHttpFile.StatusCode));
+ Assert.Equal(200, statusCode);
+
+ var file = workflowResult.GetActivityOutput(activity);
+ Assert.NotNull(file);
+ }
+
+ [Fact(DisplayName = "DownloadHttpFile includes authorization header")]
+ public async Task IncludesAuthorizationHeader()
+ {
+ // Arrange
+ var requestCapture = new HttpRequestMessage?[1];
+ var fileContent = "Authorized content"u8.ToArray();
+ var handler = CreateCapturingHandler(fileContent, requestCapture);
+
+ // Act
+ await RunActivityAsync(
+ "https://example.com/secure/file.pdf",
+ handler,
+ authorization: "Bearer test-token");
+
+ // Assert
+ var capturedRequest = requestCapture[0];
+ Assert.NotNull(capturedRequest);
+ Assert.NotNull(capturedRequest.Headers.Authorization);
+ Assert.Equal("Bearer test-token", capturedRequest.Headers.Authorization.ToString());
+ }
+
+ [Theory(DisplayName = "DownloadHttpFile extracts filename correctly")]
+ [InlineData("https://example.com/generate-report", "annual-report.xlsx", "annual-report.xlsx")] // From Content-Disposition
+ [InlineData("https://example.com/downloads/image.png", null, "image.png")] // From URL
+ public async Task ExtractsFilename(string url, string? contentDispositionFilename, string expectedFilename)
+ {
+ // Arrange
+ var fileContent = "File data"u8.ToArray();
+ var handler = CreateFileResponseHandler(fileContent, contentDispositionFilename);
+
+ // Act
+ var (workflowResult, activity) = await RunActivityAsync(url, handler);
+
+ // Assert
+ var file = workflowResult.GetActivityOutput(activity);
+ Assert.NotNull(file);
+ Assert.Equal(expectedFilename, file.Filename);
+ }
+
+ [Theory(DisplayName = "DownloadHttpFile handles various status codes")]
+ [InlineData(200)]
+ [InlineData(201)]
+ [InlineData(404)]
+ public async Task HandlesStatusCodes(int statusCode)
+ {
+ // Arrange
+ var handler = CreateFileResponseHandler(statusCode: (HttpStatusCode)statusCode);
+
+ // Act
+ var (workflowResult, activity) = await RunActivityAsync(
+ "https://example.com/file.pdf",
+ handler,
+ expectedStatusCodes: [statusCode]);
+
+ // Assert
+ var actualStatusCode = workflowResult.GetActivityOutput(activity, nameof(DownloadHttpFile.StatusCode));
+ Assert.Equal(statusCode, actualStatusCode);
+ }
+
+ [Fact(DisplayName = "DownloadHttpFile sets response headers")]
+ public async Task SetsResponseHeaders()
+ {
+ // Arrange
+ var additionalHeaders = new Dictionary
+ {
+ { "X-Rate-Limit", "100" },
+ { "X-Request-Id", "abc123" }
+ };
+ var handler = CreateFileResponseHandler(additionalHeaders: additionalHeaders);
+
+ // Act
+ var (workflowResult, activity) = await RunActivityAsync("https://api.example.com/download", handler);
+
+ // Assert
+ var responseHeaders = workflowResult.GetActivityOutput(activity, nameof(DownloadHttpFile.ResponseHeaders));
+ Assert.NotNull(responseHeaders);
+ Assert.Contains(responseHeaders.Keys, k => k.Equals("X-Rate-Limit", StringComparison.OrdinalIgnoreCase));
+ Assert.Contains(responseHeaders.Keys, k => k.Equals("X-Request-Id", StringComparison.OrdinalIgnoreCase));
+ }
+
+ private async Task<(RunWorkflowResult Result, DownloadHttpFile Activity)> RunActivityAsync(
+ string url,
+ HttpMessageHandler handler,
+ string method = "GET",
+ object? requestContent = null,
+ string? requestContentType = null,
+ string? authorization = null,
+ int[]? expectedStatusCodes = null)
+ {
+ var fixture = CreateFixture(handler);
+ var activity = new DownloadHttpFile
+ {
+ Url = new(new Uri(url)),
+ Method = new(method),
+ ExpectedStatusCodes = new(expectedStatusCodes ?? [200])
+ };
+
+ if (requestContent != null)
+ activity.RequestContent = new(requestContent);
+
+ if (requestContentType != null)
+ activity.RequestContentType = new(requestContentType);
+
+ if (authorization != null)
+ activity.Authorization = new(authorization);
+
+ var result = await fixture.RunActivityAsync(activity);
+ return (result, activity);
+ }
+
+ private WorkflowTestFixture CreateFixture(HttpMessageHandler handler) =>
+ new WorkflowTestFixture(testOutputHelper)
+ .ConfigureElsa(elsa => elsa.UseHttp(http =>
+ {
+ http.HttpClientBuilder = builder => builder.ConfigurePrimaryHttpMessageHandler(() => handler);
+ }));
+
+ private static HttpMessageHandler CreateFileResponseHandler(
+ byte[]? content = null,
+ string? filename = null,
+ string? contentType = null,
+ HttpStatusCode statusCode = HttpStatusCode.OK,
+ Dictionary? additionalHeaders = null) =>
+ new TestHttpMessageHandler((_, _) =>
+ {
+ // Response ownership is transferred to the caller (HttpClient), which will dispose it
+ var response = new HttpResponseMessage(statusCode);
+
+ // Always set Content (even if empty) because DownloadHttpFile accesses Content.Headers
+ var actualContent = content ?? (statusCode == HttpStatusCode.OK ? "Default file content"u8.ToArray() : []);
+ var actualContentType = contentType ?? "application/octet-stream";
+ response.Content = new ByteArrayContent(actualContent);
+ response.Content.Headers.ContentType = new(actualContentType);
+
+ if (filename != null)
+ {
+ response.Content.Headers.ContentDisposition = new("attachment")
+ {
+ FileName = filename
+ };
+ }
+
+ if (additionalHeaders != null)
+ foreach (var header in additionalHeaders)
+ response.Headers.Add(header.Key, header.Value);
+
+ return Task.FromResult(response);
+ });
+
+ private static HttpMessageHandler CreateCapturingHandler(byte[] content, HttpRequestMessage?[] capture) =>
+ new TestHttpMessageHandler((request, _) =>
+ {
+ capture[0] = request;
+
+ // Response ownership is transferred to the caller (HttpClient), which will dispose it
+ var response = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new ByteArrayContent(content)
+ };
+ response.Content.Headers.ContentType = new("application/octet-stream");
+
+ return Task.FromResult(response);
+ });
+}
diff --git a/test/integration/Elsa.Alterations.IntegrationTests/Elsa.Alterations.IntegrationTests.csproj b/test/integration/Elsa.Alterations.IntegrationTests/Elsa.Alterations.IntegrationTests.csproj
index c93acbf44..e749c8d4a 100644
--- a/test/integration/Elsa.Alterations.IntegrationTests/Elsa.Alterations.IntegrationTests.csproj
+++ b/test/integration/Elsa.Alterations.IntegrationTests/Elsa.Alterations.IntegrationTests.csproj
@@ -5,18 +5,18 @@
-
-
-
+
+
+
-
- Always
-
-
- Always
-
+
+ Always
+
+
+ Always
+
diff --git a/test/integration/Elsa.Common.IntegrationTests/Elsa.Common.IntegrationTests.csproj b/test/integration/Elsa.Common.IntegrationTests/Elsa.Common.IntegrationTests.csproj
index cc3bc2079..3cc54f7ff 100644
--- a/test/integration/Elsa.Common.IntegrationTests/Elsa.Common.IntegrationTests.csproj
+++ b/test/integration/Elsa.Common.IntegrationTests/Elsa.Common.IntegrationTests.csproj
@@ -6,8 +6,8 @@
-
-
+
+
-
+
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Elsa.Workflows.IntegrationTests.csproj b/test/integration/Elsa.Workflows.IntegrationTests/Elsa.Workflows.IntegrationTests.csproj
index 9fa6481ce..43f34a790 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Elsa.Workflows.IntegrationTests.csproj
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Elsa.Workflows.IntegrationTests.csproj
@@ -91,4 +91,5 @@
Always
+
diff --git a/test/performance/Elsa.Workflows.PerformanceTests/Elsa.Workflows.PerformanceTests.csproj b/test/performance/Elsa.Workflows.PerformanceTests/Elsa.Workflows.PerformanceTests.csproj
index 57ec54ffe..9f0c772af 100644
--- a/test/performance/Elsa.Workflows.PerformanceTests/Elsa.Workflows.PerformanceTests.csproj
+++ b/test/performance/Elsa.Workflows.PerformanceTests/Elsa.Workflows.PerformanceTests.csproj
@@ -1,7 +1,5 @@
-
-
diff --git a/test/unit/Elsa.Activities.UnitTests/Http/DownloadHttpFileTests.cs b/test/unit/Elsa.Activities.UnitTests/Http/DownloadHttpFileTests.cs
new file mode 100644
index 000000000..301c5e1ee
--- /dev/null
+++ b/test/unit/Elsa.Activities.UnitTests/Http/DownloadHttpFileTests.cs
@@ -0,0 +1,332 @@
+using System.Net;
+using Elsa.Activities.UnitTests.Http.Helpers;
+using Elsa.Extensions;
+using Elsa.Http;
+using Elsa.Testing.Shared;
+using Elsa.Workflows;
+using static Elsa.Activities.UnitTests.Http.Helpers.SendHttpRequestTestHelpers;
+
+namespace Elsa.Activities.UnitTests.Http;
+
+public class DownloadHttpFileTests
+{
+ [Theory]
+ [InlineData("GET", "https://example.com/file.pdf")]
+ [InlineData("POST", "https://api.example.com/download")]
+ [InlineData("PUT", "https://api.example.com/update-file")]
+ public async Task ExecuteAsync_SendsRequest_WithCorrectMethodAndUrl(string method, string url)
+ {
+ // Arrange
+ var expectedUrl = new Uri(url);
+ var expectedMethod = new HttpMethod(method);
+ var requestCapture = new RequestCapture();
+ var responseHandler = CreateFileResponseHandler(requestCapture: requestCapture);
+ var activity = CreateActivity(expectedUrl, method);
+
+ // Act
+ await ExecuteAsync(activity, responseHandler);
+
+ // Assert
+ Assert.NotNull(requestCapture.CapturedRequest);
+ Assert.Equal(expectedMethod, requestCapture.CapturedRequest.Method);
+ Assert.Equal(expectedUrl, requestCapture.CapturedRequest.RequestUri);
+ }
+
+ [Theory]
+ [InlineData("Bearer token123")]
+ [InlineData("Basic YWRtaW46cGFzcw==")]
+ public async Task ExecuteAsync_AddsAuthorizationHeader_WhenProvided(string authorizationHeader)
+ {
+ // Arrange
+ var requestCapture = new RequestCapture();
+ var responseHandler = CreateFileResponseHandler(requestCapture: requestCapture);
+ var activity = CreateActivity(new("https://example.com/file.pdf"), authorization: authorizationHeader);
+
+ // Act
+ await ExecuteAsync(activity, responseHandler);
+
+ // Assert
+ Assert.NotNull(requestCapture.CapturedRequest);
+ Assert.NotNull(requestCapture.CapturedRequest.Headers.Authorization);
+ Assert.Equal(authorizationHeader, requestCapture.CapturedRequest.Headers.Authorization.ToString());
+ }
+
+ [Theory]
+ [InlineData(200)]
+ [InlineData(201)]
+ [InlineData(404)]
+ public async Task ExecuteAsync_SetsStatusCodeOutput(int statusCode)
+ {
+ // Arrange
+ var responseHandler = CreateFileResponseHandler(statusCode: (HttpStatusCode)statusCode);
+ var activity = CreateActivity(new("https://example.com/file.pdf"));
+
+ // Act
+ var context = await ExecuteAsync(activity, responseHandler);
+
+ // Assert
+ var actualStatusCode = context.GetActivityOutput(() => activity.StatusCode);
+ Assert.Equal(statusCode, actualStatusCode);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_SetsFileOutput_WithCorrectFilename()
+ {
+ // Arrange
+ const string expectedFilename = "document.pdf";
+ var responseHandler = CreateFileResponseHandler(filename: expectedFilename);
+ var activity = CreateActivity(new("https://example.com/file.pdf"));
+
+ // Act
+ var context = await ExecuteAsync(activity, responseHandler);
+
+ // Assert
+ var file = context.GetActivityOutput(() => activity.Result) as HttpFile;
+ Assert.NotNull(file);
+ Assert.Equal(expectedFilename, file.Filename);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_SetsFileOutput_WithCorrectContentType()
+ {
+ // Arrange
+ const string expectedContentType = "application/pdf";
+ var responseHandler = CreateFileResponseHandler(contentType: expectedContentType);
+ var activity = CreateActivity(new("https://example.com/file.pdf"));
+
+ // Act
+ var context = await ExecuteAsync(activity, responseHandler);
+
+ // Assert
+ var file = context.GetActivityOutput(() => activity.Result) as HttpFile;
+ Assert.NotNull(file);
+ Assert.Equal(expectedContentType, file.ContentType);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_SetsResponseContentStream_FromFile()
+ {
+ // Arrange
+ var fileContent = "Test file content"u8.ToArray();
+ var responseHandler = CreateFileResponseHandler(content: fileContent);
+ var activity = CreateActivity(new("https://example.com/file.txt"));
+
+ // Act
+ var context = await ExecuteAsync(activity, responseHandler);
+
+ // Assert
+ var stream = context.GetActivityOutput(() => activity.ResponseContentStream) as Stream;
+ Assert.NotNull(stream);
+
+ using var reader = new StreamReader(stream);
+ var actualContent = await reader.ReadToEndAsync();
+ Assert.Equal("Test file content", actualContent);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_SetsResponseHeaders()
+ {
+ // Arrange
+ var expectedHeaders = new Dictionary
+ {
+ { "X-Custom-Header", "CustomValue" },
+ { "X-Request-Id", "12345" }
+ };
+ var responseHandler = CreateFileResponseHandler(additionalHeaders: expectedHeaders);
+ var activity = CreateActivity(new("https://example.com/file.pdf"));
+
+ // Act
+ var context = await ExecuteAsync(activity, responseHandler);
+
+ // Assert
+ var responseHeaders = context.GetActivityOutput(() => activity.ResponseHeaders);
+ Assert.NotNull(responseHeaders);
+ var httpHeaders = Assert.IsType(responseHeaders);
+ Assert.True(httpHeaders.Count > 0);
+ Assert.Contains(httpHeaders.Keys, k => k.Equals("X-Custom-Header", StringComparison.OrdinalIgnoreCase));
+ Assert.Contains(httpHeaders.Keys, k => k.Equals("X-Request-Id", StringComparison.OrdinalIgnoreCase));
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_CompletesSuccessfully()
+ {
+ // Arrange
+ var responseHandler = CreateFileResponseHandler();
+ var activity = CreateActivity(new("https://example.com/file.pdf"));
+
+ // Act
+ var context = await ExecuteAsync(activity, responseHandler);
+
+ // Assert
+ Assert.Equal(ActivityStatus.Completed, context.Status);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_HandlesHttpRequestException()
+ {
+ // Arrange
+ var responseHandler = CreateExceptionHandler("Connection failed");
+ var activity = CreateActivity(new("https://example.com/file.pdf"));
+
+ // Act
+ var context = await ExecuteAsync(activity, responseHandler);
+
+ // Assert
+ Assert.Equal(ActivityStatus.Completed, context.Status);
+ Assert.True(context.JournalData.ContainsKey("Error"));
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_HandlesTaskCanceledException()
+ {
+ // Arrange
+ var responseHandler = CreateExceptionHandler("Request timed out");
+ var activity = CreateActivity(new("https://example.com/file.pdf"));
+
+ // Act
+ var context = await ExecuteAsync(activity, responseHandler);
+
+ // Assert
+ Assert.Equal(ActivityStatus.Completed, context.Status);
+ Assert.True(context.JournalData.ContainsKey("Cancelled"));
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_ReturnsNull_WhenResponseHasNoContent()
+ {
+ // Arrange
+ var responseHandler = CreateEmptyResponseHandler();
+ var activity = CreateActivity(new("https://example.com/empty"));
+
+ // Act
+ var context = await ExecuteAsync(activity, responseHandler);
+
+ // Assert
+ var file = context.GetActivityOutput(() => activity.Result);
+ Assert.Null(file);
+
+ var stream = context.GetActivityOutput(() => activity.ResponseContentStream);
+ Assert.Null(stream);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_ExtractsFilename_FromContentDisposition()
+ {
+ // Arrange
+ const string expectedFilename = "report.xlsx";
+ var responseHandler = CreateFileResponseHandler(filename: expectedFilename);
+ var activity = CreateActivity(new("https://example.com/download"));
+
+ // Act
+ var context = await ExecuteAsync(activity, responseHandler);
+
+ // Assert
+ var file = context.GetActivityOutput(() => activity.Result) as HttpFile;
+ Assert.NotNull(file);
+ Assert.Equal(expectedFilename, file.Filename);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_ExtractsFilename_FromUrlSegment_WhenNoContentDisposition()
+ {
+ // Arrange
+ var responseHandler = CreateFileResponseHandler(filename: null); // No Content-Disposition
+ var activity = CreateActivity(new("https://example.com/files/document.pdf"));
+
+ // Act
+ var context = await ExecuteAsync(activity, responseHandler);
+
+ // Assert
+ var file = context.GetActivityOutput(() => activity.Result) as HttpFile;
+ Assert.NotNull(file);
+ Assert.Equal("document.pdf", file.Filename);
+ }
+
+ [Theory]
+ [InlineData("https://example.com", "/")]
+ [InlineData("https://example.com/", "/")]
+ [InlineData("https://example.com/download", "download")]
+ public async Task ExecuteAsync_ExtractsFilename_FromUrlWhenNoContentDisposition(string url, string expectedFilename)
+ {
+ // Arrange
+ var responseHandler = CreateFileResponseHandler(filename: null);
+ var activity = CreateActivity(new Uri(url));
+
+ // Act
+ var context = await ExecuteAsync(activity, responseHandler);
+
+ // Assert
+ var file = context.GetActivityOutput(() => activity.Result) as HttpFile;
+ Assert.NotNull(file);
+ Assert.Equal(expectedFilename, file.Filename);
+ }
+
+ private static DownloadHttpFile CreateActivity(
+ Uri url,
+ string method = "GET",
+ string? authorization = null) =>
+ new()
+ {
+ Url = new(url),
+ Method = new(method),
+ Authorization = authorization != null ? new(authorization) : null!,
+ ExpectedStatusCodes = new([200])
+ };
+
+ private static Task ExecuteAsync(
+ DownloadHttpFile activity,
+ Func> responseHandler) =>
+ new ActivityTestFixture(activity).WithHttpServices(responseHandler).ExecuteAsync();
+
+ private static Func> CreateFileResponseHandler(
+ HttpStatusCode statusCode = HttpStatusCode.OK,
+ byte[]? content = null,
+ string? filename = null,
+ string? contentType = null,
+ Dictionary? additionalHeaders = null,
+ RequestCapture? requestCapture = null)
+ {
+ return (request, _) =>
+ {
+ requestCapture?.CapturedRequest = request;
+
+ var response = new HttpResponseMessage(statusCode);
+
+ // Always set Content (even if empty) because DownloadHttpFile accesses Content.Headers
+ var actualContent = content ?? (statusCode == HttpStatusCode.OK ? "Default file content"u8.ToArray() : []);
+ var actualContentType = contentType ?? "application/octet-stream";
+ response.Content = new ByteArrayContent(actualContent);
+ response.Content.Headers.ContentType = new(actualContentType);
+
+ if (filename != null)
+ {
+ response.Content.Headers.ContentDisposition = new("attachment")
+ {
+ FileName = filename
+ };
+ }
+
+ if (additionalHeaders != null)
+ {
+ foreach (var header in additionalHeaders)
+ {
+ response.Headers.Add(header.Key, header.Value);
+ }
+ }
+
+ return Task.FromResult(response);
+ };
+ }
+
+ private static Func> CreateEmptyResponseHandler()
+ {
+ return (_, _) =>
+ {
+ var response = new HttpResponseMessage(HttpStatusCode.OK);
+ // Set empty content (ContentLength = 0) to test HasContent returning false
+ response.Content = new ByteArrayContent(Array.Empty());
+ response.Content.Headers.ContentLength = 0;
+ return Task.FromResult(response);
+ };
+ }
+}