* Add unit and integration tests for `DownloadHttpFile` activity - Developed comprehensive unit tests for the `DownloadHttpFile` activity validating method execution (GET, POST), URL handling, headers, and status codes. - Added integration tests to verify functionality like authorization headers, filename extraction, and response stream handling. - Updated `IntegrationTests` project references to include the required `Elsa.Http` module. * Refactor `DownloadHttpFileTests` to simplify test setup and improve reusability - Consolidated test initialization logic into `RunActivityAsync` for cleaner and reusable test setup. - Updated existing tests to use `RunActivityAsync`, reducing boilerplate code. - Unified filename extraction tests into a parameterized theory to improve test coverage and clarity. - Simplified helper handlers with expression-bodied members for readability. * Add comments to clarify HttpResponseMessage disposal responsibility in `DownloadHttpFileTests`. * Ensure `HttpResponseMessage.Content` is always set to prevent null reference issues in `DownloadHttpFileTests`. * Refactor `DownloadHttpFileTests` to ensure `HttpResponseMessage.Content` is always initialized and simplify content assignment logic. * Update `DownloadHttpFileTests` to set an empty `HttpResponseMessage.Content` for testing `HasContent` behavior * Add `GitHubActionsTestLogger` package to test projects and refactor `Directory.Packages.props` - Updated unit and integration test projects to include `GitHubActionsTestLogger` for improved test reporting in CI. - Refactored `Directory.Packages.props` to add conditional dependencies for .NET 8, 9, and 10 compatibility. * Remove `GitHubActionsTestLogger` package from test projects. * Add project reference for unit tests and update `DownloadHttpFileTests` - Included `Elsa.Activities.UnitTests` project reference in `IntegrationTests` to reuse helpers. - Removed redundant `TestHttpMessageHandler` by utilizing shared helper from `UnitTests`. * Refactor `DownloadHttpFileTests` to remove unused methods and simplify imports * Update target framework to .NET 10 and upgrade `Nuke.Components` package to v10.0.0
333 lines
12 KiB
C#
333 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 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<string, string>
|
|
{
|
|
{ "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<HttpHeaders>(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<HttpRequestException>("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<TaskCanceledException>("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<ActivityExecutionContext> ExecuteAsync(
|
|
DownloadHttpFile activity,
|
|
Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> responseHandler) =>
|
|
new ActivityTestFixture(activity).WithHttpServices(responseHandler).ExecuteAsync();
|
|
|
|
private static Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> CreateFileResponseHandler(
|
|
HttpStatusCode statusCode = HttpStatusCode.OK,
|
|
byte[]? content = null,
|
|
string? filename = null,
|
|
string? contentType = null,
|
|
Dictionary<string, string>? 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<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> CreateEmptyResponseHandler()
|
|
{
|
|
return (_, _) =>
|
|
{
|
|
var response = new HttpResponseMessage(HttpStatusCode.OK);
|
|
// Set empty content (ContentLength = 0) to test HasContent returning false
|
|
response.Content = new ByteArrayContent(Array.Empty<byte>());
|
|
response.Content.Headers.ContentLength = 0;
|
|
return Task.FromResult(response);
|
|
};
|
|
}
|
|
}
|