diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index 49a1af57d7..fee92a14dc 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -23,6 +23,8 @@ namespace JsonApiDotNetCore.Middleware /// public sealed class JsonApiMiddleware { + private static readonly MediaTypeHeaderValue _mediaType = MediaTypeHeaderValue.Parse(HeaderConstants.MediaType); + private readonly RequestDelegate _next; public JsonApiMiddleware(RequestDelegate next) @@ -96,7 +98,7 @@ private static async Task ValidateContentTypeHeaderAsync(HttpContext httpC private static async Task ValidateAcceptHeaderAsync(HttpContext httpContext, JsonSerializerSettings serializerSettings) { StringValues acceptHeaders = httpContext.Request.Headers["Accept"]; - if (!acceptHeaders.Any() || acceptHeaders == HeaderConstants.MediaType) + if (!acceptHeaders.Any()) { return true; } @@ -105,15 +107,17 @@ private static async Task ValidateAcceptHeaderAsync(HttpContext httpContex foreach (var acceptHeader in acceptHeaders) { - if (MediaTypeHeaderValue.TryParse(acceptHeader, out var headerValue)) + if (MediaTypeWithQualityHeaderValue.TryParse(acceptHeader, out var headerValue)) { + headerValue.Quality = null; + if (headerValue.MediaType == "*/*" || headerValue.MediaType == "application/*") { seenCompatibleMediaType = true; break; } - if (headerValue.MediaType == HeaderConstants.MediaType && !headerValue.Parameters.Any()) + if (_mediaType.Equals(headerValue)) { seenCompatibleMediaType = true; break; @@ -126,7 +130,7 @@ private static async Task ValidateAcceptHeaderAsync(HttpContext httpContex await FlushResponseAsync(httpContext.Response, serializerSettings, new Error(HttpStatusCode.NotAcceptable) { Title = "The specified Accept header value does not contain any supported media types.", - Detail = $"Please include '{HeaderConstants.MediaType}' in the Accept header values." + Detail = $"Please include '{_mediaType}' in the Accept header values." }); return false; } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiationTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiationTests.cs deleted file mode 100644 index b5bdac6497..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiationTests.cs +++ /dev/null @@ -1,296 +0,0 @@ -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Serialization.Objects; -using JsonApiDotNetCoreExample; -using JsonApiDotNetCoreExample.Models; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Newtonsoft.Json; -using Xunit; - -namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec -{ - [Collection("WebHostCollection")] - public class ContentNegotiationTests - { - private readonly TestFixture _fixture; - - public ContentNegotiationTests(TestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task Server_Sends_Correct_ContentType_Header() - { - // Arrange - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var route = "/api/v1/todoItems"; - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Get, route); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(HeaderConstants.MediaType, response.Content.Headers.ContentType.ToString()); - } - - [Fact] - public async Task Respond_415_If_Content_Type_Header_Is_Not_JsonApi_Media_Type() - { - // Arrange - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var route = "/api/v1/todoItems"; - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(new HttpMethod("POST"), route) {Content = new StringContent(string.Empty)}; - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("text/html"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.UnsupportedMediaType, errorDocument.Errors[0].StatusCode); - Assert.Equal("The specified Content-Type header value is not supported.", errorDocument.Errors[0].Title); - Assert.Equal("Please specify 'application/vnd.api+json' instead of 'text/html' for the Content-Type header value.", errorDocument.Errors[0].Detail); - } - - [Fact] - public async Task Respond_201_If_Content_Type_Header_Is_JsonApi_Media_Type() - { - // Arrange - var serializer = _fixture.GetSerializer(e => new { e.Description }); - var todoItem = new TodoItem {Description = "something not to forget"}; - - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var route = "/api/v1/todoItems"; - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Post, route) {Content = new StringContent(serializer.Serialize(todoItem))}; - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(HeaderConstants.MediaType); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - } - - [Fact] - public async Task Respond_415_If_Content_Type_Header_Is_JsonApi_Media_Type_With_Profile() - { - // Arrange - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var route = "/api/v1/todoItems"; - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(new HttpMethod("POST"), route) {Content = new StringContent(string.Empty)}; - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(HeaderConstants.MediaType + "; profile=something"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.UnsupportedMediaType, errorDocument.Errors[0].StatusCode); - Assert.Equal("The specified Content-Type header value is not supported.", errorDocument.Errors[0].Title); - Assert.Equal("Please specify 'application/vnd.api+json' instead of 'application/vnd.api+json; profile=something' for the Content-Type header value.", errorDocument.Errors[0].Detail); - } - - [Fact] - public async Task Respond_415_If_Content_Type_Header_Is_JsonApi_Media_Type_With_Extension() - { - // Arrange - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var route = "/api/v1/todoItems"; - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(new HttpMethod("POST"), route) {Content = new StringContent(string.Empty)}; - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(HeaderConstants.MediaType + "; ext=something"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.UnsupportedMediaType, errorDocument.Errors[0].StatusCode); - Assert.Equal("The specified Content-Type header value is not supported.", errorDocument.Errors[0].Title); - Assert.Equal("Please specify 'application/vnd.api+json' instead of 'application/vnd.api+json; ext=something' for the Content-Type header value.", errorDocument.Errors[0].Detail); - } - - [Fact] - public async Task Respond_415_If_Content_Type_Header_Is_JsonApi_Media_Type_With_CharSet() - { - // Arrange - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var route = "/api/v1/todoItems"; - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Post, route) {Content = new StringContent(string.Empty)}; - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(HeaderConstants.MediaType + "; charset=ISO-8859-4"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.UnsupportedMediaType, errorDocument.Errors[0].StatusCode); - Assert.Equal("The specified Content-Type header value is not supported.", errorDocument.Errors[0].Title); - Assert.Equal("Please specify 'application/vnd.api+json' instead of 'application/vnd.api+json; charset=ISO-8859-4' for the Content-Type header value.", errorDocument.Errors[0].Detail); - } - - [Fact] - public async Task Respond_415_If_Content_Type_Header_Is_JsonApi_Media_Type_With_Unknown() - { - // Arrange - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var route = "/api/v1/todoItems"; - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Post, route) {Content = new StringContent(string.Empty)}; - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(HeaderConstants.MediaType + "; unknown=unexpected"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.UnsupportedMediaType, errorDocument.Errors[0].StatusCode); - Assert.Equal("The specified Content-Type header value is not supported.", errorDocument.Errors[0].Title); - Assert.Equal("Please specify 'application/vnd.api+json' instead of 'application/vnd.api+json; unknown=unexpected' for the Content-Type header value.", errorDocument.Errors[0].Detail); - } - - [Fact] - public async Task Respond_200_If_Accept_Headers_Are_Missing() - { - // Arrange - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var route = "/api/v1/todoItems"; - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(HttpMethod.Get, route); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - [Fact] - public async Task Respond_200_If_Accept_Headers_Include_Any() - { - // Arrange - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var route = "/api/v1/todoItems"; - var server = new TestServer(builder); - var client = server.CreateClient(); - client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("text/html")); - client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("*/*")); - var request = new HttpRequestMessage(HttpMethod.Get, route); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - [Fact] - public async Task Respond_200_If_Accept_Headers_Include_Application_Prefix() - { - // Arrange - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var route = "/api/v1/todoItems"; - var server = new TestServer(builder); - var client = server.CreateClient(); - client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("text/html;q=0.8")); - client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/*;q=0.2")); - var request = new HttpRequestMessage(HttpMethod.Get, route); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - [Fact] - public async Task Respond_200_If_Accept_Headers_Contain_JsonApi_Media_Type() - { - // Arrange - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var route = "/api/v1/todoItems"; - var server = new TestServer(builder); - var client = server.CreateClient(); - client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("text/html")); - client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; profile=some")); - client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; ext=other")); - client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; unknown=unexpected")); - client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType)); - var request = new HttpRequestMessage(HttpMethod.Get, route); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - [Fact] - public async Task Respond_406_If_Accept_Headers_Only_Contain_JsonApi_Media_Type_With_Parameters() - { - // Arrange - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var route = "/api/v1/todoItems"; - var server = new TestServer(builder); - var client = server.CreateClient(); - client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; profile=some")); - client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; ext=other")); - client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; unknown=unexpected")); - client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; charset=ISO-8859-4")); - var request = new HttpRequestMessage(HttpMethod.Get, route); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.NotAcceptable, errorDocument.Errors[0].StatusCode); - Assert.Equal("The specified Accept header value does not contain any supported media types.", errorDocument.Errors[0].Title); - Assert.Equal("Please include 'application/vnd.api+json' in the Accept header values.", errorDocument.Errors[0].Detail); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTestContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTestContext.cs index 428c44586d..cb12fc2c09 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTestContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTestContext.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; @@ -102,31 +103,42 @@ public async Task RunOnDatabaseAsync(Func asyncAction) } public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> - ExecuteGetAsync(string requestUrl) + ExecuteGetAsync(string requestUrl, + IEnumerable acceptHeaders = null) { - return await ExecuteRequestAsync(HttpMethod.Get, requestUrl); + return await ExecuteRequestAsync(HttpMethod.Get, requestUrl, null, null, acceptHeaders); } public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> - ExecutePostAsync(string requestUrl, object requestBody) + ExecutePostAsync(string requestUrl, object requestBody, + string contentType = HeaderConstants.MediaType, + IEnumerable acceptHeaders = null) { - return await ExecuteRequestAsync(HttpMethod.Post, requestUrl, requestBody); + return await ExecuteRequestAsync(HttpMethod.Post, requestUrl, requestBody, contentType, + acceptHeaders); } public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> - ExecutePatchAsync(string requestUrl, object requestBody) + ExecutePatchAsync(string requestUrl, object requestBody, + string contentType = HeaderConstants.MediaType, + IEnumerable acceptHeaders = null) { - return await ExecuteRequestAsync(HttpMethod.Patch, requestUrl, requestBody); + return await ExecuteRequestAsync(HttpMethod.Patch, requestUrl, requestBody, contentType, + acceptHeaders); } public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> - ExecuteDeleteAsync(string requestUrl, object requestBody = null) + ExecuteDeleteAsync(string requestUrl, object requestBody = null, + string contentType = HeaderConstants.MediaType, + IEnumerable acceptHeaders = null) { - return await ExecuteRequestAsync(HttpMethod.Delete, requestUrl, requestBody); + return await ExecuteRequestAsync(HttpMethod.Delete, requestUrl, requestBody, contentType, + acceptHeaders); } private async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> - ExecuteRequestAsync(HttpMethod method, string requestUrl, object requestBody = null) + ExecuteRequestAsync(HttpMethod method, string requestUrl, object requestBody, + string contentType, IEnumerable acceptHeaders) { var request = new HttpRequestMessage(method, requestUrl); string requestText = SerializeRequest(requestBody); @@ -134,10 +146,23 @@ public async Task RunOnDatabaseAsync(Func asyncAction) if (!string.IsNullOrEmpty(requestText)) { request.Content = new StringContent(requestText); - request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); + + if (contentType != null) + { + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } } using HttpClient client = Factory.CreateClient(); + + if (acceptHeaders != null) + { + foreach (var acceptHeader in acceptHeaders) + { + client.DefaultRequestHeaders.Accept.Add(acceptHeader); + } + } + HttpResponseMessage responseMessage = await client.SendAsync(request); string responseText = await responseMessage.Content.ReadAsStringAsync(); @@ -148,11 +173,11 @@ public async Task RunOnDatabaseAsync(Func asyncAction) private string SerializeRequest(object requestBody) { - string requestText = requestBody is string stringRequestBody - ? stringRequestBody - : JsonConvert.SerializeObject(requestBody); - - return requestText; + return requestBody == null + ? null + : requestBody is string stringRequestBody + ? stringRequestBody + : JsonConvert.SerializeObject(requestBody); } private TResponseDocument DeserializeResponse(string responseText) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs new file mode 100644 index 0000000000..3a1019c249 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs @@ -0,0 +1,122 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ContentNegotiation +{ + public sealed class AcceptHeaderTests + : IClassFixture, PolicyDbContext>> + { + private readonly IntegrationTestContext, PolicyDbContext> _testContext; + + public AcceptHeaderTests(IntegrationTestContext, PolicyDbContext> testContext) + { + _testContext = testContext; + } + + [Fact] + public async Task Permits_no_Accept_headers() + { + // Arrange + var route = "/policies"; + + var acceptHeaders = new MediaTypeWithQualityHeaderValue[0]; + + // Act + var (httpResponse, _) = await _testContext.ExecuteGetAsync(route, acceptHeaders); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task Permits_global_wildcard_in_Accept_headers() + { + // Arrange + var route = "/policies"; + + var acceptHeaders = new[] + { + MediaTypeWithQualityHeaderValue.Parse("text/html"), + MediaTypeWithQualityHeaderValue.Parse("*/*") + }; + + // Act + var (httpResponse, _) = await _testContext.ExecuteGetAsync(route, acceptHeaders); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task Permits_application_wildcard_in_Accept_headers() + { + // Arrange + var route = "/policies"; + + var acceptHeaders = new[] + { + MediaTypeWithQualityHeaderValue.Parse("text/html;q=0.8"), + MediaTypeWithQualityHeaderValue.Parse("application/*;q=0.2") + }; + + // Act + var (httpResponse, _) = await _testContext.ExecuteGetAsync(route, acceptHeaders); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task Permits_JsonApi_without_parameters_in_Accept_headers() + { + // Arrange + var route = "/policies"; + + var acceptHeaders = new[] + { + MediaTypeWithQualityHeaderValue.Parse("text/html"), + MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; profile=some"), + MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; ext=other"), + MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; unknown=unexpected"), + MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; q=0.3") + }; + + // Act + var (httpResponse, _) = await _testContext.ExecuteGetAsync(route, acceptHeaders); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task Denies_JsonApi_with_parameters_in_Accept_headers() + { + // Arrange + var route = "/policies"; + + var acceptHeaders = new[] + { + MediaTypeWithQualityHeaderValue.Parse("text/html"), + MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; profile=some"), + MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; ext=other"), + MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; unknown=unexpected") + }; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route, acceptHeaders); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NotAcceptable); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotAcceptable); + responseDocument.Errors[0].Title.Should().Be("The specified Accept header value does not contain any supported media types."); + responseDocument.Errors[0].Detail.Should().Be("Please include 'application/vnd.api+json' in the Accept header values."); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs new file mode 100644 index 0000000000..2cce83a8fe --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs @@ -0,0 +1,215 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ContentNegotiation +{ + public sealed class ContentTypeHeaderTests + : IClassFixture, PolicyDbContext>> + { + private readonly IntegrationTestContext, PolicyDbContext> _testContext; + + public ContentTypeHeaderTests(IntegrationTestContext, PolicyDbContext> testContext) + { + _testContext = testContext; + } + + [Fact] + public async Task Returns_JsonApi_ContentType_header() + { + // Arrange + var route = "/policies"; + + // Act + var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + httpResponse.Content.Headers.ContentType.ToString().Should().Be(HeaderConstants.MediaType); + } + + [Fact] + public async Task Denies_unknown_ContentType_header() + { + // Arrange + var requestBody = new + { + data = new + { + type = "policies", + attributes = new + { + name = "some" + } + } + }; + + var route = "/policies"; + var contentType = "text/html"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); + responseDocument.Errors[0].Title.Should().Be("The specified Content-Type header value is not supported."); + responseDocument.Errors[0].Detail.Should().Be("Please specify 'application/vnd.api+json' instead of 'text/html' for the Content-Type header value."); + } + + [Fact] + public async Task Permits_JsonApi_ContentType_header() + { + // Arrange + var requestBody = new + { + data = new + { + type = "policies", + attributes = new + { + name = "some" + } + } + }; + + var route = "/policies"; + var contentType = HeaderConstants.MediaType; + + // Act + var (httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + } + + [Fact] + public async Task Denies_JsonApi_ContentType_header_with_profile() + { + // Arrange + var requestBody = new + { + data = new + { + type = "policies", + attributes = new + { + name = "some" + } + } + }; + + var route = "/policies"; + var contentType = HeaderConstants.MediaType + "; profile=something"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); + responseDocument.Errors[0].Title.Should().Be("The specified Content-Type header value is not supported."); + responseDocument.Errors[0].Detail.Should().Be("Please specify 'application/vnd.api+json' instead of 'application/vnd.api+json; profile=something' for the Content-Type header value."); + } + + [Fact] + public async Task Denies_JsonApi_ContentType_header_with_extension() + { + // Arrange + var requestBody = new + { + data = new + { + type = "policies", + attributes = new + { + name = "some" + } + } + }; + + var route = "/policies"; + var contentType = HeaderConstants.MediaType + "; ext=something"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); + responseDocument.Errors[0].Title.Should().Be("The specified Content-Type header value is not supported."); + responseDocument.Errors[0].Detail.Should().Be("Please specify 'application/vnd.api+json' instead of 'application/vnd.api+json; ext=something' for the Content-Type header value."); + } + + [Fact] + public async Task Denies_JsonApi_ContentType_header_with_CharSet() + { + // Arrange + var requestBody = new + { + data = new + { + type = "policies", + attributes = new + { + name = "some" + } + } + }; + + var route = "/policies"; + var contentType = HeaderConstants.MediaType + "; charset=ISO-8859-4"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); + responseDocument.Errors[0].Title.Should().Be("The specified Content-Type header value is not supported."); + responseDocument.Errors[0].Detail.Should().Be("Please specify 'application/vnd.api+json' instead of 'application/vnd.api+json; charset=ISO-8859-4' for the Content-Type header value."); + } + + [Fact] + public async Task Denies_JsonApi_ContentType_header_with_unknown_parameter() + { + // Arrange + var requestBody = new + { + data = new + { + type = "policies", + attributes = new + { + name = "some" + } + } + }; + + var route = "/policies"; + var contentType = HeaderConstants.MediaType + "; unknown=unexpected"; + + // Act + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); + responseDocument.Errors[0].Title.Should().Be("The specified Content-Type header value is not supported."); + responseDocument.Errors[0].Detail.Should().Be("Please specify 'application/vnd.api+json' instead of 'application/vnd.api+json; unknown=unexpected' for the Content-Type header value."); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PoliciesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PoliciesController.cs new file mode 100644 index 0000000000..d99ab9bd6a --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PoliciesController.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ContentNegotiation +{ + public sealed class PoliciesController : JsonApiController + { + public PoliciesController(IJsonApiOptions options, ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/Policy.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/Policy.cs new file mode 100644 index 0000000000..3a09cce5e6 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/Policy.cs @@ -0,0 +1,11 @@ +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ContentNegotiation +{ + public sealed class Policy : Identifiable + { + [Attr] + public string Name { get; set; } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PolicyDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PolicyDbContext.cs new file mode 100644 index 0000000000..4402f859c8 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PolicyDbContext.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; + +namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ContentNegotiation +{ + public sealed class PolicyDbContext : DbContext + { + public DbSet Policies { get; set; } + + public PolicyDbContext(DbContextOptions options) + : base(options) + { + } + } +}