fix(api): enforce overdue-window guard on skip commands (#243)#326
Conversation
Skip operations skipped the overdue-window bound that the complete/log paths enforce, so a habit occurrence older than DefaultOverdueWindowDays could still be skipped. Add the same BeyondOverdueWindow guard to SkipHabitCommand.ValidateSkipTarget and BulkSkipHabitsCommand.ProcessSkipItem, positioned right after the future-date check to mirror LogHabitCommand / BulkLogHabitsCommand ordering exactly. Refs thomasluizon/orbit-ui-mobile#243 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
PR Review — #326: enforce overdue-window guard on skip commands
Scope: SkipHabitCommand.ValidateSkipTarget and BulkSkipHabitsCommand.ProcessSkipItem gain the same targetDate < today.AddDays(-AppConstants.DefaultOverdueWindowDays) → ErrorMessages.BeyondOverdueWindow guard already enforced by LogHabitCommand.ValidateTargetDate / BulkLogHabitsCommand.ProcessLogItem. Backend-only (Orbit.Application + unit tests), no DTO/route changes.
Findings
None at Critical/High/actionable-Medium. Verified against the rubric:
- Correctness — guard is inserted in the exact same position (after the future-date check, before
HabitNotYetDue/NotScheduledOnDate) as the pre-existingLogHabitCommand.ValidateTargetDate/BulkLogHabitsCommand.ProcessLogItem, byte-for-byte identical condition and error. Boundary math verified:today - DefaultOverdueWindowDays(7 days,AppConstants.cs:29) succeeds, one day older fails — matches the new tests. - Authorization/ownership — confirmed by a dedicated security-reviewer pass:
SkipHabitCommandkeeps itshabit.UserId != request.UserIdcheck ahead of the new guard;BulkSkipHabitsCommand'shabitMapis pre-filtered byh.UserId == request.UserIdat the query, so ordering doesn't affect tenant isolation. No new endpoints/routes; both commands are reached via already-[Authorize]'d controllers. - Contract drift —
ErrorCodes.BeyondOverdueWindow/ErrorMessages.BeyondOverdueWindowalready exist and are used by the log paths, so this is not a new contract surface. (Cross-repo consumer-side verification not possible —orbit-ui-mobileisn't checked out in this job — but the PR correctly doesn't require any consumer change since the code is pre-existing and consumer-handled.) - Backend hard rules —
todayis sourced fromIUserDateService.GetUserTodayAsyncupstream in both handlers (not rawUtcNow); no logging touched; no transaction rollback changes (bulk path'sExecuteInTransactionAsyncuntouched). - Tests — new coverage added for both paths: single skip at/beyond the boundary, bulk skip mixing an in-window success with a beyond-window failure. Matches the existing test style in both files.
- Dead code / comments — none introduced.
- One-time habits (
FrequencyUnit is null) — checked whether the new guard creates an asymmetry here. It doesn't:SkipHabitCommand.HandleOneTimeSkipshort-circuits before any date validation (pre-existing, ignoresrequest.Dateentirely — it always postpones totoday+1), whileBulkSkipHabitsCommandvalidatestargetDatebefore its one-time branch. That split already existed for theCannotSkipFutureDatecheck the new line sits directly beneath; this PR just extends the same pre-existing pattern rather than introducing a new inconsistency.
Recommendation: APPROVE
Minimal, well-scoped fix that mirrors an established pattern exactly, with matching test coverage on both the single and bulk paths. No blocking issues found.
|
There was a problem hiding this comment.
PR Review — #326: enforce overdue-window guard on skip commands
Scope: SkipHabitCommand.ValidateSkipTarget and BulkSkipHabitsCommand.ProcessSkipItem gain the same targetDate < today.AddDays(-AppConstants.DefaultOverdueWindowDays) → ErrorMessages.BeyondOverdueWindow guard already enforced by LogHabitCommand.ValidateTargetDate / BulkLogHabitsCommand.ProcessLogItem. Backend-only, no DTO/route changes.
No new commits since the prior review at 0a0eaa4 — git diff 0a0eaa4..HEAD is empty (the current tip is only a merge-preview commit). Re-verified the findings below still hold; nothing new to flag.
Findings
| Severity | Count |
|---|---|
| Critical (incl. |
0 |
| High | 0 |
| Medium | 0 |
| Low / Info | 2 (non-blocking, pre-existing) |
Low / Info (unchanged from prior pass, informational only):
SkipHabitCommand.HandleOneTimeSkipdispatches beforeValidateSkipTargetruns for one-time habits, so it still bypasses the future-date and overdue-window checks — pre-existing behavior, unchanged by this diff, out of scope.- The reused
ErrorMessages.BeyondOverdueWindow.Messagetext says "log" even when returned from a skip failure — cosmetic, consumer keys offErrorCodenot the message string.
Verified
- Guard sits in the same relative position (after future-date check, before due/scheduled checks) in both single and bulk paths, mirroring the identical guard on the log commands. Boundary math confirmed:
today - DefaultOverdueWindowDayssucceeds, one day older fails. - Ownership/authorization unaffected:
SkipHabitCommand's explicitUserIdcheck andBulkSkipHabitsCommand's pre-filtered query both remain intact ahead of/independent of the new guard. - No new contract surface —
BEYOND_OVERDUE_WINDOWis a pre-existing, consumer-handled error code; cross-repo verification not possible in this job (mobile repo not checked out) but not needed here since nothing new is exposed. - Test coverage: single-skip boundary test and bulk-skip mixed success/failure test both present and match existing test conventions.
Recommendation: APPROVE
Minimal, well-scoped fix mirroring an established pattern exactly, with matching test coverage. No blocking issues found.
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>



Problem
Skip operations skipped the overdue-window guard that the complete/log paths enforce.
LogHabitCommand.ValidateTargetDateandBulkLogHabitsCommand.ProcessLogItemboth reject a target older thanAppConstants.DefaultOverdueWindowDayswithErrorMessages.BeyondOverdueWindow, but the two skip paths had no such bound — so a habit occurrence older than the window could still be skipped (overdue = DueDate < today; skips areValue=0HabitLogrows).Fix
Add the identical guard to both skip commands, positioned right after the future-date check to mirror the log/bulk-log ordering exactly:
SkipHabitCommand.ValidateSkipTarget—return Result.Failure(ErrorMessages.BeyondOverdueWindow).BulkSkipHabitsCommand.ProcessSkipItem— returns the per-itemBulkItemStatus.Failedresult carrying theBeyondOverdueWindowmessage/code.Placing the bound before
HabitNotYetDue/NotScheduledOnDatematches the log paths and yields the accurate reason on edge overlaps (a 10-day-old target reads "beyond overdue window", not "not yet due").Same shared constant + error the complete/log paths use — no new contract surface (
BEYOND_OVERDUE_WINDOWalready exists and is consumer-handled), so this is backend-only.Tests
BeyondOverdueWindow(and does not save); target at the window boundary (today - DefaultOverdueWindowDays) succeeds.BeyondOverdueWindowfailure).Full
Orbit.Application.Testssuite green (2620 passed).Refs thomasluizon/orbit-ui-mobile#243