Skip to content

Commit b32e304

Browse files
author
Bart Koelman
authored
Merge pull request #821 from json-api-dotnet/fix/resourcetype-comparison-bug
Fix: incorrect resource type comparison
2 parents 38d23a3 + 46e4f4c commit b32e304

File tree

6 files changed

+167
-88
lines changed

6 files changed

+167
-88
lines changed

src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ public void ConfigureMvc()
9292
{
9393
options.EnableEndpointRouting = true;
9494
options.Filters.AddService<IAsyncJsonApiExceptionFilter>();
95-
options.Filters.AddService<IAsyncResourceTypeMatchFilter>();
9695
options.Filters.AddService<IAsyncQueryStringActionFilter>();
9796
options.Filters.AddService<IAsyncConvertEmptyActionResultFilter>();
9897
ConfigureMvcOptions?.Invoke(options);
@@ -159,7 +158,6 @@ private void AddMiddlewareLayer()
159158
_services.AddSingleton<IJsonApiApplicationBuilder>(this);
160159
_services.TryAddSingleton<IExceptionHandler, ExceptionHandler>();
161160
_services.TryAddScoped<IAsyncJsonApiExceptionFilter, AsyncJsonApiExceptionFilter>();
162-
_services.TryAddScoped<IAsyncResourceTypeMatchFilter, AsyncResourceTypeMatchFilter>();
163161
_services.TryAddScoped<IAsyncQueryStringActionFilter, AsyncQueryStringActionFilter>();
164162
_services.TryAddScoped<IAsyncConvertEmptyActionResultFilter, AsyncConvertEmptyActionResultFilter>();
165163
_services.TryAddSingleton<IJsonApiInputFormatter, JsonApiInputFormatter>();

src/JsonApiDotNetCore/Middleware/AsyncResourceTypeMatchFilter.cs

Lines changed: 0 additions & 51 deletions
This file was deleted.

src/JsonApiDotNetCore/Middleware/IAsyncResourceTypeMatchFilter.cs

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/JsonApiDotNetCore/Serialization/JsonApiReader.cs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
using System;
22
using System.Collections;
3+
using System.Collections.Generic;
34
using System.IO;
5+
using System.Net.Http;
6+
using System.Linq;
47
using System.Threading.Tasks;
8+
using JsonApiDotNetCore.Configuration;
59
using JsonApiDotNetCore.Errors;
610
using JsonApiDotNetCore.Middleware;
711
using JsonApiDotNetCore.Resources;
812
using JsonApiDotNetCore.Serialization.Objects;
13+
using Microsoft.AspNetCore.Http;
914
using Microsoft.AspNetCore.Http.Extensions;
1015
using Microsoft.AspNetCore.Mvc.Formatters;
1116
using Microsoft.Extensions.Logging;
@@ -17,16 +22,19 @@ public class JsonApiReader : IJsonApiReader
1722
{
1823
private readonly IJsonApiDeserializer _deserializer;
1924
private readonly IJsonApiRequest _request;
25+
private readonly IResourceContextProvider _resourceContextProvider;
2026
private readonly TraceLogWriter<JsonApiReader> _traceWriter;
2127

2228
public JsonApiReader(IJsonApiDeserializer deserializer,
2329
IJsonApiRequest request,
30+
IResourceContextProvider resourceContextProvider,
2431
ILoggerFactory loggerFactory)
2532
{
2633
if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory));
2734

2835
_deserializer = deserializer ?? throw new ArgumentNullException(nameof(deserializer));
2936
_request = request ?? throw new ArgumentNullException(nameof(request));
37+
_resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider));
3038
_traceWriter = new TraceLogWriter<JsonApiReader>(loggerFactory);
3139
}
3240

@@ -63,12 +71,40 @@ public async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
6371

6472
ValidatePatchRequestIncludesId(context, model, body);
6573

74+
ValidateIncomingResourceType(context, model);
75+
6676
return await InputFormatterResult.SuccessAsync(model);
6777
}
6878

79+
private void ValidateIncomingResourceType(InputFormatterContext context, object model)
80+
{
81+
if (context.HttpContext.IsJsonApiRequest() && IsPatchOrPostRequest(context.HttpContext.Request))
82+
{
83+
var endpointResourceType = GetEndpointResourceType();
84+
if (endpointResourceType == null)
85+
{
86+
return;
87+
}
88+
89+
var bodyResourceTypes = GetBodyResourceTypes(model);
90+
foreach (var bodyResourceType in bodyResourceTypes)
91+
{
92+
if (bodyResourceType != endpointResourceType)
93+
{
94+
var resourceFromEndpoint = _resourceContextProvider.GetResourceContext(endpointResourceType);
95+
var resourceFromBody = _resourceContextProvider.GetResourceContext(bodyResourceType);
96+
97+
throw new ResourceTypeMismatchException(new HttpMethod(context.HttpContext.Request.Method),
98+
context.HttpContext.Request.Path,
99+
resourceFromEndpoint, resourceFromBody);
100+
}
101+
}
102+
}
103+
}
104+
69105
private void ValidatePatchRequestIncludesId(InputFormatterContext context, object model, string body)
70106
{
71-
if (context.HttpContext.Request.Method == "PATCH")
107+
if (context.HttpContext.Request.Method == HttpMethods.Patch)
72108
{
73109
bool hasMissingId = model is IList list ? HasMissingId(list) : HasMissingId(model);
74110
if (hasMissingId)
@@ -134,5 +170,27 @@ private async Task<string> GetRequestBody(Stream body)
134170
// https://github.com/aspnet/AspNetCore/issues/7644
135171
return await reader.ReadToEndAsync();
136172
}
173+
174+
private bool IsPatchOrPostRequest(HttpRequest request)
175+
{
176+
return request.Method == HttpMethods.Patch || request.Method == HttpMethods.Post;
177+
}
178+
179+
private IEnumerable<Type> GetBodyResourceTypes(object model)
180+
{
181+
if (model is IEnumerable<IIdentifiable> resourceCollection)
182+
{
183+
return resourceCollection.Select(r => r.GetType()).Distinct();
184+
}
185+
186+
return model == null ? new Type[0] : new[] { model.GetType() };
187+
}
188+
189+
private Type GetEndpointResourceType()
190+
{
191+
return _request.Kind == EndpointKind.Primary
192+
? _request.PrimaryResource.ResourceType
193+
: _request.SecondaryResource?.ResourceType;
194+
}
137195
}
138196
}

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -274,31 +274,6 @@ public async Task CreateResource_SimpleResource_HeaderLocationsAreCorrect()
274274
Assert.Equal($"/api/v1/todoItems/{responseItem.Id}", response.Headers.Location.ToString());
275275
}
276276

277-
[Fact]
278-
public async Task CreateResource_ResourceTypeMismatch_IsConflict()
279-
{
280-
// Arrange
281-
string content = JsonConvert.SerializeObject(new
282-
{
283-
data = new
284-
{
285-
type = "people"
286-
}
287-
});
288-
289-
// Act
290-
var (body, response) = await Post("/api/v1/todoItems", content);
291-
292-
// Assert
293-
AssertEqualStatusCode(HttpStatusCode.Conflict, response);
294-
295-
var errorDocument = JsonConvert.DeserializeObject<ErrorDocument>(body);
296-
Assert.Single(errorDocument.Errors);
297-
Assert.Equal(HttpStatusCode.Conflict, errorDocument.Errors[0].StatusCode);
298-
Assert.Equal("Resource type mismatch between request body and endpoint URL.", errorDocument.Errors[0].Title);
299-
Assert.Equal("Expected resource of type 'todoItems' in POST request body at endpoint '/api/v1/todoItems', instead of 'people'.", errorDocument.Errors[0].Detail);
300-
}
301-
302277
[Fact]
303278
public async Task CreateResource_UnknownResourceType_Fails()
304279
{
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using System.Net;
2+
using System.Threading.Tasks;
3+
using JsonApiDotNetCore.Serialization.Objects;
4+
using Newtonsoft.Json;
5+
using Xunit;
6+
7+
namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec
8+
{
9+
public sealed class ResourceTypeMismatchTests : FunctionalTestCollection<StandardApplicationFactory>
10+
{
11+
public ResourceTypeMismatchTests(StandardApplicationFactory factory) : base(factory) { }
12+
13+
[Fact]
14+
public async Task Posting_Resource_With_Mismatching_Resource_Type_Returns_Conflict()
15+
{
16+
// Arrange
17+
string content = JsonConvert.SerializeObject(new
18+
{
19+
data = new
20+
{
21+
type = "people"
22+
}
23+
});
24+
25+
// Act
26+
var (body, _) = await Post("/api/v1/todoItems", content);
27+
28+
// Assert
29+
var errorDocument = JsonConvert.DeserializeObject<ErrorDocument>(body);
30+
Assert.Single(errorDocument.Errors);
31+
Assert.Equal(HttpStatusCode.Conflict, errorDocument.Errors[0].StatusCode);
32+
Assert.Equal("Resource type mismatch between request body and endpoint URL.", errorDocument.Errors[0].Title);
33+
Assert.Equal("Expected resource of type 'todoItems' in POST request body at endpoint '/api/v1/todoItems', instead of 'people'.", errorDocument.Errors[0].Detail);
34+
}
35+
36+
[Fact]
37+
public async Task Patching_Resource_With_Mismatching_Resource_Type_Returns_Conflict()
38+
{
39+
// Arrange
40+
string content = JsonConvert.SerializeObject(new
41+
{
42+
data = new
43+
{
44+
type = "people",
45+
id = 1
46+
}
47+
});
48+
49+
// Act
50+
var (body, _) = await Patch("/api/v1/todoItems/1", content);
51+
52+
// Assert
53+
var errorDocument = JsonConvert.DeserializeObject<ErrorDocument>(body);
54+
Assert.Single(errorDocument.Errors);
55+
Assert.Equal(HttpStatusCode.Conflict, errorDocument.Errors[0].StatusCode);
56+
Assert.Equal("Resource type mismatch between request body and endpoint URL.", errorDocument.Errors[0].Title);
57+
Assert.Equal("Expected resource of type 'todoItems' in PATCH request body at endpoint '/api/v1/todoItems/1', instead of 'people'.", errorDocument.Errors[0].Detail);
58+
}
59+
60+
[Fact]
61+
public async Task Patching_Through_Relationship_Link_With_Mismatching_Resource_Type_Returns_Conflict()
62+
{
63+
// Arrange
64+
string content = JsonConvert.SerializeObject(new
65+
{
66+
data = new
67+
{
68+
type = "todoItems",
69+
id = 1
70+
}
71+
});
72+
73+
// Act
74+
var (body, _) = await Patch("/api/v1/todoItems/1/relationships/owner", content);
75+
76+
// Assert
77+
var errorDocument = JsonConvert.DeserializeObject<ErrorDocument>(body);
78+
Assert.Single(errorDocument.Errors);
79+
Assert.Equal(HttpStatusCode.Conflict, errorDocument.Errors[0].StatusCode);
80+
Assert.Equal("Resource type mismatch between request body and endpoint URL.", errorDocument.Errors[0].Title);
81+
Assert.Equal("Expected resource of type 'people' in PATCH request body at endpoint '/api/v1/todoItems/1/relationships/owner', instead of 'todoItems'.", errorDocument.Errors[0].Detail);
82+
}
83+
84+
[Fact]
85+
public async Task Patching_Through_Relationship_Link_With_Multiple_Resources_Types_Returns_Conflict()
86+
{
87+
// Arrange
88+
string content = JsonConvert.SerializeObject(new
89+
{
90+
data = new object[]
91+
{
92+
new { type = "todoItems", id = 1 },
93+
new { type = "articles", id = 2 },
94+
}
95+
});
96+
97+
// Act
98+
var (body, _) = await Patch("/api/v1/todoItems/1/relationships/childrenTodos", content);
99+
100+
// Assert
101+
var errorDocument = JsonConvert.DeserializeObject<ErrorDocument>(body);
102+
Assert.Single(errorDocument.Errors);
103+
Assert.Equal(HttpStatusCode.Conflict, errorDocument.Errors[0].StatusCode);
104+
Assert.Equal("Resource type mismatch between request body and endpoint URL.", errorDocument.Errors[0].Title);
105+
Assert.Equal("Expected resource of type 'todoItems' in PATCH request body at endpoint '/api/v1/todoItems/1/relationships/childrenTodos', instead of 'articles'.", errorDocument.Errors[0].Detail);
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)