Search code examples
c#unit-testingnunitmoqhttpmodule

How to test HttpApplication events in IHttpModules


I am writing HttpModule and need to test it, I am using C#, .NET4.5.2, NUnit and Moq.

Method I am trying to test is Context_BeginRequest:

public class XForwardedForRewriter : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.BeginRequest += Context_BeginRequest;
    }

    public void Context_BeginRequest(object sender, EventArgs e) { ... }
}

sender here is HttpApplication and this is where the problems start,... one can create instance of HttpApplication however there is no way to set HttpContext since it is read only and there is no way to pass it in (via constructor or something alike)...

I don't have VS2015 Ultimate and can't use Microsoft.Fakes (Shims), and ATM the only solution for this I have found is to create a wrapper which doesn't sound like most straightforward solution....

When I think about this I am sure that someone has already ran into this exact problem (as every time one is writing HttpModule in TDD he will need to mock HttpApplication or do some workaround)

How does one test events IHttpModules? Is there a way of Mocking HttpApplication? preferebly with Moq.

EDIT: Here is the code I am trying to test... it's header re-writer from PROXY v2 binary to good old X-Forwarded-For...

public class XForwardedForRewriter : IHttpModule
{
    public void Dispose()
    {
        throw new NotImplementedException();
    }

    byte[] proxyv2HeaderStartRequence = new byte[12] { 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A };

    public void Init(HttpApplication context)
    {
        context.BeginRequest += Context_BeginRequest;
    }

    public void Context_BeginRequest(object sender, EventArgs e)
    {
        var request = ((HttpApplication)sender).Context.Request;

        var proxyv2header = request.BinaryRead(12);
        if (!proxyv2header.SequenceEqual(proxyv2HeaderStartRequence))
        {
            request.Abort();
        }
        else
        {
            var proxyv2IpvType = request.BinaryRead(5).Skip(1).Take(1).Single();
            var isIpv4 = new byte[] { 0x11, 0x12 }.Contains(proxyv2IpvType);
            var ipInBinary = isIpv4 ? request.BinaryRead(12) : request.BinaryRead(36);
            var ip = Convert.ToString(ipInBinary);

            var headers = request.Headers;
            Type hdr = headers.GetType();
            PropertyInfo ro = hdr.GetProperty("IsReadOnly",
                BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.IgnoreCase | BindingFlags.FlattenHierarchy);

            ro.SetValue(headers, false, null);

            hdr.InvokeMember("InvalidateCachedArrays",
                BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance,
                null, headers, null);

            hdr.InvokeMember("BaseAdd",
                BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance,
                null, headers,
                new object[] { "X-Forwarded-For", new ArrayList { ip } });

            ro.SetValue(headers, true, null);
        }
    }
}

Solution

  • The following shows a potential work around for making the above case test-able

    public class XForwardedForRewriter : IHttpModule {
        public void Dispose() {
            throw new NotImplementedException();
        }
    
        byte[] proxyv2HeaderStartRequence = new byte[12] { 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A };
    
        public void Init(HttpApplication context) {
            context.BeginRequest += Context_BeginRequest;
        }
    
        public Func<object, HttpRequestBase> GetRequest = (object sender) => {
            return new HttpRequestWrapper(((HttpApplication)sender).Context.Request);
        };
    
        public void Context_BeginRequest(object sender, EventArgs e) {
            var request = GetRequest(sender);
    
            var proxyv2header = request.BinaryRead(12);
            if (!proxyv2header.SequenceEqual(proxyv2HeaderStartRequence)) {
                request.Abort();
            } else {
                var proxyv2IpvType = request.BinaryRead(5).Skip(1).Take(1).Single();
                var isIpv4 = new byte[] { 0x11, 0x12 }.Contains(proxyv2IpvType);
                var ipInBinary = isIpv4 ? request.BinaryRead(12) : request.BinaryRead(36);
                var ip = Convert.ToString(ipInBinary);
    
                var headers = request.Headers;
                var hdr = headers.GetType();
                var ro = hdr.GetProperty("IsReadOnly",
                    BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.IgnoreCase | BindingFlags.FlattenHierarchy);
    
                ro.SetValue(headers, false, null);
    
                hdr.InvokeMember("InvalidateCachedArrays",
                    BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance,
                    null, headers, null);
    
                hdr.InvokeMember("BaseAdd",
                    BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance,
                    null, headers,
                    new object[] { "X-Forwarded-For", new ArrayList { ip } });
    
                ro.SetValue(headers, true, null);
            }
        }
    }
    

    The tests would end up like

    [TestClass]
    public class XForwardedForRewriterTests {
    
        [TestMethod]
        public void Request_Should_Abort() {
            //Arrange
            var request = Mock.Of<HttpRequestBase>();
    
            var sut = new XForwardedForRewriter();
            //replace with mock request for test
            sut.GetRequest = (object sender) => request;
    
            //Act
            sut.Context_BeginRequest(new object(), EventArgs.Empty);
    
            //Assert
            var mockRequest = Mock.Get(request);
            mockRequest.Verify(m => m.Abort(), Times.AtLeastOnce);
        }
    
    
        [TestMethod]
        public void Request_Should_Forward() {
            //Arrange
            var request = Mock.Of<HttpRequestBase>();
    
            var mockRequest = Mock.Get(request);
            //setup mocked request with desired behavior for test
            var proxyv2HeaderStartRequence = new byte[12] { 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A };
            mockRequest
                .Setup(m => m.BinaryRead(12))
                .Returns(proxyv2HeaderStartRequence);
    
            var fakeProxyv2IpvType = new byte[5] { 0x00, 0x12, 0x00, 0x00, 0x00 };
            mockRequest
                .Setup(m => m.BinaryRead(5))
                .Returns(fakeProxyv2IpvType);
    
            var headers = new NameValueCollection();
            mockRequest.Setup(m => m.Headers).Returns(headers);
    
            var sut = new XForwardedForRewriter();
            //replace with mock request for test
            sut.GetRequest = (object sender) => request;
    
            //Act
            sut.Context_BeginRequest(new object(), EventArgs.Empty);
    
            //Assert
            //...check request headers
            var xForwardedFor = headers["X-Forwarded-For"];
            Assert.IsNotNull(xForwardedFor);
        }
    
    }
    

    One observation of the Sut is that the ip resolves to "System.Byte[]" which I believe is not expected behavior. Recheck the proxyv2HeaderStartRequence.

    Apart from adding the Factory method to access the request, the rest of the code under test remained the same. Observe for the actual implementation, how the request was wrapped in a HttpRequestBase derived class which allowed for a mock to be swapped in its place for testing.

    This should now allow for the application of TDD with the module.