diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index fd6ec8947a..aec2d16cfe 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.Controllers { - public class BaseJsonApiController + public class BaseJsonApiController : BaseJsonApiController where T : class, IIdentifiable { @@ -47,7 +47,7 @@ public class BaseJsonApiController private readonly ICreateService _create; private readonly IUpdateService _update; private readonly IUpdateRelationshipService _updateRelationships; - private readonly IDeleteService _delete; + private readonly IDeleteService _delete; private readonly IJsonApiContext _jsonApiContext; public BaseJsonApiController( @@ -156,7 +156,7 @@ public virtual async Task PostAsync([FromBody] T entity) return Forbidden(); if (_jsonApiContext.Options.ValidateModelState && !ModelState.IsValid) - return BadRequest(ModelState.ConvertToErrorCollection()); + return UnprocessableEntity(ModelState.ConvertToErrorCollection(_jsonApiContext.ContextGraph)); entity = await _create.CreateAsync(entity); @@ -169,8 +169,9 @@ public virtual async Task PatchAsync(TId id, [FromBody] T entity) if (entity == null) return UnprocessableEntity(); + if (_jsonApiContext.Options.ValidateModelState && !ModelState.IsValid) - return BadRequest(ModelState.ConvertToErrorCollection()); + return UnprocessableEntity(ModelState.ConvertToErrorCollection(_jsonApiContext.ContextGraph)); var updatedEntity = await _update.UpdateAsync(id, entity); diff --git a/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs b/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs index d67f7e66c4..43c33c21f2 100644 --- a/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs @@ -1,3 +1,4 @@ +using System; using JsonApiDotNetCore.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.EntityFrameworkCore.Internal; @@ -6,6 +7,7 @@ namespace JsonApiDotNetCore.Extensions { public static class ModelStateExtensions { + [Obsolete("Use Generic Method ConvertToErrorCollection(IContextGraph contextGraph) instead for full validation errors")] public static ErrorCollection ConvertToErrorCollection(this ModelStateDictionary modelState) { ErrorCollection collection = new ErrorCollection(); @@ -23,6 +25,34 @@ public static ErrorCollection ConvertToErrorCollection(this ModelStateDictionary } } + return collection; + } + public static ErrorCollection ConvertToErrorCollection(this ModelStateDictionary modelState, IContextGraph contextGraph) + { + ErrorCollection collection = new ErrorCollection(); + foreach (var entry in modelState) + { + if (entry.Value.Errors.Any() == false) + continue; + + var attrName = contextGraph.GetPublicAttributeName(entry.Key); + + foreach (var modelError in entry.Value.Errors) + { + if (modelError.Exception is JsonApiException jex) + collection.Errors.AddRange(jex.GetError().Errors); + else + collection.Errors.Add(new Error( + status: 422, + title: entry.Key, + detail: modelError.ErrorMessage, + meta: modelError.Exception != null ? ErrorMeta.FromException(modelError.Exception) : null, + source: attrName == null ? null : new { + pointer = $"/data/attributes/{attrName}" + })); + } + } + return collection; } } diff --git a/src/JsonApiDotNetCore/Internal/ContextGraph.cs b/src/JsonApiDotNetCore/Internal/ContextGraph.cs index c0c7f2274b..4b6a310527 100644 --- a/src/JsonApiDotNetCore/Internal/ContextGraph.cs +++ b/src/JsonApiDotNetCore/Internal/ContextGraph.cs @@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.Internal public interface IContextGraph { /// - /// Gets the value of the navigation property, defined by the relationshipName, + /// Gets the value of the navigation property, defined by the relationshipName, /// on the provided instance. /// /// The resource instance @@ -42,6 +42,12 @@ public interface IContextGraph /// ContextEntity GetContextEntity(Type entityType); + /// + /// Get the public attribute name for a type based on the internal attribute name. + /// + /// The internal attribute name for a . + string GetPublicAttributeName(string internalAttributeName); + /// /// Was built against an EntityFrameworkCore DbContext ? /// @@ -111,5 +117,13 @@ public string GetRelationshipName(string relationshipName) .SingleOrDefault(r => r.Is(relationshipName)) ?.InternalRelationshipName; } - } + + public string GetPublicAttributeName(string internalAttributeName) + { + return GetContextEntity(typeof(TParent)) + .Attributes + .SingleOrDefault(a => a.InternalAttributeName == internalAttributeName)? + .PublicAttributeName; + } + } } diff --git a/src/JsonApiDotNetCore/Internal/Error.cs b/src/JsonApiDotNetCore/Internal/Error.cs index 999611d79e..71852e28ea 100644 --- a/src/JsonApiDotNetCore/Internal/Error.cs +++ b/src/JsonApiDotNetCore/Internal/Error.cs @@ -9,9 +9,9 @@ public class Error { public Error() { } - + [Obsolete("Use Error constructors with int typed status")] - public Error(string status, string title, ErrorMeta meta = null, string source = null) + public Error(string status, string title, ErrorMeta meta = null, object source = null) { Status = status; Title = title; @@ -19,7 +19,7 @@ public Error(string status, string title, ErrorMeta meta = null, string source = Source = source; } - public Error(int status, string title, ErrorMeta meta = null, string source = null) + public Error(int status, string title, ErrorMeta meta = null, object source = null) { Status = status.ToString(); Title = title; @@ -28,7 +28,7 @@ public Error(int status, string title, ErrorMeta meta = null, string source = nu } [Obsolete("Use Error constructors with int typed status")] - public Error(string status, string title, string detail, ErrorMeta meta = null, string source = null) + public Error(string status, string title, string detail, ErrorMeta meta = null, object source = null) { Status = status; Title = title; @@ -37,7 +37,7 @@ public Error(string status, string title, string detail, ErrorMeta meta = null, Source = source; } - public Error(int status, string title, string detail, ErrorMeta meta = null, string source = null) + public Error(int status, string title, string detail, ErrorMeta meta = null, object source = null) { Status = status.ToString(); Title = title; @@ -45,13 +45,13 @@ public Error(int status, string title, string detail, ErrorMeta meta = null, str Meta = meta; Source = source; } - + [JsonProperty("title")] public string Title { get; set; } [JsonProperty("detail")] public string Detail { get; set; } - + [JsonProperty("status")] public string Status { get; set; } @@ -59,7 +59,7 @@ public Error(int status, string title, string detail, ErrorMeta meta = null, str public int StatusCode => int.Parse(Status); [JsonProperty("source")] - public string Source { get; set; } + public object Source { get; set; } [JsonProperty("meta")] public ErrorMeta Meta { get; set; } @@ -73,8 +73,8 @@ public class ErrorMeta [JsonProperty("stackTrace")] public string[] StackTrace { get; set; } - public static ErrorMeta FromException(Exception e) - => new ErrorMeta { + public static ErrorMeta FromException(Exception e) + => new ErrorMeta { StackTrace = e.Demystify().ToString().Split(new[] { "\n"}, int.MaxValue, StringSplitOptions.RemoveEmptyEntries) }; } diff --git a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs index 873b3f50d2..7327bb18a3 100644 --- a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs +++ b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs @@ -1,344 +1,352 @@ -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using Moq; -using Xunit; -using System.Threading.Tasks; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace UnitTests -{ - public class BaseJsonApiController_Tests - { - public class Resource : Identifiable { } - private Mock _jsonApiContextMock = new Mock(); - - [Fact] - public async Task GetAsync_Calls_Service() - { - // arrange - var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getAll: serviceMock.Object); - - // act - await controller.GetAsync(); - - // assert - serviceMock.Verify(m => m.GetAsync(), Times.Once); - VerifyApplyContext(); - } - - [Fact] - public async Task GetAsync_Throws_405_If_No_Service() - { - // arrange - var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, null); - - // act - var exception = await Assert.ThrowsAsync(() => controller.GetAsync()); - - // assert - Assert.Equal(405, exception.GetStatusCode()); - } - - [Fact] - public async Task GetAsyncById_Calls_Service() - { - // arrange - const int id = 0; - var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getById: serviceMock.Object); - - // act - await controller.GetAsync(id); - - // assert - serviceMock.Verify(m => m.GetAsync(id), Times.Once); - VerifyApplyContext(); - } - - [Fact] - public async Task GetAsyncById_Throws_405_If_No_Service() - { - // arrange - const int id = 0; - var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getById: null); - - // act - var exception = await Assert.ThrowsAsync(() => controller.GetAsync(id)); - - // assert - Assert.Equal(405, exception.GetStatusCode()); - } - - [Fact] - public async Task GetRelationshipsAsync_Calls_Service() - { - // arrange - const int id = 0; - var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationships: serviceMock.Object); - - // act - await controller.GetRelationshipsAsync(id, string.Empty); - - // assert - serviceMock.Verify(m => m.GetRelationshipsAsync(id, string.Empty), Times.Once); - VerifyApplyContext(); - } - - [Fact] - public async Task GetRelationshipsAsync_Throws_405_If_No_Service() - { - // arrange - const int id = 0; - var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationships: null); - - // act - var exception = await Assert.ThrowsAsync(() => controller.GetRelationshipsAsync(id, string.Empty)); - - // assert - Assert.Equal(405, exception.GetStatusCode()); - } - - [Fact] - public async Task GetRelationshipAsync_Calls_Service() - { - // arrange - const int id = 0; - var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationship: serviceMock.Object); - - // act - await controller.GetRelationshipAsync(id, string.Empty); - - // assert - serviceMock.Verify(m => m.GetRelationshipAsync(id, string.Empty), Times.Once); - VerifyApplyContext(); - } - - [Fact] - public async Task GetRelationshipAsync_Throws_405_If_No_Service() - { - // arrange - const int id = 0; - var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationship: null); - - // act - var exception = await Assert.ThrowsAsync(() => controller.GetRelationshipAsync(id, string.Empty)); - - // assert - Assert.Equal(405, exception.GetStatusCode()); - } - - [Fact] - public async Task PatchAsync_Calls_Service() - { - // arrange - const int id = 0; - var resource = new Resource(); - var serviceMock = new Mock>(); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions()); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); - - // act - await controller.PatchAsync(id, resource); - - // assert - serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Once); - VerifyApplyContext(); - } - - [Fact] - public async Task PatchAsync_ModelStateInvalid_ValidateModelStateDisbled() - { - // arrange - const int id = 0; - var resource = new Resource(); - var serviceMock = new Mock>(); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = false }); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); - - // act - var response = await controller.PatchAsync(id, resource); - - // assert - serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Once); - VerifyApplyContext(); - Assert.IsNotType(response); - } - - [Fact] - public async Task PatchAsync_ModelStateInvalid_ValidateModelStateEnabled() - { - // arrange - const int id = 0; - var resource = new Resource(); - var serviceMock = new Mock>(); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions{ValidateModelState = true}); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); - controller.ModelState.AddModelError("Id", "Failed Validation"); - - // act - var response = await controller.PatchAsync(id, resource); - - // assert - serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Never); - Assert.IsType(response); - Assert.IsType(((BadRequestObjectResult) response).Value); - } - - [Fact] - public async Task PatchAsync_Throws_405_If_No_Service() - { - // arrange - const int id = 0; - var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: null); - - // act - var exception = await Assert.ThrowsAsync(() => controller.PatchAsync(id, It.IsAny())); - - // assert - Assert.Equal(405, exception.GetStatusCode()); - } - - [Fact] - public async Task PostAsync_Calls_Service() - { - // arrange - var resource = new Resource(); - var serviceMock = new Mock>(); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions()); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object); - serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource); - controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext {HttpContext = new DefaultHttpContext()}; - - // act - await controller.PostAsync(resource); - - // assert - serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Once); - VerifyApplyContext(); - } - - [Fact] - public async Task PostAsync_ModelStateInvalid_ValidateModelStateDisabled() - { - // arrange - var resource = new Resource(); - var serviceMock = new Mock>(); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = false }); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object); - serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource); - controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext { HttpContext = new DefaultHttpContext() }; - - // act - var response = await controller.PostAsync(resource); - - // assert - serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Once); - VerifyApplyContext(); - Assert.IsNotType(response); - } - - [Fact] - public async Task PostAsync_ModelStateInvalid_ValidateModelStateEnabled() - { - // arrange - var resource = new Resource(); - var serviceMock = new Mock>(); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = true }); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object); - controller.ModelState.AddModelError("Id", "Failed Validation"); - - // act - var response = await controller.PostAsync(resource); - - // assert - serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Never); - Assert.IsType(response); - Assert.IsType(((BadRequestObjectResult)response).Value); - } - - [Fact] - public async Task PatchRelationshipsAsync_Calls_Service() - { - // arrange - const int id = 0; - var resource = new Resource(); - var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, updateRelationships: serviceMock.Object); - - // act - await controller.PatchRelationshipsAsync(id, string.Empty, null); - - // assert - serviceMock.Verify(m => m.UpdateRelationshipsAsync(id, string.Empty, null), Times.Once); - VerifyApplyContext(); - } - - [Fact] - public async Task PatchRelationshipsAsync_Throws_405_If_No_Service() - { - // arrange - const int id = 0; - var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, updateRelationships: null); - - // act - var exception = await Assert.ThrowsAsync(() => controller.PatchRelationshipsAsync(id, string.Empty, null)); - - // assert - Assert.Equal(405, exception.GetStatusCode()); - } - - [Fact] - public async Task DeleteAsync_Calls_Service() - { - // arrange - const int id = 0; - var resource = new Resource(); - var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, delete: serviceMock.Object); - - // act - await controller.DeleteAsync(id); - - // assert - serviceMock.Verify(m => m.DeleteAsync(id), Times.Once); - VerifyApplyContext(); - } - - [Fact] - public async Task DeleteAsync_Throws_405_If_No_Service() - { - // arrange - const int id = 0; - var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, delete: null); - - // act - var exception = await Assert.ThrowsAsync(() => controller.DeleteAsync(id)); - - // assert - Assert.Equal(405, exception.GetStatusCode()); - } - - private void VerifyApplyContext() - => _jsonApiContextMock.Verify(m => m.ApplyContext(It.IsAny>()), Times.Once); - } -} +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Moq; +using Xunit; +using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace UnitTests +{ + public class BaseJsonApiController_Tests + { + public class Resource : Identifiable + { + [Attr("test-attribute")] public string TestAttribute { get; set; } + } + private Mock _jsonApiContextMock = new Mock(); + private Mock _contextGraphMock = new Mock(); + + [Fact] + public async Task GetAsync_Calls_Service() + { + // arrange + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getAll: serviceMock.Object); + + // act + await controller.GetAsync(); + + // assert + serviceMock.Verify(m => m.GetAsync(), Times.Once); + VerifyApplyContext(); + } + + [Fact] + public async Task GetAsync_Throws_405_If_No_Service() + { + // arrange + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, null); + + // act + var exception = await Assert.ThrowsAsync(() => controller.GetAsync()); + + // assert + Assert.Equal(405, exception.GetStatusCode()); + } + + [Fact] + public async Task GetAsyncById_Calls_Service() + { + // arrange + const int id = 0; + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getById: serviceMock.Object); + + // act + await controller.GetAsync(id); + + // assert + serviceMock.Verify(m => m.GetAsync(id), Times.Once); + VerifyApplyContext(); + } + + [Fact] + public async Task GetAsyncById_Throws_405_If_No_Service() + { + // arrange + const int id = 0; + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getById: null); + + // act + var exception = await Assert.ThrowsAsync(() => controller.GetAsync(id)); + + // assert + Assert.Equal(405, exception.GetStatusCode()); + } + + [Fact] + public async Task GetRelationshipsAsync_Calls_Service() + { + // arrange + const int id = 0; + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationships: serviceMock.Object); + + // act + await controller.GetRelationshipsAsync(id, string.Empty); + + // assert + serviceMock.Verify(m => m.GetRelationshipsAsync(id, string.Empty), Times.Once); + VerifyApplyContext(); + } + + [Fact] + public async Task GetRelationshipsAsync_Throws_405_If_No_Service() + { + // arrange + const int id = 0; + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationships: null); + + // act + var exception = await Assert.ThrowsAsync(() => controller.GetRelationshipsAsync(id, string.Empty)); + + // assert + Assert.Equal(405, exception.GetStatusCode()); + } + + [Fact] + public async Task GetRelationshipAsync_Calls_Service() + { + // arrange + const int id = 0; + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationship: serviceMock.Object); + + // act + await controller.GetRelationshipAsync(id, string.Empty); + + // assert + serviceMock.Verify(m => m.GetRelationshipAsync(id, string.Empty), Times.Once); + VerifyApplyContext(); + } + + [Fact] + public async Task GetRelationshipAsync_Throws_405_If_No_Service() + { + // arrange + const int id = 0; + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationship: null); + + // act + var exception = await Assert.ThrowsAsync(() => controller.GetRelationshipAsync(id, string.Empty)); + + // assert + Assert.Equal(405, exception.GetStatusCode()); + } + + [Fact] + public async Task PatchAsync_Calls_Service() + { + // arrange + const int id = 0; + var resource = new Resource(); + var serviceMock = new Mock>(); + _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); + _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions()); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); + + // act + await controller.PatchAsync(id, resource); + + // assert + serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Once); + VerifyApplyContext(); + } + + [Fact] + public async Task PatchAsync_ModelStateInvalid_ValidateModelStateDisbled() + { + // arrange + const int id = 0; + var resource = new Resource(); + var serviceMock = new Mock>(); + _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); + _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = false }); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); + + // act + var response = await controller.PatchAsync(id, resource); + + // assert + serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Once); + VerifyApplyContext(); + Assert.IsNotType(response); + } + + [Fact] + public async Task PatchAsync_ModelStateInvalid_ValidateModelStateEnabled() + { + // arrange + const int id = 0; + var resource = new Resource(); + var serviceMock = new Mock>(); + _jsonApiContextMock.SetupGet(a => a.ContextGraph).Returns(_contextGraphMock.Object); + _contextGraphMock.Setup(a => a.GetPublicAttributeName("TestAttribute")).Returns("test-attribute"); + _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); + _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions{ValidateModelState = true}); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); + controller.ModelState.AddModelError("TestAttribute", "Failed Validation"); + + // act + var response = await controller.PatchAsync(id, resource); + + // assert + serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Never); + Assert.IsType(response); + Assert.IsType(((UnprocessableEntityObjectResult) response).Value); + } + + [Fact] + public async Task PatchAsync_Throws_405_If_No_Service() + { + // arrange + const int id = 0; + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: null); + + // act + var exception = await Assert.ThrowsAsync(() => controller.PatchAsync(id, It.IsAny())); + + // assert + Assert.Equal(405, exception.GetStatusCode()); + } + + [Fact] + public async Task PostAsync_Calls_Service() + { + // arrange + var resource = new Resource(); + var serviceMock = new Mock>(); + _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); + _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions()); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object); + serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource); + controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext {HttpContext = new DefaultHttpContext()}; + + // act + await controller.PostAsync(resource); + + // assert + serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Once); + VerifyApplyContext(); + } + + [Fact] + public async Task PostAsync_ModelStateInvalid_ValidateModelStateDisabled() + { + // arrange + var resource = new Resource(); + var serviceMock = new Mock>(); + _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); + _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = false }); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object); + serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource); + controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext { HttpContext = new DefaultHttpContext() }; + + // act + var response = await controller.PostAsync(resource); + + // assert + serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Once); + VerifyApplyContext(); + Assert.IsNotType(response); + } + + [Fact] + public async Task PostAsync_ModelStateInvalid_ValidateModelStateEnabled() + { + // arrange + var resource = new Resource(); + var serviceMock = new Mock>(); + _jsonApiContextMock.SetupGet(a => a.ContextGraph).Returns(_contextGraphMock.Object); + _contextGraphMock.Setup(a => a.GetPublicAttributeName("TestAttribute")).Returns("test-attribute"); + _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); + _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = true }); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object); + controller.ModelState.AddModelError("TestAttribute", "Failed Validation"); + + // act + var response = await controller.PostAsync(resource); + + // assert + serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Never); + Assert.IsType(response); + Assert.IsType(((UnprocessableEntityObjectResult)response).Value); + } + + [Fact] + public async Task PatchRelationshipsAsync_Calls_Service() + { + // arrange + const int id = 0; + var resource = new Resource(); + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, updateRelationships: serviceMock.Object); + + // act + await controller.PatchRelationshipsAsync(id, string.Empty, null); + + // assert + serviceMock.Verify(m => m.UpdateRelationshipsAsync(id, string.Empty, null), Times.Once); + VerifyApplyContext(); + } + + [Fact] + public async Task PatchRelationshipsAsync_Throws_405_If_No_Service() + { + // arrange + const int id = 0; + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, updateRelationships: null); + + // act + var exception = await Assert.ThrowsAsync(() => controller.PatchRelationshipsAsync(id, string.Empty, null)); + + // assert + Assert.Equal(405, exception.GetStatusCode()); + } + + [Fact] + public async Task DeleteAsync_Calls_Service() + { + // arrange + const int id = 0; + var resource = new Resource(); + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, delete: serviceMock.Object); + + // act + await controller.DeleteAsync(id); + + // assert + serviceMock.Verify(m => m.DeleteAsync(id), Times.Once); + VerifyApplyContext(); + } + + [Fact] + public async Task DeleteAsync_Throws_405_If_No_Service() + { + // arrange + const int id = 0; + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, delete: null); + + // act + var exception = await Assert.ThrowsAsync(() => controller.DeleteAsync(id)); + + // assert + Assert.Equal(405, exception.GetStatusCode()); + } + + private void VerifyApplyContext() + => _jsonApiContextMock.Verify(m => m.ApplyContext(It.IsAny>()), Times.Once); + } +}