Skip to content

[BUG] BeUniqueSquadNumber validator rejects all PUT requests for existing players #424

@nanotaboada

Description

@nanotaboada

Description

The BeUniqueSquadNumber validation rule in PlayerRequestModelValidator is applied to both POST and PUT requests without discrimination. On PUT, the player being updated already exists in the database, so the uniqueness check always fails — causing every update to return 400 Bad Request with "SquadNumber must be unique." before any business logic executes.

Steps to Reproduce

  1. Start the API with a seeded database (any existing player, e.g. squad number 10)
  2. Send PUT /players/squadNumber/10 with a valid request body containing "squadNumber": 10
  3. Observe 400 Bad Request"SquadNumber must be unique."
  4. The player is never updated

Expected Behavior

PUT /players/squadNumber/10 with a matching body should update the player and return 204 No Content.

Actual Behavior

400 Bad Request is returned with FluentValidation error "SquadNumber must be unique." because BeUniqueSquadNumber finds the existing player in the DB and returns false.

Execution order in PlayerController.PutAsync (lines 197–257):

  1. validator.ValidateAsync(player) is called at line 202 — before the route/body mismatch check
  2. BeUniqueSquadNumber calls FindBySquadNumberAsync(squadNumber) → finds the existing player → returns false
  3. validation.IsValid is false → controller returns 400 at line 214
  4. Update never executes

Environment

  • .NET SDK version: 10.0
  • .NET Core version: net10.0 (FluentValidation 12.1.1, AutoMapper 16.1.1)
  • OS: macOS 15.0 / Ubuntu 24.04 / Windows 11

Additional Context

Why the existing unit tests do not catch this bug

Two compounding factors hide the bug from the test suite:

1. Controller unit tests mock the validator
PlayerMocks.InitControllerMocks() returns a Mock<IValidator<PlayerRequestModel>>. Every PUT test manually stubs .ValidateAsync() to return an empty ValidationResult (no errors). The real BeUniqueSquadNumber logic never executes in any controller test.

2. Validator unit tests only cover POST (create) scenarios
PlayerValidatorTests.cs does exercise the real validator, including the uniqueness rule — but all fixtures use MakeRequestModelForCreate(). There is no test for the PUT scenario: "validating a player whose squad number already exists in the DB." That is precisely the case where the bug manifests.

There are also no integration tests that wire the real validator to a real (or in-memory) database and exercise the full PUT pipeline end-to-end, which would have caught this immediately.

The bug lives in the gap between the two isolated unit test layers. The fix should include a new validator test:
ValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrors to prevent regression.

Possible Solutions

Two approaches were considered:

Option A — FluentValidation Rule Sets ✅ Chosen

Use FluentValidation's built-in rule sets to scope rules by operation. All rules are organized into explicit "Create" and "Update" rule sets; BeUniqueSquadNumber is included only in "Create".

  • POST validates with opts.IncludeRuleSets("Create")
  • PUT validates with opts.IncludeRuleSets("Update")

Why chosen: Minimal footprint (3 files), no model or service signature changes, explicit CRUD-named rule sets serve as documentation of intent — valuable for an educational codebase. The "Update" rule set also provides a ready home for any future update-specific rules (e.g. squad number immutability).

Files affected:

  • Validators/PlayerRequestModelValidator.cs — split all rules into RuleSet("Create") and RuleSet("Update"); BeUniqueSquadNumber only in "Create"
  • Controllers/PlayerController.csPostAsync and PutAsync pass explicit rule set options
  • Tests/Unit/PlayerValidatorTests.cs — update existing tests to specify rule sets; add ValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrors

Option B — Split Models

Split PlayerRequestModel into two purpose-specific models with separate validators:

Model Used by Validator
PlayerCreateModel POST PlayerCreateModelValidator — includes BeUniqueSquadNumber
PlayerUpdateModel PUT PlayerUpdateModelValidator — omits BeUniqueSquadNumber

Why not chosen: Requires changes to 6+ files (Models, Validators, Controller, Service, Mappings, Tests). Introduces model and mapping duplication for no immediate benefit — the two models are structurally identical. Adds complexity that can be avoided with rule sets.

Files affected:

  • Models/PlayerRequestModel.cs → split into PlayerCreateModel.cs / PlayerUpdateModel.cs
  • Validators/PlayerRequestModelValidator.cs → split into PlayerCreateModelValidator.cs / PlayerUpdateModelValidator.cs
  • Controllers/PlayerController.cs — update POST and PUT action signatures
  • Services/PlayerService.cs — update method signatures
  • Mappings/PlayerMappingProfile.cs — add mappings for new models
  • Tests/ — add ValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrors validator test

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETPull requests that update .NET codebugSomething isn't workingpriority criticalBlocking dependency or production issue. Must be addressed before other work.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions