Repository navigation
test(purchase): pin case-insensitive email matching in 4-eyes and approver checks - #1120
Conversation
…rover checks Every email literal in the 4-eyes self-approval tests and the SQS approver-matching tests was lowercase and byte-identical on both sides, so replacing strings.EqualFold with == in checkDifferentApprover, or dropping either strings.ToLower in matchActorAgainstApprovers, left the whole suite green. - TestManager_ApproveExecution_FourEyesOn_SQSActorEqualsCreator_Denied is now table-driven: the creator's stored email and the actor email each differ only in case in one row, and both must still be denied. - TestProcessMessage_ApproveFourEyesOn_SelfApproveDenied gains a row where the creator and contact_email are stored mixed case and the SQS actor_email is lowercase, so both folds run end to end. - New TestProcessMessage_ApproverMatchIgnoresEmailCase covers each side of the approver comparison. Mocks in the new rows are bound with .Test(t) so one regressed row fails on its own instead of panicking and skipping the rest. No production code changes: the comparisons are already case-insensitive. Closes LeanerCloud#307 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing: 2
✨ Finishing Touches
Comment |
|
Independent review (claude-cc-platform reviewer, cold brief): APPROVE No blocking findings. The change is test-only: Every email comparison on a four-eyes or approver path is already case-insensitive:
The API four-eyes check ( Mutation probes, all caught:
The new rows bind their mocks with Base drift: clean merge onto current main (tree Nits:
|
|
Independent review (claude-fleet): APPROVE 91d736d Findings (none blocking):
Attacked:
Verified locally, on a
Reviewed by an independent agent with no author context. Review only: nothing was pushed, and the GitHub review state is unchanged. |
|
Local verification (macOS, exact head Gate status: two independent reviews approve this SHA (posted above), and local verification passes. Merge waits on @IAmJSD marking the PR ready (see #974). |
|
Local verification at
|
|
Marked ready and merged by the owner's agent after 2 independent reviews and local verification (see the review and verification comments above, all at |
Summary
Test-only change. The 4-eyes self-approval check (
checkDifferentApprover,strings.EqualFold) and the SQS approver match (matchActorAgainstApprovers,strings.ToLoweron both sides) are both case-insensitive onmaintoday, but no fixture tested that. Every email literal in those tests was lowercase and identical on both sides, so replacingEqualFoldwith==, or dropping eitherToLower, left the suite green.Root cause: missing test coverage, not a production bug. With ASCII emails the new case-differing tests pass against unchanged production code, so this PR makes no production change.
Closes #307
Changes
All changes are in
internal/purchasetests:approvals_test.go:TestManager_ApproveExecution_FourEyesOn_SQSActorEqualsCreator_Deniedis now table-driven. It has three rows: byte-identical emails; creator stored asCreator@Example.COMwith actorcreator@example.com; creatorcreator@example.comwith actorCREATOR@Example.com. Every row must returnErrFourEyesDeniedand must not reachTransitionExecutionStatus.coverage_extra_test.go:TestProcessMessage_ApproveFourEyesOn_SelfApproveDeniedgets a new row. The creator's user email and the accountcontact_emailare stored asOwner@Example.COM, and the SQSactor_emailisowner@example.com. This runs both case-insensitive comparisons end to end throughProcessMessage: the approver check has to accept the actor, and then 4-eyes has to deny them as the creator.TestProcessMessage_ApproverMatchIgnoresEmailCasehas one row for each side of the approver comparison (mixed-casecontact_emailwith a lowercase actor, and the reverse). Removing eitherToLowerfails one of the rows. The existingTestProcessMessage_ApproveRejectsNonMatchingActorstill catches a change that accepts everyone..Test(t). Without that, an unexpectedTransitionExecutionStatuscall panics and the remaining rows are skipped. With it, each regressed row fails on its own.Verification
All of this is unit tests with mocks. No Postgres, no real cloud, no email. Tests were run with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/nullbecause this machine's default AWS profile is invalid.There is no production fix, so "fail before" is shown with mutation probes: I edited the production code by hand, ran
go test ./internal/purchase/ -count=1 -run 'FourEyes|ApproverMatch|ProcessMessage_Approve|ProcessMessage_Cancel', then restored it.mainapprovals.gostrings.EqualFold(creatorEmail, actorEmail)changed tocreatorEmail == actorEmailok(all pass)SQSActorEqualsCreator_Denied/creator_stored_mixed_case,/actor_mixed_case,ProcessMessage_ApproveFourEyesOn_SelfApproveDenied/stored_mixed_case,_actor_lowercasemessages.gostrings.ToLowerremoved from the actor sideokApproverMatchIgnoresEmailCase/contact_email_lowercase,_actor_mixed_casemessages.gostrings.ToLowerremoved from thecontact_emailsideokApproverMatchIgnoresEmailCase/contact_email_mixed_case,_actor_lowercase,SelfApproveDenied/stored_mixed_case,_actor_lowercaseokokOther checks, all passing:
No DB or schema change, so there is no concurrency surface to race-test.
Out of scope (noted, not changed)
authorizeApprovalActionininternal/api/handler_purchases.go, uses the sameToLower-both-sides comparison. Its case handling is likewise untested, but the issue coversinternal/purchaseonly.strings.EqualFold, approver matching usesstrings.ToLower. They agree for ASCII but not for every non-ASCII character. For example,ToLowerturns the Kelvin sign U+212A into an ASCIIk, whileEqualFoldtreats U+017Fſas equal tosandToLowerdoes not. Whether this can be exploited depends on whether a non-ASCII address can become a session oractor_email. I didn't change it here. It may be worth a separate issue to normalize emails in one shared place.Maintainers: please mirror the issue's triage labels onto this PR (I can't apply labels from the fork workflow):
priority/p2,severity/medium,impact/internal,type/chore,urgency/this-quarter,effort/xs.🤖 Generated with Claude Code