From 46eb4a47eb41686a2d759a8d65a04b3108845066 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Thu, 6 Feb 2025 22:57:07 -0500 Subject: [PATCH] HttpMock: Accept block arguments Extend the `ActiveResource::HttpMock` declaration DSL to accept a block argument. When passed a block, the mock will yield an `ActiveResource::Request` instance to the block it handles a matching request. ```ruby def setup @matz = { person: { id: 1, name: "Matz" } } ActiveResource::HttpMock.respond_to do |mock| mock.get "/people.json", omit_query_params: true do |request| if request.path.split("?").includes?("name=Matz") { people: [ @matz ] }.to_json else { people: [] }.to_json end end end end def test_get_matz people = Person.where(name: "Matz") assert_equal [ "Matz" ], people.map(&:name) end ``` When a block is passed to the mock, it ignores the `body`, `status`, and `response_headers` arguments. --- lib/active_resource/http_mock.rb | 52 ++++++++++++++++++--- test/cases/http_mock_test.rb | 77 ++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 7 deletions(-) diff --git a/lib/active_resource/http_mock.rb b/lib/active_resource/http_mock.rb index 4e1c32c697..ed5f53caf6 100644 --- a/lib/active_resource/http_mock.rb +++ b/lib/active_resource/http_mock.rb @@ -51,6 +51,29 @@ class InvalidRequestError < StandardError; end # :nodoc: # assert_equal "Matz", person.name # end # + # Each method can also accept a block. The mock will yield an + # ActiveResource::Request instance to the block it handles a matching request. + # + # def setup + # @matz = { person: { id: 1, name: "Matz" } } + # + # ActiveResource::HttpMock.respond_to do |mock| + # mock.get "/people.json", omit_query_params: true do |request| + # if request.path.split("?").includes?("name=Matz") + # { people: [ @matz ] }.to_json + # else + # { people: [] }.to_json + # end + # end + # end + # end + # + # def test_get_matz + # people = Person.where(name: "Matz") + # assert_equal [ "Matz" ], people.map(&:name) + # end + # + # When a block is passed to the mock, it ignores the +body+, +status+, and +response_headers+ arguments. class HttpMock class Responder # :nodoc: def initialize(responses) @@ -62,9 +85,10 @@ def initialize(responses) # @responses[Request.new(:post, path, nil, request_headers, options)] = Response.new(body || "", status, response_headers) # end module_eval <<-EOE, __FILE__, __LINE__ + 1 - def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {}, options = {}) + def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {}, options = {}, &response) + options = body if response request = Request.new(:#{method}, path, nil, request_headers, options) - response = Response.new(body || "", status, response_headers) + response = Response.new(body || "", status, response_headers) unless response delete_duplicate_responses(request) @@ -154,12 +178,12 @@ def responses # === Example # # ActiveResource::HttpMock.respond_to do |mock| - # mock.send(:get, "/people/1", {}, "JSON1") + # mock.get("/people/1", {}, "JSON1") # end # ActiveResource::HttpMock.responses.length #=> 1 # # ActiveResource::HttpMock.respond_to(false) do |mock| - # mock.send(:get, "/people/2", {}, "JSON2") + # mock.get("/people/2", {}, "JSON2") # end # ActiveResource::HttpMock.responses.length #=> 2 # @@ -169,7 +193,7 @@ def responses # === Example # # ActiveResource::HttpMock.respond_to do |mock| - # mock.send(:get, "/people/1", {}, "JSON1") + # mock.get("/people/1", {}, "JSON1") # end # ActiveResource::HttpMock.responses.length #=> 1 # @@ -248,7 +272,10 @@ def net_connection_disabled? # request = ActiveResource::Request.new(:post, path, body, headers, options) # self.class.requests << request # if response = self.class.responses.assoc(request) - # response[1] + # response = response[1] + # response = response.call(request) if response.respond_to?(:call) + # + # Response.wrap(response) # else # raise InvalidRequestError.new("Could not find a response recorded for #{request.to_s} - Responses recorded are: - #{inspect_responses}") # end @@ -258,7 +285,10 @@ def #{method}(path, #{'body, ' if has_body}headers, options = {}) request = ActiveResource::Request.new(:#{method}, path, #{has_body ? 'body, ' : 'nil, '}headers, options) self.class.requests << request if response = self.class.responses.assoc(request) - response[1] + response = response[1] + response = response.call(request) if response.respond_to?(:call) + + Response.wrap(response) else raise InvalidRequestError.new("Could not find a response recorded for \#{request.to_s} - Responses recorded are: \#{inspect_responses}") end @@ -321,6 +351,14 @@ def headers_match?(req) class Response attr_accessor :body, :message, :code, :headers + def self.wrap(response) # :nodoc: + case response + when self then response + when String then new(response) + else new(nil) + end + end + def initialize(body, message = 200, headers = {}) @body, @message, @headers = body, message.to_s, headers @code = @message[0, 3].to_i diff --git a/test/cases/http_mock_test.rb b/test/cases/http_mock_test.rb index 75b1a468b7..3e9de19a45 100644 --- a/test/cases/http_mock_test.rb +++ b/test/cases/http_mock_test.rb @@ -10,6 +10,30 @@ class HttpMockTest < ActiveSupport::TestCase FORMAT_HEADER = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES + test "Response.wrap returns the same Response instance" do + response = ActiveResource::Response.new("hello") + + assert_same response, ActiveResource::Response.wrap(response) + end + + test "Response.wrap returns a Response instance from a String" do + response = ActiveResource::Response.wrap("hello") + + assert_equal 200, response.code + assert_equal "hello", response.body + assert_equal "hello".size, response["Content-Length"].to_i + end + + test "Response.wrap returns a Response instance from other values" do + [ nil, Object.new ].each do |value| + response = ActiveResource::Response.wrap(value) + + assert_equal 200, response.code + assert_nil response.body + assert_equal 0, response["Content-Length"].to_i + end + end + [ :post, :patch, :put, :get, :delete, :head ].each do |method| test "responds to simple #{method} request" do ActiveResource::HttpMock.respond_to do |mock| @@ -72,6 +96,38 @@ class HttpMockTest < ActiveSupport::TestCase request(method, "/people/1", FORMAT_HEADER[method] => "application/xml") end end + + test "responds to #{method} request with a block that returns a String" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(method, "/people/1", { FORMAT_HEADER[method] => "application/json" }) do + "Response" + end + end + + assert_equal "Response", request(method, "/people/1", { FORMAT_HEADER[method] => "application/json" }, "Request").body + end + + test "responds to #{method} request with a block that returns an ActiveResource::Response" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(method, "/people/1", { FORMAT_HEADER[method] => "application/json" }) do + ActiveResource::Response.new("Response") + end + end + + assert_equal "Response", request(method, "/people/1", { FORMAT_HEADER[method] => "application/json" }, "Request").body + end + + test "yields request to the #{method} mock block" do + ActiveResource::HttpMock.respond_to do |mock| + mock.send(method, "/people/1") do |request| + assert_kind_of ActiveResource::Request, request + + ActiveResource::Response.new("Response") + end + end + + assert_equal "Response", request(method, "/people/1").body + end end test "allows you to send in pairs directly to the respond_to method" do @@ -154,6 +210,27 @@ class HttpMockTest < ActiveSupport::TestCase assert_equal 1, ActiveResource::HttpMock.responses.length end + test "can ignore query params when yielding get request to the block" do + ActiveResource::HttpMock.respond_to do |mock| + mock.get "/people/1", {}, omit_query_in_path: true do |request| + assert_kind_of ActiveResource::Request, request + + ActiveResource::Response.new(request.path) + end + end + + assert_equal "/people/1?key=value", request(:get, "/people/1?key=value").body + end + + test "can map a request to a block" do + request = ActiveResource::Request.new(:get, "/people/1", nil, {}, omit_query_in_path: true) + response = ->(req) { ActiveResource::Response.new(req.path) } + + ActiveResource::HttpMock.respond_to(request => response) + + assert_equal "/people/1?key=value", request(:get, "/people/1?key=value").body + end + test "allows you to replace the existing response with the same request by passing pairs" do ActiveResource::HttpMock.respond_to do |mock| mock.send(:get, "/people/1", {}, "JSON1")