apirunner 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/VERSION +1 -1
- data/apirunner.gemspec +6 -1
- data/lib/body_checker.rb +65 -0
- data/lib/checker.rb +66 -0
- data/lib/expectation_matcher.rb +9 -139
- data/lib/header_checker.rb +22 -0
- data/lib/json_syntax_checker.rb +13 -0
- data/lib/response_code_checker.rb +13 -0
- data/lib/result.rb +8 -8
- data/lib/testcase.rb +2 -1
- metadata +8 -3
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
1
|
+
0.2.5
|
data/apirunner.gemspec
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{apirunner}
|
8
|
-
s.version = "0.2.
|
8
|
+
s.version = "0.2.5"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["jan@moviepilot.com"]
|
@@ -44,8 +44,13 @@ Gem::Specification.new do |s|
|
|
44
44
|
"lib/api_runner.rb",
|
45
45
|
"lib/apirunner.rb",
|
46
46
|
"lib/apirunner/railtie.rb",
|
47
|
+
"lib/body_checker.rb",
|
48
|
+
"lib/checker.rb",
|
47
49
|
"lib/expectation_matcher.rb",
|
50
|
+
"lib/header_checker.rb",
|
48
51
|
"lib/http_client.rb",
|
52
|
+
"lib/json_syntax_checker.rb",
|
53
|
+
"lib/response_code_checker.rb",
|
49
54
|
"lib/result.rb",
|
50
55
|
"lib/tasks/api.rake",
|
51
56
|
"lib/testcase.rb",
|
data/lib/body_checker.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
class BodyChecker < Checker
|
2
|
+
|
3
|
+
def check
|
4
|
+
result = Result.new(@testcase, @response)
|
5
|
+
|
6
|
+
# special case: the whole body has to be matched via a regular expression
|
7
|
+
if is_regex?(@testcase.response_expectation['body'])
|
8
|
+
if not regex_matches?(@testcase.response_expectation['body'], @response.body)
|
9
|
+
result.succeeded = false
|
10
|
+
result.error_message = " expected the whole body to match regex --#{@testcase.response_expectation['body']}--\n got --#{@response.body}--"
|
11
|
+
end
|
12
|
+
return result
|
13
|
+
end
|
14
|
+
|
15
|
+
expected_body_hash = @testcase.response_expectation['body']
|
16
|
+
|
17
|
+
# in case we have no body expectation we simply return success
|
18
|
+
return result if expected_body_hash.nil?
|
19
|
+
|
20
|
+
# in case the response body is nil or damaged we return an error
|
21
|
+
begin
|
22
|
+
responded_body_hash = JSON.parse(@response.body)
|
23
|
+
rescue
|
24
|
+
result = Result.new(@testcase, @response)
|
25
|
+
result.succeeded = false
|
26
|
+
result.error_message = " expected response to have a body\n got raw body --#{@response.body}-- which is nil or an unparseable hash"
|
27
|
+
return result
|
28
|
+
end
|
29
|
+
|
30
|
+
# else we build trees from both body structures...
|
31
|
+
expectation_tree = Nokogiri::XML(expected_body_hash.to_xml({ :indent => 0 }))
|
32
|
+
response_tree = Nokogiri::XML(responded_body_hash.to_xml({ :indent => 0 }))
|
33
|
+
|
34
|
+
# retrieve all the leafs pathes and match the leafs values using xpath
|
35
|
+
matcher_pathes_from(expectation_tree).each do |path|
|
36
|
+
expectation_node = expectation_tree.xpath(path).first
|
37
|
+
response_node = response_tree.xpath(path).first
|
38
|
+
|
39
|
+
debugger
|
40
|
+
# in some (not awesome) cases the root node occures as leaf, so we have to skip him here
|
41
|
+
next if expectation_node.name == "hash"
|
42
|
+
|
43
|
+
# return error if response body does not have the expected entry
|
44
|
+
if response_node.nil?
|
45
|
+
result.succeeded = false
|
46
|
+
result.error_message = " expected body to have identifier --#{expectation_node.name}--\n got nil"
|
47
|
+
return result
|
48
|
+
end
|
49
|
+
|
50
|
+
# last but not least try the regex or direct match and return errors in case of any
|
51
|
+
if is_regex?(expectation_node.text)
|
52
|
+
if not (excluded?(expectation_node.name) or regex_matches?(expectation_node.text, response_node.text))
|
53
|
+
result.succeeded = false
|
54
|
+
result.error_message = " expected body identifier --#{expectation_node.name}-- to match regex --#{expectation_node.text}--\n got --#{response_node.text}--"
|
55
|
+
end
|
56
|
+
else
|
57
|
+
if not (excluded?(expectation_node.name) or string_matches?(expectation_node.text, response_node.text))
|
58
|
+
result.succeeded = false
|
59
|
+
result.error_message = " expected body identifier --#{expectation_node.name}-- to match --#{expectation_node.text}--\n got --#{response_node.text}--"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
result
|
64
|
+
end
|
65
|
+
end
|
data/lib/checker.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
class Checker
|
2
|
+
|
3
|
+
def initialize(testcase, response, excludes=nil)
|
4
|
+
@testcase = testcase
|
5
|
+
@response = response
|
6
|
+
@excludes = excludes
|
7
|
+
end
|
8
|
+
|
9
|
+
# executes the checking routine and returns a result object
|
10
|
+
# to be overwritten in child classes
|
11
|
+
def check
|
12
|
+
result = Result.new(@testcase, @response)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# recursively parses the tree and returns a set of relative pathes
|
18
|
+
# that can be used to match the both trees leafs
|
19
|
+
def matcher_pathes_from(node, pathes = nil)
|
20
|
+
pathes ||= []
|
21
|
+
if not node.children.blank?
|
22
|
+
node.children.each do |sub_node|
|
23
|
+
matcher_pathes_from(sub_node, pathes)
|
24
|
+
end
|
25
|
+
else
|
26
|
+
pathes << relative_path(node.parent.path)
|
27
|
+
end
|
28
|
+
pathes
|
29
|
+
end
|
30
|
+
|
31
|
+
# returns relative path for matching the target tree of the response body
|
32
|
+
# explicit array adressing is replaced by *
|
33
|
+
def relative_path(path)
|
34
|
+
path.gsub(/\/([^\/]+)\[\d+\]\//i,"/*/")
|
35
|
+
end
|
36
|
+
|
37
|
+
# returns true if given attributes is an excluded item that does not have to be evaluated in this environment
|
38
|
+
def excluded?(item)
|
39
|
+
@excludes.include?(item)
|
40
|
+
end
|
41
|
+
|
42
|
+
# returns true if given string seems to be a regular expression
|
43
|
+
def is_regex?(string)
|
44
|
+
string.to_s.match(/^\/.+\/$/)
|
45
|
+
end
|
46
|
+
|
47
|
+
# returns true if the given regular expression matches the given value
|
48
|
+
def regex_matches?(regex, value)
|
49
|
+
regex = Regexp.compile( regex.gsub(/^\//, '').gsub(/\/$/,'') )
|
50
|
+
!!value.to_s.match(regex)
|
51
|
+
end
|
52
|
+
|
53
|
+
# returns true if the given string exactly matches the given value
|
54
|
+
def string_matches?(string, value)
|
55
|
+
string.to_s == value.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
# parses output into JSON object
|
59
|
+
def valid_json?(response_body)
|
60
|
+
# responses may be nil, return true then
|
61
|
+
return true if response_body.blank?
|
62
|
+
# returns true if given response is valid json, else false
|
63
|
+
JSON.parse(response_body.to_s) rescue false
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
data/lib/expectation_matcher.rb
CHANGED
@@ -2,6 +2,11 @@ class ExpectationMatcher
|
|
2
2
|
require 'result'
|
3
3
|
require 'nokogiri'
|
4
4
|
require 'JSON'
|
5
|
+
require 'checker'
|
6
|
+
require 'json_syntax_checker'
|
7
|
+
require 'header_checker'
|
8
|
+
require 'response_code_checker'
|
9
|
+
require 'body_checker'
|
5
10
|
|
6
11
|
def initialize(excludes=nil)
|
7
12
|
@test_types = [:response_code, :response_body_format, :response_headers, :response_body]
|
@@ -22,156 +27,21 @@ class ExpectationMatcher
|
|
22
27
|
|
23
28
|
# matches the given response code
|
24
29
|
def response_code(response, testcase)
|
25
|
-
|
26
|
-
if not testcase.response_expectation['status_code'].to_s == response.code.to_s
|
27
|
-
result.succeeded = false
|
28
|
-
result.error_message = " expected response code --#{testcase.response_expectation['status_code']}--\n got response code --#{response.code}--"
|
29
|
-
end
|
30
|
-
result
|
30
|
+
ResponseCodeChecker.new(testcase, response).check
|
31
31
|
end
|
32
32
|
|
33
33
|
# checks the format of the given data of JSON conformity
|
34
34
|
def response_body_format(response, testcase)
|
35
|
-
|
36
|
-
results = result_struct.new(:succeeded => true, :error => nil)
|
37
|
-
result = Result.new(testcase, response)
|
38
|
-
if not valid_json?(response.body)
|
39
|
-
result.succeeded = false
|
40
|
-
result.error_message = "expected valid JSON in body\n got --#{response.body[1..400]}--"
|
41
|
-
end
|
42
|
-
result
|
35
|
+
JsonSyntaxChecker.new(testcase, response).check
|
43
36
|
end
|
44
37
|
|
45
38
|
# matches the given response header
|
46
39
|
def response_headers(response, testcase)
|
47
|
-
|
48
|
-
|
49
|
-
testcase.response_expectation['headers'].each_pair do |header_name, header_value|
|
50
|
-
if is_regex?(header_value)
|
51
|
-
if not (excluded?(header_name) or regex_matches?(header_value, response.headers[header_name]))
|
52
|
-
result.succeeded = false
|
53
|
-
result.error_message = " expected header identifier --#{header_name}-- to match regex --#{header_value}--\n got --#{response.headers[header_name]}--"
|
54
|
-
end
|
55
|
-
else
|
56
|
-
if not (excluded?(header_name) or string_matches?(header_value, response.headers[header_name]))
|
57
|
-
result.succeeded = false
|
58
|
-
result.error_message = " expected header identifier --#{header_name}-- to match --#{header_value}--\n got --#{response.headers[header_name]}--"
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end unless (testcase.response_expectation['headers'].nil? or testcase.response_expectation['headers'].empty?)
|
62
|
-
return result
|
40
|
+
HeaderChecker.new(testcase, response, @excludes).check
|
63
41
|
end
|
64
42
|
|
65
43
|
# matches the given attributes and values against the ones from the response body
|
66
44
|
def response_body(response, testcase)
|
67
|
-
|
68
|
-
|
69
|
-
# special case: the whole body has to be matched via a regular expression
|
70
|
-
if is_regex?(testcase.response_expectation['body'])
|
71
|
-
if not regex_matches?(testcase.response_expectation['body'], response.body)
|
72
|
-
result.succeeded = false
|
73
|
-
result.error_message = " expected the whole body to match regex --#{testcase.response_expectation['body']}--\n got --#{response.body}--"
|
74
|
-
end
|
75
|
-
return result
|
76
|
-
end
|
77
|
-
|
78
|
-
expected_body_hash = testcase.response_expectation['body']
|
79
|
-
|
80
|
-
# in case we have no body expectation we simply return success
|
81
|
-
return result if expected_body_hash.nil?
|
82
|
-
|
83
|
-
# in case the response body is nil or damaged we return an error
|
84
|
-
begin
|
85
|
-
responded_body_hash = JSON.parse(response.body)
|
86
|
-
rescue
|
87
|
-
result = Result.new(testcase, response)
|
88
|
-
result.succeeded = false
|
89
|
-
result.error_message = " expected response to have a body\n got raw body --#{response.body}-- which is nil or an unparseable hash"
|
90
|
-
return result
|
91
|
-
end
|
92
|
-
|
93
|
-
# else we build trees from both body structures...
|
94
|
-
expectation_tree = Nokogiri::XML(expected_body_hash.to_xml({ :indent => 0 }))
|
95
|
-
response_tree = Nokogiri::XML(responded_body_hash.to_xml({ :indent => 0 }))
|
96
|
-
|
97
|
-
# retrieve all the leafs pathes and match the leafs values using xpath
|
98
|
-
matcher_pathes_from(expectation_tree).each do |path|
|
99
|
-
expectation_node = expectation_tree.xpath(path).first
|
100
|
-
response_node = response_tree.xpath(path).first
|
101
|
-
|
102
|
-
debugger
|
103
|
-
# in some (not awesome) cases the root node occures as leaf, so we have to skip him here
|
104
|
-
next if expectation_node.name == "hash"
|
105
|
-
|
106
|
-
# return error if response body does not have the expected entry
|
107
|
-
if response_node.nil?
|
108
|
-
result.succeeded = false
|
109
|
-
result.error_message = " expected body to have identifier --#{expectation_node.name}--\n got nil"
|
110
|
-
return result
|
111
|
-
end
|
112
|
-
|
113
|
-
# last but not least try the regex or direct match and return errors in case of any
|
114
|
-
if is_regex?(expectation_node.text)
|
115
|
-
if not (excluded?(expectation_node.name) or regex_matches?(expectation_node.text, response_node.text))
|
116
|
-
result.succeeded = false
|
117
|
-
result.error_message = " expected body identifier --#{expectation_node.name}-- to match regex --#{expectation_node.text}--\n got --#{response_node.text}--"
|
118
|
-
end
|
119
|
-
else
|
120
|
-
if not (excluded?(expectation_node.name) or string_matches?(expectation_node.text, response_node.text))
|
121
|
-
result.succeeded = false
|
122
|
-
result.error_message = " expected body identifier --#{expectation_node.name}-- to match --#{expectation_node.text}--\n got --#{response_node.text}--"
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
126
|
-
result
|
127
|
-
end
|
128
|
-
|
129
|
-
# recursively parses the tree and returns a set of relative pathes
|
130
|
-
# that can be used to match the both trees leafs
|
131
|
-
def matcher_pathes_from(node, pathes = nil)
|
132
|
-
pathes ||= []
|
133
|
-
if not node.children.blank?
|
134
|
-
node.children.each do |sub_node|
|
135
|
-
matcher_pathes_from(sub_node, pathes)
|
136
|
-
end
|
137
|
-
else
|
138
|
-
pathes << relative_path(node.parent.path)
|
139
|
-
end
|
140
|
-
pathes
|
141
|
-
end
|
142
|
-
|
143
|
-
# returns relative path for matching the target tree of the response body
|
144
|
-
# explicit array adressing is replaced by *
|
145
|
-
def relative_path(path)
|
146
|
-
path.gsub(/\/([^\/]+)\[\d+\]\//i,"/*/")
|
147
|
-
end
|
148
|
-
|
149
|
-
# returns true if given attributes is an excluded item that does not have to be evaluated in this environment
|
150
|
-
def excluded?(item)
|
151
|
-
@excludes.include?(item)
|
152
|
-
end
|
153
|
-
|
154
|
-
# returns true if given string seems to be a regular expression
|
155
|
-
def is_regex?(string)
|
156
|
-
string.to_s.match(/^\/.+\/$/)
|
157
|
-
end
|
158
|
-
|
159
|
-
# returns true if the given regular expression matches the given value
|
160
|
-
def regex_matches?(regex, value)
|
161
|
-
regex = Regexp.compile( regex.gsub(/^\//, '').gsub(/\/$/,'') )
|
162
|
-
!!value.to_s.match(regex)
|
163
|
-
end
|
164
|
-
|
165
|
-
# returns true if the given string exactly matches the given value
|
166
|
-
def string_matches?(string, value)
|
167
|
-
string.to_s == value.to_s
|
168
|
-
end
|
169
|
-
|
170
|
-
# parses output into JSON object
|
171
|
-
def valid_json?(response_body)
|
172
|
-
# responses may be nil, return true then
|
173
|
-
return true if response_body.blank?
|
174
|
-
# returns true if given response is valid json, else false
|
175
|
-
JSON.parse(response_body.to_s) rescue false
|
45
|
+
BodyChecker.new(testcase, response, @excludes).check
|
176
46
|
end
|
177
47
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class HeaderChecker < Checker
|
2
|
+
|
3
|
+
# checks given header against the given expepctation and returns a result object
|
4
|
+
def check
|
5
|
+
result = Result.new(@testcase, @response)
|
6
|
+
@testcase.response_expectation['headers'].each_pair do |header_name, header_value|
|
7
|
+
if is_regex?(header_value)
|
8
|
+
if not (excluded?(header_name) or regex_matches?(header_value, @response.headers[header_name]))
|
9
|
+
result.succeeded = false
|
10
|
+
result.error_message = " expected header identifier --#{header_name}-- to match regex --#{header_value}--\n got --#{@response.headers[header_name]}--"
|
11
|
+
end
|
12
|
+
else
|
13
|
+
if not (excluded?(header_name) or string_matches?(header_value, @response.headers[header_name]))
|
14
|
+
result.succeeded = false
|
15
|
+
result.error_message = " expected header identifier --#{header_name}-- to match --#{header_value}--\n got --#{@response.headers[header_name]}--"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end unless (@testcase.response_expectation['headers'].nil? or @testcase.response_expectation['headers'].empty?)
|
19
|
+
result
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class JsonSyntaxChecker < Checker
|
2
|
+
|
3
|
+
# checks if the given testcase body represents syntactically valid JSON
|
4
|
+
def check
|
5
|
+
result = Result.new(@testcase, @response)
|
6
|
+
if not valid_json?(@response.body)
|
7
|
+
result.succeeded = false
|
8
|
+
result.error_message = "expected valid JSON in body\n got --#{@response.body[1..400]}--"
|
9
|
+
end
|
10
|
+
result
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class ResponseCodeChecker < Checker
|
2
|
+
|
3
|
+
# checks the given responses status code against the one in the expectation and returns result object
|
4
|
+
def check
|
5
|
+
result = Result.new(@testcase, @response)
|
6
|
+
if not @testcase.response_expectation['status_code'].to_s == @response.code.to_s
|
7
|
+
result.succeeded = false
|
8
|
+
result.error_message = " expected response code --#{@testcase.response_expectation['status_code']}--\n got response code --#{@response.code}--"
|
9
|
+
end
|
10
|
+
result
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
data/lib/result.rb
CHANGED
@@ -43,17 +43,17 @@ class Result
|
|
43
43
|
|
44
44
|
# yields the verbose error messages
|
45
45
|
def be_verbose(index)
|
46
|
-
puts "\n#{result_case} (#{index+1}) - \"#{@testcase
|
46
|
+
puts "\n#{result_case} (#{index+1}) - \"#{@testcase.name}\""
|
47
47
|
puts @error_message
|
48
48
|
puts(" More more more verbosity\n")
|
49
|
-
puts(" request method: #{@testcase
|
50
|
-
puts(" resource path: #{@testcase
|
51
|
-
puts(" request headers: #{@testcase
|
52
|
-
puts(" JSON body sent: #{@testcase
|
49
|
+
puts(" request method: #{@testcase.request['method']}")
|
50
|
+
puts(" resource path: #{@testcase.request['path']}")
|
51
|
+
puts(" request headers: #{@testcase.request['headers']}")
|
52
|
+
puts(" JSON body sent: #{@testcase.request['body']}")
|
53
53
|
puts(" expectation:")
|
54
|
-
puts(" response status code: #{@testcase
|
55
|
-
puts(" response headers: #{@testcase
|
56
|
-
puts(" response body: #{@testcase
|
54
|
+
puts(" response status code: #{@testcase.response_expectation['status_code']}")
|
55
|
+
puts(" response headers: #{@testcase.response_expectation['headers']}")
|
56
|
+
puts(" response body: #{@testcase.response_expectation['body']}")
|
57
57
|
puts(" result:")
|
58
58
|
puts(" response status code: #{@response.code}")
|
59
59
|
puts(" response headers: #{@response.headers}")
|
data/lib/testcase.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
class Testcase
|
2
2
|
|
3
|
-
attr_reader :raw, :request, :response_expectation
|
3
|
+
attr_reader :raw, :name, :request, :response_expectation
|
4
4
|
|
5
5
|
def initialize(raw)
|
6
6
|
@raw = raw
|
7
|
+
@name = raw['name']
|
7
8
|
@request = @raw['request']
|
8
9
|
@response_expectation = @raw['response_expectation']
|
9
10
|
end
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 2
|
8
|
-
-
|
9
|
-
version: 0.2.
|
8
|
+
- 5
|
9
|
+
version: 0.2.5
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- jan@moviepilot.com
|
@@ -248,8 +248,13 @@ files:
|
|
248
248
|
- lib/api_runner.rb
|
249
249
|
- lib/apirunner.rb
|
250
250
|
- lib/apirunner/railtie.rb
|
251
|
+
- lib/body_checker.rb
|
252
|
+
- lib/checker.rb
|
251
253
|
- lib/expectation_matcher.rb
|
254
|
+
- lib/header_checker.rb
|
252
255
|
- lib/http_client.rb
|
256
|
+
- lib/json_syntax_checker.rb
|
257
|
+
- lib/response_code_checker.rb
|
253
258
|
- lib/result.rb
|
254
259
|
- lib/tasks/api.rake
|
255
260
|
- lib/testcase.rb
|
@@ -272,7 +277,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
272
277
|
requirements:
|
273
278
|
- - ">="
|
274
279
|
- !ruby/object:Gem::Version
|
275
|
-
hash:
|
280
|
+
hash: 197723826225118285
|
276
281
|
segments:
|
277
282
|
- 0
|
278
283
|
version: "0"
|