File: ProblemDetailsDefaultWriterTest.cs
Web Access
Project: src\src\Http\Http.Extensions\test\Microsoft.AspNetCore.Http.Extensions.Tests.csproj (Microsoft.AspNetCore.Http.Extensions.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions;
 
namespace Microsoft.AspNetCore.Http.Extensions.Tests;
 
public partial class DefaultProblemDetailsWriterTest
{
    private static readonly JsonSerializerOptions SerializerOptions = JsonOptions.DefaultSerializerOptions;
 
    [Fact]
    public async Task WriteAsync_Works()
    {
        // Arrange
        var writer = GetWriter();
        var stream = new MemoryStream();
        var context = CreateContext(stream);
 
        var expectedTraceId = Activity.Current?.Id ?? context.TraceIdentifier;
        var expectedProblem = new ProblemDetails()
        {
            Detail = "Custom Bad Request",
            Instance = "Custom Bad Request",
            Status = StatusCodes.Status400BadRequest,
            Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom",
            Title = "Custom Bad Request",
        };
        var problemDetailsContext = new ProblemDetailsContext()
        {
            HttpContext = context,
            ProblemDetails = expectedProblem
        };
 
        //Act
        await writer.WriteAsync(problemDetailsContext);
 
        //Assert
        stream.Position = 0;
        var problemDetails = await JsonSerializer.DeserializeAsync<ProblemDetails>(stream, SerializerOptions);
        Assert.NotNull(problemDetails);
        Assert.Equal(expectedProblem.Status, problemDetails.Status);
        Assert.Equal(expectedProblem.Type, problemDetails.Type);
        Assert.Equal(expectedProblem.Title, problemDetails.Title);
        Assert.Equal(expectedProblem.Detail, problemDetails.Detail);
        Assert.Equal(expectedProblem.Instance, problemDetails.Instance);
        Assert.Equal(expectedTraceId, problemDetails.Extensions["traceId"].ToString());
    }
 
    [Fact]
    public async Task WriteAsync_Works_ProperCasing()
    {
        // Arrange
        var writer = GetWriter();
        var stream = new MemoryStream();
        var context = CreateContext(stream);
        var expectedTraceId = Activity.Current?.Id ?? context.TraceIdentifier;
        var expectedProblem = new ProblemDetails()
        {
            Detail = "Custom Bad Request",
            Instance = "Custom Bad Request",
            Status = StatusCodes.Status400BadRequest,
            Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom",
            Title = "Custom Bad Request",
            Extensions = new Dictionary<string, object>() { { "extensionKey", 1 } }
        };
        var problemDetailsContext = new ProblemDetailsContext()
        {
            HttpContext = context,
            ProblemDetails = expectedProblem
        };
 
        //Act
        await writer.WriteAsync(problemDetailsContext);
 
        //Assert
        stream.Position = 0;
        var result = await JsonSerializer.DeserializeAsync<Dictionary<string, object>>(stream, JsonSerializerOptions.Default);
        Assert.Equal(result.Keys, new(new() { { "type", 0 }, { "title", 1 }, { "status", 2 }, { "detail", 3 }, { "instance", 4 }, { "extensionKey", 5 }, {"traceId", expectedTraceId } }));
    }
 
    [Fact]
    public async Task WriteAsync_Works_ProperCasing_ValidationProblemDetails()
    {
        // Arrange
        var writer = GetWriter();
        var stream = new MemoryStream();
        var context = CreateContext(stream);
        var expectedTraceId = Activity.Current?.Id ?? context.TraceIdentifier;
        var expectedProblem = new ValidationProblemDetails()
        {
            Detail = "Custom Bad Request",
            Instance = "Custom Bad Request",
            Status = StatusCodes.Status400BadRequest,
            Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom",
            Title = "Custom Bad Request",
            Errors = new Dictionary<string, string[]>() { { "name", ["Name is invalid."] } }
        };
        var problemDetailsContext = new ProblemDetailsContext()
        {
            HttpContext = context,
            ProblemDetails = expectedProblem
        };
 
        //Act
        await writer.WriteAsync(problemDetailsContext);
 
        //Assert
        stream.Position = 0;
        var result = await JsonSerializer.DeserializeAsync<Dictionary<string, object>>(stream, JsonSerializerOptions.Default);
        Assert.Equal(result.Keys, new(new() { { "type", 0 }, { "title", 1 }, { "status", 2 }, { "detail", 3 }, { "instance", 4 }, { "errors", 5 }, {"traceId", expectedTraceId } }));
    }
 
    [Fact]
    public async Task WriteAsync_Works_WhenReplacingProblemDetailsUsingSetter()
    {
        // Arrange
        var writer = GetWriter();
        var stream = new MemoryStream();
        var context = CreateContext(stream);
        var originalProblemDetails = new ProblemDetails();
 
        var expectedTraceId = Activity.Current?.Id ?? context.TraceIdentifier;
        var expectedProblem = new ProblemDetails()
        {
            Detail = "Custom Bad Request",
            Instance = "Custom Bad Request",
            Status = StatusCodes.Status400BadRequest,
            Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom",
            Title = "Custom Bad Request",
        };
        var problemDetailsContext = new ProblemDetailsContext()
        {
            HttpContext = context,
            ProblemDetails = originalProblemDetails
        };
 
        problemDetailsContext.ProblemDetails = expectedProblem;
 
        //Act
        await writer.WriteAsync(problemDetailsContext);
 
        //Assert
        stream.Position = 0;
        var problemDetails = await JsonSerializer.DeserializeAsync<ProblemDetails>(stream, SerializerOptions);
        Assert.NotNull(problemDetails);
        Assert.Equal(expectedProblem.Status, problemDetails.Status);
        Assert.Equal(expectedProblem.Type, problemDetails.Type);
        Assert.Equal(expectedProblem.Title, problemDetails.Title);
        Assert.Equal(expectedProblem.Detail, problemDetails.Detail);
        Assert.Equal(expectedProblem.Instance, problemDetails.Instance);
        Assert.Equal(expectedTraceId, problemDetails.Extensions["traceId"].ToString());
    }
 
    [Fact]
    public async Task WriteAsync_Works_WithJsonContext()
    {
        // Arrange
        var options = new JsonOptions();
        options.SerializerOptions.TypeInfoResolver = ProblemDetailsJsonContext.Default;
 
        var writer = GetWriter(jsonOptions: options);
        var stream = new MemoryStream();
        var context = CreateContext(stream);
        var expectedTraceId = Activity.Current?.Id ?? context.TraceIdentifier;
        var expectedProblem = new ProblemDetails()
        {
            Detail = "Custom Bad Request",
            Instance = "Custom Bad Request",
            Status = StatusCodes.Status400BadRequest,
            Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom",
            Title = "Custom Bad Request",
        };
        var problemDetailsContext = new ProblemDetailsContext()
        {
            HttpContext = context,
            ProblemDetails = expectedProblem
        };
 
        //Act
        await writer.WriteAsync(problemDetailsContext);
 
        //Assert
        stream.Position = 0;
        var problemDetails = await JsonSerializer.DeserializeAsync<ProblemDetails>(stream, SerializerOptions);
        Assert.NotNull(problemDetails);
        Assert.Equal(expectedProblem.Status, problemDetails.Status);
        Assert.Equal(expectedProblem.Type, problemDetails.Type);
        Assert.Equal(expectedProblem.Title, problemDetails.Title);
        Assert.Equal(expectedProblem.Detail, problemDetails.Detail);
        Assert.Equal(expectedProblem.Instance, problemDetails.Instance);
        Assert.Equal(expectedTraceId, problemDetails.Extensions["traceId"].ToString());
    }
 
    [Fact]
    public async Task WriteAsync_Works_WithMultipleJsonContext()
    {
        // Arrange
        var options = new JsonOptions();
        options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(CustomProblemDetailsContext.Default, CustomProblemDetailsContext2.Default, ProblemDetailsJsonContext.Default);
 
        var writer = GetWriter(jsonOptions: options);
        var stream = new MemoryStream();
        var context = CreateContext(stream);
        var expectedTraceId = Activity.Current?.Id ?? context.TraceIdentifier;
        var expectedProblem = new ProblemDetails()
        {
            Detail = "Custom Bad Request",
            Instance = "Custom Bad Request",
            Status = StatusCodes.Status400BadRequest,
            Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom",
            Title = "Custom Bad Request",
        };
        var problemDetailsContext = new ProblemDetailsContext()
        {
            HttpContext = context,
            ProblemDetails = expectedProblem
        };
 
        //Act
        await writer.WriteAsync(problemDetailsContext);
 
        //Assert
        stream.Position = 0;
        var problemDetails = await JsonSerializer.DeserializeAsync<ProblemDetails>(stream, SerializerOptions);
        Assert.NotNull(problemDetails);
        Assert.Equal(expectedProblem.Status, problemDetails.Status);
        Assert.Equal(expectedProblem.Type, problemDetails.Type);
        Assert.Equal(expectedProblem.Title, problemDetails.Title);
        Assert.Equal(expectedProblem.Detail, problemDetails.Detail);
        Assert.Equal(expectedProblem.Instance, problemDetails.Instance);
        Assert.Equal(expectedTraceId, problemDetails.Extensions["traceId"].ToString());
    }
 
    [Fact]
    public async Task WriteAsync_Works_WithHttpValidationProblemDetails()
    {
        // Arrange
        var writer = GetWriter();
        var stream = new MemoryStream();
        var context = CreateContext(stream);
        var expectedTraceId = Activity.Current?.Id ?? context.TraceIdentifier;
        var expectedProblem = new HttpValidationProblemDetails()
        {
            Detail = "Custom Bad Request",
            Instance = "Custom Bad Request",
            Status = StatusCodes.Status400BadRequest,
            Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom",
            Title = "Custom Bad Request",
        };
        expectedProblem.Errors.Add("sample", new string[] { "error-message" });
 
        var problemDetailsContext = new ProblemDetailsContext()
        {
            HttpContext = context,
            ProblemDetails = expectedProblem
        };
 
        //Act
        await writer.WriteAsync(problemDetailsContext);
 
        //Assert
        stream.Position = 0;
        var problemDetails = await JsonSerializer.DeserializeAsync<HttpValidationProblemDetails>(stream, SerializerOptions);
        Assert.NotNull(problemDetails);
        Assert.Equal(expectedProblem.Status, problemDetails.Status);
        Assert.Equal(expectedProblem.Type, problemDetails.Type);
        Assert.Equal(expectedProblem.Title, problemDetails.Title);
        Assert.Equal(expectedProblem.Detail, problemDetails.Detail);
        Assert.Equal(expectedProblem.Instance, problemDetails.Instance);
        Assert.Equal(expectedProblem.Errors, problemDetails.Errors);
        Assert.Equal(expectedTraceId, problemDetails.Extensions["traceId"].ToString());
    }
 
    [Fact]
    public async Task WriteAsync_Works_WithHttpValidationProblemDetails_AndJsonContext()
    {
        // Arrange
        var options = new JsonOptions();
        options.SerializerOptions.TypeInfoResolver = ProblemDetailsJsonContext.Default;
 
        var writer = GetWriter(jsonOptions: options);
        var stream = new MemoryStream();
        var context = CreateContext(stream);
        var expectedTraceId = Activity.Current?.Id ?? context.TraceIdentifier;
        var expectedProblem = new HttpValidationProblemDetails()
        {
            Detail = "Custom Bad Request",
            Instance = "Custom Bad Request",
            Status = StatusCodes.Status400BadRequest,
            Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom",
            Title = "Custom Bad Request",
        };
        expectedProblem.Errors.Add("sample", new string[] { "error-message" });
 
        var problemDetailsContext = new ProblemDetailsContext()
        {
            HttpContext = context,
            ProblemDetails = expectedProblem
        };
 
        //Act
        await writer.WriteAsync(problemDetailsContext);
 
        //Assert
        stream.Position = 0;
        var problemDetails = await JsonSerializer.DeserializeAsync<HttpValidationProblemDetails>(stream, SerializerOptions);
        Assert.NotNull(problemDetails);
        Assert.Equal(expectedProblem.Status, problemDetails.Status);
        Assert.Equal(expectedProblem.Type, problemDetails.Type);
        Assert.Equal(expectedProblem.Title, problemDetails.Title);
        Assert.Equal(expectedProblem.Detail, problemDetails.Detail);
        Assert.Equal(expectedProblem.Instance, problemDetails.Instance);
        Assert.Equal(expectedProblem.Errors, problemDetails.Errors);
        Assert.Equal(expectedTraceId, problemDetails.Extensions["traceId"].ToString());
    }
 
    [Fact]
    public async Task WriteAsync_Works_WithCustomDerivedProblemDetails()
    {
        // Arrange
        var options = new JsonOptions();
        options.SerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver();
 
        var writer = GetWriter(jsonOptions: options);
        var stream = new MemoryStream();
        var context = CreateContext(stream);
        var expectedTraceId = Activity.Current?.Id ?? context.TraceIdentifier;
        var expectedProblem = new CustomProblemDetails()
        {
            Detail = "Custom Bad Request",
            Instance = "Custom Bad Request",
            Status = StatusCodes.Status400BadRequest,
            Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom",
            Title = "Custom Bad Request",
            ExtraProperty = "My Extra property"
        };
 
        var problemDetailsContext = new ProblemDetailsContext()
        {
            HttpContext = context,
            ProblemDetails = expectedProblem
        };
 
        //Act
        await writer.WriteAsync(problemDetailsContext);
 
        //Assert
        stream.Position = 0;
        var problemDetails = await JsonSerializer.DeserializeAsync<CustomProblemDetails>(stream, options.SerializerOptions);
        Assert.NotNull(problemDetails);
        Assert.Equal(expectedProblem.Status, problemDetails.Status);
        Assert.Equal(expectedProblem.Type, problemDetails.Type);
        Assert.Equal(expectedProblem.Title, problemDetails.Title);
        Assert.Equal(expectedProblem.Detail, problemDetails.Detail);
        Assert.Equal(expectedProblem.Instance, problemDetails.Instance);
        Assert.Equal(expectedProblem.ExtraProperty, problemDetails.ExtraProperty);
        Assert.Equal(expectedTraceId, problemDetails.Extensions["traceId"].ToString());
    }
 
    [Fact]
    public async Task WriteAsync_Works_WithCustomDerivedProblemDetails_AndJsonContext()
    {
        // Arrange
        var options = new JsonOptions();
        options.SerializerOptions.TypeInfoResolver = CustomProblemDetailsContext.Default;
 
        var writer = GetWriter(jsonOptions: options);
        var stream = new MemoryStream();
        var context = CreateContext(stream);
        var expectedTraceId = Activity.Current?.Id ?? context.TraceIdentifier;
        var expectedProblem = new CustomProblemDetails()
        {
            Detail = "Custom Bad Request",
            Instance = "Custom Bad Request",
            Status = StatusCodes.Status400BadRequest,
            Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom",
            Title = "Custom Bad Request",
            ExtraProperty = "My Extra property"
        };
 
        var problemDetailsContext = new ProblemDetailsContext()
        {
            HttpContext = context,
            ProblemDetails = expectedProblem
        };
 
        //Act
        await writer.WriteAsync(problemDetailsContext);
 
        //Assert
        stream.Position = 0;
        var problemDetails = await JsonSerializer.DeserializeAsync<CustomProblemDetails>(stream, options.SerializerOptions);
        Assert.NotNull(problemDetails);
        Assert.Equal(expectedProblem.Status, problemDetails.Status);
        Assert.Equal(expectedProblem.Type, problemDetails.Type);
        Assert.Equal(expectedProblem.Title, problemDetails.Title);
        Assert.Equal(expectedProblem.Detail, problemDetails.Detail);
        Assert.Equal(expectedProblem.Instance, problemDetails.Instance);
        Assert.Equal(expectedProblem.ExtraProperty, problemDetails.ExtraProperty);
        Assert.Equal(expectedTraceId, problemDetails.Extensions["traceId"].ToString());
    }
 
    [Fact]
    public async Task WriteAsync_Works_WithCustomDerivedProblemDetails_AndMultipleJsonContext()
    {
        // Arrange
        var options = new JsonOptions();
        options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(CustomProblemDetailsContext.Default, ProblemDetailsJsonContext.Default);
 
        var writer = GetWriter(jsonOptions: options);
        var stream = new MemoryStream();
        var context = CreateContext(stream);
        var expectedTraceId = Activity.Current?.Id ?? context.TraceIdentifier;
        var expectedProblem = new CustomProblemDetails()
        {
            Detail = "Custom Bad Request",
            Instance = "Custom Bad Request",
            Status = StatusCodes.Status400BadRequest,
            Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom",
            Title = "Custom Bad Request",
            ExtraProperty = "My Extra property"
        };
 
        var problemDetailsContext = new ProblemDetailsContext()
        {
            HttpContext = context,
            ProblemDetails = expectedProblem
        };
 
        //Act
        await writer.WriteAsync(problemDetailsContext);
 
        //Assert
        stream.Position = 0;
        var problemDetails = await JsonSerializer.DeserializeAsync<CustomProblemDetails>(stream, options.SerializerOptions);
        Assert.NotNull(problemDetails);
        Assert.Equal(expectedProblem.Status, problemDetails.Status);
        Assert.Equal(expectedProblem.Type, problemDetails.Type);
        Assert.Equal(expectedProblem.Title, problemDetails.Title);
        Assert.Equal(expectedProblem.Detail, problemDetails.Detail);
        Assert.Equal(expectedProblem.Instance, problemDetails.Instance);
        Assert.Equal(expectedProblem.ExtraProperty, problemDetails.ExtraProperty);
        Assert.Equal(expectedTraceId, problemDetails.Extensions["traceId"].ToString());
    }
 
    [Fact]
    public async Task WriteAsync_AddExtensions()
    {
        // Arrange
        var writer = GetWriter();
        var stream = new MemoryStream();
        var context = CreateContext(stream);
        var expectedProblem = new ProblemDetails();
        var expectedTraceId = Activity.Current?.Id ?? context.TraceIdentifier;
        expectedProblem.Extensions["Extension1"] = "Extension1-Value";
        expectedProblem.Extensions["Extension2"] = "Extension2-Value";
 
        var problemDetailsContext = new ProblemDetailsContext()
        {
            HttpContext = context,
            ProblemDetails = expectedProblem
        };
 
        //Act
        await writer.WriteAsync(problemDetailsContext);
 
        //Assert
        stream.Position = 0;
        var problemDetails = await JsonSerializer.DeserializeAsync<ProblemDetails>(stream, SerializerOptions);
        Assert.NotNull(problemDetails);
        Assert.Collection(problemDetails.Extensions,
            (extension) =>
            {
                Assert.Equal("Extension1", extension.Key);
                Assert.Equal("Extension1-Value", extension.Value.ToString());
            },
            (extension) =>
            {
                Assert.Equal("Extension2", extension.Key);
                Assert.Equal("Extension2-Value", extension.Value.ToString());
            },
            (extension) =>
            {
                Assert.Equal("traceId", extension.Key);
                Assert.Equal(expectedTraceId, extension.Value.ToString());
            });
    }
 
    [Fact]
    public async Task WriteAsync_AddExtensions_WithJsonContext()
    {
        // Arrange
        var options = new JsonOptions();
        options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(CustomProblemDetailsContext.Default, ProblemDetailsJsonContext.Default);
 
        var writer = GetWriter(jsonOptions: options);
        var stream = new MemoryStream();
        var context = CreateContext(stream);
        var expectedProblem = new ProblemDetails();
        var customExtensionData = new CustomExtensionData("test");
        var expectedTraceId = Activity.Current?.Id ?? context.TraceIdentifier;
        expectedProblem.Extensions["Extension"] = customExtensionData;
 
        var problemDetailsContext = new ProblemDetailsContext()
        {
            HttpContext = context,
            ProblemDetails = expectedProblem
        };
 
        //Act
        await writer.WriteAsync(problemDetailsContext);
 
        //Assert
        stream.Position = 0;
 
        var problemDetails = await JsonSerializer.DeserializeAsync<ProblemDetails>(stream, options.SerializerOptions);
        Assert.NotNull(problemDetails);
 
        Assert.Collection(problemDetails.Extensions,
            (extension) =>
            {
                Assert.Equal("Extension", extension.Key);
                var expectedExtension = JsonSerializer.SerializeToElement(customExtensionData, options.SerializerOptions);
                var value = Assert.IsType<JsonElement>(extension.Value);
 
                Assert.Equal(expectedExtension.GetProperty("data").GetString(), value.GetProperty("data").GetString());
            },
            (extension) =>
            {
                Assert.Equal("traceId", extension.Key);
                Assert.Equal(expectedTraceId, extension.Value.ToString());
            });
    }
 
    [Fact]
    public async Task WriteAsync_Applies_Defaults()
    {
        // Arrange
        var writer = GetWriter();
        var stream = new MemoryStream();
        var context = CreateContext(stream, StatusCodes.Status500InternalServerError);
        var expectedTraceId = Activity.Current?.Id ?? context.TraceIdentifier;
 
        //Act
        await writer.WriteAsync(new ProblemDetailsContext() { HttpContext = context });
 
        //Assert
        stream.Position = 0;
        var problemDetails = await JsonSerializer.DeserializeAsync<ProblemDetails>(stream, SerializerOptions);
        Assert.NotNull(problemDetails);
        Assert.Equal(StatusCodes.Status500InternalServerError, problemDetails.Status);
        Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.6.1", problemDetails.Type);
        Assert.Equal("An error occurred while processing your request.", problemDetails.Title);
        Assert.Equal(expectedTraceId, problemDetails.Extensions["traceId"].ToString());
    }
 
    [Fact]
    public async Task WriteAsync_Applies_CustomConfiguration()
    {
        // Arrange
        const string expectedTraceId = "new-traceId-Value";
        var options = new ProblemDetailsOptions()
        {
            CustomizeProblemDetails = (context) =>
            {
                context.ProblemDetails.Status = StatusCodes.Status406NotAcceptable;
                context.ProblemDetails.Title = "Custom Title";
                context.ProblemDetails.Extensions["new-extension"] = new { TraceId = Guid.NewGuid() };
                context.ProblemDetails.Extensions["traceId"] = expectedTraceId;
            }
        };
        var writer = GetWriter(options);
        var stream = new MemoryStream();
        var context = CreateContext(stream, StatusCodes.Status500InternalServerError);
 
        //Act
        await writer.WriteAsync(new ProblemDetailsContext()
        {
            HttpContext = context,
            ProblemDetails = { Status = StatusCodes.Status400BadRequest }
        });
 
        //Assert
        stream.Position = 0;
        var problemDetails = await JsonSerializer.DeserializeAsync<ProblemDetails>(stream, SerializerOptions);
        Assert.NotNull(problemDetails);
        Assert.Equal(StatusCodes.Status406NotAcceptable, problemDetails.Status);
        Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.5.1", problemDetails.Type);
        Assert.Equal("Custom Title", problemDetails.Title);
        Assert.Contains("new-extension", problemDetails.Extensions);
        Assert.Equal(expectedTraceId, problemDetails.Extensions["traceId"].ToString());
    }
 
    [Theory]
    [InlineData(StatusCodes.Status400BadRequest, "Bad Request", "https://tools.ietf.org/html/rfc9110#section-15.5.1")]
    [InlineData(StatusCodes.Status418ImATeapot, "I'm a teapot", null)]
    [InlineData(498, null, null)]
    public async Task WriteAsync_UsesStatusCode_FromProblemDetails_WhenSpecified(
        int statusCode,
        string title,
        string type)
    {
        // Arrange
        var writer = GetWriter();
        var stream = new MemoryStream();
        var context = CreateContext(stream, StatusCodes.Status500InternalServerError);
 
        //Act
        await writer.WriteAsync(new ProblemDetailsContext()
        {
            HttpContext = context,
            ProblemDetails = { Status = statusCode }
        });
 
        //Assert
        stream.Position = 0;
        var problemDetails = await JsonSerializer.DeserializeAsync<ProblemDetails>(stream, SerializerOptions);
        Assert.NotNull(problemDetails);
        Assert.Equal(statusCode, problemDetails.Status);
        Assert.Equal(type, problemDetails.Type);
        Assert.Equal(title, problemDetails.Title);
    }
 
    [Theory]
    [InlineData("")]
    [InlineData("*/*")]
    [InlineData("application/*")]
    [InlineData("application/json")]
    [InlineData("application/problem+json")]
    [InlineData("application/json; charset=utf-8")]
    [InlineData("application/json; v=1.0")]
    public void CanWrite_ReturnsTrue_WhenJsonAccepted(string contentType)
    {
        // Arrange
        var writer = GetWriter();
        var stream = new MemoryStream();
        var context = CreateContext(stream, contentType: contentType);
 
        //Act
        var result = writer.CanWrite(new() { HttpContext = context });
 
        //Assert
        Assert.True(result);
    }
 
    [Theory]
    [InlineData("application/xml")]
    [InlineData("application/problem+xml")]
    public void CanWrite_ReturnsFalse_WhenJsonNotAccepted(string contentType)
    {
        // Arrange
        var writer = GetWriter();
        var stream = new MemoryStream();
        var context = CreateContext(stream, contentType: contentType);
 
        //Act
        var result = writer.CanWrite(new() { HttpContext = context });
 
        //Assert
        Assert.False(result);
    }
 
    private static HttpContext CreateContext(
        Stream body,
        int statusCode = StatusCodes.Status400BadRequest,
        string contentType = "application/json")
    {
        var context = new DefaultHttpContext()
        {
            Response = { Body = body, StatusCode = statusCode },
            RequestServices = CreateServices()
        };
 
        if (!string.IsNullOrEmpty(contentType))
        {
            context.Request.Headers.Accept = contentType;
        }
 
        return context;
    }
 
    private static IServiceProvider CreateServices()
    {
        var services = new ServiceCollection();
        services.AddTransient(typeof(ILogger<>), typeof(NullLogger<>));
        services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
 
        return services.BuildServiceProvider();
    }
 
    private static DefaultProblemDetailsWriter GetWriter(ProblemDetailsOptions options = null, JsonOptions jsonOptions = null)
    {
        options ??= new ProblemDetailsOptions();
        jsonOptions ??= new JsonOptions();
 
        return new DefaultProblemDetailsWriter(Options.Create(options), Options.Create(jsonOptions));
    }
 
    internal class CustomProblemDetails : ProblemDetails
    {
        public string ExtraProperty { get; set; }
    }
 
    [JsonSerializable(typeof(CustomProblemDetails))]
    [JsonSerializable(typeof(CustomExtensionData))]
    internal partial class CustomProblemDetailsContext : JsonSerializerContext
    { }
 
    [JsonSerializable(typeof(CustomProblemDetails))]
    internal partial class CustomProblemDetailsContext2 : JsonSerializerContext
    { }
 
    internal record CustomExtensionData(string Data);
}