Conversation
|
|
||
| attr_accessor :params | ||
| end | ||
| host.permittable_contracts = [rule.merge(mode: :enforce).freeze] |
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 |
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>
a5fb7b3 to
c70a594
Compare
|
Held back from the v0.9.0 merge train — the other eight open PRs went in. One reproduced defect, on a test matcher, which is the worst place for one: it passes on input the contract actually rejects.
host = Class.new do
include Permittable
attr_accessor :params
endWhen the subject is a c = Permittable::Contract.define(unknown: :error) { required :name, :string }
payload = { "name" => "Jo", "controller" => "users", "action" => "create" }
c.call(payload).violations
# => [{param: "controller", code: "unknown"}, {param: "action", code: "unknown"}]
expect(c).to accept_params(payload) # PASSESA green test for a payload the contract refuses in production. Secondary, worth a look while you're in there: forcing The matchers themselves read well and the rest of the suite passes on current master — it's this one that needs fixing before it lands. |
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