Why don't form fields get returned from this HttpContext mock

41 views Asked by At

I tried to mock the HttpContext for unit tests for a .net 6 web app. What's interesting is that I can get any files added to the FileCollection. However, if I try to access the form fields, it throws an Object reference not set to an instance of an object exception. Here's the minimal test code.

using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;
using Moq;
using NUnit.Framework;
using TestWebApp.Controllers;

namespace TestDemo;

[TestFixture]
public class TestControllerFixture
{
    [Test]
    public void returns_sum()
    {
        var content = "Hello World from a Fake File";
        var fileName = "test.pdf";
        var stream = new MemoryStream();
        var writer = new StreamWriter(stream);
        writer.Write(content);
        writer.Flush();
        stream.Position = 0;
        IFormFile file = new FormFile(stream, 0, stream.Length, "fileId", fileName);

        var _httpContextAccessor = new MockHttpContextAccessor();
        _httpContextAccessor.AddFormFieldsOrFile(new Dictionary<string, StringValues>()
        {
            {"expense-ids", new StringValues(new[] {"1","2","3"})}
        }, new List<IFormFile>() {file});

        var controller = new TestController(_httpContextAccessor.ContextAccessor.Object);
        var result = controller.Test();
        Assert.AreEqual(3, result);
    }
}

public class MockHttpContextAccessor
{
    public Mock<IHttpContextAccessor> ContextAccessor { get; private set; }
    private FormFileCollection fileCollection;
    private FormCollection requestForm;

    public MockHttpContextAccessor()
    {
        var requestFeature = new HttpRequestFeature();
        var features = new FeatureCollection();

        features.Set<IHttpRequestFeature>(requestFeature);
        ContextAccessor = new Mock<IHttpContextAccessor>();
        var session = new MockHttpSession();
        var context = new DefaultHttpContext(features);

        fileCollection = new FormFileCollection();
        requestForm = new FormCollection(new Dictionary<string, StringValues>(), fileCollection);

        ContextAccessor.Setup(x => x.HttpContext).Returns(context);
        ContextAccessor.Setup(x => x.HttpContext.Session).Returns(session);
        ContextAccessor.Setup(x => x.HttpContext.Request.Path).Returns("/path");
        ContextAccessor.Setup(x => x.HttpContext.Request.Form).Returns(requestForm);
        ContextAccessor.Setup(x => x.HttpContext.Request.Form.Files).Returns(requestForm.Files);
        ContextAccessor.Setup(x => x.HttpContext.Request.Headers).Returns(new Mock<IHeaderDictionary>().Object);

        context.Request.Form = requestForm;
    }

    public void AddRequestBody(string content)
    {
        ContextAccessor.Setup(x => x.HttpContext.Request.Body)
            .Returns(new MemoryStream(System.Text.Encoding.UTF8.GetBytes(content)));
    }

    public void AddFormFieldsOrFile(Dictionary<string, StringValues> fields, List<IFormFile> files = null)
    {
        if (files != null && files.Any())
        {
            ContextAccessor.Object.HttpContext.Request.Headers.Add("Content-Type", "multipart/form-data");
            fileCollection.AddRange(files);
        }

        requestForm = new FormCollection(fields, fileCollection);
    }

    public void ClearFormFiles()
    {
        fileCollection.Clear();
    }
}

public class MockHttpSession : ISession
{
    Dictionary<string, object> sessionStorage = new Dictionary<string, object>();

    public object this[string name]
    {
        get { return sessionStorage[name]; }
        set { sessionStorage[name] = value; }
    }

    string ISession.Id
    {
        get { throw new NotImplementedException(); }
    }

    bool ISession.IsAvailable
    {
        get { throw new NotImplementedException(); }
    }

    IEnumerable<string> ISession.Keys
    {
        get { return sessionStorage.Keys; }
    }

    void ISession.Clear()
    {
        sessionStorage.Clear();
    }

    void ISession.Remove(string key)
    {
        sessionStorage.Remove(key);
    }

    void ISession.Set(string key, byte[] value)
    {
        sessionStorage[key] = value;
    }

    public Task LoadAsync(CancellationToken cancellationToken = new CancellationToken())
    {
        throw new NotImplementedException();
    }

    public Task CommitAsync(CancellationToken cancellationToken = new CancellationToken())
    {
        throw new NotImplementedException();
    }

    bool ISession.TryGetValue(string key, out byte[] value)
    {
        object storageValue = null;
        if (sessionStorage.TryGetValue(key, out storageValue))
        {
            var bytes = storageValue as byte[];

            if (bytes != null)
            {
                value = bytes;
                return true;
            }

            value = Encoding.UTF8.GetBytes(storageValue.ToString());
            return true;
        }

        value = null;
        return false;
    }
}

This is the demo controller method being tested. The file is found, but not the form field.

using Microsoft.AspNetCore.Mvc;

namespace TestWebApp.Controllers;

public class TestController : Controller
{
    private IHttpContextAccessor _httpContextAccessor;

    public TestController(IHttpContextAccessor context)
    {
        _httpContextAccessor = context;
    }

    [HttpPost("[action]")]
    public IActionResult Test()
    {
        var form = _httpContextAccessor.HttpContext.Request.Form;
        var file = form.Files[0];
        var ids = form["expense-ids"].ToString().Split(',')
            .Select(int.Parse).ToList();

        var total = ids.Sum();

        return Json(new {total});
    }
}

And here's a screenshot of the mocked object. In it, you can see there's 1 file in the Files collection. The stack trace of the exception only provides this.

   at System.Collections.Generic.LargeArrayBuilder`1.AddRange(IEnumerable`1 items)
   at System.Collections.Generic.EnumerableHelpers.ToArray[T](IEnumerable`1 source)
   at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
   at System.Linq.SystemCore_EnumerableDebugView`1.get_Items()

enter image description here

0

There are 0 answers