@@ -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