apirunner 0.3.3 → 0.3.4
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 +3 -2
- data/lib/api_runner.rb +7 -4
- data/lib/checker.rb +14 -1
- data/lib/core_extensions.rb +17 -0
- data/lib/expectation_matcher.rb +16 -31
- data/lib/plugins/response_body_checker.rb +49 -45
- data/lib/plugins/response_code_checker.rb +7 -2
- data/lib/plugins/response_header_checker.rb +17 -12
- data/lib/result.rb +4 -0
- data/lib/testcase.rb +2 -1
- metadata +5 -4
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.3.
|
1
|
+
0.3.4
|
data/apirunner.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{apirunner}
|
8
|
-
s.version = "0.3.
|
8
|
+
s.version = "0.3.4"
|
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"]
|
12
|
-
s.date = %q{2010-10-
|
12
|
+
s.date = %q{2010-10-07}
|
13
13
|
s.description = %q{apirunner is a testsuite to query your RESTful JSON API and match response with your defined expectations}
|
14
14
|
s.email = %q{developers@moviepilot.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -45,6 +45,7 @@ Gem::Specification.new do |s|
|
|
45
45
|
"lib/apirunner.rb",
|
46
46
|
"lib/apirunner/railtie.rb",
|
47
47
|
"lib/checker.rb",
|
48
|
+
"lib/core_extensions.rb",
|
48
49
|
"lib/expectation_matcher.rb",
|
49
50
|
"lib/http_client.rb",
|
50
51
|
"lib/plugins/response_body_checker.rb",
|
data/lib/api_runner.rb
CHANGED
@@ -4,6 +4,8 @@ class ApiRunner
|
|
4
4
|
require 'http_client'
|
5
5
|
require 'api_configuration'
|
6
6
|
require 'testcase'
|
7
|
+
require 'core_extensions'
|
8
|
+
include CoreExtensions
|
7
9
|
|
8
10
|
CONFIG_FILE = "config/api_runner.yml"
|
9
11
|
SPEC_PATH = "test/api_runner/"
|
@@ -41,15 +43,16 @@ class ApiRunner
|
|
41
43
|
def run_tests
|
42
44
|
puts "Running exactly #{@spec.size} tests."
|
43
45
|
@spec.each do |test_case|
|
46
|
+
sleep test_case.wait_before_request
|
44
47
|
response = send_request_for(test_case)
|
45
|
-
|
46
|
-
result = @expectation.check(
|
47
|
-
if not result.
|
48
|
+
Checker.available_plugins.each do |plugin|
|
49
|
+
result = @expectation.check(plugin, response, test_case)
|
50
|
+
if not result.success?
|
48
51
|
putc "F"
|
49
52
|
@results << result
|
50
53
|
break
|
51
54
|
else
|
52
|
-
if
|
55
|
+
if plugin == Checker.available_plugins.last
|
53
56
|
@results << result
|
54
57
|
putc "."
|
55
58
|
end
|
data/lib/checker.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
class Checker
|
2
2
|
|
3
|
+
@@children = []
|
4
|
+
|
3
5
|
def initialize(testcase, response, excludes=nil)
|
4
6
|
@testcase = testcase
|
5
7
|
@response = response
|
@@ -12,7 +14,18 @@ class Checker
|
|
12
14
|
result = Result.new(@testcase, @response)
|
13
15
|
end
|
14
16
|
|
15
|
-
|
17
|
+
# returns a list of symbolized plugin names
|
18
|
+
def self.available_plugins
|
19
|
+
return @@children.map{ |child| child.to_s }
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# tracks all children of this class
|
25
|
+
# this way plugins can be loaded automagically
|
26
|
+
def self.inherited(child)
|
27
|
+
@@children << child
|
28
|
+
end
|
16
29
|
|
17
30
|
# recursively parses the tree and returns a set of relative pathes
|
18
31
|
# that can be used to match the both trees leafs
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module CoreExtensions
|
2
|
+
class String
|
3
|
+
# generates filenames from classnames the rails way
|
4
|
+
def underscore(string)
|
5
|
+
string.to_s.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').tr("-", "_").downcase
|
6
|
+
end
|
7
|
+
|
8
|
+
# opposites underscore defined above
|
9
|
+
def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
|
10
|
+
if first_letter_in_uppercase
|
11
|
+
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
|
12
|
+
else
|
13
|
+
lower_case_and_underscored_word.first + camelize(lower_case_and_underscored_word)[1..-1]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/expectation_matcher.rb
CHANGED
@@ -1,45 +1,30 @@
|
|
1
1
|
class ExpectationMatcher
|
2
2
|
require 'result'
|
3
3
|
require 'checker'
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
|
5
|
+
# dynamically load plugins
|
6
|
+
dir = File.dirname(__FILE__) + '/plugins/'
|
7
|
+
$LOAD_PATH.unshift(dir)
|
8
|
+
Dir[File.join(dir, "*.rb")].each {|file| require File.basename(file) }
|
8
9
|
|
9
10
|
def initialize(excludes=nil)
|
10
|
-
@test_types = [:response_code, :response_json_syntax, :response_header, :response_body]
|
11
11
|
@excludes = excludes || []
|
12
12
|
end
|
13
13
|
|
14
|
-
# returns the available test types if this matcher class
|
15
|
-
def test_types
|
16
|
-
return @test_types
|
17
|
-
end
|
18
|
-
|
19
14
|
# dispatches incoming matching requests
|
20
|
-
def check(
|
21
|
-
self.send(
|
15
|
+
def check(plugin, response, testcase)
|
16
|
+
self.send(plugin.underscore, response, testcase)
|
22
17
|
end
|
23
18
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
19
|
+
# dynamically generates methods that invoke "check" on all available plugins
|
20
|
+
def self.initialize_plugins
|
21
|
+
Checker.available_plugins.each do |plugin|
|
22
|
+
define_method(plugin.underscore) do |response, testcase|
|
23
|
+
eval(plugin).new(testcase, response, @excludes).check
|
24
|
+
end
|
25
|
+
private plugin.underscore.to_sym
|
26
|
+
end
|
29
27
|
end
|
30
28
|
|
31
|
-
|
32
|
-
def response_json_syntax(response, testcase)
|
33
|
-
ResponseJsonSyntaxChecker.new(testcase, response).check
|
34
|
-
end
|
35
|
-
|
36
|
-
# matches the given response header
|
37
|
-
def response_header(response, testcase)
|
38
|
-
ResponseHeaderChecker.new(testcase, response, @excludes).check
|
39
|
-
end
|
40
|
-
|
41
|
-
# matches the given attributes and values against the ones from the response body
|
42
|
-
def response_body(response, testcase)
|
43
|
-
ResponseBodyChecker.new(testcase, response, @excludes).check
|
44
|
-
end
|
29
|
+
initialize_plugins
|
45
30
|
end
|
@@ -1,64 +1,68 @@
|
|
1
1
|
class ResponseBodyChecker < Checker
|
2
|
-
require 'nokogiri'
|
2
|
+
require 'nokogiri'
|
3
3
|
|
4
4
|
def check
|
5
5
|
result = Result.new(@testcase, @response)
|
6
|
-
|
7
|
-
# special case: the whole body has to be matched via a regular expression
|
8
|
-
if is_regex?(@testcase.response_expectation['body'])
|
9
|
-
if not regex_matches?(@testcase.response_expectation['body'], @response.body)
|
10
|
-
result.succeeded = false
|
11
|
-
result.error_message = " expected the whole body to match regex --#{@testcase.response_expectation['body']}--\n got --#{@response.body}--"
|
12
|
-
end
|
13
|
-
return result
|
14
|
-
end
|
15
|
-
|
16
|
-
expected_body_hash = @testcase.response_expectation['body']
|
17
|
-
|
18
|
-
# in case we have no body expectation we simply return success
|
19
|
-
return result if expected_body_hash.nil?
|
20
|
-
|
21
|
-
# in case the response body is nil or damaged we return an error
|
22
6
|
begin
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
# else we build trees from both body structures...
|
32
|
-
expectation_tree = Nokogiri::XML(expected_body_hash.to_xml({ :indent => 0 }))
|
33
|
-
response_tree = Nokogiri::XML(responded_body_hash.to_xml({ :indent => 0 }))
|
7
|
+
# special case: the whole body has to be matched via a regular expression
|
8
|
+
if is_regex?(@testcase.response_expectation['body'])
|
9
|
+
if not regex_matches?(@testcase.response_expectation['body'], @response.body)
|
10
|
+
result.succeeded = false
|
11
|
+
result.error_message = " expected the whole body to match regex --#{@testcase.response_expectation['body']}--\n got --#{@response.body}--"
|
12
|
+
end
|
13
|
+
return result
|
14
|
+
end
|
34
15
|
|
35
|
-
|
36
|
-
matcher_pathes_from(expectation_tree).each do |path|
|
37
|
-
expectation_node = expectation_tree.xpath(path).first
|
38
|
-
response_node = response_tree.xpath(path).first
|
16
|
+
expected_body_hash = @testcase.response_expectation['body']
|
39
17
|
|
40
|
-
# in
|
41
|
-
|
18
|
+
# in case we have no body expectation we simply return success
|
19
|
+
return result if expected_body_hash.nil?
|
42
20
|
|
43
|
-
#
|
44
|
-
|
21
|
+
# in case the response body is nil or damaged we return an error
|
22
|
+
begin
|
23
|
+
responded_body_hash = JSON.parse(@response.body)
|
24
|
+
rescue
|
25
|
+
result = Result.new(@testcase, @response)
|
45
26
|
result.succeeded = false
|
46
|
-
result.error_message = " expected
|
27
|
+
result.error_message = " expected response to have a body\n got raw body --#{@response.body}-- which is nil or an unparseable hash"
|
47
28
|
return result
|
48
29
|
end
|
49
30
|
|
50
|
-
#
|
51
|
-
|
52
|
-
|
31
|
+
# else we build trees from both body structures...
|
32
|
+
expectation_tree = Nokogiri::XML(expected_body_hash.to_xml({ :indent => 0 }))
|
33
|
+
response_tree = Nokogiri::XML(responded_body_hash.to_xml({ :indent => 0 }))
|
34
|
+
|
35
|
+
# retrieve all the leafs pathes and match the leafs values using xpath
|
36
|
+
matcher_pathes_from(expectation_tree).each do |path|
|
37
|
+
expectation_node = expectation_tree.xpath(path).first
|
38
|
+
response_node = response_tree.xpath(path).first
|
39
|
+
|
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?
|
53
45
|
result.succeeded = false
|
54
|
-
result.error_message = " expected body
|
46
|
+
result.error_message = " expected body to have identifier --#{expectation_node.name}--\n got nil"
|
47
|
+
return result
|
55
48
|
end
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
60
61
|
end
|
61
62
|
end
|
63
|
+
rescue
|
64
|
+
result.succeeded = false
|
65
|
+
result.error_message = " unexpected error while parsing testcase/response. Check your testcase format!"
|
62
66
|
end
|
63
67
|
result
|
64
68
|
end
|
@@ -3,9 +3,14 @@ class ResponseCodeChecker < Checker
|
|
3
3
|
# checks the given responses status code against the one in the expectation and returns result object
|
4
4
|
def check
|
5
5
|
result = Result.new(@testcase, @response)
|
6
|
-
|
6
|
+
begin
|
7
|
+
if not @testcase.response_expectation['status_code'].to_s == @response.code.to_s
|
8
|
+
result.succeeded = false
|
9
|
+
result.error_message = " expected response code --#{@testcase.response_expectation['status_code']}--\n got response code --#{@response.code}--"
|
10
|
+
end
|
11
|
+
rescue
|
7
12
|
result.succeeded = false
|
8
|
-
result.error_message = "
|
13
|
+
result.error_message = " unexpected error while parsing testcase/response. Check your testcase format!"
|
9
14
|
end
|
10
15
|
result
|
11
16
|
end
|
@@ -3,19 +3,24 @@ class ResponseHeaderChecker < Checker
|
|
3
3
|
# checks given header against the given expepctation and returns a result object
|
4
4
|
def check
|
5
5
|
result = Result.new(@testcase, @response)
|
6
|
-
|
7
|
-
|
8
|
-
if
|
9
|
-
|
10
|
-
|
6
|
+
begin
|
7
|
+
@testcase.response_expectation['headers'].each_pair do |header_name, header_value|
|
8
|
+
if is_regex?(header_value)
|
9
|
+
if not (excluded?(header_name) or regex_matches?(header_value, @response.headers[header_name]))
|
10
|
+
result.succeeded = false
|
11
|
+
result.error_message = " expected header identifier --#{header_name}-- to match regex --#{header_value}--\n got --#{@response.headers[header_name]}--"
|
12
|
+
end
|
13
|
+
else
|
14
|
+
if not (excluded?(header_name) or string_matches?(header_value, @response.headers[header_name]))
|
15
|
+
result.succeeded = false
|
16
|
+
result.error_message = " expected header identifier --#{header_name}-- to match --#{header_value}--\n got --#{@response.headers[header_name]}--"
|
17
|
+
end
|
11
18
|
end
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
end
|
18
|
-
end unless (@testcase.response_expectation['headers'].nil? or @testcase.response_expectation['headers'].empty?)
|
19
|
+
end unless (@testcase.response_expectation['headers'].nil? or @testcase.response_expectation['headers'].empty?)
|
20
|
+
rescue
|
21
|
+
result.succeeded = false
|
22
|
+
result.error_message = " unexpected error while parsing testcase/response. Check your testcase format!"
|
23
|
+
end
|
19
24
|
result
|
20
25
|
end
|
21
26
|
|
data/lib/result.rb
CHANGED
data/lib/testcase.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
class Testcase
|
2
2
|
|
3
|
-
attr_reader :raw, :name, :request, :response_expectation
|
3
|
+
attr_reader :raw, :name, :request, :response_expectation, :wait_before_request
|
4
4
|
|
5
5
|
def initialize(raw)
|
6
6
|
@raw = raw
|
7
7
|
@name = raw['name']
|
8
8
|
@request = @raw['request']
|
9
9
|
@response_expectation = @raw['response_expectation']
|
10
|
+
@wait_before_request = @raw['wait_before_request'].nil? ? 0 : @raw['wait_before_request']
|
10
11
|
end
|
11
12
|
|
12
13
|
end
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 3
|
8
|
-
-
|
9
|
-
version: 0.3.
|
8
|
+
- 4
|
9
|
+
version: 0.3.4
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- jan@moviepilot.com
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-10-
|
17
|
+
date: 2010-10-07 00:00:00 +02:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -234,6 +234,7 @@ files:
|
|
234
234
|
- lib/apirunner.rb
|
235
235
|
- lib/apirunner/railtie.rb
|
236
236
|
- lib/checker.rb
|
237
|
+
- lib/core_extensions.rb
|
237
238
|
- lib/expectation_matcher.rb
|
238
239
|
- lib/http_client.rb
|
239
240
|
- lib/plugins/response_body_checker.rb
|
@@ -262,7 +263,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
262
263
|
requirements:
|
263
264
|
- - ">="
|
264
265
|
- !ruby/object:Gem::Version
|
265
|
-
hash:
|
266
|
+
hash: -3679568694912151594
|
266
267
|
segments:
|
267
268
|
- 0
|
268
269
|
version: "0"
|