Module: Clickwrap::TestHelpers
- Defined in:
- lib/clickwrap/test_helpers.rb
Overview
Test helpers for Minitest. Include in your test helper to get evidence-related test utilities.
=========================================================================== EVERY HELPER HERE GOES THROUGH THE REAL PATH.
submit_clickwrap presents the policy through the actual Presenter, takes
the signed token it produced, builds an actual Clickwrap::Submission from
it, and calls the actual Clickwrap.capture!. It does not insert rows.
That is not fastidiousness. Hand-built evidence rows are the single most effective way to make a suite green while the product is broken: they skip the manifest, the digests, the answer validation, the idempotency key, the projection, and the very checks the gem exists to perform, and then every assertion downstream is checking that a fixture matches itself. Evidence created by these helpers is internally consistent because it was created the same way real evidence is.
The deliberately-broken cases — an expired presentation, a stale revision, another actor's token — are built by taking a REAL presentation and changing exactly one thing about it, so what fails is the server's own check rather than a mock standing in for it.
Defined Under Namespace
Modules: ClassMethods
Class Method Summary collapse
-
.clickwrap_granting_choice(statement) ⇒ Object
The choice a policy declared as meaning "grant"; otherwise the first one offered, so a statement with domain-specific choice names still works.
-
.clickwrap_params_from(path, answers: {}, form_css_selector: nil) ⇒ Object
The one-line form of the pattern below, for integration tests: GET the page that renders a clickwrap presentation, read the signed token and controls back out of it, return the params for the POST.
-
.clickwrap_submission_params_from(rendered, answers: {}, form_css_selector: nil) ⇒ Hash
For integration tests that POST a form the application really rendered — the signed presentation token is minted per render and bound to the session, so a test cannot fabricate it; it has to read it back out of the page, exactly like a browser:.
- .clickwrap_test_form_scope(page, form_css_selector) ⇒ Object
- .clickwrap_token_without_message_expiry(manifest) ⇒ Object
-
.committed_test_receipt(result) ⇒ Object
Transactional test wrappers intentionally never commit.
-
.default_clickwrap_answers(policy_key, answers) ⇒ Object
Required statements get an affirmative answer; optional ones are left untouched.
-
.default_rendered_control_answer(controls) ⇒ Object
A checkbox's affirmative value is conventionally "1".
-
.expired_clickwrap_presentation_for(policy_key) ⇒ Clickwrap::Presenter::Result
A presentation whose manifest has ALREADY EXPIRED.
- .included(base) ⇒ Object
- .normalize_rendered_control_answer(controls, value) ⇒ Object
- .normalize_test_answer(value) ⇒ Object
-
.other_actors_clickwrap_token_for(policy_key, actor:, other_actor:) ⇒ String
A token legitimately issued to somebody else, for the swapped-token case.
-
.present_clickwrap(policy_key, actor: nil, subject: nil, tenant: nil, locale: nil, submit_button_text: "Continue", capture_channel: :web_browser, prospective_actor: nil, registration_flow_id: nil, acting_for: nil) ⇒ Clickwrap::Presenter::Result
Presents a policy server-side and returns the Presenter::Result, whose
tokenis a real signed presentation token. -
.stale_clickwrap_token_for(policy_key) ⇒ String
A token bound to a policy revision that is no longer on file — the token a browser holds when the policy was edited and redeployed between the GET and the POST.
-
.submission_for(presentation_result, answers = {}) ⇒ Clickwrap::Submission
Builds a real Submission from a presentation result.
-
.submit_clickwrap(policy_key, actor:, answers: {}, subject: nil, tenant: nil, locale: nil, capture_channel: :web_browser, http_request: nil, acting_for: nil) ⇒ Clickwrap::Receipt
Presents, answers, and captures — the everyday way to get real evidence into a test database.
-
.submit_clickwrap_and(policy_key, actor:, answers: {}, subject: nil, tenant: nil, capture_channel: :web_browser, http_request: nil, acting_for: nil) ⇒ Clickwrap::Receipt
The same, with a protected action in the same transaction.
-
.submit_clickwrap_twice(policy_key, actor:, answers: {}, subject: nil, tenant: nil) ⇒ Array<Clickwrap::Receipt>
Submits the SAME presentation twice, which is what a double-click, a retried request, and a replayed token all look like to the server.
Instance Method Summary collapse
- #assert_clickwrap_acknowledged(statement_key, actor:, subject: nil, tenant: nil) ⇒ Object
- #assert_clickwrap_agreed_to(statement_key, actor:, subject: nil, tenant: nil) ⇒ Object
- #assert_clickwrap_authorized(statement_key, actor:, subject: nil, tenant: nil) ⇒ Object
- #assert_clickwrap_consented_to(purpose_key, actor:, subject: nil, tenant: nil) ⇒ Object
-
#assert_clickwrap_current(policy_key, actor:, subject: nil, tenant: nil) ⇒ Object
Assert that an actor currently satisfies a whole policy.
- #assert_clickwrap_declared(statement_key, actor:, subject: nil, tenant: nil) ⇒ Object
-
#assert_clickwrap_exempted_from(policy_key, actor:, subject: nil, tenant: nil) ⇒ Object
Assert that an actor is exempted from a policy — a deliberately separate question from having agreed to it.
-
#assert_clickwrap_receipt_verifies(receipt) ⇒ Object
Assert that a receipt still verifies: its digest covers its bytes, its document versions still match what was published, and the event has not been disposed of.
-
#assert_no_clickwrap_event(policy_key, actor: nil) ⇒ Object
Assert that no event exists for a policy — the other half of a fault injection test, where the point is that nothing was recorded.
-
#clickwrap_params_from(path, answers: {}, form_css_selector: nil) ⇒ Object
The one-line form of the pattern below, for integration tests: GET the page that renders a clickwrap presentation, read the signed token and controls back out of it, return the params for the POST.
-
#clickwrap_submission_params_from(rendered, answers: {}, form_css_selector: nil) ⇒ Hash
For integration tests that POST a form the application really rendered — the signed presentation token is minted per render and bound to the session, so a test cannot fabricate it; it has to read it back out of the page, exactly like a browser:.
-
#complete_clickwrap(policy_key) ⇒ Object
Checks every required control the policy declares, on the page currently rendered, and leaves optional ones alone.
-
#expired_clickwrap_presentation_for(policy_key) ⇒ Clickwrap::Presenter::Result
A presentation whose manifest has ALREADY EXPIRED.
-
#other_actors_clickwrap_token_for(policy_key, actor:, other_actor:) ⇒ String
A token legitimately issued to somebody else, for the swapped-token case.
-
#present_clickwrap(policy_key, actor: nil, subject: nil, tenant: nil, locale: nil, submit_button_text: "Continue", capture_channel: :web_browser, prospective_actor: nil, registration_flow_id: nil, acting_for: nil) ⇒ Clickwrap::Presenter::Result
Presents a policy server-side and returns the Presenter::Result, whose
tokenis a real signed presentation token. -
#refute_clickwrap_current(policy_key, actor:, subject: nil, tenant: nil) ⇒ Object
Assert that an actor does NOT currently satisfy a policy.
-
#stale_clickwrap_token_for(policy_key) ⇒ String
A token bound to a policy revision that is no longer on file — the token a browser holds when the policy was edited and redeployed between the GET and the POST.
-
#submission_for(presentation_result, answers = {}) ⇒ Clickwrap::Submission
Builds a real Submission from a presentation result.
-
#submit_clickwrap(policy_key, actor:, answers: {}, subject: nil, tenant: nil, locale: nil, capture_channel: :web_browser, http_request: nil, acting_for: nil) ⇒ Clickwrap::Receipt
Presents, answers, and captures — the everyday way to get real evidence into a test database.
-
#submit_clickwrap_and(policy_key, actor:, answers: {}, subject: nil, tenant: nil, capture_channel: :web_browser, http_request: nil, acting_for: nil) ⇒ Clickwrap::Receipt
The same, with a protected action in the same transaction.
-
#submit_clickwrap_twice(policy_key, actor:, answers: {}, subject: nil, tenant: nil) ⇒ Array<Clickwrap::Receipt>
Submits the SAME presentation twice, which is what a double-click, a retried request, and a replayed token all look like to the server.
Class Method Details
.clickwrap_granting_choice(statement) ⇒ Object
The choice a policy declared as meaning "grant"; otherwise the first one offered, so a statement with domain-specific choice names still works.
611 612 613 |
# File 'lib/clickwrap/test_helpers.rb', line 611 def clickwrap_granting_choice(statement) statement.choices.find { |_, meaning| meaning == "grant" }&.first || statement.choices.keys.first end |
.clickwrap_params_from(path, answers: {}, form_css_selector: nil) ⇒ Object
The one-line form of the pattern below, for integration tests: GET the page that renders a clickwrap presentation, read the signed token and controls back out of it, return the params for the POST.
post user_registration_path, params: {
user: { ... },
**clickwrap_params_from(new_user_registration_path)
}
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/clickwrap/test_helpers.rb', line 140 def clickwrap_params_from(path, answers: {}, form_css_selector: nil) unless respond_to?(:get) raise ArgumentError, "clickwrap_params_from drives a real GET, so it needs an integration test " \ "(ActionDispatch::IntegrationTest). In other tests, build the submission with " \ "present_clickwrap + submission_for instead." end get path clickwrap_submission_params_from( response, answers: answers, form_css_selector: form_css_selector ) end |
.clickwrap_submission_params_from(rendered, answers: {}, form_css_selector: nil) ⇒ Hash
For integration tests that POST a form the application really rendered — the signed presentation token is minted per render and bound to the session, so a test cannot fabricate it; it has to read it back out of the page, exactly like a browser:
get new_user_registration_path
post user_registration_path, params: {
user: { email: "person@example.com", password: "..." },
**clickwrap_submission_params_from(response)
}
Every control the page rendered is answered affirmatively by default —
what a person completing the form normally does. Decline or skip one
explicitly with answers::
clickwrap_submission_params_from(response, answers: { product_updates: false })
A page with several independent clickwrap forms must name the exact form. The helper refuses ambiguity rather than mixing one form's token with another form's answers:
clickwrap_submission_params_from(
response,
form_css_selector: "form[action='/withdrawals/confirm']"
)
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
# File 'lib/clickwrap/test_helpers.rb', line 188 def clickwrap_submission_params_from(rendered, answers: {}, form_css_selector: nil) require "nokogiri" html = rendered.respond_to?(:body) ? rendered.body : rendered.to_s page = Nokogiri::HTML(html) scope = clickwrap_test_form_scope(page, form_css_selector) token_fields = scope.css('input[name="clickwrap_submission[presentation_token]"]') if token_fields.many? raise ArgumentError, "The rendered page carries #{token_fields.size} clickwrap presentation tokens. " \ "Pass form_css_selector: with a CSS selector that matches exactly one form." end token = token_fields.first&.[]("value") if token.to_s.empty? raise ArgumentError, "The rendered page carries no clickwrap presentation token. GET the page that " \ "renders `form.clickwrap` (or `form.clickwrap_fields`) first, then pass that " \ "response to clickwrap_submission_params_from." end overrides = answers.transform_keys(&:to_s) controls_by_statement = scope.css(%([name^="clickwrap_submission[answers]["])) .group_by { |control| control["name"][/\[answers\]\[([^\]]+)\]/, 1] } answered = controls_by_statement.each_with_object({}) do |(key, controls), result| next if key.nil? answer = if overrides.key?(key) normalize_rendered_control_answer(controls, overrides.fetch(key)) else default_rendered_control_answer(controls) end result[key] = answer unless answer.nil? end { "clickwrap_submission" => { "presentation_token" => token, "answers" => answered.compact } } end |
.clickwrap_test_form_scope(page, form_css_selector) ⇒ Object
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 |
# File 'lib/clickwrap/test_helpers.rb', line 230 def clickwrap_test_form_scope(page, form_css_selector) return page if form_css_selector.nil? forms = page.css(form_css_selector.to_s) unless forms.one? && forms.first.name == "form" raise ArgumentError, "form_css_selector must match exactly one <form>; it matched #{forms.size}. " \ "Received #{form_css_selector.inspect}." end forms.first rescue Nokogiri::CSS::SyntaxError => error raise ArgumentError, "form_css_selector must be valid CSS. #{error.}" end |
.clickwrap_token_without_message_expiry(manifest) ⇒ Object
616 617 618 619 620 |
# File 'lib/clickwrap/test_helpers.rb', line 616 def (manifest) PresentationManifest.verifier.generate( manifest.to_h, purpose: PresentationManifest::SIGNING_PURPOSE ) end |
.committed_test_receipt(result) ⇒ Object
Transactional test wrappers intentionally never commit. The production
API therefore returns PendingReceipt inside them, correctly, while tests
still need to inspect the rows they just created. This test-only adapter
exposes a Receipt projection without changing the pending object's
committed? answer or weakening the production finality contract.
365 366 367 368 369 |
# File 'lib/clickwrap/test_helpers.rb', line 365 def committed_test_receipt(result) return result unless result.is_a?(PendingReceipt) Receipt.new(result.event) end |
.default_clickwrap_answers(policy_key, answers) ⇒ Object
Required statements get an affirmative answer; optional ones are left untouched. Anything the caller passed wins over both. Explicit beats implicit, and the rule is deliberately all-or-nothing.
answers: {} affirms every required statement, which is what a person
completing the form normally does and what most tests want. But the moment
a test names ANY answer, it gets exactly what it named and nothing else —
because the tests that matter most here are the ones that deliberately
leave a required statement unanswered, and a helper that quietly filled it
in would turn "the server refuses an incomplete submission" into a test
that passes for the wrong reason.
600 601 602 603 604 605 606 |
# File 'lib/clickwrap/test_helpers.rb', line 600 def default_clickwrap_answers(policy_key, answers) return answers.transform_keys(&:to_s) if answers.present? Clickwrap.policy!(policy_key).required_statements.to_h do |statement| [statement.key, statement.choices ? clickwrap_granting_choice(statement) : "1"] end end |
.default_rendered_control_answer(controls) ⇒ Object
A checkbox's affirmative value is conventionally "1". A radio group's value is the exact choice key the server offered — sometimes "yes", but just as legitimately "employee", "contractor", or another domain word. Reading the rendered value keeps this integration helper faithful to the real form instead of fabricating a checkbox answer for every control.
253 254 255 256 |
# File 'lib/clickwrap/test_helpers.rb', line 253 def default_rendered_control_answer(controls) first = controls.first first["type"] == "radio" ? first["value"].to_s : "1" end |
.expired_clickwrap_presentation_for(policy_key) ⇒ Clickwrap::Presenter::Result
A presentation whose manifest has ALREADY EXPIRED.
Built by taking a real presentation and rewriting its issue and expiry
times into the past, then signing it with the manifest verifier directly
rather than through to_token.
That last part is the subtle bit and it is deliberate. to_token passes
the manifest's expires_at to the message verifier, so an
already-expired manifest produces a token the signature layer refuses
before Clickwrap's own expiry check ever runs — and the test would then
be asserting that ActiveSupport::MessageVerifier works. Signing without
the message-level TTL leaves a perfectly valid signature over a manifest
that is plainly out of date, so what rejects it is the server's own
PresentationExpired path. Real check, not a mocked one.
402 403 404 405 406 407 408 409 410 411 412 413 414 415 |
# File 'lib/clickwrap/test_helpers.rb', line 402 def expired_clickwrap_presentation_for(policy_key, **) presentation = present_clickwrap(policy_key, **) window = Clickwrap.config.presentation_valid_for now = Clickwrap.now expired = PresentationManifest.new( presentation.manifest.to_h.merge( "issued_at" => Receipt.format_time(now - (window * 2)), "expires_at" => Receipt.format_time(now - window) ) ) presentation.with(manifest: expired, token: (expired)) end |
.included(base) ⇒ Object
81 82 83 |
# File 'lib/clickwrap/test_helpers.rb', line 81 def self.included(base) base.extend(ClassMethods) end |
.normalize_rendered_control_answer(controls, value) ⇒ Object
259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/clickwrap/test_helpers.rb', line 259 def normalize_rendered_control_answer(controls, value) return normalize_test_answer(value) unless controls.first["type"] == "radio" return nil if value.nil? return controls.first["value"].to_s if value == true if value == false negative = controls.find do |control| control["value"].to_s.in?(%w[no decline false 0]) end return (negative || controls.last)["value"].to_s end value.to_s end |
.normalize_test_answer(value) ⇒ Object
276 277 278 279 280 281 282 283 |
# File 'lib/clickwrap/test_helpers.rb', line 276 def normalize_test_answer(value) case value when true then "1" when false then "0" when nil then nil else value.to_s end end |
.other_actors_clickwrap_token_for(policy_key, actor:, other_actor:) ⇒ String
A token legitimately issued to somebody else, for the swapped-token case.
Submit it while capturing as actor and Capture rejects it with
:presentation_actor_mismatch.
445 446 447 448 449 450 451 452 453 454 |
# File 'lib/clickwrap/test_helpers.rb', line 445 def other_actors_clickwrap_token_for(policy_key, actor:, other_actor:) if Reference.actor(actor) == Reference.actor(other_actor) raise ArgumentError, "other_actors_clickwrap_token_for was given the same actor twice, so the token it " \ "returned would be the actor's own and the mismatch it exists to trigger could " \ "never happen. Pass two different records." end present_clickwrap(policy_key, actor: other_actor).token end |
.present_clickwrap(policy_key, actor: nil, subject: nil, tenant: nil, locale: nil, submit_button_text: "Continue", capture_channel: :web_browser, prospective_actor: nil, registration_flow_id: nil, acting_for: nil) ⇒ Clickwrap::Presenter::Result
Presents a policy server-side and returns the Presenter::Result, whose
token is a real signed presentation token.
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/clickwrap/test_helpers.rb', line 94 def present_clickwrap(policy_key, actor: nil, subject: nil, tenant: nil, locale: nil, submit_button_text: "Continue", capture_channel: :web_browser, prospective_actor: nil, registration_flow_id: nil, acting_for: nil) registration_flow_id ||= SecureRandom.uuid if prospective_actor Presenter.new( policy: Clickwrap.policy!(policy_key), actor: actor, prospective_actor: prospective_actor, registration_flow_id: registration_flow_id, subject: subject, tenant: tenant, acting_for: acting_for, locale: locale, submit_button_text: , capture_channel: capture_channel ).present end |
.stale_clickwrap_token_for(policy_key) ⇒ String
A token bound to a policy revision that is no longer on file — the token a browser holds when the policy was edited and redeployed between the GET and the POST.
The revision digest is replaced with a derived one rather than by
deleting the real PolicyRevision row, so the fixture other tests rely on
survives. Capture rejects it with :stale_policy_revision.
428 429 430 431 432 433 434 435 436 |
# File 'lib/clickwrap/test_helpers.rb', line 428 def stale_clickwrap_token_for(policy_key, **) presentation = present_clickwrap(policy_key, **) attributes = presentation.manifest.to_h superseded = Digest.digest("superseded:#{attributes.dig("policy", "revision")}") PresentationManifest .new(attributes.merge("policy" => attributes["policy"].merge("revision" => superseded))) .to_token end |
.submission_for(presentation_result, answers = {}) ⇒ Clickwrap::Submission
Builds a real Submission from a presentation result.
123 124 125 126 127 128 |
# File 'lib/clickwrap/test_helpers.rb', line 123 def submission_for(presentation_result, answers = {}) Submission.new( presentation_token: presentation_result.token, answers: answers.transform_keys(&:to_s) ) end |
.submit_clickwrap(policy_key, actor:, answers: {}, subject: nil, tenant: nil, locale: nil, capture_channel: :web_browser, http_request: nil, acting_for: nil) ⇒ Clickwrap::Receipt
Presents, answers, and captures — the everyday way to get real evidence into a test database.
An empty answers: affirms every required statement and leaves every
optional one alone, which is what a person completing the form normally
does. Optional controls are never auto-answered: leaving one unselected
creates no grant, and a helper that quietly granted an optional consent
would hide exactly the bug that distinction exists to catch.
A different verb from the CONTROLLER helper because it is a different
act. This one SUBMITS a presentation it just built — it is a test factory
verb, like create_user, and a factory that cannot deliver raises, since
in a test a failed capture is a failed test.
Clickwrap::ControllerHelpers#capture_clickwrap CAPTURES a submission a
person actually sent, follows save/save!, and absorbs refusals,
because in a controller a refused submission is a person to answer rather
than a bug.
They used to share the name capture_clickwrap and contradict each other
about what "no" means. Both modules can end up on one object, and a
helper whose failure mode depends on which module won is not a helper.
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 |
# File 'lib/clickwrap/test_helpers.rb', line 310 def submit_clickwrap(policy_key, actor:, answers: {}, subject: nil, tenant: nil, locale: nil, capture_channel: :web_browser, http_request: nil, acting_for: nil) presentation = present_clickwrap(policy_key, actor: actor, subject: subject, tenant: tenant, locale: locale, acting_for: acting_for, capture_channel: capture_channel) result = Clickwrap.capture!( policy_key, actor: actor, subject: subject, tenant: tenant, acting_for: acting_for, http_request: http_request, capture_channel: capture_channel, submission: submission_for(presentation, default_clickwrap_answers(policy_key, answers)) ) committed_test_receipt(result) end |
.submit_clickwrap_and(policy_key, actor:, answers: {}, subject: nil, tenant: nil, capture_channel: :web_browser, http_request: nil, acting_for: nil) ⇒ Clickwrap::Receipt
The same, with a protected action in the same transaction. Use it to prove that your domain write and its evidence commit together.
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 |
# File 'lib/clickwrap/test_helpers.rb', line 337 def submit_clickwrap_and(policy_key, actor:, answers: {}, subject: nil, tenant: nil, capture_channel: :web_browser, http_request: nil, acting_for: nil, &) presentation = present_clickwrap(policy_key, actor: actor, subject: subject, tenant: tenant, acting_for: acting_for, capture_channel: capture_channel) result = Clickwrap.capture_and!( policy_key, actor: actor, subject: subject, tenant: tenant, acting_for: acting_for, http_request: http_request, capture_channel: capture_channel, submission: submission_for(presentation, default_clickwrap_answers(policy_key, answers)), & ) committed_test_receipt(result) end |
.submit_clickwrap_twice(policy_key, actor:, answers: {}, subject: nil, tenant: nil) ⇒ Array<Clickwrap::Receipt>
Submits the SAME presentation twice, which is what a double-click, a retried request, and a replayed token all look like to the server.
Returns both results. An identical repeat returns the original receipt
without running anything twice, so the two share an event_id; a repeat
with different answers raises Clickwrap::ReplayRejected.
466 467 468 469 470 471 472 473 474 |
# File 'lib/clickwrap/test_helpers.rb', line 466 def submit_clickwrap_twice(policy_key, actor:, answers: {}, subject: nil, tenant: nil) presentation = present_clickwrap(policy_key, actor: actor, subject: subject, tenant: tenant) submission = submission_for(presentation, default_clickwrap_answers(policy_key, answers)) = { actor: actor, subject: subject, tenant: tenant, capture_channel: "web_browser", submission: submission } [Clickwrap.capture!(policy_key, **), Clickwrap.capture!(policy_key, **)] end |
Instance Method Details
#assert_clickwrap_acknowledged(statement_key, actor:, subject: nil, tenant: nil) ⇒ Object
528 529 530 |
# File 'lib/clickwrap/test_helpers.rb', line 528 def assert_clickwrap_acknowledged(statement_key, actor:, subject: nil, tenant: nil) assert_clickwrap_kind("acknowledgment", :acknowledged?, statement_key, actor, subject, tenant) end |
#assert_clickwrap_agreed_to(statement_key, actor:, subject: nil, tenant: nil) ⇒ Object
524 525 526 |
# File 'lib/clickwrap/test_helpers.rb', line 524 def assert_clickwrap_agreed_to(statement_key, actor:, subject: nil, tenant: nil) assert_clickwrap_kind("agreement", :agreed_to?, statement_key, actor, subject, tenant) end |
#assert_clickwrap_authorized(statement_key, actor:, subject: nil, tenant: nil) ⇒ Object
540 541 542 |
# File 'lib/clickwrap/test_helpers.rb', line 540 def (statement_key, actor:, subject: nil, tenant: nil) assert_clickwrap_kind("authorization", :authorized?, statement_key, actor, subject, tenant) end |
#assert_clickwrap_consented_to(purpose_key, actor:, subject: nil, tenant: nil) ⇒ Object
532 533 534 |
# File 'lib/clickwrap/test_helpers.rb', line 532 def (purpose_key, actor:, subject: nil, tenant: nil) assert_clickwrap_kind("consent", :consented_to?, purpose_key, actor, subject, tenant) end |
#assert_clickwrap_current(policy_key, actor:, subject: nil, tenant: nil) ⇒ Object
Assert that an actor currently satisfies a whole policy.
505 506 507 508 509 510 511 512 |
# File 'lib/clickwrap/test_helpers.rb', line 505 def assert_clickwrap_current(policy_key, actor:, subject: nil, tenant: nil) result = Clickwrap.verify(policy_key, actor: actor, subject: subject, tenant: tenant) assert result.success?, "Expected #{clickwrap_actor_label(actor)} to currently satisfy the #{policy_key} " \ "policy, but verification failed with #{result.error.inspect}: #{result.}. " \ "#{clickwrap_state_summary(actor, policy_key)}" end |
#assert_clickwrap_declared(statement_key, actor:, subject: nil, tenant: nil) ⇒ Object
536 537 538 |
# File 'lib/clickwrap/test_helpers.rb', line 536 def assert_clickwrap_declared(statement_key, actor:, subject: nil, tenant: nil) assert_clickwrap_kind("declaration", :declared?, statement_key, actor, subject, tenant) end |
#assert_clickwrap_exempted_from(policy_key, actor:, subject: nil, tenant: nil) ⇒ Object
Assert that an actor is exempted from a policy — a deliberately separate
question from having agreed to it. An exemption records that no human
action occurred, so it can never satisfy assert_clickwrap_agreed_to,
and a test that expects it to has found a real bug rather than a helper
limitation.
549 550 551 552 553 554 555 556 557 |
# File 'lib/clickwrap/test_helpers.rb', line 549 def assert_clickwrap_exempted_from(policy_key, actor:, subject: nil, tenant: nil) proxy = ActorProxy.new(actor) assert proxy.exempted_from?(policy_key, subject: subject, tenant: tenant), "Expected #{clickwrap_actor_label(actor)} to be exempted from #{policy_key}, but no " \ "exemption event was recorded for them. An exemption is created explicitly with " \ "Clickwrap.exempt!(#{policy_key.to_sym.inspect}, actor:, because:), and the policy " \ "must permit one. #{clickwrap_state_summary(actor, policy_key)}" end |
#assert_clickwrap_receipt_verifies(receipt) ⇒ Object
Assert that a receipt still verifies: its digest covers its bytes, its document versions still match what was published, and the event has not been disposed of.
562 563 564 565 566 567 568 569 570 |
# File 'lib/clickwrap/test_helpers.rb', line 562 def assert_clickwrap_receipt_verifies(receipt) result = receipt.verify assert result.success?, "Expected receipt #{receipt.event_id} to verify, but it failed with " \ "#{result.error.inspect}: #{result.}. A digest failure means the event row's " \ "meaningful bytes changed after it was written; a document failure means a " \ "published version was edited in place instead of republished under a new label." end |
#assert_no_clickwrap_event(policy_key, actor: nil) ⇒ Object
Assert that no event exists for a policy — the other half of a fault injection test, where the point is that nothing was recorded.
574 575 576 577 578 579 580 581 582 583 584 585 |
# File 'lib/clickwrap/test_helpers.rb', line 574 def assert_no_clickwrap_event(policy_key, actor: nil) scope = Event.for_policy(policy_key) scope = scope.for_actor(Reference.actor(actor)) if actor found = scope.chronological.to_a listed = found.join("; ") whose = actor ? " and #{clickwrap_actor_label(actor)}" : "" assert found.empty?, "Expected no Clickwrap event for #{policy_key}#{whose}, but found " \ "#{found.length}: #{listed}. If this followed a fault injection block, the evidence " \ "write was not rolled back with the action it was supposed to commit alongside." end |
#clickwrap_params_from(path, answers: {}, form_css_selector: nil) ⇒ Object
The one-line form of the pattern below, for integration tests: GET the page that renders a clickwrap presentation, read the signed token and controls back out of it, return the params for the POST.
post user_registration_path, params: {
user: { ... },
**clickwrap_params_from(new_user_registration_path)
}
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/clickwrap/test_helpers.rb', line 140 def clickwrap_params_from(path, answers: {}, form_css_selector: nil) unless respond_to?(:get) raise ArgumentError, "clickwrap_params_from drives a real GET, so it needs an integration test " \ "(ActionDispatch::IntegrationTest). In other tests, build the submission with " \ "present_clickwrap + submission_for instead." end get path clickwrap_submission_params_from( response, answers: answers, form_css_selector: form_css_selector ) end |
#clickwrap_submission_params_from(rendered, answers: {}, form_css_selector: nil) ⇒ Hash
For integration tests that POST a form the application really rendered — the signed presentation token is minted per render and bound to the session, so a test cannot fabricate it; it has to read it back out of the page, exactly like a browser:
get new_user_registration_path
post user_registration_path, params: {
user: { email: "person@example.com", password: "..." },
**clickwrap_submission_params_from(response)
}
Every control the page rendered is answered affirmatively by default —
what a person completing the form normally does. Decline or skip one
explicitly with answers::
clickwrap_submission_params_from(response, answers: { product_updates: false })
A page with several independent clickwrap forms must name the exact form. The helper refuses ambiguity rather than mixing one form's token with another form's answers:
clickwrap_submission_params_from(
response,
form_css_selector: "form[action='/withdrawals/confirm']"
)
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
# File 'lib/clickwrap/test_helpers.rb', line 188 def clickwrap_submission_params_from(rendered, answers: {}, form_css_selector: nil) require "nokogiri" html = rendered.respond_to?(:body) ? rendered.body : rendered.to_s page = Nokogiri::HTML(html) scope = clickwrap_test_form_scope(page, form_css_selector) token_fields = scope.css('input[name="clickwrap_submission[presentation_token]"]') if token_fields.many? raise ArgumentError, "The rendered page carries #{token_fields.size} clickwrap presentation tokens. " \ "Pass form_css_selector: with a CSS selector that matches exactly one form." end token = token_fields.first&.[]("value") if token.to_s.empty? raise ArgumentError, "The rendered page carries no clickwrap presentation token. GET the page that " \ "renders `form.clickwrap` (or `form.clickwrap_fields`) first, then pass that " \ "response to clickwrap_submission_params_from." end overrides = answers.transform_keys(&:to_s) controls_by_statement = scope.css(%([name^="clickwrap_submission[answers]["])) .group_by { |control| control["name"][/\[answers\]\[([^\]]+)\]/, 1] } answered = controls_by_statement.each_with_object({}) do |(key, controls), result| next if key.nil? answer = if overrides.key?(key) normalize_rendered_control_answer(controls, overrides.fetch(key)) else default_rendered_control_answer(controls) end result[key] = answer unless answer.nil? end { "clickwrap_submission" => { "presentation_token" => token, "answers" => answered.compact } } end |
#complete_clickwrap(policy_key) ⇒ Object
Checks every required control the policy declares, on the page currently rendered, and leaves optional ones alone.
complete_clickwrap :signup
"Create account"
Controls are found by their form-field name rather than by an id, so this keeps working after you eject the views and restyle everything: the name is part of the submission contract, the markup around it is yours.
489 490 491 492 493 494 495 496 497 498 499 500 |
# File 'lib/clickwrap/test_helpers.rb', line 489 def complete_clickwrap(policy_key) unless respond_to?(:page) raise NoMethodError, "complete_clickwrap drives a rendered page and needs Capybara, so it works in a " \ "system test. In a model or integration test use submit_clickwrap, which goes " \ "through the same presenter and capture path without a browser." end Clickwrap.policy!(policy_key).statements.reject(&:optional?).each do |statement| complete_clickwrap_statement(statement) end end |
#expired_clickwrap_presentation_for(policy_key) ⇒ Clickwrap::Presenter::Result
A presentation whose manifest has ALREADY EXPIRED.
Built by taking a real presentation and rewriting its issue and expiry
times into the past, then signing it with the manifest verifier directly
rather than through to_token.
That last part is the subtle bit and it is deliberate. to_token passes
the manifest's expires_at to the message verifier, so an
already-expired manifest produces a token the signature layer refuses
before Clickwrap's own expiry check ever runs — and the test would then
be asserting that ActiveSupport::MessageVerifier works. Signing without
the message-level TTL leaves a perfectly valid signature over a manifest
that is plainly out of date, so what rejects it is the server's own
PresentationExpired path. Real check, not a mocked one.
402 403 404 405 406 407 408 409 410 411 412 413 414 415 |
# File 'lib/clickwrap/test_helpers.rb', line 402 def expired_clickwrap_presentation_for(policy_key, **) presentation = present_clickwrap(policy_key, **) window = Clickwrap.config.presentation_valid_for now = Clickwrap.now expired = PresentationManifest.new( presentation.manifest.to_h.merge( "issued_at" => Receipt.format_time(now - (window * 2)), "expires_at" => Receipt.format_time(now - window) ) ) presentation.with(manifest: expired, token: (expired)) end |
#other_actors_clickwrap_token_for(policy_key, actor:, other_actor:) ⇒ String
A token legitimately issued to somebody else, for the swapped-token case.
Submit it while capturing as actor and Capture rejects it with
:presentation_actor_mismatch.
445 446 447 448 449 450 451 452 453 454 |
# File 'lib/clickwrap/test_helpers.rb', line 445 def other_actors_clickwrap_token_for(policy_key, actor:, other_actor:) if Reference.actor(actor) == Reference.actor(other_actor) raise ArgumentError, "other_actors_clickwrap_token_for was given the same actor twice, so the token it " \ "returned would be the actor's own and the mismatch it exists to trigger could " \ "never happen. Pass two different records." end present_clickwrap(policy_key, actor: other_actor).token end |
#present_clickwrap(policy_key, actor: nil, subject: nil, tenant: nil, locale: nil, submit_button_text: "Continue", capture_channel: :web_browser, prospective_actor: nil, registration_flow_id: nil, acting_for: nil) ⇒ Clickwrap::Presenter::Result
Presents a policy server-side and returns the Presenter::Result, whose
token is a real signed presentation token.
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/clickwrap/test_helpers.rb', line 94 def present_clickwrap(policy_key, actor: nil, subject: nil, tenant: nil, locale: nil, submit_button_text: "Continue", capture_channel: :web_browser, prospective_actor: nil, registration_flow_id: nil, acting_for: nil) registration_flow_id ||= SecureRandom.uuid if prospective_actor Presenter.new( policy: Clickwrap.policy!(policy_key), actor: actor, prospective_actor: prospective_actor, registration_flow_id: registration_flow_id, subject: subject, tenant: tenant, acting_for: acting_for, locale: locale, submit_button_text: , capture_channel: capture_channel ).present end |
#refute_clickwrap_current(policy_key, actor:, subject: nil, tenant: nil) ⇒ Object
Assert that an actor does NOT currently satisfy a policy.
515 516 517 518 519 520 521 522 |
# File 'lib/clickwrap/test_helpers.rb', line 515 def refute_clickwrap_current(policy_key, actor:, subject: nil, tenant: nil) result = Clickwrap.verify(policy_key, actor: actor, subject: subject, tenant: tenant) refute result.success?, "Expected #{clickwrap_actor_label(actor)} NOT to satisfy the #{policy_key} policy, " \ "but verification succeeded against event #{result.event_id}. " \ "#{clickwrap_state_summary(actor, policy_key)}" end |
#stale_clickwrap_token_for(policy_key) ⇒ String
A token bound to a policy revision that is no longer on file — the token a browser holds when the policy was edited and redeployed between the GET and the POST.
The revision digest is replaced with a derived one rather than by
deleting the real PolicyRevision row, so the fixture other tests rely on
survives. Capture rejects it with :stale_policy_revision.
428 429 430 431 432 433 434 435 436 |
# File 'lib/clickwrap/test_helpers.rb', line 428 def stale_clickwrap_token_for(policy_key, **) presentation = present_clickwrap(policy_key, **) attributes = presentation.manifest.to_h superseded = Digest.digest("superseded:#{attributes.dig("policy", "revision")}") PresentationManifest .new(attributes.merge("policy" => attributes["policy"].merge("revision" => superseded))) .to_token end |
#submission_for(presentation_result, answers = {}) ⇒ Clickwrap::Submission
Builds a real Submission from a presentation result.
123 124 125 126 127 128 |
# File 'lib/clickwrap/test_helpers.rb', line 123 def submission_for(presentation_result, answers = {}) Submission.new( presentation_token: presentation_result.token, answers: answers.transform_keys(&:to_s) ) end |
#submit_clickwrap(policy_key, actor:, answers: {}, subject: nil, tenant: nil, locale: nil, capture_channel: :web_browser, http_request: nil, acting_for: nil) ⇒ Clickwrap::Receipt
Presents, answers, and captures — the everyday way to get real evidence into a test database.
An empty answers: affirms every required statement and leaves every
optional one alone, which is what a person completing the form normally
does. Optional controls are never auto-answered: leaving one unselected
creates no grant, and a helper that quietly granted an optional consent
would hide exactly the bug that distinction exists to catch.
A different verb from the CONTROLLER helper because it is a different
act. This one SUBMITS a presentation it just built — it is a test factory
verb, like create_user, and a factory that cannot deliver raises, since
in a test a failed capture is a failed test.
Clickwrap::ControllerHelpers#capture_clickwrap CAPTURES a submission a
person actually sent, follows save/save!, and absorbs refusals,
because in a controller a refused submission is a person to answer rather
than a bug.
They used to share the name capture_clickwrap and contradict each other
about what "no" means. Both modules can end up on one object, and a
helper whose failure mode depends on which module won is not a helper.
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 |
# File 'lib/clickwrap/test_helpers.rb', line 310 def submit_clickwrap(policy_key, actor:, answers: {}, subject: nil, tenant: nil, locale: nil, capture_channel: :web_browser, http_request: nil, acting_for: nil) presentation = present_clickwrap(policy_key, actor: actor, subject: subject, tenant: tenant, locale: locale, acting_for: acting_for, capture_channel: capture_channel) result = Clickwrap.capture!( policy_key, actor: actor, subject: subject, tenant: tenant, acting_for: acting_for, http_request: http_request, capture_channel: capture_channel, submission: submission_for(presentation, default_clickwrap_answers(policy_key, answers)) ) committed_test_receipt(result) end |
#submit_clickwrap_and(policy_key, actor:, answers: {}, subject: nil, tenant: nil, capture_channel: :web_browser, http_request: nil, acting_for: nil) ⇒ Clickwrap::Receipt
The same, with a protected action in the same transaction. Use it to prove that your domain write and its evidence commit together.
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 |
# File 'lib/clickwrap/test_helpers.rb', line 337 def submit_clickwrap_and(policy_key, actor:, answers: {}, subject: nil, tenant: nil, capture_channel: :web_browser, http_request: nil, acting_for: nil, &) presentation = present_clickwrap(policy_key, actor: actor, subject: subject, tenant: tenant, acting_for: acting_for, capture_channel: capture_channel) result = Clickwrap.capture_and!( policy_key, actor: actor, subject: subject, tenant: tenant, acting_for: acting_for, http_request: http_request, capture_channel: capture_channel, submission: submission_for(presentation, default_clickwrap_answers(policy_key, answers)), & ) committed_test_receipt(result) end |
#submit_clickwrap_twice(policy_key, actor:, answers: {}, subject: nil, tenant: nil) ⇒ Array<Clickwrap::Receipt>
Submits the SAME presentation twice, which is what a double-click, a retried request, and a replayed token all look like to the server.
Returns both results. An identical repeat returns the original receipt
without running anything twice, so the two share an event_id; a repeat
with different answers raises Clickwrap::ReplayRejected.
466 467 468 469 470 471 472 473 474 |
# File 'lib/clickwrap/test_helpers.rb', line 466 def submit_clickwrap_twice(policy_key, actor:, answers: {}, subject: nil, tenant: nil) presentation = present_clickwrap(policy_key, actor: actor, subject: subject, tenant: tenant) submission = submission_for(presentation, default_clickwrap_answers(policy_key, answers)) = { actor: actor, subject: subject, tenant: tenant, capture_channel: "web_browser", submission: submission } [Clickwrap.capture!(policy_key, **), Clickwrap.capture!(policy_key, **)] end |