Search code examples
unit-testingazure-ad-msalmicrosoft-identity-platformmicrosoft.identity.web

TypeLoadException when trying to UnitTest Azure Function with Microsoft.Identity.Web


I am trying to UnitTest an Azure Function that reads a parameter directly from the querystring

[FunctionName("GetEntityId")]
public async Task<IActionResult> GetEntityId(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "entityid")] HttpRequest req)
{

    if (!int.TryParse(req.Query["quantity"], out int quantity))
    {
        quantity = 1;
    }

    quantity = Math.Min(quantity, 100);

    var entityIds = await this.entityIdGenerator.NewEntityIdAsync(userId, quantity);

    if (entityIds is null)
    {
        return new System.Web.Http.InternalServerErrorResult();
    }
    else
    {
        return new OkObjectResult(entityIds);
    }
}

There are unit tests that have mocked the HttpRequest and HttpContext

var reqMock = new Mock<HttpRequest>();
var httpContextMock = new Mock<HttpContext>();
var responseMock = new Mock<HttpResponse>();

responseMock.Setup(res => res.Headers).Returns(new HeaderDictionary());
httpContextMock.Setup(http => http.Response).Returns(responseMock.Object);
httpContextMock.Setup(http => http.Request).Returns(reqMock.Object);

The mock for the querystring uses QueryCollection

reqMock.Setup(req => req.Query).Returns(new QueryCollection(queryString));

Up until recently this worked fine but since I introduced Microsoft.Identity.Web to the Azure Function I get the following error. If I remove the package reference the Unit Test passes

System.TypeLoadException: 'Could not load type 'Microsoft.AspNetCore.Http.Internal.QueryCollection' from assembly 'Microsoft.AspNetCore.Http, Version=3.1.31.0, Culture=neutral, PublicKeyToken=adb9793829ddae60'.'

I realise that using a class inside an Internal MS library looks wrong but this is one of the main ways to mock the HttpRequest and was working

If I try and used DefaultHttpRequest and DefaultHttpContext I get a different error but for a similar reason

Could not load type 'Microsoft.AspNetCore.Http.Internal.DefaultHttpRequest' from assembly 'Microsoft.AspNetCore.Http, Version=3.1.31.0, Culture=neutral, PublicKeyToken=adb9793829ddae60'.'

Something in the Microsoft.Identity library is causing a different dependent library to be called from what I can tell. I have tried updating Microsoft.Identity to latest but same error

The packages in the CSPROJ file are

<ItemGroup>
    <PackageReference Include="AutoMapper" Version="11.0.1" />
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="7.0.0" />
    <PackageReference Include="Avatar.Infrastructure.EfCore" Version="1.0.0-CI-20230130-181631" />
    <PackageReference Include="Azure.Storage.Blobs" Version="12.6.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
    <PackageReference Include="Microsoft.Azure.WebJobs.Logging.ApplicationInsights" Version="3.0.30" />
    <PackageReference Include="Microsoft.Data.SqlClient" Version="2.1.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.28" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.28" />
    <PackageReference Include="Microsoft.Identity.Client" Version="4.49.1" />
    <PackageReference Include="Microsoft.Identity.Web" Version="1.25.10" />
    <PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="5.2.9" />
    <PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.26.0" />
    <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.25.1" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.13" />
    <PackageReference Include="StyleCop.Analyzers" Version="1.0.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    </PackageReference>
    <AdditionalFiles Include="stylecop.json" />
  </ItemGroup>

The full UnitTest code is

[TestCategory("AvatarFunctionAppTest Story 893")]
[TestMethod]
public async Task GetEntityIdNotAuthorised()
{
    HttpRequest mockedRequest = this.HttpRequestSetup();
    var result = await new AvatarFunctionApp(
        this.EasyAuthProxy, 
        this.EntityIdGenerator, 
        this.TableStorage, 
        this.mockSignalService.Object, 
        this.log.Object)
            .GetEntityId(mockedRequest);

    Assert.IsTrue(result is UnauthorizedResult);
}

public HttpRequest HttpRequestSetup(IHeaderDictionary headers = null)
        {
            return this.HttpRequestSetup(new Dictionary<string, StringValues>(), headers);
        }

public HttpRequest HttpRequestSetup(Dictionary<string, StringValues> queryString, IHeaderDictionary headers = null, Uri uri = null)
{
    var reqMock = new Mock<HttpRequest>();
    var httpContextMock = new Mock<HttpContext>();
    var responseMock = new Mock<HttpResponse>();

    responseMock.Setup(res => res.Headers).Returns(new HeaderDictionary());
    httpContextMock.Setup(http => http.Response).Returns(responseMock.Object);
    httpContextMock.Setup(http => http.Request).Returns(reqMock.Object);

    reqMock.Setup(req => req.Query).Returns(new QueryCollection(queryString));
    reqMock.Setup(req => req.Body).Returns(default(Stream));
    reqMock.Setup(req => req.Headers).Returns(headers ?? new HeaderDictionary());
    reqMock.Setup(req => req.HttpContext).Returns(httpContextMock.Object);

    if (uri is Uri)
    {
        reqMock.Setup(req => req.Scheme).Returns(uri.Scheme);
        reqMock.Setup(req => req.Host).Returns(new HostString(uri.Host, uri.Port));
        reqMock.Setup(req => req.Path).Returns(uri.AbsolutePath);
    }

    this.mockHttpContextAccessor.Setup(hca => hca.HttpContext).Returns(httpContextMock.Object);

    return reqMock.Object;
}

Solution

  • I stopped trying to create a QueryCollection obect for use in my Mock setup and instead created my own FakeQueryCollection that implements IQueryCollection

    internal class FakeQueryCollection : IQueryCollection
    {
    private Dictionary<string, StringValues> query;
    
    public FakeQueryCollection(Dictionary<string, StringValues> query)
    {
        this.query = query;
    }
    
    public FakeQueryCollection()
    {
        this.query = new Dictionary<string, StringValues>();
    }
    
    
    public StringValues this[string key]
    {
        get
        { 
            return this.query[key];
        }
    
        set
        {
            this.query[key] = value;
        }
    }
    
    public int Count => this.query.Count;
    
    public ICollection<string> Keys => this.query.Keys;
    
    public bool ContainsKey(string key)
    {
        return this.query.ContainsKey(key);
    }
    
    public IEnumerator<KeyValuePair<string, StringValues>> GetEnumerator()
    {
        foreach (KeyValuePair<string, StringValues> pair in this.query)
        {
            yield return new KeyValuePair<string, StringValues>(pair.Key, pair.Value);
        }
    }
    
    public bool TryGetValue(string key, out StringValues value)
    {
        return this.query.TryGetValue(key, out value);
    }
    
    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.query.GetEnumerator();
    }
    }