require 'vcr' require 'multi_json' module VCRHelpers def normalize_cassette_hash(cassette_hash) cassette_hash['recorded_with'] = "VCR #{VCR.version}" cassette_hash['http_interactions'].map! { |h| normalize_http_interaction(h) } cassette_hash end def normalize_headers(object) object.headers = {} and return if object.headers.nil? object.headers = {}.tap do |hash| object.headers.each do |key, value| hash[key.downcase] = value end end end def static_timestamp @static_timestamp ||= Time.now end def normalize_http_interaction(hash) VCR::HTTPInteraction.from_hash(hash).tap do |i| normalize_headers(i.request) normalize_headers(i.response) i.recorded_at &&= static_timestamp i.request.body ||= '' i.response.body ||= '' i.response.status.message ||= '' i.response.adapter_metadata.clear # Remove non-deterministic headers and headers # that get added by a particular HTTP library (but not by others) i.response.headers.reject! { |k, v| %w[ server date connection ].include?(k) } i.request.headers.reject! { |k, v| %w[ accept user-agent connection expect date ].include?(k) } # Some HTTP libraries include an extra space ("OK " instead of "OK") i.response.status.message = i.response.status.message.strip if @scenario_parameters.to_s =~ /excon|faraday/ # Excon/Faraday do not expose the status message or http version, # so we have no way to record these attributes. i.response.status.message = nil i.response.http_version = nil elsif @scenario_parameters.to_s.include?('webmock') # WebMock does not expose the HTTP version so we have no way to record it i.response.http_version = nil end end end def normalize_cassette_content(content) return content unless @scenario_parameters.to_s.include?('patron') cassette_hash = YAML.load(content) cassette_hash['http_interactions'].map! do |hash| VCR::HTTPInteraction.from_hash(hash).tap do |i| i.request.headers = (i.request.headers || {}).merge!('Expect' => ['']) end.to_hash end YAML.dump(cassette_hash) end def modify_file(file_name, orig_text, new_text) in_current_dir do file = File.read(file_name) regex = /#{Regexp.escape(orig_text)}/ expect(file).to match(regex) file = file.gsub(regex, new_text) File.open(file_name, 'w') { |f| f.write(file) } end end end World(VCRHelpers) Given(/the following files do not exist:/) do |files| check_file_presence(files.raw.map{|file_row| file_row[0]}, false) end Given(/^the directory "([^"]*)" does not exist$/) do |dir| check_directory_presence([dir], false) end Given(/^a previously recorded cassette file "([^"]*)" with:$/) do |file_name, content| write_file(file_name, normalize_cassette_content(content)) end Given(/^it is (.*)$/) do |date_string| set_env('DATE_STRING', date_string) end Given(/^that port numbers in "([^"]*)" are normalized to "([^"]*)"$/) do |file_name, port| in_current_dir do contents = File.read(file_name) contents = contents.gsub(/:\d{2,}\//, ":#{port}/") File.open(file_name, 'w') { |f| f.write(contents) } end end When(/^I modify the file "([^"]*)" to replace "([^"]*)" with "([^"]*)"$/) do |file_name, orig_text, new_text| modify_file(file_name, orig_text, new_text) end When(/^I append to file "([^"]*)":$/) do |file_name, content| append_to_file(file_name, "\n" + content) end When(/^I set the "([^"]*)" environment variable to "([^"]*)"$/) do |var, value| set_env(var, value) end Then(/^the file "([^"]*)" should exist$/) do |file_name| check_file_presence([file_name], true) end Then(/^it should (pass|fail) with "([^"]*)"$/) do |pass_fail, partial_output| assert_exit_status_and_partial_output(pass_fail == 'pass', partial_output) end Then(/^it should (pass|fail) with an error like:$/) do |pass_fail, partial_output| assert_success(pass_fail == 'pass') # different implementations place the exception class at different # places relative to the message (i.e. with a multiline error message) process_output = all_output.gsub(/\s*\(VCR::Errors::\w+\)/, '') # Some implementations include extra leading spaces, for some reason... process_output.gsub!(/^\s*/, '') partial_output.gsub!(/^\s*/, '') assert_partial_output(partial_output, process_output) end Then(/^the output should contain each of the following:$/) do |table| table.raw.flatten.each do |string| assert_partial_output(string, all_output) end end Then(/^the file "([^"]*)" should contain YAML like:$/) do |file_name, expected_content| actual_content = in_current_dir { File.read(file_name) } expect(normalize_cassette_hash(YAML.load(actual_content))).to eq(normalize_cassette_hash(YAML.load(expected_content.to_s))) end Then(/^the file "([^"]*)" should contain JSON like:$/) do |file_name, expected_content| actual_content = in_current_dir { File.read(file_name) } actual = MultiJson.decode(actual_content) expected = MultiJson.decode(expected_content.to_s) expect(normalize_cassette_hash(actual)).to eq(normalize_cassette_hash(expected)) end Then(/^the file "([^"]*)" should contain compressed YAML like:$/) do |file_name, expected_content| actual_content = in_current_dir { File.read(file_name) } unzipped_content = Zlib::Inflate.inflate(actual_content) expect(normalize_cassette_hash(YAML.load(unzipped_content))).to eq(normalize_cassette_hash(YAML.load(expected_content.to_s))) end Then(/^the file "([^"]*)" should contain ruby like:$/) do |file_name, expected_content| actual_content = in_current_dir { File.read(file_name) } actual = eval(actual_content) expected = eval(expected_content) expect(normalize_cassette_hash(actual)).to eq(normalize_cassette_hash(expected)) end Then(/^the file "([^"]*)" should contain each of these:$/) do |file_name, table| table.raw.flatten.each do |string| check_file_content(file_name, string, true) end end Then(/^the file "([^"]*)" should contain a YAML fragment like:$/) do |file_name, fragment| in_current_dir do file_content = File.read(file_name) # Normalize by removing leading and trailing whitespace... file_content = file_content.split("\n").map do |line| # Different versions of psych use single vs. double quotes # And then 2.1 sometimes adds quotes... line.strip.gsub('"', "'").gsub("'", '') end.join("\n") expect(file_content).to include(fragment.gsub("'", '')) end end Then(/^the cassette "([^"]*)" should have the following response bodies:$/) do |file, table| interactions = in_current_dir { YAML.load_file(file) }['http_interactions'].map { |h| VCR::HTTPInteraction.from_hash(h) } actual_response_bodies = interactions.map { |i| i.response.body } expected_response_bodies = table.raw.flatten expect(actual_response_bodies).to match(expected_response_bodies) end Then(/^it should (pass|fail)$/) do |pass_fail| assert_success(pass_fail == 'pass') end