diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/OperationsController.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/OperationsController.cs new file mode 100644 index 0000000000..3679bafad3 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/OperationsController.cs @@ -0,0 +1,13 @@ +using JsonApiDotNetCore.AtomicOperations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCoreTests.IntegrationTests.Meta; + +public sealed class OperationsController( + IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request, + ITargetedFields targetedFields, IAtomicOperationFilter operationFilter) + : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields, operationFilter); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/RequestMetaTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/RequestMetaTests.cs new file mode 100644 index 0000000000..f3d7eb7730 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/RequestMetaTests.cs @@ -0,0 +1,173 @@ +using System.Net; +using System.Text.Json; +using FluentAssertions; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.Serialization.Request.Adapters; +using JsonApiDotNetCore.Serialization.Response; +using Microsoft.Extensions.DependencyInjection; +using TestBuildingBlocks; +using Xunit; + +namespace JsonApiDotNetCoreTests.IntegrationTests.Meta; + +public sealed class RequestMetaTests : IClassFixture, MetaDbContext>> +{ + private readonly IntegrationTestContext, MetaDbContext> _testContext; + private readonly MetaFakers _fakers = new(); + + public RequestMetaTests(IntegrationTestContext, MetaDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + testContext.UseController(); + testContext.UseController(); + + testContext.ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + + services.AddScoped(serviceProvider => + { + var documentAdapter = serviceProvider.GetRequiredService(); + var requestDocumentStore = serviceProvider.GetRequiredService(); + return new CapturingDocumentAdapter(documentAdapter, requestDocumentStore); + }); + }); + } + + [Fact] + public async Task Accepts_top_level_meta_in_patch_resource_request() + { + // Arrange + var store = _testContext.Factory.Services.GetRequiredService(); + + SupportTicket existingTicket = _fakers.SupportTicket.GenerateOne(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.SupportTickets.Add(existingTicket); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + type = "supportTickets", + id = existingTicket.StringId + }, + meta = new + { + category = "bug", + priority = 1, + components = new[] + { + "login", + "single-sign-on" + }, + relatedTo = new[] + { + new + { + id = 123, + link = "https://www.ticket-system.com/bugs/123" + }, + new + { + id = 789, + link = "https://www.ticket-system.com/bugs/789" + } + } + } + }; + + string route = $"/supportTickets/{existingTicket.StringId}"; + + // Act + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); + + store.Document.Should().NotBeNull(); + store.Document.Meta.Should().HaveCount(4); + + store.Document.Meta.Should().ContainKey("category").WhoseValue.With(value => + { + JsonElement element = value.Should().BeOfType().Subject; + element.GetString().Should().Be("bug"); + }); + + store.Document.Meta.Should().ContainKey("priority").WhoseValue.With(value => + { + JsonElement element = value.Should().BeOfType().Subject; + element.GetInt32().Should().Be(1); + }); + + store.Document.Meta.Should().ContainKey("components").WhoseValue.With(value => + { + string innerJson = value.Should().BeOfType().Subject.ToString(); + + innerJson.Should().BeJson(""" + [ + "login", + "single-sign-on" + ] + """); + }); + + store.Document.Meta.Should().ContainKey("relatedTo").WhoseValue.With(value => + { + string innerJson = value.Should().BeOfType().Subject.ToString(); + + innerJson.Should().BeJson(""" + [ + { + "id": 123, + "link": "https://www.ticket-system.com/bugs/123" + }, + { + "id": 789, + "link": "https://www.ticket-system.com/bugs/789" + } + ] + """); + }); + } + + // TODO: Add more tests, creating a mixture of: + // - Different endpoints: post resource, patch resource, post relationship, patch relationship, delete relationship, atomic:operations + // - Meta at different depths in the request body + // For example, assert on store.Document.Data.SingleValue.Meta + // See IHasMeta usage at https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/openapi/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiObjects for where meta can occur + // - Varying data structures: primitive types such as string/int/bool, arrays, dictionaries, and nested combinations of them + + private sealed class CapturingDocumentAdapter : IDocumentAdapter + { + private readonly IDocumentAdapter _innerAdapter; + private readonly RequestDocumentStore _requestDocumentStore; + + public CapturingDocumentAdapter(IDocumentAdapter innerAdapter, RequestDocumentStore requestDocumentStore) + { + ArgumentNullException.ThrowIfNull(innerAdapter); + ArgumentNullException.ThrowIfNull(requestDocumentStore); + + _innerAdapter = innerAdapter; + _requestDocumentStore = requestDocumentStore; + } + + public object? Convert(Document document) + { + _requestDocumentStore.Document = document; + return _innerAdapter.Convert(document); + } + } + + private sealed class RequestDocumentStore + { + public Document? Document { get; set; } + } +}