Skip to content

Commit 7bda8a8

Browse files
committed
Produce error on invalid ID in request body
1 parent 4895875 commit 7bda8a8

File tree

2 files changed

+246
-0
lines changed

2 files changed

+246
-0
lines changed

src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs

+20
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ private IIdentifiable CreateResource(ResourceIdentity identity, ResourceIdentity
116116
AssertHasNoId(identity, state);
117117
}
118118

119+
AssertNoBrokenId(identity, resourceType.IdentityClrType, state);
119120
AssertSameIdValue(identity, requirements.IdValue, state);
120121
AssertSameLidValue(identity, requirements.LidValue, state);
121122

@@ -177,6 +178,25 @@ private static void AssertHasNoId(ResourceIdentity identity, RequestAdapterState
177178
}
178179
}
179180

181+
private static void AssertNoBrokenId(ResourceIdentity identity, Type resourceIdClrType, RequestAdapterState state)
182+
{
183+
if (identity.Id != null)
184+
{
185+
if (resourceIdClrType == typeof(string))
186+
{
187+
// Empty and whitespace strings are valid when TId is string.
188+
return;
189+
}
190+
191+
string? defaultIdValue = RuntimeTypeConverter.GetDefaultValue(resourceIdClrType)?.ToString();
192+
193+
if (string.IsNullOrWhiteSpace(identity.Id) || identity.Id == defaultIdValue)
194+
{
195+
throw new ModelConversionException(state.Position, "The 'id' element is invalid.", null);
196+
}
197+
}
198+
}
199+
180200
private static void AssertSameIdValue(ResourceIdentity identity, string? expected, RequestAdapterState state)
181201
{
182202
if (expected != null && identity.Id != expected)

test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs

+226
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public CreateResourceWithClientGeneratedIdTests(IntegrationTestContext<TestableS
2121

2222
testContext.UseController<WorkItemGroupsController>();
2323
testContext.UseController<RgbColorsController>();
24+
testContext.UseController<UserAccountsController>();
2425

2526
testContext.ConfigureServices(services =>
2627
{
@@ -340,6 +341,231 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
340341
error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty());
341342
}
342343

344+
[Theory]
345+
[InlineData(ClientIdGenerationMode.Allowed)]
346+
[InlineData(ClientIdGenerationMode.Required)]
347+
public async Task Cannot_create_resource_with_client_generated_zero_guid_ID(ClientIdGenerationMode mode)
348+
{
349+
// Arrange
350+
var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
351+
options.ClientIdGeneration = mode;
352+
353+
WorkItemGroup newGroup = _fakers.WorkItemGroup.Generate();
354+
355+
var requestBody = new
356+
{
357+
data = new
358+
{
359+
type = "workItemGroups",
360+
id = Guid.Empty.ToString(),
361+
attributes = new
362+
{
363+
name = newGroup.Name
364+
}
365+
}
366+
};
367+
368+
const string route = "/workItemGroups";
369+
370+
// Act
371+
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync<Document>(route, requestBody);
372+
373+
// Assert
374+
httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity);
375+
376+
responseDocument.Errors.ShouldHaveCount(1);
377+
378+
ErrorObject error = responseDocument.Errors[0];
379+
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
380+
error.Title.Should().Be("Failed to deserialize request body: The 'id' element is invalid.");
381+
error.Detail.Should().BeNull();
382+
error.Source.ShouldNotBeNull();
383+
error.Source.Pointer.Should().Be("/data");
384+
error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty());
385+
}
386+
387+
[Theory]
388+
[InlineData(ClientIdGenerationMode.Allowed)]
389+
[InlineData(ClientIdGenerationMode.Required)]
390+
public async Task Cannot_create_resource_with_client_generated_empty_guid_ID(ClientIdGenerationMode mode)
391+
{
392+
// Arrange
393+
var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
394+
options.ClientIdGeneration = mode;
395+
396+
WorkItemGroup newGroup = _fakers.WorkItemGroup.Generate();
397+
398+
var requestBody = new
399+
{
400+
data = new
401+
{
402+
type = "workItemGroups",
403+
id = string.Empty,
404+
attributes = new
405+
{
406+
name = newGroup.Name
407+
}
408+
}
409+
};
410+
411+
const string route = "/workItemGroups";
412+
413+
// Act
414+
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync<Document>(route, requestBody);
415+
416+
// Assert
417+
httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity);
418+
419+
responseDocument.Errors.ShouldHaveCount(1);
420+
421+
ErrorObject error = responseDocument.Errors[0];
422+
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
423+
error.Title.Should().Be("Failed to deserialize request body: The 'id' element is invalid.");
424+
error.Detail.Should().BeNull();
425+
error.Source.ShouldNotBeNull();
426+
error.Source.Pointer.Should().Be("/data");
427+
error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty());
428+
}
429+
430+
[Theory]
431+
[InlineData(ClientIdGenerationMode.Allowed)]
432+
[InlineData(ClientIdGenerationMode.Required)]
433+
public async Task Can_create_resource_with_client_generated_empty_string_ID(ClientIdGenerationMode mode)
434+
{
435+
// Arrange
436+
var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
437+
options.ClientIdGeneration = mode;
438+
439+
RgbColor newColor = _fakers.RgbColor.Generate();
440+
441+
await _testContext.RunOnDatabaseAsync(async dbContext =>
442+
{
443+
await dbContext.ClearTableAsync<RgbColor>();
444+
});
445+
446+
var requestBody = new
447+
{
448+
data = new
449+
{
450+
type = "rgbColors",
451+
id = string.Empty,
452+
attributes = new
453+
{
454+
displayName = newColor.DisplayName
455+
}
456+
}
457+
};
458+
459+
const string route = "/rgbColors?fields[rgbColors]=id";
460+
461+
// Act
462+
(HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync<string>(route, requestBody);
463+
464+
// Assert
465+
httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent);
466+
467+
responseDocument.Should().BeEmpty();
468+
469+
await _testContext.RunOnDatabaseAsync(async dbContext =>
470+
{
471+
RgbColor colorInDatabase = await dbContext.RgbColors.FirstWithIdAsync((string?)string.Empty);
472+
473+
colorInDatabase.DisplayName.Should().Be(newColor.DisplayName);
474+
});
475+
476+
PropertyInfo? property = typeof(RgbColor).GetProperty(nameof(Identifiable<object>.Id));
477+
property.ShouldNotBeNull();
478+
property.PropertyType.Should().Be(typeof(string));
479+
}
480+
481+
[Theory]
482+
[InlineData(ClientIdGenerationMode.Allowed)]
483+
[InlineData(ClientIdGenerationMode.Required)]
484+
public async Task Cannot_create_resource_with_client_generated_zero_long_ID(ClientIdGenerationMode mode)
485+
{
486+
// Arrange
487+
var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
488+
options.ClientIdGeneration = mode;
489+
490+
UserAccount newAccount = _fakers.UserAccount.Generate();
491+
492+
var requestBody = new
493+
{
494+
data = new
495+
{
496+
type = "userAccounts",
497+
id = "0",
498+
attributes = new
499+
{
500+
firstName = newAccount.FirstName,
501+
lastName = newAccount.LastName
502+
}
503+
}
504+
};
505+
506+
const string route = "/userAccounts";
507+
508+
// Act
509+
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync<Document>(route, requestBody);
510+
511+
// Assert
512+
httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity);
513+
514+
responseDocument.Errors.ShouldHaveCount(1);
515+
516+
ErrorObject error = responseDocument.Errors[0];
517+
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
518+
error.Title.Should().Be("Failed to deserialize request body: The 'id' element is invalid.");
519+
error.Detail.Should().BeNull();
520+
error.Source.ShouldNotBeNull();
521+
error.Source.Pointer.Should().Be("/data");
522+
error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty());
523+
}
524+
525+
[Theory]
526+
[InlineData(ClientIdGenerationMode.Allowed)]
527+
[InlineData(ClientIdGenerationMode.Required)]
528+
public async Task Cannot_create_resource_with_client_generated_empty_long_ID(ClientIdGenerationMode mode)
529+
{
530+
// Arrange
531+
var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
532+
options.ClientIdGeneration = mode;
533+
534+
UserAccount newAccount = _fakers.UserAccount.Generate();
535+
536+
var requestBody = new
537+
{
538+
data = new
539+
{
540+
type = "userAccounts",
541+
id = string.Empty,
542+
attributes = new
543+
{
544+
firstName = newAccount.FirstName,
545+
lastName = newAccount.LastName
546+
}
547+
}
548+
};
549+
550+
const string route = "/userAccounts";
551+
552+
// Act
553+
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync<Document>(route, requestBody);
554+
555+
// Assert
556+
httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity);
557+
558+
responseDocument.Errors.ShouldHaveCount(1);
559+
560+
ErrorObject error = responseDocument.Errors[0];
561+
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
562+
error.Title.Should().Be("Failed to deserialize request body: The 'id' element is invalid.");
563+
error.Detail.Should().BeNull();
564+
error.Source.ShouldNotBeNull();
565+
error.Source.Pointer.Should().Be("/data");
566+
error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty());
567+
}
568+
343569
[Theory]
344570
[InlineData(ClientIdGenerationMode.Allowed)]
345571
[InlineData(ClientIdGenerationMode.Required)]

0 commit comments

Comments
 (0)