Add unit and integration tests for DownloadHttpFile activity (#7094)

* 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
This commit is contained in:
Sipke Schoorstra 2025-11-24 14:37:06 +01:00 committed by GitHub
parent f0f09e2bce
commit b0d93d347b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 747 additions and 211 deletions

View file

@ -36,6 +36,7 @@ jobs:
dotnet-version: |
8.0.x
9.0.x
10.0.x
- name: Cache NuGet packages
uses: actions/cache@v4

View file

@ -1,191 +1,174 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<PropertyGroup>
<MicrosoftVersion>10.0.0</MicrosoftVersion>
<ResilienceVersion>10.0.0</ResilienceVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Antlr4.Runtime.Standard" Version="4.13.1" PrivateAssets="All" />
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations" Version="0.5.1" />
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.MySql" Version="0.5.1" />
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.PostgreSQL" Version="0.5.1" />
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.SqlServer" Version="0.5.1" />
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.SQLite" Version="0.5.1" />
<PackageVersion Include="AspNetCore.Authentication.ApiKey" Version="9.0.0" />
<PackageVersion Include="AutoMapper" Version="14.0.0" PrivateAssets="All" />
<PackageVersion Include="Azure.Identity" Version="1.14.2" />
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.19.0" />
<PackageVersion Include="Azure.ResourceManager" Version="1.13.1" />
<PackageVersion Include="Azure.ResourceManager.AppContainers" Version="1.3.0" />
<PackageVersion Include="Azure.ResourceManager.Resources" Version="1.9.1" />
<PackageVersion Include="Azure.ResourceManager.ServiceBus" Version="1.1.0" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.6" />
<PackageVersion Include="Bogus" Version="35.6.3" />
<PackageVersion Include="ConfigureAwait.Fody" Version="3.3.2" PrivateAssets="All" />
<PackageVersion Include="Confluent.Kafka" Version="2.10.0" />
<PackageVersion Include="Confluent.SchemaRegistry.Serdes.Avro" Version="2.10.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" PrivateAssets="All" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" PrivateAssets="All" />
<PackageVersion Include="Cronos" Version="0.11.1" />
<PackageVersion Include="Dapper" Version="2.1.66" />
<PackageVersion Include="Datadog.Trace.Bundle" Version="3.16.0" />
<PackageVersion Include="DistributedLock" Version="2.7.1" />
<PackageVersion Include="DistributedLock.Core" Version="1.0.8" />
<PackageVersion Include="DistributedLock.FileSystem" Version="1.0.3" />
<PackageVersion Include="DistributedLock.Postgres" Version="1.3.0" />
<PackageVersion Include="DistributedLock.Redis" Version="1.0.3" />
<PackageVersion Include="Elastic.Clients.Elasticsearch" Version="9.0.6" />
<PackageVersion Include="Elsa.Studio" Version="$(ElsaStudioVersion)" />
<PackageVersion Include="Elsa.Studio.Agents" Version="$(ElsaStudioVersion)" />
<PackageVersion Include="Elsa.Studio.Core.BlazorWasm" Version="$(ElsaStudioVersion)" />
<PackageVersion Include="Elsa.Studio.Login.BlazorWasm" Version="$(ElsaStudioVersion)" />
<PackageVersion Include="FastEndpoints" Version="7.1.1" />
<PackageVersion Include="FastEndpoints.Security" Version="7.1.1" />
<PackageVersion Include="FastEndpoints.Swagger" Version="7.1.1" />
<PackageVersion Include="FluentMigrator" Version="7.1.0" />
<PackageVersion Include="FluentMigrator.Runner" Version="7.1.0" />
<PackageVersion Include="FluentStorage" Version="6.0.0" />
<PackageVersion Include="FluentStorage.Azure.Blobs" Version="5.3.0" />
<PackageVersion Include="Fluid.Core" Version="2.31.0" />
<PackageVersion Include="Fody" Version="6.9.3" PrivateAssets="All" />
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" PrivateAssets="All" />
<PackageVersion Include="Google.Protobuf" Version="3.31.1" />
<PackageVersion Include="Grpc.Net.Client" Version="2.71.0" />
<PackageVersion Include="Grpc.Tools" Version="2.72.0" PrivateAssets="All" />
<PackageVersion Include="Hangfire" Version="1.8.20" />
<PackageVersion Include="Hangfire.MemoryStorage" Version="1.8.1.1" />
<PackageVersion Include="Hangfire.PostgreSql" Version="1.20.12" />
<PackageVersion Include="Hangfire.Storage.SQLite" Version="0.4.2" />
<PackageVersion Include="Humanizer.Core" Version="3.0.1" />
<PackageVersion Include="IronCompress" Version="1.7.0" />
<PackageVersion Include="JetBrains.Annotations" Version="2025.2.2" />
<PackageVersion Include="Jint" Version="4.4.2" />
<PackageVersion Include="LinqKit.Core" Version="1.2.9" />
<PackageVersion Include="MailKit" Version="4.12.1" />
<PackageVersion Include="MassTransit" Version="8.4.1" />
<PackageVersion Include="MassTransit.Azure.ServiceBus.Core" Version="8.4.1" />
<PackageVersion Include="MassTransit.Extensions.DependencyInjection" Version="7.3.1" />
<PackageVersion Include="MassTransit.RabbitMQ" Version="8.4.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.0.0" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.0.2" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="$(ResilienceVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Resilience" Version="$(ResilienceVersion)" />
<PackageVersion Include="Microsoft.Identity.Client" Version="4.72.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="MongoDB.Driver" Version="3.4.0" />
<PackageVersion Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="2.1.0" />
<PackageVersion Include="MongoDB.Driver.Extensions" Version="2.0.2" />
<PackageVersion Include="MySql.Data" Version="9.3.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="Npgsql" Version="9.0.3" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="NuGet.Packaging" Version="6.14.0" />
<PackageVersion Include="NuGet.Protocol" Version="6.14.0" />
<PackageVersion Include="Nuke.Components" Version="10.0.0" />
<PackageVersion Include="Open.Linq.AsyncExtensions" Version="1.2.0" />
<PackageVersion Include="OpenTelemetry" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.AutoInstrumentation" Version="1.11.0" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.12.0-beta.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.StackExchangeRedis" Version="1.12.0-beta.1" />
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="9.23.26000" />
<PackageVersion Include="Polly" Version="8.6.4" />
<PackageVersion Include="Polly.Extensions" Version="8.6.4" />
<PackageVersion Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageVersion Include="PolySharp" Version="1.15.0" PrivateAssets="All" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
<PackageVersion Include="Proto.Actor" Version="1.7.0" />
<PackageVersion Include="Proto.Cluster" Version="1.7.0" />
<PackageVersion Include="Proto.Cluster.CodeGen" Version="1.7.0" />
<PackageVersion Include="Proto.Cluster.AzureContainerApps" Version="1.7.0" />
<PackageVersion Include="Proto.Cluster.Kubernetes" Version="1.7.0" />
<PackageVersion Include="Proto.Cluster.TestProvider" Version="1.7.0" />
<PackageVersion Include="Proto.OpenTelemetry" Version="1.7.0" />
<PackageVersion Include="Proto.Persistence" Version="1.7.0" />
<PackageVersion Include="Proto.Persistence.Sqlite" Version="1.7.0" />
<PackageVersion Include="Proto.Persistence.SqlServer" Version="1.7.0" />
<PackageVersion Include="Proto.Remote" Version="1.7.0" />
<PackageVersion Include="pythonnet" Version="3.1.0-preview2024-09-06" />
<PackageVersion Include="Quartz.Extensions.Hosting" Version="3.14.0" />
<PackageVersion Include="Quartz.Serialization.Json" Version="3.14.0" />
<PackageVersion Include="Refit" Version="8.0.0" />
<PackageVersion Include="Refit.HttpClientFactory" Version="8.0.0" />
<PackageVersion Include="Scrutor" Version="6.1.0" />
<PackageVersion Include="Serilog" Version="4.3.0" />
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageVersion Include="ShortGuid" Version="2.0.1" />
<PackageVersion Include="StackExchange.Redis" Version="2.8.37" />
<PackageVersion Include="System.CommandLine" Version="2.0.0" />
<PackageVersion Include="System.Data.SqlClient" Version="4.9.0" />
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
<PackageVersion Include="System.Linq.Dynamic.Core" Version="1.7.0" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageVersion Include="System.Formats.Asn1" Version="$(MicrosoftVersion)" />
<PackageVersion Include="System.Text.Json" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Testcontainers" Version="4.6.0" />
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.8.1" />
<PackageVersion Include="Testcontainers.RabbitMq" Version="4.8.1" />
<PackageVersion Include="Testcontainers.Redis" Version="4.6.0" />
<PackageVersion Include="ThrottleDebounce" Version="2.0.1" />
<PackageVersion Include="WebhooksCore" Version="0.0.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
<PackageVersion Include="xunit.assert" Version="2.9.3" />
<PackageVersion Include="xunit.extensibility.core" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" PrivateAssets="All" />
<PackageVersion Include="Yarp.ReverseProxy" Version="2.3.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0' or '$(TargetFramework)' == 'net9.0'">
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.11" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(MicrosoftVersion)" />
</ItemGroup>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<!-- Conditional package versions for .NET 8/9 compatibility -->
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0' or '$(TargetFramework)' == 'net9.0'">
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.11"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.11"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.11"/>
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
<PackageVersion Include="Npgsql" Version="9.0.4"/>
</ItemGroup>
<!-- .NET 10 specific package versions -->
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.0"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.0"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0"/>
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0"/>
<PackageVersion Include="Npgsql" Version="10.0.0"/>
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Antlr4.Runtime.Standard" Version="4.13.1" PrivateAssets="All"/>
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations" Version="0.5.1"/>
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.MySql" Version="0.5.1"/>
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.PostgreSQL" Version="0.5.1"/>
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.SqlServer" Version="0.5.1"/>
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.SQLite" Version="0.5.1"/>
<PackageVersion Include="AspNetCore.Authentication.ApiKey" Version="9.0.0"/>
<PackageVersion Include="AutoMapper" Version="15.1.0" PrivateAssets="All"/>
<PackageVersion Include="Azure.Identity" Version="1.17.1"/>
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.20.1"/>
<PackageVersion Include="Azure.ResourceManager" Version="1.13.2"/>
<PackageVersion Include="Azure.ResourceManager.AppContainers" Version="1.5.0"/>
<PackageVersion Include="Azure.ResourceManager.Resources" Version="1.11.1"/>
<PackageVersion Include="Azure.ResourceManager.ServiceBus" Version="1.1.0"/>
<PackageVersion Include="BenchmarkDotNet" Version="0.15.6"/>
<PackageVersion Include="Bogus" Version="35.6.5"/>
<PackageVersion Include="ConfigureAwait.Fody" Version="3.3.2" PrivateAssets="All"/>
<PackageVersion Include="coverlet.collector" Version="6.0.4" PrivateAssets="All"/>
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" PrivateAssets="All"/>
<PackageVersion Include="Cronos" Version="0.11.1"/>
<PackageVersion Include="Dapper" Version="2.1.66"/>
<PackageVersion Include="Datadog.Trace.Bundle" Version="3.31.0"/>
<PackageVersion Include="DistributedLock" Version="2.7.1"/>
<PackageVersion Include="DistributedLock.Core" Version="1.0.8"/>
<PackageVersion Include="DistributedLock.FileSystem" Version="1.0.3"/>
<PackageVersion Include="DistributedLock.Postgres" Version="1.3.0"/>
<PackageVersion Include="DistributedLock.Redis" Version="1.1.1"/>
<PackageVersion Include="Elastic.Clients.Elasticsearch" Version="9.2.1"/>
<PackageVersion Include="FastEndpoints" Version="7.1.1"/>
<PackageVersion Include="FastEndpoints.Security" Version="7.1.1"/>
<PackageVersion Include="FastEndpoints.Swagger" Version="7.1.1"/>
<PackageVersion Include="FluentMigrator" Version="7.1.0"/>
<PackageVersion Include="FluentMigrator.Runner" Version="7.1.0"/>
<PackageVersion Include="FluentStorage" Version="6.0.0"/>
<PackageVersion Include="FluentStorage.Azure.Blobs" Version="6.0.0"/>
<PackageVersion Include="Fluid.Core" Version="2.31.0"/>
<PackageVersion Include="Fody" Version="6.9.3" PrivateAssets="All"/>
<PackageVersion Include="GitHubActionsTestLogger" Version="3.0.0" PrivateAssets="All"/>
<PackageVersion Include="Google.Protobuf" Version="3.31.1"/>
<PackageVersion Include="Grpc.Net.Client" Version="2.71.0"/>
<PackageVersion Include="Grpc.Tools" Version="2.76.0" PrivateAssets="All"/>
<PackageVersion Include="Hangfire" Version="1.8.22"/>
<PackageVersion Include="Hangfire.MemoryStorage" Version="1.8.1.2"/>
<PackageVersion Include="Hangfire.PostgreSql" Version="1.20.12"/>
<PackageVersion Include="Hangfire.Storage.SQLite" Version="0.4.2"/>
<PackageVersion Include="Humanizer.Core" Version="3.0.1"/>
<PackageVersion Include="IronCompress" Version="1.7.0"/>
<PackageVersion Include="JetBrains.Annotations" Version="2025.2.2"/>
<PackageVersion Include="Jint" Version="4.4.2"/>
<PackageVersion Include="LinqKit.Core" Version="1.2.9"/>
<PackageVersion Include="MailKit" Version="4.14.1"/>
<PackageVersion Include="MassTransit" Version="8.5.5"/>
<PackageVersion Include="MassTransit.Azure.ServiceBus.Core" Version="8.5.5"/>
<PackageVersion Include="MassTransit.Extensions.DependencyInjection" Version="7.3.1"/>
<PackageVersion Include="MassTransit.RabbitMQ" Version="8.5.5"/>
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.0"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="10.0.0"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.0"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.0"/>
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="10.0.0"/>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.6"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.0.0"/>
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.1.3"/>
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Resilience" Version="10.0.0"/>
<PackageVersion Include="Microsoft.Identity.Client" Version="4.79.2"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1"/>
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
<PackageVersion Include="Moq" Version="4.20.72"/>
<PackageVersion Include="MongoDB.Driver" Version="3.5.1"/>
<PackageVersion Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="2.1.0"/>
<PackageVersion Include="MongoDB.Driver.Extensions" Version="2.0.2"/>
<PackageVersion Include="MySql.Data" Version="9.5.0"/>
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4"/>
<PackageVersion Include="NSubstitute" Version="5.3.0"/>
<PackageVersion Include="NuGet.Packaging" Version="7.0.0"/>
<PackageVersion Include="NuGet.Protocol" Version="7.0.0"/>
<PackageVersion Include="Nuke.Components" Version="10.0.0"/>
<PackageVersion Include="Open.Linq.AsyncExtensions" Version="1.2.0"/>
<PackageVersion Include="OpenTelemetry" Version="1.14.0"/>
<PackageVersion Include="OpenTelemetry.AutoInstrumentation" Version="1.12.0"/>
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.14.0"/>
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.14.0"/>
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.14.0"/>
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.14.0"/>
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.14.0"/>
<PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.14.0-beta.1"/>
<PackageVersion Include="OpenTelemetry.Instrumentation.StackExchangeRedis" Version="1.14.0-beta.1"/>
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="9.23.26000"/>
<PackageVersion Include="Polly" Version="8.6.5"/>
<PackageVersion Include="Polly.Extensions" Version="8.6.5"/>
<PackageVersion Include="Polly.Extensions.Http" Version="3.0.0"/>
<PackageVersion Include="PolySharp" Version="1.15.0" PrivateAssets="All"/>
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0"/>
<PackageVersion Include="pythonnet" Version="3.1.0-preview2024-09-06"/>
<PackageVersion Include="Quartz.Extensions.Hosting" Version="3.15.1"/>
<PackageVersion Include="Quartz.Serialization.Json" Version="3.15.1"/>
<PackageVersion Include="Refit" Version="8.0.0"/>
<PackageVersion Include="Refit.HttpClientFactory" Version="8.0.0"/>
<PackageVersion Include="Scrutor" Version="6.1.0"/>
<PackageVersion Include="Serilog" Version="4.3.0"/>
<PackageVersion Include="Serilog.Extensions.Logging" Version="10.0.0"/>
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0"/>
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0"/>
<PackageVersion Include="ShortGuid" Version="2.0.1"/>
<PackageVersion Include="StackExchange.Redis" Version="2.10.1"/>
<PackageVersion Include="System.CommandLine" Version="2.0.0"/>
<PackageVersion Include="System.Data.SqlClient" Version="4.9.0"/>
<PackageVersion Include="System.Linq.Async" Version="7.0.0"/>
<PackageVersion Include="System.Linq.Dynamic.Core" Version="1.7.0"/>
<PackageVersion Include="System.Net.Http" Version="4.3.4"/>
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1"/>
<PackageVersion Include="System.Formats.Asn1" Version="10.0.0"/>
<PackageVersion Include="System.Text.Json" Version="10.0.0"/>
<PackageVersion Include="Testcontainers" Version="4.9.0"/>
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.9.0"/>
<PackageVersion Include="Testcontainers.RabbitMq" Version="4.9.0"/>
<PackageVersion Include="Testcontainers.Redis" Version="4.9.0"/>
<PackageVersion Include="ThrottleDebounce" Version="2.0.1"/>
<PackageVersion Include="WebhooksCore" Version="0.0.1"/>
<PackageVersion Include="xunit" Version="2.9.3"/>
<PackageVersion Include="xunit.abstractions" Version="2.0.3"/>
<PackageVersion Include="xunit.assert" Version="2.9.3"/>
<PackageVersion Include="xunit.extensibility.core" Version="2.9.3"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" PrivateAssets="All"/>
<PackageVersion Include="Yarp.ReverseProxy" Version="2.3.0"/>
</ItemGroup>
</Project>

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace />
<NoWarn>CS0649;CS0169;CA1050;CA1822;CA2211;IDE1006</NoWarn>
<NukeRootDirectory>..</NukeRootDirectory>
@ -19,12 +19,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nuke.Components" Version="9.0.4" />
</ItemGroup>
<!--Overridden for vulnerability reasons with dependencies referencing older versions.-->
<ItemGroup>
<PackageReference Include="System.Formats.Asn1" Version="9.0.8" />
<PackageReference Include="Nuke.Components" Version="10.0.0" />
</ItemGroup>
</Project>

View file

@ -8,7 +8,9 @@
<ItemGroup>
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared.Integration\Elsa.Testing.Shared.Integration.csproj" />
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Http\Elsa.Http.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
<ProjectReference Include="..\..\unit\Elsa.Activities.UnitTests\Elsa.Activities.UnitTests.csproj" />
</ItemGroup>
</Project>

View file

@ -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<HttpFile>(activity);
Assert.NotNull(file);
Assert.Equal("document.pdf", file.Filename);
Assert.Equal("application/pdf", file.ContentType);
var stream = workflowResult.GetActivityOutput<Stream>(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<int>(activity, nameof(DownloadHttpFile.StatusCode));
Assert.Equal(200, statusCode);
var file = workflowResult.GetActivityOutput<HttpFile>(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<HttpFile>(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<int>(activity, nameof(DownloadHttpFile.StatusCode));
Assert.Equal(statusCode, actualStatusCode);
}
[Fact(DisplayName = "DownloadHttpFile sets response headers")]
public async Task SetsResponseHeaders()
{
// Arrange
var additionalHeaders = new Dictionary<string, string>
{
{ "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<HttpHeaders>(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<string, string>? 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);
});
}

View file

@ -5,18 +5,18 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared.Integration\Elsa.Testing.Shared.Integration.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Alterations\Elsa.Alterations.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared.Integration\Elsa.Testing.Shared.Integration.csproj"/>
<ProjectReference Include="..\..\..\src\modules\Elsa.Alterations\Elsa.Alterations.csproj"/>
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj"/>
</ItemGroup>
<ItemGroup>
<None Update="Workflows\version-1.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Workflows\version-2.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Workflows\version-1.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Workflows\version-2.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -6,8 +6,8 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared.Integration\Elsa.Testing.Shared.Integration.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Common\Elsa.Common.csproj" />
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared.Integration\Elsa.Testing.Shared.Integration.csproj"/>
<ProjectReference Include="..\..\..\src\modules\Elsa.Common\Elsa.Common.csproj"/>
</ItemGroup>
</Project>

View file

@ -91,4 +91,5 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -1,7 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\..\..\src\modules\Elsa\Elsa.csproj" />
</ItemGroup>

View file

@ -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<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);
};
}
}