elasticsearch-api/api-spec-testing/rspec_matchers.rb (263 lines of code) (raw):
# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# Match the `length` of a field.
RSpec::Matchers.define :match_response_field_length do |expected_pairs, test|
match do |response|
expected_pairs.all? do |expected_key, expected_value|
# ssl test returns results at '$body' key. See ssl/10_basic.yml
expected_pairs = expected_pairs['$body'] if expected_pairs['$body']
split_key = TestFile::Test.split_and_parse_key(expected_key).collect do |k|
test.get_cached_value(k)
end
actual_value = split_key.inject(response) do |_response, key|
# If the key is an index, indicating element of a list
if _response.empty? && key == '$body'
_response
else
_response[key] || _response[key.to_s]
end
end
actual_value.size == expected_value
end
end
end
# Validate that a field is `true`.
RSpec::Matchers.define :match_true_field do |field, test|
match do |response|
# TODO: Refactor! split_key for is_true
if (match = field.match(/(^\$[a-z]+)/))
keys = field.split('.')
keys.delete(match[1])
dynamic_key = test.cached_values[match[1].gsub('$', '')]
return !!dynamic_key.dig(*keys)
end
# Handle is_true: ''
return !!response if field == ''
split_key = TestFile::Test.split_and_parse_key(field).collect do |k|
test.get_cached_value(k)
end
!!TestFile::Test.find_value_in_document(split_key, response)
end
failure_message do |response|
"the response `#{response}` does not have `true` in the field `#{field}`"
end
end
# Validate that a field is `false`.
RSpec::Matchers.define :match_false_field do |field, test|
match do |response|
# Handle is_false: ''
return !response if field == ''
split_key = TestFile::Test.split_and_parse_key(field).collect do |k|
test.get_cached_value(k)
end
value_in_doc = TestFile::Test.find_value_in_document(split_key, response)
value_in_doc == 0 || !value_in_doc
end
failure_message do |response|
"the response `#{response}` does not have `false` in the field `#{field}`"
end
end
# Validate that a field is `gte` than a given value.
RSpec::Matchers.define :match_gte_field do |expected_pairs, test|
match do |response|
expected_pairs.all? do |expected_key, expected_value|
split_key = TestFile::Test.split_and_parse_key(expected_key).collect do |k|
test.get_cached_value(k)
end
actual_value = split_key.inject(response) do |_response, key|
# If the key is an index, indicating element of a list
if _response.empty? && key == '$body'
_response
else
_response[key] || _response[key.to_s]
end
end
actual_value >= test.get_cached_value(expected_value)
end
end
end
# Validate that a field is `gt` than a given value.
RSpec::Matchers.define :match_gt_field do |expected_pairs, test|
match do |response|
expected_pairs.all? do |expected_key, expected_value|
split_key = TestFile::Test.split_and_parse_key(expected_key).collect do |k|
test.get_cached_value(k)
end
actual_value = split_key.inject(response) do |_response, key|
# If the key is an index, indicating element of a list
if _response.empty? && key == '$body'
_response
else
_response[key] || _response[key.to_s]
end
end
actual_value > test.get_cached_value(expected_value)
end
end
end
# Validate that a field is `lte` than a given value.
RSpec::Matchers.define :match_lte_field do |expected_pairs, test|
match do |response|
expected_pairs.all? do |expected_key, expected_value|
split_key = TestFile::Test.split_and_parse_key(expected_key).collect do |k|
test.get_cached_value(k)
end
actual_value = split_key.inject(response) do |_response, key|
# If the key is an index, indicating element of a list
if _response.empty? && key == '$body'
_response
else
_response[key] || _response[key.to_s]
end
end
actual_value <= test.get_cached_value(expected_value)
end
end
end
# Validate that a field is `lt` than a given value.
RSpec::Matchers.define :match_lt_field do |expected_pairs, test|
match do |response|
expected_pairs.all? do |expected_key, expected_value|
split_key = TestFile::Test.split_and_parse_key(expected_key).collect do |k|
test.get_cached_value(k)
end
actual_value = split_key.inject(response) do |_response, key|
# If the key is an index, indicating element of a list
if _response.empty? && key == '$body'
_response
else
_response[key] || _response[key.to_s]
end
end
actual_value < test.get_cached_value(expected_value)
end
end
end
# Match an arbitrary field of a response to a given value.
RSpec::Matchers.define :match_response do |pairs, test|
match do |response|
pairs = sanitize_pairs(pairs)
compare_pairs(pairs, response, test).empty?
end
failure_message do |response|
"the expected response pair/value(s) #{@mismatched_pairs}" +
" does not match the pair/value(s) in the response #{response}"
end
def sanitize_pairs(expected_pairs)
# sql test returns results at '$body' key. See sql/translate.yml
@pairs ||= expected_pairs['$body'] ? expected_pairs['$body'] : expected_pairs
end
def compare_pairs(expected_pairs, response, test)
@mismatched_pairs = {}
if expected_pairs.is_a?(String)
@mismatched_pairs = expected_pairs unless compare_string_response(expected_pairs, response)
else
compare_hash(expected_pairs, response, test)
end
@mismatched_pairs
end
def compare_hash(expected_pairs, actual_hash, test)
expected_pairs.each do |expected_key, expected_value|
# TODO: Refactor! split_key
if (match = expected_key.match(/(^\$[a-z]+)/))
keys = expected_key.split('.')
keys.delete(match[1])
dynamic_key = test.cached_values[match[1].gsub('$', '')]
value = dynamic_key.dig(*keys)
if expected_pairs.values.first.is_a?(String) && expected_pairs.values.first.match?(/^\$/)
return test.cached_values[expected_pairs.values.first.gsub('$','')] == value
else
return expected_pairs.values.first == value
end
else
split_key = TestFile::Test.split_and_parse_key(expected_key).collect do |k|
# Sometimes the expected *key* is a cached value from a previous request.
test.get_cached_value(k)
end
end
# We now accept 'nested.keys' so let's try the previous implementation and if that doesn't
# work, try with the nested key, otherwise, raise exception.
begin
actual_value = TestFile::Test.find_value_in_document(split_key, actual_hash)
rescue TypeError => e
actual_value = TestFile::Test.find_value_in_document(expected_key, actual_hash)
rescue StandardError => e
raise e
end
# When the expected_key is ''
actual_value = actual_hash if split_key.empty?
# Sometimes the key includes dots. See watcher/put_watch/60_put_watch_with_action_condition.yml
actual_value = TestFile::Test.find_value_in_document(expected_key, actual_hash) if actual_value.nil?
# Sometimes the expected *value* is a cached value from a previous request.
# See test api_key/10_basic.yml
expected_value = test.get_cached_value(expected_value)
case expected_value
when Hash
compare_hash(expected_value, actual_value, test)
when Array
begin
unless compare_array(expected_value.sort, actual_value.sort, test, actual_hash)
@mismatched_pairs.merge!(expected_key => expected_value)
end
rescue TypeError, ArgumentError
unless compare_array(expected_value, actual_value, test, actual_hash)
@mismatched_pairs.merge!(expected_key => expected_value)
end
end
when String
unless compare_string(expected_value, actual_value, test, actual_hash)
@mismatched_pairs.merge!(expected_key => expected_value)
end
when Time
compare_string(expected_value.to_s, Time.new(actual_value).to_s, test, actual_hash)
else
unless expected_value == actual_value
@mismatched_pairs.merge!(expected_key => expected_value)
end
end
end
end
def compare_string(expected, actual_value, test, response)
# When you must match a regex. For example:
# match: {task: '/.+:\d+/'}
if expected[0] == '/' && expected[-1] == '/'
parsed = expected
expected.scan(/\$\{([a-z_0-9]+)\}/) do |match|
parsed = parsed.gsub(/\$\{?#{match.first}\}?/, test.cached_values[match.first])
end
/#{parsed.tr("/", "")}/ =~ actual_value
elsif !!(expected.match?(/^-?[0-9]{1}\.[0-9]+E[0-9]+/))
# When the value in the yaml test is a big number, the format is
# different from what Ruby uses, so we try different options:
actual_value.to_s == expected.gsub('E', 'e+') || # transform X.XXXXEXX to X.XXXXXe+XX to compare thme
actual_value == expected || # compare the actual values
expected.to_f.to_s == actual_value.to_f.to_s # transform both to Float and compare them
elsif expected == '' && actual_value != ''
actual_value == response
else
expected == actual_value
end
end
def compare_array(expected, actual, test, response)
expected.each_with_index do |value, i|
case value
when Hash
return false unless compare_hash(value, actual[i], test)
when Array
return false unless compare_array(value, actual[i], test, response)
when String
return false unless compare_string(value, actual[i], test, response)
else
true
end
end
end
def compare_string_response(expected_string, response)
regexp = Regexp.new(expected_string.strip[1..-2], Regexp::EXTENDED|Regexp::MULTILINE)
regexp =~ response
end
end
# Match that a request returned a given error.
RSpec::Matchers.define :match_error do |expected_error|
match do |actual_error|
# Remove surrounding '/' in string representing Regex
expected_error = expected_error.chomp("/")
expected_error = expected_error[1..-1] if expected_error =~ /^\//
message = actual_error.message.tr('\\', '')
case expected_error
when 'request_timeout'
message =~ /\[408\]/
when 'missing'
message =~ /\[404\]/
when 'conflict'
message =~ /\[409\]/
when 'request'
message =~ /\[500\]/
when 'bad_request'
message =~ /\[400\]/
when 'param'
message =~ /\[400\]/ ||
actual_error.is_a?(ArgumentError)
when 'unauthorized'
actual_error.is_a?(Elastic::Transport::Transport::Errors::Unauthorized)
when 'forbidden'
actual_error.is_a?(Elastic::Transport::Transport::Errors::Forbidden)
when /error parsing field/, /illegal_argument_exception/
message =~ /\[400\]/ ||
actual_error.is_a?(Elastic::Transport::Transport::Errors::BadRequest)
when /NullPointerException/
message =~ /\[400\]/
else
message =~ /#{expected_error}/
end
end
failure_message do |actual_error|
"the error `#{actual_error}` does not match the expected error `#{expected_error}`"
end
end