diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs index 7af5c3b594..5e4754c7c5 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs @@ -1,10 +1,13 @@ +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Bogus; +using JsonApiDotNetCore.Models; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCoreExampleTests.Helpers.Extensions; using Microsoft.AspNetCore.Hosting; using Newtonsoft.Json; using Xunit; @@ -22,6 +25,14 @@ public DeeplyNestedInclusionTests(TestFixture<TestStartup> fixture) _fixture = fixture; } + private void ResetContext(AppDbContext context) + { + context.TodoItems.RemoveRange(context.TodoItems); + context.TodoItemCollections.RemoveRange(context.TodoItemCollections); + context.People.RemoveRange(context.People); + context.PersonRoles.RemoveRange(context.PersonRoles); + } + [Fact] public async Task Can_Include_Nested_Relationships() { @@ -53,5 +64,253 @@ public async Task Can_Include_Nested_Relationships() Assert.NotNull(responseTodoItem.Collection); Assert.NotNull(responseTodoItem.Collection.Owner); } + + [Fact] + public async Task Can_Include_Nested_HasMany_Relationships() + { + // arrange + const string route = "/api/v1/todo-items?include=collection.todo-items"; + + var todoItem = new TodoItem { + Collection = new TodoItemCollection { + Owner = new Person(), + TodoItems = new List<TodoItem> { + new TodoItem(), + new TodoItem() + } + } + }; + + + var context = _fixture.GetService<AppDbContext>(); + ResetContext(context); + + context.TodoItems.Add(todoItem); + await context.SaveChangesAsync(); + + // act + var response = await _fixture.Client.GetAsync(route); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var documents = JsonConvert.DeserializeObject<Documents>(body); + var included = documents.Included; + + Assert.Equal(4, included.Count); + + Assert.Equal(3, included.CountOfType("todo-items")); + Assert.Equal(1, included.CountOfType("todo-collections")); + } + + [Fact] + public async Task Can_Include_Nested_HasMany_Relationships_BelongsTo() + { + // arrange + const string route = "/api/v1/todo-items?include=collection.todo-items.owner"; + + var todoItem = new TodoItem { + Collection = new TodoItemCollection { + Owner = new Person(), + TodoItems = new List<TodoItem> { + new TodoItem { + Owner = new Person() + }, + new TodoItem() + } + } + }; + + var context = _fixture.GetService<AppDbContext>(); + ResetContext(context); + + context.TodoItems.Add(todoItem); + await context.SaveChangesAsync(); + + // act + var response = await _fixture.Client.GetAsync(route); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var documents = JsonConvert.DeserializeObject<Documents>(body); + var included = documents.Included; + + Assert.Equal(5, included.Count); + + Assert.Equal(3, included.CountOfType("todo-items")); + Assert.Equal(1, included.CountOfType("people")); + Assert.Equal(1, included.CountOfType("todo-collections")); + } + + [Fact] + public async Task Can_Include_Nested_Relationships_With_Multiple_Paths() + { + // arrange + const string route = "/api/v1/todo-items?include=collection.owner.role,collection.todo-items.owner"; + + var todoItem = new TodoItem { + Collection = new TodoItemCollection { + Owner = new Person { + Role = new PersonRole() + }, + TodoItems = new List<TodoItem> { + new TodoItem { + Owner = new Person() + }, + new TodoItem() + } + } + }; + + var context = _fixture.GetService<AppDbContext>(); + ResetContext(context); + + context.TodoItems.Add(todoItem); + await context.SaveChangesAsync(); + + // act + var response = await _fixture.Client.GetAsync(route); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var documents = JsonConvert.DeserializeObject<Documents>(body); + var included = documents.Included; + + Assert.Equal(7, included.Count); + + Assert.Equal(3, included.CountOfType("todo-items")); + Assert.Equal(2, included.CountOfType("people")); + Assert.Equal(1, included.CountOfType("person-roles")); + Assert.Equal(1, included.CountOfType("todo-collections")); + } + + [Fact] + public async Task Included_Resources_Are_Correct() + { + // arrange + var role = new PersonRole(); + var assignee = new Person { Role = role }; + var collectionOwner = new Person(); + var someOtherOwner = new Person(); + var collection = new TodoItemCollection { Owner = collectionOwner }; + var todoItem1 = new TodoItem { Collection = collection, Assignee = assignee }; + var todoItem2 = new TodoItem { Collection = collection, Assignee = assignee }; + var todoItem3 = new TodoItem { Collection = collection, Owner = someOtherOwner }; + var todoItem4 = new TodoItem { Collection = collection, Owner = assignee }; + + var context = _fixture.GetService<AppDbContext>(); + ResetContext(context); + + context.TodoItems.Add(todoItem1); + context.TodoItems.Add(todoItem2); + context.TodoItems.Add(todoItem3); + context.TodoItems.Add(todoItem4); + context.PersonRoles.Add(role); + context.People.Add(assignee); + context.People.Add(collectionOwner); + context.People.Add(someOtherOwner); + context.TodoItemCollections.Add(collection); + + + await context.SaveChangesAsync(); + + string route = + "/api/v1/todo-items/" + todoItem1.Id + "?include=" + + "collection.owner," + + "assignee.role," + + "assignee.assigned-todo-items"; + + // act + var response = await _fixture.Client.GetAsync(route); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var documents = JsonConvert.DeserializeObject<Document>(body); + var included = documents.Included; + + // 1 collection, 1 owner, + // 1 assignee, 1 assignee role, + // 2 assigned todo items (including the primary resource) + Assert.Equal(6, included.Count); + + var collectionDocument = included.FindResource("todo-collections", collection.Id); + var ownerDocument = included.FindResource("people", collectionOwner.Id); + var assigneeDocument = included.FindResource("people", assignee.Id); + var roleDocument = included.FindResource("person-roles", role.Id); + var assignedTodo1 = included.FindResource("todo-items", todoItem1.Id); + var assignedTodo2 = included.FindResource("todo-items", todoItem2.Id); + + Assert.NotNull(assignedTodo1); + Assert.Equal(todoItem1.Id.ToString(), assignedTodo1.Id); + + Assert.NotNull(assignedTodo2); + Assert.Equal(todoItem2.Id.ToString(), assignedTodo2.Id); + + Assert.NotNull(collectionDocument); + Assert.Equal(collection.Id.ToString(), collectionDocument.Id); + + Assert.NotNull(ownerDocument); + Assert.Equal(collectionOwner.Id.ToString(), ownerDocument.Id); + + Assert.NotNull(assigneeDocument); + Assert.Equal(assignee.Id.ToString(), assigneeDocument.Id); + + Assert.NotNull(roleDocument); + Assert.Equal(role.Id.ToString(), roleDocument.Id); + } + + [Fact] + public async Task Can_Include_Doubly_HasMany_Relationships() + { + // arrange + var person = new Person { + TodoItemCollections = new List<TodoItemCollection> { + new TodoItemCollection { + TodoItems = new List<TodoItem> { + new TodoItem(), + new TodoItem() + } + }, + new TodoItemCollection { + TodoItems = new List<TodoItem> { + new TodoItem(), + new TodoItem(), + new TodoItem() + } + } + } + }; + + var context = _fixture.GetService<AppDbContext>(); + ResetContext(context); + + context.People.Add(person); + + await context.SaveChangesAsync(); + + string route = "/api/v1/people/" + person.Id + "?include=todo-collections.todo-items"; + + // act + var response = await _fixture.Client.GetAsync(route); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var documents = JsonConvert.DeserializeObject<Document>(body); + var included = documents.Included; + + Assert.Equal(7, included.Count); + + Assert.Equal(5, included.CountOfType("todo-items")); + Assert.Equal(2, included.CountOfType("todo-collections")); + } } } \ No newline at end of file diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/DocumentExtensions.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/DocumentExtensions.cs new file mode 100644 index 0000000000..f467e17f5b --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/DocumentExtensions.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using JsonApiDotNetCore.Models; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.Internal; +using Microsoft.EntityFrameworkCore.Storage; +using Database = Microsoft.EntityFrameworkCore.Storage.Database; + +namespace JsonApiDotNetCoreExampleTests.Helpers.Extensions +{ + public static class DocumentExtensions + { + public static DocumentData FindResource<TId>(this List<DocumentData> included, string type, TId id) + { + var document = included.Where(documentData => ( + documentData.Type == type + && documentData.Id == id.ToString() + )).FirstOrDefault(); + + return document; + } + + public static int CountOfType(this List<DocumentData> included, string type) { + return included.Where(documentData => documentData.Type == type).Count(); + } + } +} \ No newline at end of file diff --git a/test/UnitTests/Serialization/JsonApiSerializerTests.cs b/test/UnitTests/Serialization/JsonApiSerializerTests.cs index 8a67b80434..c6b4a09128 100644 --- a/test/UnitTests/Serialization/JsonApiSerializerTests.cs +++ b/test/UnitTests/Serialization/JsonApiSerializerTests.cs @@ -1,9 +1,15 @@ -using JsonApiDotNetCore.Builders; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Request; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; +using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; @@ -17,19 +23,9 @@ public void Can_Serialize_Complex_Types() // arrange var contextGraphBuilder = new ContextGraphBuilder(); contextGraphBuilder.AddResource<TestResource>("test-resource"); - var contextGraph = contextGraphBuilder.Build(); - - var jsonApiContextMock = new Mock<IJsonApiContext>(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); - jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions()); - jsonApiContextMock.Setup(m => m.RequestEntity) - .Returns(contextGraph.GetContextEntity("test-resource")); - jsonApiContextMock.Setup(m => m.MetaBuilder).Returns(new MetaBuilder()); - jsonApiContextMock.Setup(m => m.PageManager).Returns(new PageManager()); + + var serializer = GetSerializer(contextGraphBuilder); - var documentBuilder = new DocumentBuilder(jsonApiContextMock.Object); - var serializer = new JsonApiSerializer(jsonApiContextMock.Object, documentBuilder); var resource = new TestResource { ComplexMember = new ComplexType @@ -43,18 +39,235 @@ public void Can_Serialize_Complex_Types() // assert Assert.NotNull(result); - Assert.Equal("{\"data\":{\"attributes\":{\"complex-member\":{\"compound-name\":\"testname\"}},\"type\":\"test-resource\",\"id\":\"\"}}", result); + + var expectedFormatted = + @"{ + ""data"": { + ""attributes"": { + ""complex-member"": { + ""compound-name"": ""testname"" + } + }, + ""relationships"": { + ""children"": { + ""links"": { + ""self"": ""/test-resource//relationships/children"", + ""related"": ""/test-resource//children"" + } + } + }, + ""type"": ""test-resource"", + ""id"": """" + } + }"; + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + + Assert.Equal(expected, result); + } + + [Fact] + public void Can_Serialize_Deeply_Nested_Relationships() + { + // arrange + var contextGraphBuilder = new ContextGraphBuilder(); + contextGraphBuilder.AddResource<TestResource>("test-resource"); + contextGraphBuilder.AddResource<ChildResource>("children"); + contextGraphBuilder.AddResource<InfectionResource>("infections"); + + var serializer = GetSerializer( + contextGraphBuilder, + included: new List<string> { "children.infections" } + ); + + var resource = new TestResource + { + Id = 1, + Children = new List<ChildResource> { + new ChildResource { + Id = 2, + Infections = new List<InfectionResource> { + new InfectionResource { Id = 4 }, + new InfectionResource { Id = 5 }, + } + }, + new ChildResource { + Id = 3 + } + } + }; + + // act + var result = serializer.Serialize(resource); + + // assert + Assert.NotNull(result); + + var expectedFormatted = + @"{ + ""data"": { + ""attributes"": { + ""complex-member"": null + }, + ""relationships"": { + ""children"": { + ""links"": { + ""self"": ""/test-resource/1/relationships/children"", + ""related"": ""/test-resource/1/children"" + }, + ""data"": [{ + ""type"": ""children"", + ""id"": ""2"" + }, { + ""type"": ""children"", + ""id"": ""3"" + }] + } + }, + ""type"": ""test-resource"", + ""id"": ""1"" + }, + ""included"": [ + { + ""attributes"": {}, + ""relationships"": { + ""infections"": { + ""links"": { + ""self"": ""/children/2/relationships/infections"", + ""related"": ""/children/2/infections"" + }, + ""data"": [{ + ""type"": ""infections"", + ""id"": ""4"" + }, { + ""type"": ""infections"", + ""id"": ""5"" + }] + }, + ""parent"": { + ""links"": { + ""self"": ""/children/2/relationships/parent"", + ""related"": ""/children/2/parent"" + } + } + }, + ""type"": ""children"", + ""id"": ""2"" + }, + { + ""attributes"": {}, + ""relationships"": { + ""infected"": { + ""links"": { + ""self"": ""/infections/4/relationships/infected"", + ""related"": ""/infections/4/infected"" + } + } + }, + ""type"": ""infections"", + ""id"": ""4"" + }, + { + ""attributes"": {}, + ""relationships"": { + ""infected"": { + ""links"": { + ""self"": ""/infections/5/relationships/infected"", + ""related"": ""/infections/5/infected"" + } + } + }, + ""type"": ""infections"", + ""id"": ""5"" + }, + { + ""attributes"": {}, + ""relationships"": { + ""infections"": { + ""links"": { + ""self"": ""/children/3/relationships/infections"", + ""related"": ""/children/3/infections"" + } + }, + ""parent"": { + ""links"": { + ""self"": ""/children/3/relationships/parent"", + ""related"": ""/children/3/parent"" + } + } + }, + ""type"": ""children"", + ""id"": ""3"" + } + ] + }"; + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + + Assert.Equal(expected, result); + } + + private JsonApiSerializer GetSerializer( + ContextGraphBuilder contextGraphBuilder, + List<string> included = null) + { + var contextGraph = contextGraphBuilder.Build(); + + var jsonApiContextMock = new Mock<IJsonApiContext>(); + jsonApiContextMock.SetupAllProperties(); + jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions()); + jsonApiContextMock.Setup(m => m.RequestEntity).Returns(contextGraph.GetContextEntity("test-resource")); + // jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary<AttrAttribute, object>()); + // jsonApiContextMock.Setup(m => m.RelationshipsToUpdate).Returns(new Dictionary<RelationshipAttribute, object>()); + // jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers()); + // jsonApiContextMock.Setup(m => m.HasOneRelationshipPointers).Returns(new HasOneRelationshipPointers()); + jsonApiContextMock.Setup(m => m.MetaBuilder).Returns(new MetaBuilder()); + jsonApiContextMock.Setup(m => m.PageManager).Returns(new PageManager()); + + if (included != null) + jsonApiContextMock.Setup(m => m.IncludedRelationships).Returns(included); + + var jsonApiOptions = new JsonApiOptions(); + jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); + + var services = new ServiceCollection(); + + var mvcBuilder = services.AddMvcCore(); + + services + .AddJsonApiInternals(jsonApiOptions); + + var provider = services.BuildServiceProvider(); + var scoped = new TestScopedServiceProvider(provider); + + var documentBuilder = new DocumentBuilder(jsonApiContextMock.Object, scopedServiceProvider: scoped); + var serializer = new JsonApiSerializer(jsonApiContextMock.Object, documentBuilder); + + return serializer; } private class TestResource : Identifiable { [Attr("complex-member")] public ComplexType ComplexMember { get; set; } + + [HasMany("children")] public List<ChildResource> Children { get; set; } } private class ComplexType { public string CompoundName { get; set; } } + + private class ChildResource : Identifiable + { + [HasMany("infections")] public List<InfectionResource> Infections { get; set;} + + [HasOne("parent")] public TestResource Parent { get; set; } + } + + private class InfectionResource : Identifiable + { + [HasOne("infected")] public ChildResource Infected { get; set; } + } } }