I'm working on a CustomClaimsProvider, although for the purposes of this question, all that matters is that it's an Azure Function with an HTTP trigger. The code here is based on the example code in the edit the function section of the get started page.
The issue is that the same code gives different responses across different implementations, and I want a unit test to verify that the response is correct.
Repro steps:
- In Visual Studio, create a new project, use "Azure Functions" (you need to have the Azure workload installed to do this) choose ".NET 6.0 (Long term support)" (this is critical, it gives an "in-process" implementation), but use the other default options
- Replace the content of Function1.cs with the following
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using System.Collections.Generic;
namespace FunctionApp6;
public class Function1
{
[FunctionName("CustomClaimsProvider")]
public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest request)
{
string requestBody = await new StreamReader(request.Body).ReadToEndAsync();
// Claims to return to Azure AD
ResponseContent r = new ResponseContent();
r.data.actions[0].claims.ApiVersion = "1.0.0";
r.data.actions[0].claims.DateOfBirth = "2000-01-01";
r.data.actions[0].claims.CustomRoles.Add("Writer");
r.data.actions[0].claims.CustomRoles.Add("Editor");
return new OkObjectResult(r);
}
}
public class ResponseContent
{
public Data data { get; set; } = new();
}
public class Data
{
public Data()
{
actions = new List<Action>();
actions.Add(new Action());
}
[JsonProperty("@odata.type")]
public string odatatype { get; set; } = "microsoft.graph.onTokenIssuanceStartResponseData";
public List<Action> actions { get; set; }
}
public class Action
{
[JsonProperty("@odata.type")]
public string odatatype { get; set; } = "microsoft.graph.tokenIssuanceStart.provideClaimsForToken";
public Claims claims { get; set; } = new();
}
public class Claims
{
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string AnotherValue { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string DateOfBirth { get; set; }
public string ApiVersion { get; set; }
public List<string> CustomRoles { get; set; } = new();
}
- Set that project as the start project and run the solution. When it runs, it will give a GET,POST URL... copy that URL into a browser to view the returned JSON. The output JSON looks like this, which is correct:
{
"data": {
"@odata.type": "microsoft.graph.onTokenIssuanceStartResponseData",
"actions": [
{
"@odata.type": "microsoft.graph.tokenIssuanceStart.provideClaimsForToken",
"claims": {
"dateOfBirth": "2000-01-01",
"apiVersion": "1.0.0",
"customRoles": [
"Writer",
"Editor"
]
}
}
]
}
}
However, if we repeat those steps, and just change the following things...
- This time, select ".NET 6.0 Isolated (Long Term Support)"
- Use the same code from above, but replace all the previous "using"s with:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Newtonsoft.Json;
- Change the name of the attribute on the function from
FunctiontoFunctionName - Change the type of the 3
stringproperties withinClaimsto bestring?
That's all. Now set this project to be the startup project and run it. The output now looks like this
{
"data": {
"odatatype": "microsoft.graph.onTokenIssuanceStartResponseData",
"actions": [
{
"odatatype": "microsoft.graph.tokenIssuanceStart.provideClaimsForToken",
"claims": {
"anotherValue": null,
"dateOfBirth": "2000-01-01",
"apiVersion": "1.0.0",
"customRoles": [
"Writer",
"Editor"
]
}
}
]
}
}
This output is different, even though the implementation is the same. It has been serialized with "System.Text.Json" rather than "Newtonsoft.Json". There is no visibility from the code of how this decision is being taken; but this difference is critical, as it breaks the implementation.
I tried starting to write a NUnit test, but realised I have no idea how the IActionResult becomes the JSON in the response body:
[Test]
public void Verify_json_data_is_serialized_correctly()
{
// Arrange.
ResponseContent r = new ResponseContent();
// Act.
var actual = new OkObjectResult(r);
// Assert.
Assert.That(actual, Is.EqualTo("doesn't matter"));
}
How can I unit test that the JSON response is being serialized correctly? (i.e. test what the method returns without needing the function to be running)
P.S. I know I can add System.Text.Json attributes to make it work properly, like [JsonPropertyName("@odata.type")]... that isn't the point. The point is the days that have been wasted trying to track down why a new custom claims provider wasn't working, even though it was based on code which is already working elsewhere. A unit test of what the JSON looks like would have shown the problem.