apirunner 0.2.4 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- 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"
|