2025-11-12 14:53:23 +00:00
using System.Diagnostics.CodeAnalysis ;
2025-10-27 09:05:26 +00:00
using System.Text ;
using System.IO.Compression ;
using Elsa.Common ;
using Elsa.Http ;
using Elsa.Http.ContentWriters ;
using Elsa.Http.Parsers ;
using Elsa.Http.Options ;
using Elsa.Testing.Shared ;
using Elsa.Workflows ;
using Microsoft.AspNetCore.Http ;
using Microsoft.AspNetCore.Mvc.Infrastructure ;
2025-10-27 16:07:57 +00:00
using Microsoft.AspNetCore.StaticFiles ;
2025-10-27 09:05:26 +00:00
using Microsoft.Extensions.DependencyInjection ;
using Microsoft.Extensions.Logging ;
using NSubstitute ;
2026-01-12 15:51:30 +00:00
using Elsa.Workflows.Exceptions ;
2025-10-27 09:05:26 +00:00
namespace Elsa.Activities.UnitTests.Http ;
public class WriteFileHttpResponseTests
{
[Theory]
[InlineData("text/plain", "test.txt")]
[InlineData("application/pdf", "document.pdf")]
[InlineData("image/jpeg", "photo.jpg")]
[InlineData("application/zip", "archive.zip")]
public async Task Should_Set_Correct_Content_Type_And_Filename ( string contentType , string filename )
{
// Arrange
var ( activity , httpContext ) = CreateWriteFileHttpResponseActivity ( ) ;
var testContent = "Hello World" u8 . ToArray ( ) ;
2025-11-12 14:53:23 +00:00
activity . Content = new ( testContent ) ;
activity . ContentType = new ( contentType ) ;
activity . Filename = new ( filename ) ;
2025-10-27 09:05:26 +00:00
// Act
var context = await ExecuteActivityAsync ( activity , httpContext ) ;
// Assert
Assert . True ( context . IsCompleted ) ;
Assert . Equal ( contentType , httpContext . Response . ContentType ) ;
2025-10-27 16:07:57 +00:00
Assert . Contains ( $"filename={filename}" , httpContext . Response . Headers . ContentDisposition . ToString ( ) ) ;
2025-10-27 09:05:26 +00:00
}
[Theory]
[InlineData("Hello World")]
[InlineData("")]
public async Task Should_Handle_String_Content ( string content )
{
// Arrange
var ( activity , httpContext ) = CreateWriteFileHttpResponseActivity ( ) ;
2025-11-12 14:53:23 +00:00
activity . Content = new ( content ) ;
activity . Filename = new ( "test.txt" ) ;
2025-10-27 09:05:26 +00:00
// Act
var context = await ExecuteActivityAsync ( activity , httpContext ) ;
// Assert
Assert . True ( context . IsCompleted ) ;
var responseContent = GetResponseContent ( httpContext ) ;
Assert . Equal ( content , responseContent ) ;
}
[Fact]
public async Task Should_Handle_Byte_Array_Content ( )
{
// Arrange
var ( activity , httpContext ) = CreateWriteFileHttpResponseActivity ( ) ;
var testBytes = "Hello World" u8 . ToArray ( ) ;
2025-11-12 14:53:23 +00:00
activity . Content = new ( testBytes ) ;
activity . Filename = new ( "test.bin" ) ;
2025-10-27 09:05:26 +00:00
// Act
var context = await ExecuteActivityAsync ( activity , httpContext ) ;
// Assert
Assert . True ( context . IsCompleted ) ;
var responseBytes = GetResponseBytes ( httpContext ) ;
Assert . Equal ( testBytes , responseBytes ) ;
}
[Fact]
public async Task Should_Handle_Stream_Content ( )
{
// Arrange
var ( activity , httpContext ) = CreateWriteFileHttpResponseActivity ( ) ;
var testContent = "Stream content" ;
var stream = new MemoryStream ( Encoding . UTF8 . GetBytes ( testContent ) ) ;
2025-11-12 14:53:23 +00:00
activity . Content = new ( stream ) ;
activity . Filename = new ( "stream.txt" ) ;
2025-10-27 09:05:26 +00:00
// Act
var context = await ExecuteActivityAsync ( activity , httpContext ) ;
// Assert
Assert . True ( context . IsCompleted ) ;
var responseContent = GetResponseContent ( httpContext ) ;
Assert . Equal ( testContent , responseContent ) ;
}
[Fact]
public async Task Should_Handle_Uri_Content ( )
{
// Arrange
var ( activity , httpContext ) = CreateWriteFileHttpResponseActivity ( ) ;
var testUri = new Uri ( "https://example.com/file.txt" ) ;
2025-11-12 14:53:23 +00:00
activity . Content = new ( testUri ) ;
activity . Filename = new ( "downloaded.txt" ) ;
2025-10-27 09:05:26 +00:00
// Act
var context = await ExecuteActivityAsync ( activity , httpContext ) ;
// Assert
Assert . True ( context . IsCompleted ) ;
// The actual file download would be mocked through the IDownloadableManager
}
[Fact]
public async Task Should_Handle_Multiple_Files_As_Zip ( )
{
// Arrange
var ( activity , httpContext ) = CreateWriteFileHttpResponseActivity ( ) ;
var files = new object [ ]
{
"File 1 content" ,
"File 2 content" u8 . ToArray ( )
} ;
2025-11-12 14:53:23 +00:00
activity . Content = new ( files ) ;
activity . Filename = new ( "archive.zip" ) ;
2025-10-27 09:05:26 +00:00
// Act
var context = await ExecuteActivityAsync ( activity , httpContext ) ;
// Assert
Assert . True ( context . IsCompleted ) ;
Assert . Equal ( "application/zip" , httpContext . Response . ContentType ) ;
2025-10-27 16:07:57 +00:00
Assert . Contains ( "filename=archive.zip" , httpContext . Response . Headers . ContentDisposition . ToString ( ) ) ;
2025-10-27 09:05:26 +00:00
}
[Fact]
public async Task Should_Return_NoContent_When_Content_Is_Null ( )
{
// Arrange
var ( activity , httpContext ) = CreateWriteFileHttpResponseActivity ( ) ;
2025-11-12 14:53:23 +00:00
activity . Content = new ( ( object? ) null ! , string . Empty ) ;
2025-10-27 09:05:26 +00:00
// Act
var context = await ExecuteActivityAsync ( activity , httpContext ) ;
// Assert
Assert . True ( context . IsCompleted ) ;
Assert . Equal ( StatusCodes . Status204NoContent , httpContext . Response . StatusCode ) ;
}
[Fact]
2025-10-27 16:07:57 +00:00
public async Task Should_Return_ApplicationZip_When_Content_Is_Empty_Array ( )
2025-10-27 09:05:26 +00:00
{
// Arrange
var ( activity , httpContext ) = CreateWriteFileHttpResponseActivity ( ) ;
2025-11-12 14:53:23 +00:00
activity . Content = new ( Array . Empty < object > ( ) ) ;
2025-10-27 09:05:26 +00:00
// Act
var context = await ExecuteActivityAsync ( activity , httpContext ) ;
// Assert
Assert . True ( context . IsCompleted ) ;
2025-10-27 16:07:57 +00:00
Assert . Equal ( StatusCodes . Status200OK , httpContext . Response . StatusCode ) ;
Assert . Equal ( "application/zip" , httpContext . Response . ContentType ) ;
2025-10-27 09:05:26 +00:00
}
[Theory]
[InlineData("\"12345\"")]
[InlineData("\"abcdef\"")]
public async Task Should_Set_Entity_Tag_Header ( string entityTag )
{
// Arrange
var ( activity , httpContext ) = CreateWriteFileHttpResponseActivity ( ) ;
var testContent = "Hello World" u8 . ToArray ( ) ;
2025-11-12 14:53:23 +00:00
activity . Content = new ( testContent ) ;
activity . EntityTag = new ( entityTag ) ;
activity . EnableResumableDownloads = new ( true ) ;
2025-10-27 09:05:26 +00:00
// Act
var context = await ExecuteActivityAsync ( activity , httpContext ) ;
// Assert
Assert . True ( context . IsCompleted ) ;
2025-10-27 16:07:57 +00:00
// Strong ETags should match exactly
Assert . Equal ( entityTag , httpContext . Response . Headers . ETag . ToString ( ) ) ;
2025-10-27 09:05:26 +00:00
}
[Fact]
public async Task Should_Enable_Range_Processing_When_Resumable_Downloads_Enabled ( )
{
// Arrange
var ( activity , httpContext ) = CreateWriteFileHttpResponseActivity ( ) ;
var testContent = "Hello World" u8 . ToArray ( ) ;
2025-11-12 14:53:23 +00:00
activity . Content = new ( testContent ) ;
activity . EnableResumableDownloads = new ( true ) ;
activity . EntityTag = new ( "\"test-etag\"" ) ;
2025-10-27 09:05:26 +00:00
// Act
var context = await ExecuteActivityAsync ( activity , httpContext ) ;
// Assert
Assert . True ( context . IsCompleted ) ;
2025-10-27 16:07:57 +00:00
2026-01-12 15:51:30 +00:00
// Range processing and ETag should be handled by FileStreamResult
2025-10-27 09:05:26 +00:00
Assert . True ( httpContext . Response . Headers . ContainsKey ( "ETag" ) ) ;
}
[Fact]
public async Task Should_Not_Set_ETag_When_Resumable_Downloads_Disabled ( )
{
// Arrange
var ( activity , httpContext ) = CreateWriteFileHttpResponseActivity ( ) ;
var testContent = "Hello World" u8 . ToArray ( ) ;
2025-11-12 14:53:23 +00:00
activity . Content = new ( testContent ) ;
activity . EnableResumableDownloads = new ( false ) ;
activity . EntityTag = new ( "\"test-etag\"" ) ;
2025-10-27 09:05:26 +00:00
// Act
var context = await ExecuteActivityAsync ( activity , httpContext ) ;
// Assert
Assert . True ( context . IsCompleted ) ;
Assert . False ( httpContext . Response . Headers . ContainsKey ( "ETag" ) ) ;
}
[Theory]
[InlineData("file.txt", "text/plain")]
[InlineData("document.pdf", "application/pdf")]
[InlineData("image.jpg", "image/jpeg")]
[InlineData("archive.zip", "application/zip")]
[InlineData("unknown.xyz", "application/octet-stream")]
public async Task Should_Determine_Content_Type_From_Filename_When_Not_Specified ( string filename , string expectedContentType )
{
// Arrange
var ( activity , httpContext ) = CreateWriteFileHttpResponseActivity ( ) ;
var testContent = "Hello World" u8 . ToArray ( ) ;
2025-11-12 14:53:23 +00:00
activity . Content = new ( testContent ) ;
activity . Filename = new ( filename ) ;
2025-10-27 09:05:26 +00:00
// ContentType not set, should be determined from filename
// Act
var context = await ExecuteActivityAsync ( activity , httpContext ) ;
// Assert
Assert . True ( context . IsCompleted ) ;
Assert . Equal ( expectedContentType , httpContext . Response . ContentType ) ;
}
[Fact]
public async Task Should_Use_Default_Filename_When_Not_Specified ( )
{
// Arrange
var ( activity , httpContext ) = CreateWriteFileHttpResponseActivity ( ) ;
var testContent = "Hello World" u8 . ToArray ( ) ;
2025-11-12 14:53:23 +00:00
activity . Content = new ( testContent ) ;
2025-10-27 09:05:26 +00:00
// Filename not set, should default to "file.bin"
// Act
var context = await ExecuteActivityAsync ( activity , httpContext ) ;
// Assert
Assert . True ( context . IsCompleted ) ;
2025-10-27 16:07:57 +00:00
Assert . Contains ( "filename=file.bin" , httpContext . Response . Headers . ContentDisposition . ToString ( ) ) ;
2025-10-27 09:05:26 +00:00
}
[Fact]
2026-01-12 15:51:30 +00:00
public async Task Should_Fault_When_No_HttpContext_Available ( )
2025-10-27 09:05:26 +00:00
{
// Arrange
var activity = new WriteFileHttpResponse ( ) ;
var fixture = new ActivityTestFixture ( activity ) ;
fixture . ConfigureServices ( services = >
{
var mockHttpContextAccessor = Substitute . For < IHttpContextAccessor > ( ) ;
mockHttpContextAccessor . HttpContext . Returns ( ( HttpContext ? ) null ) ;
services . AddSingleton ( mockHttpContextAccessor ) ;
AddMockServices ( services ) ;
} ) ;
2026-01-12 15:51:30 +00:00
// Act + Assert
await Assert . ThrowsAsync < FaultException > ( ( ) = > fixture . ExecuteAsync ( ) ) ;
2025-10-27 09:05:26 +00:00
}
[Fact]
public async Task Should_Handle_Downloadable_Object_With_Metadata ( )
{
// Arrange
var ( activity , httpContext ) = CreateWriteFileHttpResponseActivity ( ) ;
var downloadable = new Downloadable
{
Stream = new MemoryStream ( "Test content" u8 . ToArray ( ) ) ,
Filename = "metadata-file.txt" ,
ContentType = "text/plain" ,
ETag = "\"metadata-etag\""
} ;
2025-11-12 14:53:23 +00:00
activity . Content = new ( downloadable ) ;
2025-10-27 09:05:26 +00:00
// Act
var context = await ExecuteActivityAsync ( activity , httpContext ) ;
// Assert
Assert . True ( context . IsCompleted ) ;
Assert . Equal ( "text/plain" , httpContext . Response . ContentType ) ;
2025-10-27 16:07:57 +00:00
Assert . Contains ( "filename=metadata-file.txt" , httpContext . Response . Headers . ContentDisposition . ToString ( ) ) ;
2025-10-27 09:05:26 +00:00
}
2025-10-27 16:07:57 +00:00
[Fact]
public async Task Should_Throw_FormatException_For_Malformed_ETag ( )
2025-10-27 09:05:26 +00:00
{
// Arrange
var ( activity , httpContext ) = CreateWriteFileHttpResponseActivity ( ) ;
2025-10-27 16:07:57 +00:00
var testContent = "Hello World" u8 . ToArray ( ) ;
2025-10-27 09:05:26 +00:00
2025-11-12 14:53:23 +00:00
activity . Content = new ( testContent ) ;
activity . EntityTag = new ( "W/\"weak-etag\"" ) ; // Malformed weak ETag format
activity . EnableResumableDownloads = new ( true ) ;
2025-10-27 09:05:26 +00:00
2025-10-27 16:07:57 +00:00
// Act & Assert
var exception = await Assert . ThrowsAsync < FormatException > ( async ( ) = >
{
await ExecuteActivityAsync ( activity , httpContext ) ;
} ) ;
Assert . Contains ( "The format of value 'W/\"weak-etag\"' is invalid" , exception . Message ) ;
2025-10-27 09:05:26 +00:00
}
// Helper Methods
private static ( WriteFileHttpResponse activity , HttpContext httpContext ) CreateWriteFileHttpResponseActivity ( )
{
var activity = new WriteFileHttpResponse ( ) ;
2025-10-27 16:07:57 +00:00
var httpContext = CreateMockHttpContext ( ) ;
2025-10-27 09:05:26 +00:00
return ( activity , httpContext ) ;
}
2025-10-27 16:07:57 +00:00
private static DefaultHttpContext CreateMockHttpContext ( )
2025-10-27 09:05:26 +00:00
{
var httpContext = new DefaultHttpContext ( ) ;
var services = new ServiceCollection ( ) ;
services . AddLogging ( ) ;
services . AddOptions ( ) ;
// Add required services for FileStreamResult
services . AddSingleton < IActionResultExecutor < Microsoft . AspNetCore . Mvc . FileStreamResult > > (
2025-10-27 16:07:57 +00:00
_ = > new FileStreamResultExecutor ( LoggerFactory . Create ( b = > b . AddConsole ( ) ) )
2025-10-27 09:05:26 +00:00
) ;
2025-10-27 16:07:57 +00:00
// Add IContentTypeProvider mock
services . AddSingleton ( CreateContentTypeProviderMock ( ) ) ;
2025-10-27 09:05:26 +00:00
2025-10-27 16:07:57 +00:00
httpContext . RequestServices = services . BuildServiceProvider ( ) ;
2025-10-27 09:05:26 +00:00
httpContext . Response . Body = new MemoryStream ( ) ;
return httpContext ;
}
2025-10-27 16:07:57 +00:00
private static IContentTypeProvider CreateContentTypeProviderMock ( )
{
var mockContentTypeProvider = Substitute . For < IContentTypeProvider > ( ) ;
mockContentTypeProvider . TryGetContentType ( Arg . Any < string > ( ) , out Arg . Any < string? > ( ) )
. Returns ( callInfo = >
{
var filename = callInfo . ArgAt < string > ( 0 ) ;
var extension = Path . GetExtension ( filename ) . ToLowerInvariant ( ) ;
var contentType = extension switch
{
".txt" = > "text/plain" ,
".pdf" = > "application/pdf" ,
".jpg" or ".jpeg" = > "image/jpeg" ,
".png" = > "image/png" ,
".zip" = > "application/zip" ,
".html" = > "text/html" ,
".css" = > "text/css" ,
".js" = > "application/javascript" ,
".json" = > "application/json" ,
".xml" = > "application/xml" ,
_ = > null
} ;
callInfo [ 1 ] = contentType ;
return contentType ! = null ;
} ) ;
return mockContentTypeProvider ;
}
2025-10-27 09:05:26 +00:00
private static async Task < ActivityExecutionContext > ExecuteActivityAsync ( WriteFileHttpResponse activity , HttpContext httpContext )
{
var fixture = new ActivityTestFixture ( activity ) ;
fixture . ConfigureServices ( services = >
{
var mockHttpContextAccessor = Substitute . For < IHttpContextAccessor > ( ) ;
mockHttpContextAccessor . HttpContext . Returns ( httpContext ) ;
services . AddSingleton ( mockHttpContextAccessor ) ;
services . AddSingleton ( CreateMockHttpActivityOptions ( ) ) ;
AddHttpContentFactories ( services ) ;
AddHttpContentParsers ( services ) ;
AddMockServices ( services ) ;
services . AddSingleton ( Substitute . For < IStimulusHasher > ( ) ) ;
services . AddLogging ( ) ;
} ) ;
return await fixture . ExecuteAsync ( ) ;
}
private static Microsoft . Extensions . Options . IOptions < HttpActivityOptions > CreateMockHttpActivityOptions ( )
{
var options = new HttpActivityOptions
{
WriteHttpResponseSynchronously = false
} ;
var mockOptions = Substitute . For < Microsoft . Extensions . Options . IOptions < HttpActivityOptions > > ( ) ;
mockOptions . Value . Returns ( options ) ;
return mockOptions ;
}
private static void AddHttpContentFactories ( IServiceCollection services )
{
services . AddSingleton < IHttpContentFactory , JsonContentFactory > ( ) ;
services . AddSingleton < IHttpContentFactory , TextContentFactory > ( ) ;
services . AddSingleton < IHttpContentFactory , XmlContentFactory > ( ) ;
services . AddSingleton < IHttpContentFactory , FormUrlEncodedHttpContentFactory > ( ) ;
}
private static void AddHttpContentParsers ( IServiceCollection services )
{
services . AddSingleton < IHttpContentParser , JsonHttpContentParser > ( ) ;
services . AddSingleton < IHttpContentParser , PlainTextHttpContentParser > ( ) ;
services . AddSingleton < IHttpContentParser , XmlHttpContentParser > ( ) ;
services . AddSingleton < IHttpContentParser , TextHtmlHttpContentParser > ( ) ;
services . AddSingleton < IHttpContentParser , FileHttpContentParser > ( ) ;
}
private static void AddMockServices ( IServiceCollection services )
{
2025-10-27 16:07:57 +00:00
services . AddSingleton ( CreateDownloadableManagerMock ( ) ) ;
services . AddSingleton ( CreateContentTypeProviderMock ( ) ) ;
services . AddSingleton ( Substitute . For < IFileCacheStorageProvider > ( ) ) ;
services . AddSingleton ( CreateFileCacheOptionsMock ( ) ) ;
services . AddSingleton ( CreateSystemClockMock ( ) ) ;
services . AddSingleton ( Substitute . For < IStimulusHasher > ( ) ) ;
services . AddLogging ( ) ;
RegisterZipManager ( services ) ;
}
private static IDownloadableManager CreateDownloadableManagerMock ( )
{
var mock = Substitute . For < IDownloadableManager > ( ) ;
mock . GetDownloadablesAsync ( Arg . Any < object > ( ) , Arg . Any < DownloadableOptions > ( ) , Arg . Any < CancellationToken > ( ) )
2025-10-27 09:05:26 +00:00
. Returns ( callInfo = >
{
var content = callInfo . ArgAt < object > ( 0 ) ;
return CreateMockDownloadables ( content ) . Select ( d = > new Func < ValueTask < Downloadable > > ( ( ) = > ValueTask . FromResult ( d ) ) ) ;
} ) ;
2025-10-27 16:07:57 +00:00
return mock ;
}
2025-10-27 09:05:26 +00:00
2025-10-27 16:07:57 +00:00
private static Microsoft . Extensions . Options . IOptions < HttpFileCacheOptions > CreateFileCacheOptionsMock ( )
{
var mock = Substitute . For < Microsoft . Extensions . Options . IOptions < HttpFileCacheOptions > > ( ) ;
mock . Value . Returns ( new HttpFileCacheOptions { TimeToLive = TimeSpan . FromHours ( 1 ) } ) ;
return mock ;
}
2025-10-27 09:05:26 +00:00
2025-10-27 16:07:57 +00:00
private static ISystemClock CreateSystemClockMock ( )
{
var mock = Substitute . For < ISystemClock > ( ) ;
mock . UtcNow . Returns ( DateTimeOffset . UtcNow ) ;
return mock ;
}
2025-10-27 09:05:26 +00:00
2025-10-27 16:07:57 +00:00
private static void RegisterZipManager ( IServiceCollection services )
{
2025-10-27 09:05:26 +00:00
var zipManagerType = typeof ( WriteFileHttpResponse ) . Assembly . GetType ( "Elsa.Http.Services.ZipManager" ) ;
if ( zipManagerType ! = null )
{
2025-10-27 16:07:57 +00:00
services . AddSingleton ( zipManagerType , CreateZipManagerInstance ) ;
2025-10-27 09:05:26 +00:00
}
else
{
2025-11-12 14:53:23 +00:00
services . AddSingleton < object > ( _ = > new ( ) ) ;
2025-10-27 09:05:26 +00:00
}
}
2025-10-27 16:07:57 +00:00
private static object CreateZipManagerInstance ( IServiceProvider serviceProvider )
2025-10-27 09:05:26 +00:00
{
2025-10-27 16:07:57 +00:00
var zipManagerType = typeof ( WriteFileHttpResponse ) . Assembly . GetType ( "Elsa.Http.Services.ZipManager" ) ;
2025-11-12 14:53:23 +00:00
if ( zipManagerType = = null ) return new ( ) ;
2025-10-27 16:07:57 +00:00
try
2025-10-27 09:05:26 +00:00
{
2025-10-27 16:07:57 +00:00
var constructor = zipManagerType . GetConstructors ( ) . FirstOrDefault ( ) ;
if ( constructor = = null ) return CreateZipManagerFallback ( zipManagerType ) ;
var parameters = constructor . GetParameters ( ) ;
var args = new object [ parameters . Length ] ;
for ( var i = 0 ; i < parameters . Length ; i + + )
2025-10-27 09:05:26 +00:00
{
2025-10-27 16:07:57 +00:00
args [ i ] = ResolveConstructorParameter ( serviceProvider , parameters [ i ] . ParameterType ) ;
}
return constructor . Invoke ( args ) ;
2025-10-27 09:05:26 +00:00
}
2025-10-27 16:07:57 +00:00
catch
2025-10-27 09:05:26 +00:00
{
2025-10-27 16:07:57 +00:00
return CreateZipManagerFallback ( zipManagerType ) ;
2025-10-27 09:05:26 +00:00
}
2025-10-27 16:07:57 +00:00
}
private static object ResolveConstructorParameter ( IServiceProvider serviceProvider , Type paramType )
{
return paramType switch
{
_ when paramType = = typeof ( ISystemClock ) = > serviceProvider . GetRequiredService < ISystemClock > ( ) ,
_ when paramType = = typeof ( IFileCacheStorageProvider ) = > serviceProvider . GetRequiredService < IFileCacheStorageProvider > ( ) ,
_ when paramType = = typeof ( Microsoft . Extensions . Options . IOptions < HttpFileCacheOptions > ) = > serviceProvider . GetRequiredService < Microsoft . Extensions . Options . IOptions < HttpFileCacheOptions > > ( ) ,
{ IsGenericType : true } when paramType . GetGenericTypeDefinition ( ) = = typeof ( ILogger < > ) = > CreateLoggerMock ( paramType ) ,
_ when paramType = = typeof ( ILogger ) = > Substitute . For < ILogger > ( ) ,
_ = > CreateGenericMock ( paramType )
} ;
}
2025-11-12 14:53:23 +00:00
[UnconditionalSuppressMessage("Trimming", "IL2060:Call to MakeGenericMethod can not be statically analyzed", Justification = "Test helper method that creates mocks dynamically for any logger type.")]
2025-10-27 16:07:57 +00:00
private static object CreateLoggerMock ( Type loggerType )
{
try
2025-10-27 09:05:26 +00:00
{
2025-10-27 16:07:57 +00:00
var mockLoggerMethod = typeof ( Substitute ) . GetMethods ( )
. FirstOrDefault ( m = > m is { Name : "For" , IsGenericMethodDefinition : true } & & m . GetParameters ( ) . Length = = 0 ) ;
2025-11-12 14:53:23 +00:00
2025-10-27 16:07:57 +00:00
if ( mockLoggerMethod ! = null )
2025-10-27 09:05:26 +00:00
{
2025-10-27 16:07:57 +00:00
var genericMethod = mockLoggerMethod . MakeGenericMethod ( loggerType ) ;
return genericMethod . Invoke ( null , null ) ? ? Substitute . For < ILogger > ( ) ;
}
2025-10-27 09:05:26 +00:00
}
2025-10-27 16:07:57 +00:00
catch
2025-10-27 09:05:26 +00:00
{
2025-10-27 16:07:57 +00:00
// Fall back to basic ILogger mock
2025-10-27 09:05:26 +00:00
}
2025-10-27 16:07:57 +00:00
return Substitute . For < ILogger > ( ) ;
}
private static object CreateGenericMock ( Type paramType )
{
try
2025-10-27 09:05:26 +00:00
{
2025-10-27 16:07:57 +00:00
return Substitute . For ( [ paramType ] , [ ] ) ;
2025-10-27 09:05:26 +00:00
}
2025-10-27 16:07:57 +00:00
catch
2025-10-27 09:05:26 +00:00
{
2025-10-27 16:07:57 +00:00
return null ! ;
2025-10-27 09:05:26 +00:00
}
2025-10-27 16:07:57 +00:00
}
private static object CreateZipManagerFallback ( Type zipManagerType )
{
return Substitute . For ( [ zipManagerType ] , [ ] ) ;
}
private static IEnumerable < Downloadable > CreateMockDownloadables ( object content )
{
switch ( content )
2025-10-27 09:05:26 +00:00
{
2025-10-27 16:07:57 +00:00
// Simplified mock creation for different content types
case byte [ ] byteArray :
2025-11-12 14:53:23 +00:00
yield return new ( )
2025-10-27 16:07:57 +00:00
{
Stream = new MemoryStream ( byteArray ) ,
ContentType = null ,
Filename = null
} ;
break ;
case string text :
2025-11-12 14:53:23 +00:00
yield return new ( )
2025-10-27 16:07:57 +00:00
{
Stream = new MemoryStream ( Encoding . UTF8 . GetBytes ( text ) ) ,
ContentType = null ,
Filename = null
} ;
break ;
case Stream stream :
// For Stream objects, return them as-is
2025-11-12 14:53:23 +00:00
yield return new ( )
2025-10-27 16:07:57 +00:00
{
Stream = stream ,
ContentType = null ,
Filename = null
} ;
break ;
case Uri uri :
{
// For Uri objects, simulate downloaded content
var mockContent = $"Downloaded content from {uri}" ;
2025-11-12 14:53:23 +00:00
yield return new ( )
2025-10-27 16:07:57 +00:00
{
Stream = new MemoryStream ( Encoding . UTF8 . GetBytes ( mockContent ) ) ,
ContentType = null ,
Filename = null
} ;
break ;
}
case Downloadable downloadable :
// For Downloadable objects, return them as-is
yield return downloadable ;
break ;
case IEnumerable < object > fileContents :
{
// For collections, create a zip downloadable
var zipDownloadable = CreateZipDownloadable ( fileContents ) ;
yield return zipDownloadable ;
break ;
}
default :
throw new NotSupportedException ( "Unsupported content type" ) ;
2025-10-27 09:05:26 +00:00
}
}
private static Downloadable CreateZipDownloadable ( IEnumerable < object > fileContents )
{
// Don't use 'using' here to avoid disposing the stream prematurely
var memoryStream = new MemoryStream ( ) ;
try
{
using ( var archive = new ZipArchive ( memoryStream , ZipArchiveMode . Create , leaveOpen : true ) )
{
var fileIndex = 0 ;
foreach ( var fileContent in fileContents )
{
var entryName = $"file{fileIndex++}.txt" ;
var entry = archive . CreateEntry ( entryName , CompressionLevel . Optimal ) ;
2025-10-28 09:25:17 +00:00
2025-10-27 09:05:26 +00:00
using ( var entryStream = entry . Open ( ) )
{
2025-10-27 16:07:57 +00:00
var contentBytes = Encoding . UTF8 . GetBytes ( fileContent . ToString ( ) ? ? "" ) ;
2025-10-27 09:05:26 +00:00
entryStream . Write ( contentBytes , 0 , contentBytes . Length ) ;
}
}
}
memoryStream . Position = 0 ;
2025-11-12 14:53:23 +00:00
return new ( )
2025-10-27 09:05:26 +00:00
{
Stream = memoryStream ,
ContentType = "application/zip" ,
Filename = "archive.zip"
} ;
}
catch
{
// If something goes wrong, dispose the stream to prevent leaks
memoryStream . Dispose ( ) ;
throw ;
}
}
private static string GetResponseContent ( HttpContext httpContext )
{
httpContext . Response . Body . Seek ( 0 , SeekOrigin . Begin ) ;
using var reader = new StreamReader ( httpContext . Response . Body , Encoding . UTF8 , leaveOpen : true ) ;
var content = reader . ReadToEnd ( ) ;
httpContext . Response . Body . Seek ( 0 , SeekOrigin . Begin ) ; // Reset stream position
return content ;
}
private static byte [ ] GetResponseBytes ( HttpContext httpContext )
{
httpContext . Response . Body . Seek ( 0 , SeekOrigin . Begin ) ;
using var memoryStream = new MemoryStream ( ) ;
httpContext . Response . Body . CopyTo ( memoryStream ) ;
return memoryStream . ToArray ( ) ;
}
}