Add accept_params / reject_params matchers - #35
Open
VSN2015 wants to merge 1 commit into
Open
Conversation
VSN2015
commented
Sep 5, 2026
|
|
||
| attr_accessor :params | ||
| end | ||
| host.permittable_contracts = [rule.merge(mode: :enforce).freeze] |
Owner
Author
There was a problem hiding this comment.
Forcing mode: :enforce in the test harness (rule.merge(mode: :enforce)) ensures matchers test what the contract declares, enabling contracts under rollout in monitor mode to be verified against their intended rejection rules.
| def accepted_ok? | ||
| return false unless @violations.empty? | ||
|
|
||
| @returning.nil? || @result.to_h == ActiveSupport::HashWithIndifferentAccess.new(@returning).to_h |
Owner
Author
There was a problem hiding this comment.
Comparing @result.to_h == ActiveSupport::HashWithIndifferentAccess.new(@returning).to_h in accepted_ok? makes assertions on .returning(...) indifferent to symbol vs string keys, matching developer expectations.
permit_param reads the declaration, which leaves the behaviour
untested: whether a payload is accepted, and what it casts to. A
contract can be fully specified by permit_param and still be wrong
about the thing it exists to do.
accept_params / reject_params run the rule against a payload directly
— still no request dispatched — and assert the outcome:
expect(described_class).to accept_params(user: { name: "Jo", age: "30" })
.for_action(:create).returning("name" => "Jo", "age" => 30, "plan" => "free")
expect(described_class).to reject_params(user: { email: "nope" })
.for_action(:create).with_violation("user.email", :format)
returning pins the cast, defaulted, transformed output, which the
declaration matcher cannot reach. with_violation is repeatable and its
code is optional.
Two decisions worth knowing:
* They read the CONTRACT, not the rollout mode. The rule is copied into
a throwaway host with mode: :enforce, so a monitor-mode rule still
reject_params — the question a spec asks is what the contract says,
not what the deploy currently does with it.
* Subject and rule resolution are shared with permit_param, so
for_action behaves identically and ambiguity fails just as loudly.
Failure messages name what actually happened, since that is the whole
value of a matcher over a hand-rolled call:
expected UsersController to reject those params with user.age
(inclusion), but the violations were: user.name (missing),
user.email (missing)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
VSN2015
force-pushed
the
feature/behavioural-matchers
branch
from
September 11, 2026 22:01
a5fb7b3 to
c70a594
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
permit_paramreads the declaration. That leaves the behaviour untested — whether a payload is accepted, and what it casts to. A contract can be fully specified bypermit_paramand still be wrong about the thing it exists to do.The matchers
Still no request dispatched — the rule runs against the payload directly.
returningpins the cast, defaulted, transformed output, which the declaration matcher cannot reach.with_violationis repeatable and its code is optional.Failure messages, which are the whole point
A hand-rolled
expect { ... }.to raise_error(Permittable::InvalidParameters)tells you nothing about why; that third message is the reason to have a matcher at all.Two decisions worth reviewing
They read the contract, not the rollout mode. The rule is copied into a throwaway host with
mode: :enforce, so a monitor-mode rule stillreject_params. The question a spec asks is what the contract says, not what the deploy currently does with it — and a spec that silently stopped asserting because someone flipped a rollout switch would be worse than useless. There's a spec pinning this.Subject and rule resolution are shared with
permit_param, sofor_actionbehaves identically and ambiguity fails just as loudly (declares 2 contracts — disambiguate with accept_params(...).for_action(:action)).Verification
returningmatches and mismatches, all three failure messages, monitor-mode behaviour, the single-contract and ambiguous-contract paths, and a standaloneContractsubject