From 2a655ddfd046cf4018c501be497fa85323b1eb94 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 30 May 2017 22:26:51 -0500 Subject: [PATCH 1/7] add support for deserializing datetimeoffset format --- src/JsonApiDotNetCore/Internal/TypeHelper.cs | 30 ++++++++++++++------ test/UnitTests/Internal/TypeHelper_Tests.cs | 23 +++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 test/UnitTests/Internal/TypeHelper_Tests.cs diff --git a/src/JsonApiDotNetCore/Internal/TypeHelper.cs b/src/JsonApiDotNetCore/Internal/TypeHelper.cs index a0bb133e01..6ceb3b06ac 100644 --- a/src/JsonApiDotNetCore/Internal/TypeHelper.cs +++ b/src/JsonApiDotNetCore/Internal/TypeHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; namespace JsonApiDotNetCore.Internal { @@ -6,17 +7,30 @@ public static class TypeHelper { public static object ConvertType(object value, Type type) { - if(value == null) - return null; + try + { + if (value == null) + return null; + + type = Nullable.GetUnderlyingType(type) ?? type; + + var stringValue = value.ToString(); - type = Nullable.GetUnderlyingType(type) ?? type; + if (type == typeof(Guid)) + return Guid.Parse(stringValue); - var stringValue = value.ToString(); - - if(type == typeof(Guid)) - return Guid.Parse(stringValue); + if (type == typeof(DateTimeOffset)) + return DateTimeOffset.Parse(stringValue); - return Convert.ChangeType(stringValue, type); + return Convert.ChangeType(stringValue, type); + } + catch (Exception) + { + if (type.GetTypeInfo().IsValueType) + return Activator.CreateInstance(type); + + return null; + } } } } diff --git a/test/UnitTests/Internal/TypeHelper_Tests.cs b/test/UnitTests/Internal/TypeHelper_Tests.cs new file mode 100644 index 0000000000..912e516af9 --- /dev/null +++ b/test/UnitTests/Internal/TypeHelper_Tests.cs @@ -0,0 +1,23 @@ +using System; +using JsonApiDotNetCore.Internal; +using Xunit; + +namespace UnitTests.Internal +{ + public class TypeHelper_Tests + { + [Fact] + public void Can_Convert_DateTimeOffsets() + { + // arrange + var dto = DateTimeOffset.Now; + var formattedString = dto.ToString("O"); + + // act + var result = TypeHelper.ConvertType(formattedString, typeof(DateTimeOffset)); + + // assert + Assert.Equal(dto, result); + } + } +} From 2401baf89e67ef0566a56daf988c027d0e10dfb5 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 30 May 2017 22:29:29 -0500 Subject: [PATCH 2/7] chore(csproj): bump package version --- src/JsonApiDotNetCore/JsonApiDotNetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 1dbf2ce662..699a20076b 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@  - 2.0.7 + 2.0.8 netstandard1.6 JsonApiDotNetCore JsonApiDotNetCore From 773073e007e718566f2e214761c107aa2759f8e3 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 31 May 2017 06:52:55 -0500 Subject: [PATCH 3/7] fix(type-helper): do not silently return default something unexpected happened, we should return an error --- src/JsonApiDotNetCore/Internal/TypeHelper.cs | 7 ++----- test/UnitTests/Internal/TypeHelper_Tests.cs | 11 +++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/JsonApiDotNetCore/Internal/TypeHelper.cs b/src/JsonApiDotNetCore/Internal/TypeHelper.cs index 6ceb3b06ac..60574ff295 100644 --- a/src/JsonApiDotNetCore/Internal/TypeHelper.cs +++ b/src/JsonApiDotNetCore/Internal/TypeHelper.cs @@ -24,12 +24,9 @@ public static object ConvertType(object value, Type type) return Convert.ChangeType(stringValue, type); } - catch (Exception) + catch (Exception e) { - if (type.GetTypeInfo().IsValueType) - return Activator.CreateInstance(type); - - return null; + throw new FormatException($"{ value } cannot be converted to { type.GetTypeInfo().Name }", e); } } } diff --git a/test/UnitTests/Internal/TypeHelper_Tests.cs b/test/UnitTests/Internal/TypeHelper_Tests.cs index 912e516af9..7f05edd892 100644 --- a/test/UnitTests/Internal/TypeHelper_Tests.cs +++ b/test/UnitTests/Internal/TypeHelper_Tests.cs @@ -19,5 +19,16 @@ public void Can_Convert_DateTimeOffsets() // assert Assert.Equal(dto, result); } + + [Fact] + public void Bad_DateTimeOffset_String_Throws() + { + // arrange + var formattedString = "this_is_not_a_valid_dto"; + + // act + // assert + Assert.Throws(() => TypeHelper.ConvertType(formattedString, typeof(DateTimeOffset))); + } } } From 734bd3e06915520389df9680b062c6c5bbab2f22 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 31 May 2017 06:53:11 -0500 Subject: [PATCH 4/7] fix(de-serialization): improve error handling --- .../Formatters/JsonApiReader.cs | 9 +++ .../Serialization/JsonApiDeSerializer.cs | 79 ++++++++++++------- 2 files changed, 59 insertions(+), 29 deletions(-) diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs index f9d38c2cfb..103c1b9ab5 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs @@ -1,6 +1,8 @@ using System; using System.IO; +using System.Text; using System.Threading.Tasks; +using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc.Formatters; @@ -50,6 +52,13 @@ public Task ReadAsync(InputFormatterContext context) context.HttpContext.Response.StatusCode = 422; return InputFormatterResult.FailureAsync(); } + catch(JsonApiException jex) + { + _logger?.LogError(new EventId(), jex, "An error occurred while de-serializing the payload"); + context.HttpContext.Response.StatusCode = jex.GetStatusCode(); + context.HttpContext.Response.Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(jex.GetError()))); + return InputFormatterResult.FailureAsync(); + } } private string GetRequestBody(Stream body) diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index d64d18366f..f9cb803c27 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -16,7 +16,7 @@ public class JsonApiDeSerializer : IJsonApiDeSerializer private readonly IJsonApiContext _jsonApiContext; private readonly IGenericProcessorFactory _genericProcessorFactor; - public JsonApiDeSerializer( + public JsonApiDeSerializer( IJsonApiContext jsonApiContext, IGenericProcessorFactory genericProcessorFactory) { @@ -26,9 +26,16 @@ public JsonApiDeSerializer( public object Deserialize(string requestBody) { - var document = JsonConvert.DeserializeObject(requestBody); - var entity = DocumentToObject(document.Data); - return entity; + try + { + var document = JsonConvert.DeserializeObject(requestBody); + var entity = DocumentToObject(document.Data); + return entity; + } + catch (Exception e) + { + throw new JsonApiException("400", "Failed to deserialize request body", e.Message); + } } public object Deserialize(string requestBody) @@ -38,26 +45,40 @@ public object Deserialize(string requestBody) public object DeserializeRelationship(string requestBody) { - var data = JToken.Parse(requestBody)["data"]; + try + { + var data = JToken.Parse(requestBody)["data"]; - if(data is JArray) - return data.ToObject>(); + if (data is JArray) + return data.ToObject>(); - return new List { data.ToObject() }; + return new List { data.ToObject() }; + } + catch (Exception e) + { + throw new JsonApiException("400", "Failed to deserialize request body", e.Message); + } } public List DeserializeList(string requestBody) { - var documents = JsonConvert.DeserializeObject(requestBody); + try + { + var documents = JsonConvert.DeserializeObject(requestBody); + + var deserializedList = new List(); + foreach (var data in documents.Data) + { + var entity = DocumentToObject(data); + deserializedList.Add((TEntity)entity); + } - var deserializedList = new List(); - foreach (var data in documents.Data) + return deserializedList; + } + catch (Exception e) { - var entity = DocumentToObject(data); - deserializedList.Add((TEntity)entity); + throw new JsonApiException("400", "Failed to deserialize request body", e.Message); } - - return deserializedList; } private object DocumentToObject(DocumentData data) @@ -66,7 +87,7 @@ private object DocumentToObject(DocumentData data) _jsonApiContext.RequestEntity = contextEntity; var entity = Activator.CreateInstance(contextEntity.EntityType); - + entity = SetEntityAttributes(entity, contextEntity, data.Attributes); entity = SetRelationships(entity, contextEntity, data.Relationships); @@ -106,8 +127,8 @@ private object SetEntityAttributes( } private object SetRelationships( - object entity, - ContextEntity contextEntity, + object entity, + ContextEntity contextEntity, Dictionary relationships) { if (relationships == null || relationships.Count == 0) @@ -117,18 +138,18 @@ private object SetRelationships( foreach (var attr in contextEntity.Relationships) { - entity = attr.IsHasOne - ? SetHasOneRelationship(entity, entityProperties, attr, contextEntity, relationships) + entity = attr.IsHasOne + ? SetHasOneRelationship(entity, entityProperties, attr, contextEntity, relationships) : SetHasManyRelationship(entity, entityProperties, attr, contextEntity, relationships); } return entity; } - private object SetHasOneRelationship(object entity, - PropertyInfo[] entityProperties, - RelationshipAttribute attr, - ContextEntity contextEntity, + private object SetHasOneRelationship(object entity, + PropertyInfo[] entityProperties, + RelationshipAttribute attr, + ContextEntity contextEntity, Dictionary relationships) { var entityProperty = entityProperties.FirstOrDefault(p => p.Name == $"{attr.InternalRelationshipName}Id"); @@ -142,7 +163,7 @@ private object SetHasOneRelationship(object entity, { var relationshipAttr = _jsonApiContext.RequestEntity.Relationships .SingleOrDefault(r => r.PublicRelationshipName == relationshipName); - + var data = (Dictionary)relationshipData.ExposedData; if (data == null) return entity; @@ -159,9 +180,9 @@ private object SetHasOneRelationship(object entity, } private object SetHasManyRelationship(object entity, - PropertyInfo[] entityProperties, - RelationshipAttribute attr, - ContextEntity contextEntity, + PropertyInfo[] entityProperties, + RelationshipAttribute attr, + ContextEntity contextEntity, Dictionary relationships) { var entityProperty = entityProperties.FirstOrDefault(p => p.Name == attr.InternalRelationshipName); @@ -179,7 +200,7 @@ private object SetHasManyRelationship(object entity, var genericProcessor = _genericProcessorFactor.GetProcessor(attr.Type); var ids = relationshipData.ManyData.Select(r => r["id"]); - genericProcessor.SetRelationships(entity, attr, ids); + genericProcessor.SetRelationships(entity, attr, ids); } return entity; From 2b650ab28ec3c62862c32c826bf101533b4a4f7e Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 31 May 2017 06:56:38 -0500 Subject: [PATCH 5/7] chore(appveyor): dont skip symbols on deploy --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 55c43a7f82..ec135d19bb 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -43,14 +43,14 @@ deploy: server: https://www.myget.org/F/research-institute/api/v2/package api_key: secure: 6CeYcZ4Ze+57gxfeuHzqP6ldbUkPtF6pfpVM1Gw/K2jExFrAz763gNAQ++tiacq3 - skip_symbols: true + skip_symbols: false on: branch: develop - provider: NuGet server: https://www.myget.org/F/jadnc/api/v2/package api_key: secure: 6CeYcZ4Ze+57gxfeuHzqP6ldbUkPtF6pfpVM1Gw/K2jExFrAz763gNAQ++tiacq3 - skip_symbols: true + skip_symbols: false on: branch: unstable - provider: NuGet From c112f2b17622d4057040229b351489747a47fc78 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 31 May 2017 22:20:08 -0500 Subject: [PATCH 6/7] include inner exception messages in detail --- .../Internal/JsonApiExceptionFactory.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs b/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs index 41d676f5da..7e251ebae0 100644 --- a/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs +++ b/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs @@ -15,8 +15,19 @@ public static JsonApiException GetException(Exception exception) case "InvalidCastException": return new JsonApiException("409", exception.Message); default: - return new JsonApiException("500", exception.Message); + return new JsonApiException("500", exception.Message, GetExceptionDetail(exception.InnerException)); } } + + private static string GetExceptionDetail(Exception exception) + { + string detail = null; + while(exception != null) + { + detail = $"{detail}{exception.Message}; "; + exception = exception.InnerException; + } + return detail; + } } } From 6b77e0d047fa8b172f8496b8a167e706cd4997d4 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Thu, 1 Jun 2017 19:23:46 -0500 Subject: [PATCH 7/7] filter actions using attributes --- .../HttpMethodRestrictionFilter.cs | 52 ++++++++++++ .../Restricted/ReadOnlyController.cs | 73 +++++++++++++++++ .../HttpReadOnlyTests.cs | 82 +++++++++++++++++++ .../NoHttpDeleteTests.cs | 82 +++++++++++++++++++ .../NoHttpPatchTests.cs | 82 +++++++++++++++++++ .../HttpMethodRestrictions/NoHttpPostTests.cs | 82 +++++++++++++++++++ 6 files changed, 453 insertions(+) create mode 100644 src/JsonApiDotNetCore/Controllers/HttpMethodRestrictionFilter.cs create mode 100644 src/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpDeleteTests.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPatchTests.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPostTests.cs diff --git a/src/JsonApiDotNetCore/Controllers/HttpMethodRestrictionFilter.cs b/src/JsonApiDotNetCore/Controllers/HttpMethodRestrictionFilter.cs new file mode 100644 index 0000000000..3ef96c26f5 --- /dev/null +++ b/src/JsonApiDotNetCore/Controllers/HttpMethodRestrictionFilter.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Controllers +{ + public abstract class HttpRestrictAttribute : ActionFilterAttribute, IAsyncActionFilter + { + protected abstract string[] Methods { get; } + + public override async Task OnActionExecutionAsync( + ActionExecutingContext context, + ActionExecutionDelegate next) + { + var method = context.HttpContext.Request.Method; + + if(CanExecuteAction(method) == false) + throw new JsonApiException("405", $"This resource does not support {method} requests."); + + await next(); + } + + private bool CanExecuteAction(string requestMethod) + { + return Methods.Contains(requestMethod) == false; + } + } + + public class HttpReadOnlyAttribute : HttpRestrictAttribute + { + protected override string[] Methods { get; } = new string[] { "POST", "PATCH", "DELETE" }; + } + + public class NoHttpPostAttribute : HttpRestrictAttribute + { + protected override string[] Methods { get; } = new string[] { "POST" }; + } + + public class NoHttpPatchAttribute : HttpRestrictAttribute + { + protected override string[] Methods { get; } = new string[] { "PATCH" }; + } + + public class NoHttpDeleteAttribute : HttpRestrictAttribute + { + protected override string[] Methods { get; } = new string[] { "DELETE" }; + } +} diff --git a/src/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs b/src/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs new file mode 100644 index 0000000000..57fafc8fc6 --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs @@ -0,0 +1,73 @@ +using JsonApiDotNetCore.Controllers; +using Microsoft.AspNetCore.Mvc; + +namespace JsonApiDotNetCoreExample.Controllers.Restricted +{ + [Route("[controller]")] + [HttpReadOnly] + public class ReadOnlyController : Controller + { + [HttpGet] + public IActionResult Get() => Ok(); + + [HttpPost] + public IActionResult Post() => Ok(); + + [HttpPatch] + public IActionResult Patch() => Ok(); + + [HttpDelete] + public IActionResult Delete() => Ok(); + } + + [Route("[controller]")] + [NoHttpPost] + public class NoHttpPostController : Controller + { + [HttpGet] + public IActionResult Get() => Ok(); + + [HttpPost] + public IActionResult Post() => Ok(); + + [HttpPatch] + public IActionResult Patch() => Ok(); + + [HttpDelete] + public IActionResult Delete() => Ok(); + } + + [Route("[controller]")] + [NoHttpPatch] + public class NoHttpPatchController : Controller + { + [HttpGet] + public IActionResult Get() => Ok(); + + [HttpPost] + public IActionResult Post() => Ok(); + + [HttpPatch] + public IActionResult Patch() => Ok(); + + [HttpDelete] + public IActionResult Delete() => Ok(); + } + + [Route("[controller]")] + [NoHttpDelete] + public class NoHttpDeleteController : Controller + { + [HttpGet] + public IActionResult Get() => Ok(); + + [HttpPost] + public IActionResult Post() => Ok(); + + [HttpPatch] + public IActionResult Patch() => Ok(); + + [HttpDelete] + public IActionResult Delete() => Ok(); + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs new file mode 100644 index 0000000000..90496b3690 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs @@ -0,0 +1,82 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCoreExample; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance +{ + [Collection("WebHostCollection")] + public class HttpReadOnlyTests + { + [Fact] + public async Task Allows_GET_Requests() + { + // arrange + const string route = "readonly"; + const string method = "GET"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Rejects_POST_Requests() + { + // arrange + const string route = "readonly"; + const string method = "POST"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); + } + + [Fact] + public async Task Rejects_PATCH_Requests() + { + // arrange + const string route = "readonly"; + const string method = "PATCH"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); + } + + [Fact] + public async Task Rejects_DELETE_Requests() + { + // arrange + const string route = "readonly"; + const string method = "DELETE"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); + } + + private async Task MakeRequestAsync(string route, string method) + { + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod(method); + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + var response = await client.SendAsync(request); + return response.StatusCode; + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpDeleteTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpDeleteTests.cs new file mode 100644 index 0000000000..32e7eaf109 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpDeleteTests.cs @@ -0,0 +1,82 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCoreExample; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance +{ + [Collection("WebHostCollection")] + public class nohttpdeleteTests + { + [Fact] + public async Task Allows_GET_Requests() + { + // arrange + const string route = "nohttpdelete"; + const string method = "GET"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Allows_POST_Requests() + { + // arrange + const string route = "nohttpdelete"; + const string method = "POST"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Allows_PATCH_Requests() + { + // arrange + const string route = "nohttpdelete"; + const string method = "PATCH"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Rejects_DELETE_Requests() + { + // arrange + const string route = "nohttpdelete"; + const string method = "DELETE"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); + } + + private async Task MakeRequestAsync(string route, string method) + { + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod(method); + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + var response = await client.SendAsync(request); + return response.StatusCode; + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPatchTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPatchTests.cs new file mode 100644 index 0000000000..5b8a33f16a --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPatchTests.cs @@ -0,0 +1,82 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCoreExample; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance +{ + [Collection("WebHostCollection")] + public class nohttppatchTests + { + [Fact] + public async Task Allows_GET_Requests() + { + // arrange + const string route = "nohttppatch"; + const string method = "GET"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Allows_POST_Requests() + { + // arrange + const string route = "nohttppatch"; + const string method = "POST"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Rejects_PATCH_Requests() + { + // arrange + const string route = "nohttppatch"; + const string method = "PATCH"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); + } + + [Fact] + public async Task Allows_DELETE_Requests() + { + // arrange + const string route = "nohttppatch"; + const string method = "DELETE"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + private async Task MakeRequestAsync(string route, string method) + { + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod(method); + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + var response = await client.SendAsync(request); + return response.StatusCode; + } + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPostTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPostTests.cs new file mode 100644 index 0000000000..f68a65a037 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPostTests.cs @@ -0,0 +1,82 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCoreExample; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance +{ + [Collection("WebHostCollection")] + public class NoHttpPostTests + { + [Fact] + public async Task Allows_GET_Requests() + { + // arrange + const string route = "nohttppost"; + const string method = "GET"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Rejects_POST_Requests() + { + // arrange + const string route = "nohttppost"; + const string method = "POST"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); + } + + [Fact] + public async Task Allows_PATCH_Requests() + { + // arrange + const string route = "nohttppost"; + const string method = "PATCH"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + [Fact] + public async Task Allows_DELETE_Requests() + { + // arrange + const string route = "nohttppost"; + const string method = "DELETE"; + + // act + var statusCode = await MakeRequestAsync(route, method); + + // assert + Assert.Equal(HttpStatusCode.OK, statusCode); + } + + private async Task MakeRequestAsync(string route, string method) + { + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod(method); + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + var response = await client.SendAsync(request); + return response.StatusCode; + } + } +} \ No newline at end of file