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.
Caveat
I've not worked on any projects where we've needed Unit Tests to check the serialisation formats, so I'm not sure it's a valid problem to solve. (maybe integration tests would be better?) But you asked for help making it unit-testable so I'll give some thoughts.
Suggestions
I'll suggest three options. All are based on the principle of moving your logic out of the Azure Function, into a separate Service class. Unit Tests are best done away from your endpoint tech stack, so they just act on the domain logic.
Option 1 - serialise in the Unit Test
Remove the logic from the Azure Function, and put it in a Service class (probably in a separate project). You don't specify what information needs to come from the user's request body, so I can't guess that.
All your
ResponseContentclasses would then also go into the Service layer project, along with the above class.Your Azure Function then injects this Service:
Then, write your Unit Test so that it tests the MyCustomClaimsProvider. In the Unit Test, make sure you use the same Json serialiser as the default for your Azure Functions SDK. You might add some helper methods into the Unit Tests so that you don't repeat that code all the time.
I've omitted that detail here because I'm assuming you know already how to do that. If not, let me know and I'll add that too.
Option 2 - have the Service class return a string
This is a variant on the above. Have the Service method return an actual
stringas its output, instead ofResponseContent. I don't particularly like this, because it's mixing up two different things into one Service - and in fact into one layer of the application:So I prefer option 1.
Option 3 (a bit of an aside)
It feels like you should re-structure the application so that the user authentication code isn't in the main Function or even the main Service layer. It might be better to abstract your authentication logic entirely; perhaps bringing it in as a dependency and using some Middleware classes in the Azure Function to do it.
Basically authentication is a slightly different concern from either the endpoints or the application logic, so it probably sits separately too. This is out of scope for your question though.