Ensure `response.parsed_body` support for pattern matching

Both `Nokogiri` and `Minitest` have merged the PRs mentioned to
integrate support for Ruby's Pattern matching
(https://github.com/sparklemotion/nokogiri/pull/2523 and
https://github.com/minitest/minitest/pull/936, respectively).

This commit adds coverage for those new assertions, and incorporates
examples into the documentation for the `response.parsed_body` method.

In order to incorporate pattern-matching support for JSON responses,
this commit changes the response parser to call `JSON.parse` with
[object_class: ActiveSupport::HashWithIndifferentAccess][object_class],
since String instances for `Hash` keys are incompatible with Ruby's
syntactically pattern matching.

For example:

```ruby
irb(main):001:0> json = {"key" => "value"}
=> {"key"=>"value"}
irb(main):002:0> json in {key: /value/}
=> false

irb(main):001:0> json = {"key" => "value"}
=> {"key"=>"value"}
irb(main):002:0> json in {"key" => /value/}
.../3.2.0/lib/ruby/gems/3.2.0/gems/irb-1.7.4/lib/irb/workspace.rb:113:in `eval': (irb):2: syntax error, unexpected terminator, expecting literal content or tSTRING_DBEG or tSTRING_DVAR or tLABEL_END (SyntaxError)
json in {"key" => /value/}
             ^

        .../ruby/3.2.0/lib/ruby/gems/3.2.0/gems/irb-1.7.4/exe/irb:9:in `<top (required)>'
        .../ruby/3.2.0/bin/irb:25:in `load'
        .../ruby/3.2.0/bin/irb:25:in `<main>'
```

When the Hash maps String keys to Symbol keys, it's able to be pattern
matched:

```ruby
irb(main):005:0> json = {"key" => "value"}.with_indifferent_access
=> {"key"=>"value"}
irb(main):006:0> json in {key: /value/}
=> true
```

[object_class]: https://docs.ruby-lang.org/en/3.2/JSON.html#module-JSON-label-Parsing+Options
This commit is contained in:
Sean Doyle 2023-08-22 15:46:38 -04:00
parent ed5af00459
commit 0f4ab82082
6 changed files with 84 additions and 8 deletions

View File

@ -1,3 +1,11 @@
* Parse JSON `response.parsed_body` with `ActiveSupport::HashWithIndifferentAccess`
Integrate with Minitest's new `assert_pattern` by parsing the JSON contents
of `response.parsed_body` with `ActiveSupport::HashWithIndifferentAccess`, so
that it's pattern-matching compatible.
*Sean Doyle*
* Add support for Playwright as a driver for system tests.
*Yuki Nishijima*

View File

@ -53,6 +53,6 @@ module ActionDispatch
end
register_encoder :html, response_parser: -> body { Rails::Dom::Testing.html_document.parse(body) }
register_encoder :json, response_parser: -> body { JSON.parse(body) }
register_encoder :json, response_parser: -> body { JSON.parse(body, object_class: ActiveSupport::HashWithIndifferentAccess) }
end
end

View File

@ -23,15 +23,29 @@ module ActionDispatch
# response.parsed_body.class # => Nokogiri::HTML5::Document
# response.parsed_body.to_html # => "<!DOCTYPE html>\n<html>\n..."
#
# assert_pattern { response.parsed_body.at("main") => { content: "Hello, world" } }
#
# response.parsed_body.at("main") => {name:, content:}
# assert_equal "main", name
# assert_equal "Some main content", content
#
# get "/posts.json"
# response.content_type # => "application/json; charset=utf-8"
# response.parsed_body.class # => Array
# response.parsed_body # => [{"id"=>42, "title"=>"Title"},...
#
# assert_pattern { response.parsed_body => [{ id: 42 }] }
#
# get "/posts/42.json"
# response.content_type # => "application/json; charset=utf-8"
# response.parsed_body.class # => Hash
# response.parsed_body.class # => ActiveSupport::HashWithIndifferentAccess
# response.parsed_body # => {"id"=>42, "title"=>"Title"}
#
# assert_pattern { response.parsed_body => [{ title: /title/i }] }
#
# response.parsed_body => {id:, title:}
# assert_equal 42, id
# assert_equal "Title", title
def parsed_body
@parsed_body ||= response_parser.call(body)
end

View File

@ -25,6 +25,7 @@ class TestResponseTest < ActiveSupport::TestCase
assert_equal response.body, response.parsed_body
response = ActionDispatch::TestResponse.create(200, { "Content-Type" => "application/json" }, '{ "foo": "fighters" }')
assert_kind_of ActiveSupport::HashWithIndifferentAccess, response.parsed_body
assert_equal({ "foo" => "fighters" }, response.parsed_body)
response = ActionDispatch::TestResponse.create(200, { "Content-Type" => "text/html" }, <<~HTML)
@ -38,4 +39,10 @@ class TestResponseTest < ActiveSupport::TestCase
assert_kind_of(Nokogiri::XML::Document, response.parsed_body)
assert_equal(response.parsed_body.at_xpath("/html/body/div").text, "Content")
end
if RUBY_VERSION >= "3.1"
require_relative "./test_response_test/pattern_matching_test_cases"
include PatternMatchingTestCases
end
end

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
module TestResponseTest::PatternMatchingTestCases
extend ActiveSupport::Concern
included do
test "JSON response Hash pattern matching" do
response = ActionDispatch::TestResponse.create(200, { "Content-Type" => "application/json" }, '{ "foo": "fighters" }')
# rubocop:disable Lint/Syntax
assert_pattern { response.parsed_body => { foo: /fighter/ } }
# rubocop:enable Lint/Syntax
end
test "JSON response Array pattern matching" do
response = ActionDispatch::TestResponse.create(200, { "Content-Type" => "application/json" }, '[{ "foo": "fighters" }, { "nir": "vana" }]')
# rubocop:disable Lint/Syntax
assert_pattern { response.parsed_body => [{ foo: /fighter/ }, { nir: /vana/ }] }
# rubocop:enable Lint/Syntax
end
test "HTML response pattern matching" do
response = ActionDispatch::TestResponse.create(200, { "Content-Type" => "text/html" }, <<~HTML)
<html>
<head></head>
<body>
<main><h1>Some main content</h1></main>
</body>
</html>
HTML
html = response.parsed_body
# rubocop:disable Lint/Syntax
html.at("main") => {name:, content:}
# rubocop:enable Lint/Syntax
assert_equal "main", name
assert_equal "Some main content", content
# rubocop:disable Lint/Syntax
assert_pattern { html.at("main") => { content: "Some main content" } }
assert_pattern { html.at("main") => { content: /content/ } }
assert_pattern { html.at("main") => { children: [{ name: "h1", content: /content/ }] } }
# rubocop:enable Lint/Syntax
end
end
end

View File

@ -138,11 +138,11 @@ class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::Integrati
test "creating new direct upload" do
checksum = OpenSSL::Digest::MD5.base64digest("Hello")
metadata = {
"foo": "bar",
"my_key_1": "my_value_1",
"my_key_2": "my_value_2",
"platform": "my_platform",
"library_ID": "12345"
"foo" => "bar",
"my_key_1" => "my_value_1",
"my_key_2" => "my_value_2",
"platform" => "my_platform",
"library_ID" => "12345"
}
post rails_direct_uploads_url, params: { blob: {
@ -153,7 +153,7 @@ class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::Integrati
assert_equal "hello.txt", details["filename"]
assert_equal 6, details["byte_size"]
assert_equal checksum, details["checksum"]
assert_equal metadata, details["metadata"].deep_transform_keys(&:to_sym)
assert_equal metadata, details["metadata"]
assert_equal "text/plain", details["content_type"]
assert_match(/rails\/active_storage\/disk/, details["direct_upload"]["url"])
assert_equal({ "Content-Type" => "text/plain" }, details["direct_upload"]["headers"])