Repository navigation
test(api): cover retry-any:purchases carve-out at the retry handler - #1853
Conversation
retry-any:purchases is the third #923 money verb, and the only one whose enforcement point is not requirePermission: retryPurchase authorizes through authorizeSessionRetry, which calls HasPermissionAPI directly (handler_purchases.go). The verb IS enforced there, and the path is reachable (POST /api/purchases/retry/{id} -> retryPurchase -> authorizeSessionRetry -> Service.permissionsAllow, which consults adminCarvedOuts). What was missing is coverage of that path. Every existing retryPurchase test goes through buildSessionRetryHandler, which hand-registers HasPermissionAPI with a caller-supplied boolean and the bare literal "retry-any", so the real matcher and adminCarvedOuts are never consulted. Removing the verb from adminCarvedOuts therefore failed only the generic requirePermission-level test added by #1744; not one retryPurchase test noticed. That is also why instrumenting grantAdmin recorded zero asks for the pair. Add two tests that drive the real retryPurchase end to end against the real matcher, differing in the principal only so the carve-out is the sole variable: a plain admin:* is refused, and admin + Purchaser is allowed. The fixture row is created by a different user, since a creator-owned row is reachable via retry-own, which is deliberately not carved out. The refusal asserts which gate refused rather than just that an error occurred. Dropping the verb from adminCarvedOuts lets the caller past the RBAC gate, but the SEC-01 constraint gate then refuses it on execute:purchases with a different message and still a 403, so a bare require.Error would pass with the carve-out deleted. Refs #1743, #1596, #1744, #923
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour. 📝 WalkthroughWalkthroughThe PR adds handler-level regression tests for ChangesRetry authorization coverage
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to This is a localized test-only change with build, test, and lint checks reported as passing; no actionable merge-blocking risk remains. Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Closes #1743
What
Handler-level coverage for the
retry-any:purchasescarve-out (#923) at the retry handler.Test-only: one new file, no production code touched.
The issue's premise was wrong, and the reason is the finding
#1743 was filed on the possibility that this verb might be unenforced. It is not. Full premise
correction with evidence is on the issue:
#1743 (comment)
Please read that before reviewing, so the disproved "no non-test reference in
internal/api" claimdoes not get re-derived from scratch.
Short version. The verb is enforced at
internal/api/handler_purchases.go:1962and the path isreachable:
Why the gap existed
retry-anyis the only one of the three carved-out money verbs whose enforcement point is notrequirePermission, so the handler coverage added by #1744 sits at a seam this path never crosses.Every existing
retryPurchasetest goes throughbuildSessionRetryHandler(
handler_purchases_test.go:3053-3076), which hand-registers the decision with a caller-suppliedboolean and the bare literal
"retry-any"rather thanauth.ActionRetryAny. The real matcher, andwith it
adminCarvedOuts, is never consulted on this path. That is also why instrumentinggrantAdminrecorded zero asks for the pair.Measured: removing
retry-anyfromadminCarvedOutsfails only the genericrequirePermissiontest from #1744. Zero
retryPurchasetests notice.REVIEWERS: please do not simplify the message assertion
TestRetryCarveOut_PlainAdminIsRefusedasserts thaterr.Error()contains"another user's failed purchase". That single line is the regression barrier.Dropping
retry-anyfromadminCarvedOutslets the caller past the RBAC gate, butretryPurchasethen hits the SEC-01 constraint gate, which asks
execute:purchases, still carved out, and refuseswith a different message and still a 403. So a bare
require.Errorpasses against the mutated codeand asserts nothing.
Relaxing that
assert.Containsdeletes the coverage with no test going red. There is a comment onthe line saying exactly this; please keep both.
What the tests do
Two tests differing in the PRINCIPAL only, same failed row, same request, same fixture, so the
carve-out is the sole variable:
TestRetryCarveOut_PlainAdminIsRefused:admin:*is refused.TestRetryCarveOut_AdminPurchaserIsAllowed: admin + Purchaser is allowed, successor written andlinked to the original.
Both drive the real
retryPurchaseend to end against the real matcher, rather thanrequirePermission. The fixture row is created by a different user on purpose: a creator-owned rowis reachable via
retry-own, which is deliberately not carved out, so only a foreign row isolatesretry-anyas the deciding grant.Verification
adminCarvedOuts):TestRetryCarveOut_PlainAdminIsRefusedfails by assertion, not panic. Restored, both pass. Re-run against the committed code.
go build ./...passes.go test ./...: 7073 passed, 0 failed, 43 packages.golangci-lintat the CI-pinned v2.10.1:0 issues, exit 0. Checked at the pin deliberately,since a newer local version returns a false exit 0.
internal/mocks.TestNoUnfailableMockAssertionspasses. It rejected the first draft, where themocks were returned from a helper via multi-value assignment and the guard could not resolve the
receivers; each test now binds
new(MockAuthService)in its own scope so the sites are actuallychecked rather than reported as unanalyzable.
Vacuity traps handled explicitly:
AssertExpectationsviat.Cleanup; a positiveAssertCalledonthe
retry-anypair before anyAssertNotCalled, so the negative cannot be satisfied by a requestthat never reached the permission gate; and the name-only
AssertNotCalledform onMockConfigStore, which shadows that helper (internal/mocks/assertions.go:154) and reads a calllog where name-only is the strongest form rather than the vacuous one.
Known residual gap, pre-existing and not introduced here
The tests exercise
AuthContext.HasPermissionthrough the mock, but production reaches thecarve-out via
Service.permissionsAllow. Both consultadminCarvedOutsand both were read toconfirm it, but nothing pins them in sync: a regression removing the check from
permissionsAllowalone would not be caught. Identical in shape for the two sibling verbs and for #1644, so it may
warrant a separate issue.
Label note
The
type/securityandseverity/highlabels were applied on the unenforced-control premise, whichdoes not hold. Labels are mirrored from the issue as-is here; re-triage is the maintainer's call and
is flagged on the issue thread rather than acted on.
Related: #1596, #1744, #923, #1644
Summary by CodeRabbit