Search code examples
c#postmockingxunitiformfile

C# XUnit (.Net 8): IFormFile parameter in endpoint to be tested always received as null


I need to XUnit test this next endpoint:

/// <summary>
/// Cease all account in csv file
/// </summary>
/// <param name="dispatcher"></param>
/// <param name="csvFile">CSV file with format (Id,Name,SID,CeaseDate,Note)</param>
/// <param name="ct"></param>
/// <returns></returns>
[HttpPost("cease/bulk")]
[Authorize(Roles = VdcSecurity.Role.ManagementAdmin)]
[AllowAnonymous]
public async Task<ActionResult<bool>> CeaseBulkAccountAsync(
    [FromServices][IsSensitive] ICommandDispatcher dispatcher, 
    [FromForm] IFormFile csvFile,
    [IsSensitive] CancellationToken ct = default
)
{
    var identity = Vdc.Libs.AspNet.Controller.HttpContextExtensions.GetIdentity(HttpContext);
    var ipAddress = HttpContext.GetIpAddress();

    var command = new CeaseBulkCommand(identity, HttpContext.TraceIdentifier)
    {
        Stream = csvFile.OpenReadStream(),
        IpAddress = ipAddress
    };
    var result = await dispatcher.DispatchAsync(_provider, command, ct);

    return result.ToActionResult(this);
}

My problem is no matter how I create the IFormFile object, it is always received as null.

This is one of my attempts:

const string filePath = "CeaseBulkAccount.csv";

using (var httpClient = ApiClient.HttpClient)
{
    var form = new MultipartFormDataContent();

    byte[] fileData = File.ReadAllBytes(filePath);

    ByteArrayContent byteContent = new ByteArrayContent(fileData);

    byteContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data");

    form.Add(byteContent, "file", Path.GetFileName(filePath));

    var result = await httpClient.PostAsync("/api/accounts/cease/bulk", form);
}

I reach the controller, but csvFile is received as null.

ApiClient.HttpClient is our own client, but I wouldn't mind using a generic one.

I have to say our httpClient "PostAsync" receives a HttpContent.

A second attempt:

var httpClient = ApiClient.HttpClient;

var fileContent = new ByteArrayContent(ReadFully(file));
fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
{
    FileName = "CeaseBulkAccount.csv"
};

var response = await httpClient.PostAsync("/api/accounts/cease/bulk", fileContent, ct);

public static byte[] ReadFully(Stream input)
{
    byte[] buffer = new byte[16 * 1024];
    using (MemoryStream ms = new MemoryStream())
    {
        int read;
        while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
        {
            ms.Write(buffer, 0, read);
        }
        return ms.ToArray();
    }
}

Again, csvFile null.

Our PostAsync:

//
// Summary:
//     Send a POST request with a cancellation token as an asynchronous operation.
//
// Parameters:
//   requestUri:
//     The Uri the request is sent to.
//
//   content:
//     The HTTP request content sent to the server.
//
//   cancellationToken:
//     A cancellation token that can be used by other objects or threads to receive
//     notice of cancellation.
//
// Returns:
//     The task object representing the asynchronous operation.
//
// Exceptions:
//   T:System.InvalidOperationException:
//     The requestUri must be an absolute URI or System.Net.Http.HttpClient.BaseAddress
//     must be set.
//
//   T:System.Net.Http.HttpRequestException:
//     The request failed due to an underlying issue such as network connectivity, DNS
//     failure, server certificate validation or timeout.
//
//   T:System.Threading.Tasks.TaskCanceledException:
//     .NET Core and .NET 5 and later only: The request failed due to timeout.
//
//   T:System.UriFormatException:
//     The provided request URI is not valid relative or absolute URI.
public Task<HttpResponseMessage> PostAsync([StringSyntax("Uri")] string? requestUri, HttpContent? content, CancellationToken cancellationToken);

Solution

  • The name passed to the form.Add must match with the one in the action method of the controller; [FromForm] IFormFile csvFile.

    Since that one is csvFile, you must add the file as below.

    form.Add(byteContent, "csvFile", Path.GetFileName(filePath));
    

    With above change, your first attempt works fine.


    Alternatively, you set a fixed name via FromForm. That also guards yourself against any renames while refactoring any related code.

    It's the name specified in the attribute that needs to be used when posting a file.

    [FromForm(Name = "file")] IFormFile csvFile