Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ This project uses famous football stadiums (A-Z) that hosted FIFA World Cup matc

### Changed

- Field validation failures now return `422 Unprocessable Entity` (RFC 4918) instead of `400 Bad Request`; `400 Bad Request` is now reserved for malformed requests (unparseable JSON, wrong `Content-Type`, route/body mismatch) per RFC 9457
Comment thread
nanotaboada marked this conversation as resolved.

### Fixed

### Removed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ IValidator<PlayerRequestModel> validator
/// </summary>
/// <param name="player">The PlayerRequestModel</param>
/// <response code="201">Created</response>
/// <response code="400">Bad Request</response>
/// <response code="409">Conflict</response>
/// <response code="422">Unprocessable Entity</response>
[HttpPost(Name = "Create")]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType<PlayerResponseModel>(StatusCodes.Status201Created)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status409Conflict)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status422UnprocessableEntity)]
public async Task<IResult> PostAsync([FromBody] PlayerRequestModel player)
{
// Use the "Create" rule set, which includes BeUniqueSquadNumber.
Expand All @@ -49,10 +49,13 @@ public async Task<IResult> PostAsync([FromBody] PlayerRequestModel player)
.ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());

logger.LogWarning("POST /players validation failed: {@Errors}", errors);
return TypedResults.ValidationProblem(
errors,
detail: "See the errors field for details.",
instance: HttpContext?.Request?.Path.ToString()
return TypedResults.Problem(
new HttpValidationProblemDetails(errors)
{
Status = StatusCodes.Status422UnprocessableEntity,
Detail = "See the errors field for details.",
Instance = HttpContext?.Request?.Path.ToString(),
}
);
}

Expand Down Expand Up @@ -177,13 +180,15 @@ public async Task<IResult> GetBySquadNumberAsync([FromRoute] int squadNumber)
/// <param name="player">The PlayerRequestModel</param>
/// <param name="squadNumber">The Squad Number of the Player</param>
/// <response code="204">No Content</response>
/// <response code="400">Bad Request</response>
/// <response code="400">Bad Request (route/body squad number mismatch)</response>
/// <response code="404">Not Found</response>
/// <response code="422">Unprocessable Entity (field validation failure)</response>
[HttpPut("squadNumber/{squadNumber:int}", Name = "Update")]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status422UnprocessableEntity)]
public async Task<IResult> PutAsync(
[FromRoute] int squadNumber,
[FromBody] PlayerRequestModel player
Expand All @@ -207,10 +212,13 @@ [FromBody] PlayerRequestModel player
squadNumber,
errors
);
return TypedResults.ValidationProblem(
errors,
detail: "See the errors field for details.",
instance: HttpContext?.Request?.Path.ToString()
return TypedResults.Problem(
new HttpValidationProblemDetails(errors)
{
Status = StatusCodes.Status422UnprocessableEntity,
Detail = "See the errors field for details.",
Instance = HttpContext?.Request?.Path.ToString(),
}
);
}
if (player.SquadNumber != squadNumber)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ private static (int StatusCode, string Title) MapExceptionToStatusCode(Exception
{
return exception switch
{
ValidationException => (StatusCodes.Status400BadRequest, "Validation Error"),
ValidationException => (StatusCodes.Status422UnprocessableEntity, "Validation Error"),
ArgumentException
or ArgumentNullException
=> (StatusCodes.Status400BadRequest, "Bad Request"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ public async Task Post_Players_Nonexistent_Returns201Created()

[Fact]
[Trait("Category", "Integration")]
public async Task Post_Players_ValidationError_Returns400BadRequest()
public async Task Post_Players_ValidationError_Returns422UnprocessableEntity()
{
// Arrange — SquadNumber 0 is the int default, fails NotEmpty
var request = PlayerFakes.MakeRequestModelForCreate();
Expand All @@ -216,9 +216,9 @@ public async Task Post_Players_ValidationError_Returns400BadRequest()
var response = await _client.PostAsJsonAsync("/players", request);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
problem!.Status.Should().Be(StatusCodes.Status400BadRequest);
problem!.Status.Should().Be(StatusCodes.Status422UnprocessableEntity);
}

/* -------------------------------------------------------------------------
Expand Down Expand Up @@ -259,7 +259,7 @@ public async Task Put_PlayerBySquadNumber_Unknown_Returns404NotFound()

[Fact]
[Trait("Category", "Integration")]
public async Task Put_PlayerBySquadNumber_ValidationError_Returns400BadRequest()
public async Task Put_PlayerBySquadNumber_ValidationError_Returns422UnprocessableEntity()
{
// Arrange — SquadNumber -1 fails GreaterThan(0)
var request = PlayerFakes.MakeRequestModelForUpdate(23);
Expand All @@ -269,9 +269,9 @@ public async Task Put_PlayerBySquadNumber_ValidationError_Returns400BadRequest()
var response = await _client.PutAsJsonAsync("/players/squadNumber/23", request);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
problem!.Status.Should().Be(StatusCodes.Status400BadRequest);
problem!.Status.Should().Be(StatusCodes.Status422UnprocessableEntity);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public PlayerControllerTests()

[Fact]
[Trait("Category", "Unit")]
public async Task Post_Players_ValidationError_Returns400BadRequest()
public async Task Post_Players_ValidationError_Returns422UnprocessableEntity()
{
// Arrange
var request = PlayerFakes.MakeRequestModelForCreate();
Expand Down Expand Up @@ -63,8 +63,8 @@ public async Task Post_Players_ValidationError_Returns400BadRequest()
),
Times.Once
);
var httpResult = result.Should().BeOfType<ValidationProblem>().Subject;
httpResult.StatusCode.Should().Be(StatusCodes.Status400BadRequest);
var httpResult = result.Should().BeOfType<ProblemHttpResult>().Subject;
httpResult.StatusCode.Should().Be(StatusCodes.Status422UnprocessableEntity);
}

[Fact]
Expand Down Expand Up @@ -299,7 +299,7 @@ public async Task Get_PlayerBySquadNumber_Existing_Returns200OK()

[Fact]
[Trait("Category", "Unit")]
public async Task Put_PlayerBySquadNumber_ValidationError_Returns400BadRequest()
public async Task Put_PlayerBySquadNumber_ValidationError_Returns422UnprocessableEntity()
{
// Arrange
var squadNumber = 20;
Expand Down Expand Up @@ -339,8 +339,8 @@ public async Task Put_PlayerBySquadNumber_ValidationError_Returns400BadRequest()
),
Times.Once
);
var httpResult = result.Should().BeOfType<ValidationProblem>().Subject;
httpResult.StatusCode.Should().Be(StatusCodes.Status400BadRequest);
var httpResult = result.Should().BeOfType<ProblemHttpResult>().Subject;
httpResult.StatusCode.Should().Be(StatusCodes.Status422UnprocessableEntity);
}

[Fact]
Expand Down
Loading