From 66492a225281cbaee8d52b54ea9db3e5e5ccdebc Mon Sep 17 00:00:00 2001
From: jaredcnance <jaredcnance@gmail.com>
Date: Thu, 26 Jul 2018 21:22:48 -0700
Subject: [PATCH 1/2] fix(GetOpProcessor): #340.1 not properly handling HasMany
 relationships

---
 .../Builders/DocumentBuilder.cs               |  1 -
 .../Operations/Processors/GetOpProcessor.cs   | 40 ++++++++++++++++---
 .../Get/GetRelationshipTests.cs               | 37 ++++++++++++++++-
 3 files changed, 70 insertions(+), 8 deletions(-)

diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs
index c6f5f999b4..ce060f57b7 100644
--- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs
+++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs
@@ -138,7 +138,6 @@ public DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity, I
 
             return data;
         }
-
         private bool ShouldIncludeAttribute(AttrAttribute attr, object attributeValue)
         {
             return OmitNullValuedAttribute(attr, attributeValue) == false
diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs
index a3cb6e7da8..954bfadd55 100644
--- a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs
+++ b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs
@@ -1,3 +1,4 @@
+using System.Collections;
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
@@ -83,10 +84,10 @@ public async Task<Operation> ProcessAsync(Operation operation)
             };
 
             operationResult.Data = string.IsNullOrWhiteSpace(operation.Ref.Id)
-            ? await GetAllAsync(operation)
-            : string.IsNullOrWhiteSpace(operation.Ref.Relationship)
-                ? await GetByIdAsync(operation)
-                : await GetRelationshipAsync(operation);
+                ? await GetAllAsync(operation)
+                : string.IsNullOrWhiteSpace(operation.Ref.Relationship)
+                    ? await GetByIdAsync(operation)
+                    : await GetRelationshipAsync(operation);
 
             return operationResult;
         }
@@ -135,11 +136,38 @@ private async Task<object> GetRelationshipAsync(Operation operation)
             // when no generic parameter is available
             var relationshipType = _contextGraph.GetContextEntity(operation.GetResourceTypeName())
                 .Relationships.Single(r => r.Is(operation.Ref.Relationship)).Type;
+
             var relatedContextEntity = _jsonApiContext.ContextGraph.GetContextEntity(relationshipType);
 
-            var doc = _documentBuilder.GetData(relatedContextEntity, result as IIdentifiable); // TODO: if this is safe, then it should be cast in the GetRelationshipAsync call
+            if (result == null)
+                return null;
+
+            if (result is IIdentifiable singleResource)
+                return GetData(relatedContextEntity, singleResource);
 
-            return doc;
+            if (result is IEnumerable multipleResults)
+                return GetData(relatedContextEntity, multipleResults);
+
+            throw new JsonApiException(500,
+                $"An unexpected type was returned from '{_getRelationship.GetType()}.{nameof(IGetRelationshipService<T, TId>.GetRelationshipAsync)}'.",
+                detail: $"Type '{result.GetType()} does not implement {nameof(IIdentifiable)} nor {nameof(IEnumerable<IIdentifiable>)}'");
+        }
+
+        private DocumentData GetData(ContextEntity contextEntity, IIdentifiable singleResource)
+        {
+            return _documentBuilder.GetData(contextEntity, singleResource);
+        }
+
+        private List<DocumentData> GetData(ContextEntity contextEntity, IEnumerable multipleResults)
+        {
+            var resources = new List<DocumentData>();
+            foreach (var singleResult in multipleResults)
+            {
+                if (singleResult is IIdentifiable resource)
+                    resources.Add(_documentBuilder.GetData(contextEntity, resource));
+            }
+
+            return resources;
         }
 
         private TId GetReferenceId(Operation operation) => TypeHelper.ConvertType<TId>(operation.Ref.Id);
diff --git a/test/OperationsExampleTests/Get/GetRelationshipTests.cs b/test/OperationsExampleTests/Get/GetRelationshipTests.cs
index 87edaba282..0aeef6f3ec 100644
--- a/test/OperationsExampleTests/Get/GetRelationshipTests.cs
+++ b/test/OperationsExampleTests/Get/GetRelationshipTests.cs
@@ -16,7 +16,7 @@ public class GetRelationshipTests : Fixture, IDisposable
         private readonly Faker _faker = new Faker();
 
         [Fact]
-        public async Task Can_Get_Article_Author()
+        public async Task Can_Get_HasOne_Relationship()
         {
             // arrange
             var context = GetService<AppDbContext>();
@@ -48,5 +48,40 @@ public async Task Can_Get_Article_Author()
             Assert.Equal(author.Id.ToString(), resourceObject.Id);
             Assert.Equal("authors", resourceObject.Type);
         }
+
+        [Fact]
+        public async Task Can_Get_HasMany_Relationship()
+        {
+            // arrange
+            var context = GetService<AppDbContext>();
+            var author = AuthorFactory.Get();
+            var article = ArticleFactory.Get();
+            article.Author = author;
+            context.Articles.Add(article);
+            context.SaveChanges();
+
+            var content = new
+            {
+                operations = new[] {
+                    new Dictionary<string, object> {
+                        { "op", "get"},
+                        { "ref",  new { type = "authors", id = author.StringId, relationship = "articles" } }
+                    }
+                }
+            };
+
+            // act
+            var (response, data) = await PatchAsync<OperationsDocument>("api/bulk", content);
+
+            // assert
+            Assert.NotNull(response);
+            Assert.NotNull(data);
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Single(data.Operations);
+
+            var resourceObject = data.Operations.Single().DataList.Single();
+            Assert.Equal(article.Id.ToString(), resourceObject.Id);
+            Assert.Equal("articles", resourceObject.Type);
+        }
     }
 }

From 8e065ce18c30cd38406ebecd29cf440032bfd319 Mon Sep 17 00:00:00 2001
From: jaredcnance <jaredcnance@gmail.com>
Date: Thu, 26 Jul 2018 21:45:30 -0700
Subject: [PATCH 2/2] fix(OperationProcessorResolver): #340.2 wrong type throws
 null ref

---
 .../Operations/OperationProcessorResolver.cs  |  22 +++-
 test/OperationsExampleTests/Get/GetTests.cs   | 118 +++++++++++-------
 .../OperationsProcessorResolverTests.cs       | 102 +++++++++++++++
 3 files changed, 188 insertions(+), 54 deletions(-)
 create mode 100644 test/UnitTests/Services/Operations/OperationsProcessorResolverTests.cs

diff --git a/src/JsonApiDotNetCore/Services/Operations/OperationProcessorResolver.cs b/src/JsonApiDotNetCore/Services/Operations/OperationProcessorResolver.cs
index 1004ed30dc..a702638c5d 100644
--- a/src/JsonApiDotNetCore/Services/Operations/OperationProcessorResolver.cs
+++ b/src/JsonApiDotNetCore/Services/Operations/OperationProcessorResolver.cs
@@ -51,7 +51,8 @@ public IOpProcessor LocateCreateService(Operation operation)
         {
             var resource = operation.GetResourceTypeName();
 
-            var contextEntity = _context.ContextGraph.GetContextEntity(resource);
+            var contextEntity = GetResourceMetadata(resource);
+
             var processor = _processorFactory.GetProcessor<IOpProcessor>(
                 typeof(ICreateOpProcessor<,>), contextEntity.EntityType, contextEntity.IdentityType
             );
@@ -64,7 +65,8 @@ public IOpProcessor LocateGetService(Operation operation)
         {
             var resource = operation.GetResourceTypeName();
 
-            var contextEntity = _context.ContextGraph.GetContextEntity(resource);
+            var contextEntity = GetResourceMetadata(resource);
+
             var processor = _processorFactory.GetProcessor<IOpProcessor>(
                 typeof(IGetOpProcessor<,>), contextEntity.EntityType, contextEntity.IdentityType
             );
@@ -77,7 +79,8 @@ public IOpProcessor LocateRemoveService(Operation operation)
         {
             var resource = operation.GetResourceTypeName();
 
-            var contextEntity = _context.ContextGraph.GetContextEntity(resource);
+            var contextEntity = GetResourceMetadata(resource);
+
             var processor = _processorFactory.GetProcessor<IOpProcessor>(
                 typeof(IRemoveOpProcessor<,>), contextEntity.EntityType, contextEntity.IdentityType
             );
@@ -90,9 +93,7 @@ public IOpProcessor LocateUpdateService(Operation operation)
         {
             var resource = operation.GetResourceTypeName();
 
-            var contextEntity = _context.ContextGraph.GetContextEntity(resource);
-            if (contextEntity == null)
-                throw new JsonApiException(400, $"This API does not expose a resource of type '{resource}'.");
+            var contextEntity = GetResourceMetadata(resource);
 
             var processor = _processorFactory.GetProcessor<IOpProcessor>(
                 typeof(IUpdateOpProcessor<,>), contextEntity.EntityType, contextEntity.IdentityType
@@ -100,5 +101,14 @@ public IOpProcessor LocateUpdateService(Operation operation)
 
             return processor;
         }
+
+        private ContextEntity GetResourceMetadata(string resourceName)
+        {
+            var contextEntity = _context.ContextGraph.GetContextEntity(resourceName);
+            if(contextEntity == null)
+                throw new JsonApiException(400, $"This API does not expose a resource of type '{resourceName}'.");
+
+            return contextEntity;
+        }
     }
 }
diff --git a/test/OperationsExampleTests/Get/GetTests.cs b/test/OperationsExampleTests/Get/GetTests.cs
index 78e7eeb976..f0d3fdffd8 100644
--- a/test/OperationsExampleTests/Get/GetTests.cs
+++ b/test/OperationsExampleTests/Get/GetTests.cs
@@ -1,51 +1,73 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-using Bogus;
-using JsonApiDotNetCore.Models.Operations;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Bogus;
+using JsonApiDotNetCore.Models.Operations;
 using JsonApiDotNetCoreExample.Data;
-using OperationsExampleTests.Factories;
-using Xunit;
-
-namespace OperationsExampleTests
-{
-    public class GetByIdTests : Fixture, IDisposable
-    {
+using OperationsExampleTests.Factories;
+using Xunit;
+
+namespace OperationsExampleTests
+{
+    public class GetByIdTests : Fixture, IDisposable
+    {
         private readonly Faker _faker = new Faker();
-
-        [Fact]
-        public async Task Can_Get_Authors()
-        {
-            // arrange
-            var expectedCount = _faker.Random.Int(1, 10);
-            var context = GetService<AppDbContext>();
-            context.Articles.RemoveRange(context.Articles);
-            context.Authors.RemoveRange(context.Authors);
-            var authors = AuthorFactory.Get(expectedCount);
-            context.AddRange(authors);
-            context.SaveChanges();
-
-            var content = new
-            {
-                operations = new[] {
-                    new Dictionary<string, object> {
-                        { "op", "get"},
-                        { "ref",  new { type = "authors" } }
-                    }
-                }
-            };
-
-            // act
-            var result = await PatchAsync<OperationsDocument>("api/bulk", content);
-
-            // assert
-            Assert.NotNull(result.response);
-            Assert.NotNull(result.data);
-            Assert.Equal(HttpStatusCode.OK, result.response.StatusCode);
-            Assert.Single(result.data.Operations);
-            Assert.Equal(expectedCount, result.data.Operations.Single().DataList.Count);
+
+        [Fact]
+        public async Task Can_Get_Authors()
+        {
+            // arrange
+            var expectedCount = _faker.Random.Int(1, 10);
+            var context = GetService<AppDbContext>();
+            context.Articles.RemoveRange(context.Articles);
+            context.Authors.RemoveRange(context.Authors);
+            var authors = AuthorFactory.Get(expectedCount);
+            context.AddRange(authors);
+            context.SaveChanges();
+
+            var content = new
+            {
+                operations = new[] {
+                    new Dictionary<string, object> {
+                        { "op", "get"},
+                        { "ref",  new { type = "authors" } }
+                    }
+                }
+            };
+
+            // act
+            var result = await PatchAsync<OperationsDocument>("api/bulk", content);
+
+            // assert
+            Assert.NotNull(result.response);
+            Assert.NotNull(result.data);
+            Assert.Equal(HttpStatusCode.OK, result.response.StatusCode);
+            Assert.Single(result.data.Operations);
+            Assert.Equal(expectedCount, result.data.Operations.Single().DataList.Count);
         }
-    }
-}
+
+        [Fact]
+        public async Task Get_Non_Existent_Type_Returns_400()
+        {
+            // arrange
+            var content = new
+            {
+                operations = new[] {
+                    new Dictionary<string, object> {
+                        { "op", "get"},
+                        { "ref",  new { type = "non-existent-type" } }
+                    }
+                }
+            };
+
+            // act
+            var result = await PatchAsync<OperationsDocument>("api/bulk", content);
+
+            // assert
+            Assert.Equal(HttpStatusCode.BadRequest, result.response.StatusCode);
+        }
+    }
+}
diff --git a/test/UnitTests/Services/Operations/OperationsProcessorResolverTests.cs b/test/UnitTests/Services/Operations/OperationsProcessorResolverTests.cs
new file mode 100644
index 0000000000..1e7026f2ee
--- /dev/null
+++ b/test/UnitTests/Services/Operations/OperationsProcessorResolverTests.cs
@@ -0,0 +1,102 @@
+using JsonApiDotNetCore.Builders;
+using JsonApiDotNetCore.Internal;
+using JsonApiDotNetCore.Internal.Generics;
+using JsonApiDotNetCore.Models.Operations;
+using JsonApiDotNetCore.Services;
+using JsonApiDotNetCore.Services.Operations;
+using Moq;
+using Xunit;
+
+namespace UnitTests.Services
+{
+    public class OperationProcessorResolverTests
+    {
+        private readonly Mock<IGenericProcessorFactory> _processorFactoryMock;
+        public readonly Mock<IJsonApiContext> _jsonApiContextMock;
+
+        public OperationProcessorResolverTests()
+        {
+            _processorFactoryMock = new Mock<IGenericProcessorFactory>();
+            _jsonApiContextMock = new Mock<IJsonApiContext>();
+        }
+
+        [Fact]
+        public void LocateCreateService_Throws_400_For_Entity_Not_Registered()
+        {
+            // arrange
+            _jsonApiContextMock.Setup(m => m.ContextGraph).Returns(new ContextGraphBuilder().Build());
+            var service = GetService();
+            var op = new Operation
+            {
+                Ref = new ResourceReference
+                {
+                    Type = "non-existent-type"
+                }
+            };
+
+            // act, assert
+            var e = Assert.Throws<JsonApiException>(() => service.LocateCreateService(op));
+            Assert.Equal(400, e.GetStatusCode());
+        }
+
+        [Fact]
+        public void LocateGetService_Throws_400_For_Entity_Not_Registered()
+        {
+            // arrange
+            _jsonApiContextMock.Setup(m => m.ContextGraph).Returns(new ContextGraphBuilder().Build());
+            var service = GetService();
+            var op = new Operation
+            {
+                Ref = new ResourceReference
+                {
+                    Type = "non-existent-type"
+                }
+            };
+
+            // act, assert
+            var e = Assert.Throws<JsonApiException>(() => service.LocateGetService(op));
+            Assert.Equal(400, e.GetStatusCode());
+        }
+
+        [Fact]
+        public void LocateRemoveService_Throws_400_For_Entity_Not_Registered()
+        {
+            // arrange
+            _jsonApiContextMock.Setup(m => m.ContextGraph).Returns(new ContextGraphBuilder().Build());
+            var service = GetService();
+            var op = new Operation
+            {
+                Ref = new ResourceReference
+                {
+                    Type = "non-existent-type"
+                }
+            };
+
+            // act, assert
+            var e = Assert.Throws<JsonApiException>(() => service.LocateRemoveService(op));
+            Assert.Equal(400, e.GetStatusCode());
+        }
+
+        [Fact]
+        public void LocateUpdateService_Throws_400_For_Entity_Not_Registered()
+        {
+            // arrange
+            _jsonApiContextMock.Setup(m => m.ContextGraph).Returns(new ContextGraphBuilder().Build());
+            var service = GetService();
+            var op = new Operation
+            {
+                Ref = new ResourceReference
+                {
+                    Type = "non-existent-type"
+                }
+            };
+
+            // act, assert
+            var e = Assert.Throws<JsonApiException>(() => service.LocateUpdateService(op));
+            Assert.Equal(400, e.GetStatusCode());
+        }
+
+        private OperationProcessorResolver GetService()
+            => new OperationProcessorResolver(_processorFactoryMock.Object, _jsonApiContextMock.Object);
+    }
+}