Skip to content

Commit e82ab39

Browse files
author
Bart Koelman
committed
Fixed: fail on mismatch between ID in patch request body and ID in URL.
1 parent 8ff14c5 commit e82ab39

File tree

3 files changed

+92
-23
lines changed

3 files changed

+92
-23
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Net;
2+
using JsonApiDotNetCore.Models.JsonApiDocuments;
3+
4+
namespace JsonApiDotNetCore.Exceptions
5+
{
6+
/// <summary>
7+
/// The error that is thrown when the resource id in the request body does not match the id in the current endpoint URL.
8+
/// </summary>
9+
public sealed class ResourceIdMismatchException : JsonApiException
10+
{
11+
public ResourceIdMismatchException(string bodyId, string endpointId, string requestPath)
12+
: base(new Error(HttpStatusCode.Conflict)
13+
{
14+
Title = "Resource id mismatch between request body and endpoint URL.",
15+
Detail = $"Expected resource id '{endpointId}' in PATCH request body at endpoint '{requestPath}', instead of '{bodyId}'."
16+
})
17+
{
18+
}
19+
}
20+
}

src/JsonApiDotNetCore/Formatters/JsonApiReader.cs

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.IO;
44
using System.Threading.Tasks;
55
using JsonApiDotNetCore.Exceptions;
6+
using JsonApiDotNetCore.Managers.Contracts;
67
using JsonApiDotNetCore.Models;
78
using JsonApiDotNetCore.Serialization.Server;
89
using Microsoft.AspNetCore.Http.Extensions;
@@ -15,12 +16,15 @@ namespace JsonApiDotNetCore.Formatters
1516
public class JsonApiReader : IJsonApiReader
1617
{
1718
private readonly IJsonApiDeserializer _deserializer;
19+
private readonly ICurrentRequest _currentRequest;
1820
private readonly ILogger<JsonApiReader> _logger;
1921

2022
public JsonApiReader(IJsonApiDeserializer deserializer,
21-
ILoggerFactory loggerFactory)
23+
ICurrentRequest currentRequest,
24+
ILoggerFactory loggerFactory)
2225
{
2326
_deserializer = deserializer;
27+
_currentRequest = currentRequest;
2428
_logger = loggerFactory.CreateLogger<JsonApiReader>();
2529
}
2630

@@ -57,46 +61,56 @@ public async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
5761

5862
if (context.HttpContext.Request.Method == "PATCH")
5963
{
60-
var hasMissingId = model is IList list ? CheckForId(list) : CheckForId(model);
64+
bool hasMissingId = model is IList list ? HasMissingId(list) : HasMissingId(model);
6165
if (hasMissingId)
6266
{
6367
throw new InvalidRequestBodyException("Payload must include id attribute.", null, body);
6468
}
69+
70+
if (!_currentRequest.IsRelationshipPath && TryGetId(model, out var bodyId) && bodyId != _currentRequest.BaseId)
71+
{
72+
throw new ResourceIdMismatchException(bodyId, _currentRequest.BaseId, context.HttpContext.Request.GetDisplayUrl());
73+
}
6574
}
6675

6776
return await InputFormatterResult.SuccessAsync(model);
6877
}
6978

7079
/// <summary> Checks if the deserialized payload has an ID included </summary>
71-
private bool CheckForId(object model)
80+
private bool HasMissingId(object model)
7281
{
73-
if (model == null) return false;
74-
if (model is ResourceObject ro)
75-
{
76-
if (string.IsNullOrEmpty(ro.Id)) return true;
77-
}
78-
else if (model is IIdentifiable identifiable)
82+
return TryGetId(model, out string id) && string.IsNullOrEmpty(id);
83+
}
84+
85+
/// <summary> Checks if all elements in the deserialized payload have an ID included </summary>
86+
private bool HasMissingId(IEnumerable models)
87+
{
88+
foreach (var model in models)
7989
{
80-
if (string.IsNullOrEmpty(identifiable.StringId)) return true;
90+
if (TryGetId(model, out string id) && string.IsNullOrEmpty(id))
91+
{
92+
return true;
93+
}
8194
}
95+
8296
return false;
8397
}
8498

85-
/// <summary> Checks if the elements in the deserialized payload have an ID included </summary>
86-
private bool CheckForId(IList modelList)
99+
private static bool TryGetId(object model, out string id)
87100
{
88-
foreach (var model in modelList)
101+
if (model is ResourceObject resourceObject)
89102
{
90-
if (model == null) continue;
91-
if (model is ResourceObject ro)
92-
{
93-
if (string.IsNullOrEmpty(ro.Id)) return true;
94-
}
95-
else if (model is IIdentifiable identifiable)
96-
{
97-
if (string.IsNullOrEmpty(identifiable.StringId)) return true;
98-
}
103+
id = resourceObject.Id;
104+
return true;
99105
}
106+
107+
if (model is IIdentifiable identifiable)
108+
{
109+
id = identifiable.StringId;
110+
return true;
111+
}
112+
113+
id = null;
100114
return false;
101115
}
102116

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,42 @@ public async Task Respond_422_If_IdNotInAttributeList()
178178
Assert.Equal("Failed to deserialize request body: Payload must include id attribute.", error.Title);
179179
Assert.StartsWith("Request body: <<", error.Detail);
180180
}
181-
181+
182+
[Fact]
183+
public async Task Respond_409_If_IdInUrlIsDifferentFromIdInRequestBody()
184+
{
185+
// Arrange
186+
var todoItem = _todoItemFaker.Generate();
187+
todoItem.CreatedDate = DateTime.Now;
188+
189+
_context.TodoItems.Add(todoItem);
190+
_context.SaveChanges();
191+
192+
var wrongTodoItemId = todoItem.Id + 1;
193+
194+
var builder = new WebHostBuilder().UseStartup<Startup>();
195+
var server = new TestServer(builder);
196+
var client = server.CreateClient();
197+
var serializer = _fixture.GetSerializer<TodoItem>(ti => new {ti.Description, ti.Ordinal, ti.CreatedDate});
198+
var content = serializer.Serialize(todoItem);
199+
var request = PrepareRequest("PATCH", $"/api/v1/todoItems/{wrongTodoItemId}", content);
200+
201+
// Act
202+
var response = await client.SendAsync(request);
203+
204+
// Assert
205+
Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
206+
207+
var body = await response.Content.ReadAsStringAsync();
208+
var document = JsonConvert.DeserializeObject<ErrorDocument>(body);
209+
Assert.Single(document.Errors);
210+
211+
var error = document.Errors.Single();
212+
Assert.Equal(HttpStatusCode.Conflict, error.StatusCode);
213+
Assert.Equal("Resource id mismatch between request body and endpoint URL.", error.Title);
214+
Assert.Equal($"Expected resource id '{wrongTodoItemId}' in PATCH request body at endpoint 'http://localhost/api/v1/todoItems/{wrongTodoItemId}', instead of '{todoItem.Id}'.", error.Detail);
215+
}
216+
182217
[Fact]
183218
public async Task Respond_422_If_Broken_JSON_Payload()
184219
{

0 commit comments

Comments
 (0)