Skip to content

fix(api): add boundary date rule to SkipHabitCommandValidator - #342

Merged
thomasluizon merged 2 commits into
mainfrom
fix/skip-habit-validator-date-boundary
Jul 12, 2026
Merged

fix(api): add boundary date rule to SkipHabitCommandValidator#342
thomasluizon merged 2 commits into
mainfrom
fix/skip-habit-validator-date-boundary

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

What

Adds a boundary-appropriate validation rule for the optional Date on SkipHabitCommand, so skip validation isn't only handler-deferred.

Context — verified current state

The task premise was that SkipHabitCommandValidator is empty with all validation in the handler. On inspection that's stale: the UserId/HabitId NotEmpty rules have existed since the original skip endpoint (#21). What was genuinely missing is a boundary rule for the optional Date.

Change

RuleFor(x => x.Date)
    .NotEqual(default(DateOnly))
    .When(x => x.Date.HasValue)
    .WithMessage("Date must be a valid calendar date");
  • Rejects the default(DateOnly) / DateOnly.MinValue (0001-01-01) sentinel — a malformed/zeroed date payload — at the trust boundary, before it reaches business logic.
  • null stays valid (the field is optional; the handler defaults to the user's today).

What deliberately stays in the handler's domain guard

The timezone-dependent checks cannot live at the boundary because the validator has no access to the user's resolved "today":

These remain in ValidateSkipTarget unchanged — boundary handles structural validity, handler handles domain rules.

Tests

Added to SkipHabitCommandValidatorTests:

  • Validate_NullDate_NoDateError — omitted date is valid
  • Validate_DateOnlyMinValueSentinel_HasError — zeroed sentinel is rejected
  • Validate_RealPastDate_NoError — a real date passes the boundary (domain window check is the handler's job)

dotnet build clean; full Orbit.Application.Tests suite green (2631 passed).

Refs thomasluizon/orbit-ui-mobile#243

The required-id rules (UserId/HabitId NotEmpty) already existed since the
original endpoint, but the optional Date carried no boundary validation.
Reject the default(DateOnly) sentinel at the trust boundary so a malformed
zeroed date is caught before the handler. The timezone-dependent guards
(future date, overdue window #326, scheduling) stay in the handler's domain
guard because they need the user's resolved "today".

Refs thomasluizon/orbit-ui-mobile#243

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Complete

Scope: PR #342 — fix(api): add boundary date rule to SkipHabitCommandValidator
Recommendation: APPROVE

Severity Count
Critical (incl. old-client breaks) 0
High 0
Medium 1
Low / Info 2

Findings

Medium — Sentinel-date boundary rule added to SkipHabitCommandValidator (src/Orbit.Application/Habits/Validators/SkipHabitCommandValidator.cs:16-19) but not mirrored to the sibling BulkSkipHabitsCommandValidator (src/Orbit.Application/Habits/Validators/BulkSkipHabitsCommandValidator.cs), whose BulkSkipItem.Date is the identical optional DateOnly? field. Not a merge blocker: BulkSkipHabitsCommand.cs:86-88 (ProcessSkipItem) already re-checks the same overdue-window bound per item and fails gracefully, so no crash/data-loss risk — just a validation-boundary/DRY inconsistency between the two skip paths. Suggested follow-up: add the matching item.RuleFor(i => i.Date).NotEqual(default(DateOnly)).When(i => i.Date.HasValue) rule + a mirrored test.

Info: the plain-string WithMessage (no i18n key, no ErrorMessages catalog entry) matches this folder's existing convention.
Info: confirmed default(DateOnly) == DateOnly.MinValue (0001-01-01), so the new sentinel test exercises the intended condition.

Subagents

Agent Verdict
security-reviewer PASS — pure value-object equality check, no auth/injection/data-exposure impact; new rule is defense-in-depth (domain-level ValidateSkipTarget already rejected DateOnly.MinValue via the overdue-window bound)
contract-aligner N/A — diff only touches a validator + its test; no DTO/route/packages/shared change

Validation

Check Result
Build (dotnet) N/A — separate required CI check (per workflow instructions)
Tests (dotnet) N/A — separate required CI check (per workflow instructions)

Deferred

Dimensions 8/9/10/14 (DESIGN.md, parity, i18n, FEATURES.md) — N/A, backend-only bugfix, no UI/feature-surface/locale surface touched. Dimension 11 (contract drift) — N/A, no DTO/shared-schema field changed. /second-opinion not triggered — no Critical finding survived to require it. Both changed files (SkipHabitCommandValidator.cs, SkipHabitCommandValidatorTests.cs) read in full and verdicted.

What's good

Correctly keeps timezone-dependent checks in the handler (validator has no IUserDateService access); PR body re-verified a stale task premise before scoping the change narrowly; new tests cover exactly the three meaningful cases with clear naming.

@thomasluizon
thomasluizon merged commit 8da6511 into main Jul 12, 2026
@thomasluizon
thomasluizon deleted the fix/skip-habit-validator-date-boundary branch July 12, 2026 13:51
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant