apirunner 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,11 @@
1
+ # .document is used by rdoc and yard to know how to generate documentation
2
+ # for example, it can be used to control how rdoc gets built when you do `gem install foo`
3
+
4
+ README.rdoc
5
+ lib/**/*.rb
6
+ bin/*
7
+
8
+ # Files below this - are treated as 'extra files', and aren't parsed for ruby code
9
+ -
10
+ features/**/*.feature
11
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,42 @@
1
+ # rcov generated
2
+ coverage
3
+
4
+ # rdoc generated
5
+ rdoc
6
+
7
+ # yard generated
8
+ doc
9
+ .yardoc
10
+
11
+ # bundler
12
+ .bundle
13
+
14
+ # jeweler generated
15
+ pkg
16
+
17
+ # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
18
+ #
19
+ # * Create a file at ~/.gitignore
20
+ # * Include files you want ignored
21
+ # * Run: git config --global core.excludesfile ~/.gitignore
22
+ #
23
+ # After doing this, these files will be ignored in all your git projects,
24
+ # saving you from having to 'pollute' every project you touch with them
25
+ #
26
+ # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
27
+ #
28
+ # For MacOS:
29
+ #
30
+ #.DS_Store
31
+ #
32
+ # For TextMate
33
+ #*.tmproj
34
+ #tmtags
35
+ #
36
+ # For emacs:
37
+ #*~
38
+ #\#*
39
+ #.\#*
40
+ #
41
+ # For vim:
42
+ #*.swp
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+
9
+ gem 'nokogiri', '~> 1.4.3.1'
10
+ gem 'httparty', '~> 0.6.1'
11
+
12
+ group :development do
13
+ gem "rspec", ">= 2.0.0.beta.19"
14
+ gem "cucumber", ">= 0"
15
+ gem "bundler", "~> 1.0.0"
16
+ gem "jeweler", "~> 1.5.0.pre3"
17
+ gem "rcov", ">= 0"
18
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,49 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ builder (2.1.2)
5
+ crack (0.1.8)
6
+ cucumber (0.8.5)
7
+ builder (~> 2.1.2)
8
+ diff-lcs (~> 1.1.2)
9
+ gherkin (~> 2.1.4)
10
+ json_pure (~> 1.4.3)
11
+ term-ansicolor (~> 1.0.4)
12
+ diff-lcs (1.1.2)
13
+ gherkin (2.1.5)
14
+ trollop (~> 1.16.2)
15
+ git (1.2.5)
16
+ httparty (0.6.1)
17
+ crack (= 0.1.8)
18
+ jeweler (1.5.0.pre3)
19
+ bundler (~> 1.0.0)
20
+ git (>= 1.2.5)
21
+ rake
22
+ json_pure (1.4.6)
23
+ nokogiri (1.4.3.1)
24
+ rake (0.8.7)
25
+ rcov (0.9.9)
26
+ rspec (2.0.0.beta.22)
27
+ rspec-core (= 2.0.0.beta.22)
28
+ rspec-expectations (= 2.0.0.beta.22)
29
+ rspec-mocks (= 2.0.0.beta.22)
30
+ rspec-core (2.0.0.beta.22)
31
+ rspec-expectations (2.0.0.beta.22)
32
+ diff-lcs (>= 1.1.2)
33
+ rspec-mocks (2.0.0.beta.22)
34
+ rspec-core (= 2.0.0.beta.22)
35
+ rspec-expectations (= 2.0.0.beta.22)
36
+ term-ansicolor (1.0.5)
37
+ trollop (1.16.2)
38
+
39
+ PLATFORMS
40
+ ruby
41
+
42
+ DEPENDENCIES
43
+ bundler (~> 1.0.0)
44
+ cucumber
45
+ httparty (~> 0.6.1)
46
+ jeweler (~> 1.5.0.pre3)
47
+ nokogiri (~> 1.4.3.1)
48
+ rcov
49
+ rspec (>= 2.0.0.beta.19)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 moviepilot
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,17 @@
1
+ = apirunner
2
+
3
+ Description goes here.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2010 moviepilot. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,58 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "apirunner"
16
+ gem.summary = %Q{one-line summary of your gem}
17
+ gem.description = %Q{longer description of your gem}
18
+ gem.email = "developers@moviepilot.com"
19
+ gem.homepage = "http://github.com/janroesner/apirunner"
20
+ gem.authors = ["moviepilot"]
21
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
22
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
23
+ # spec.add_runtime_dependency 'jabber4r', '> 0.1'
24
+ # spec.add_development_dependency 'rspec', '> 1.2.3'
25
+ gem.add_development_dependency "rspec", ">= 2.0.0.beta.19"
26
+ gem.add_development_dependency "cucumber", ">= 0"
27
+ gem.add_development_dependency "bundler", "~> 1.0.0"
28
+ gem.add_development_dependency "jeweler", "~> 1.5.0.pre3"
29
+ gem.add_development_dependency "rcov", ">= 0"
30
+ gem.files += Dir['lib/**/*.rb', 'lib/tasks/*.rake']
31
+ end
32
+ Jeweler::RubygemsDotOrgTasks.new
33
+
34
+ require 'rspec/core'
35
+ require 'rspec/core/rake_task'
36
+ RSpec::Core::RakeTask.new(:spec) do |spec|
37
+ spec.pattern = FileList['spec/**/*_spec.rb']
38
+ end
39
+
40
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
41
+ spec.pattern = 'spec/**/*_spec.rb'
42
+ spec.rcov = true
43
+ end
44
+
45
+ require 'cucumber/rake/task'
46
+ Cucumber::Rake::Task.new(:features)
47
+
48
+ task :default => :spec
49
+
50
+ require 'rake/rdoctask'
51
+ Rake::RDocTask.new do |rdoc|
52
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
53
+
54
+ rdoc.rdoc_dir = 'rdoc'
55
+ rdoc.title = "apirunner #{version}"
56
+ rdoc.rdoc_files.include('README*')
57
+ rdoc.rdoc_files.include('lib/**/*.rb')
58
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.12
data/apirunner.gemspec ADDED
@@ -0,0 +1,101 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{apirunner}
8
+ s.version = "0.0.12"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["moviepilot"]
12
+ s.date = %q{2010-09-20}
13
+ s.description = %q{longer description of your gem}
14
+ s.email = %q{developers@moviepilot.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "Gemfile",
23
+ "Gemfile.lock",
24
+ "LICENSE",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "apirunner.gemspec",
29
+ "examples/config/api_runner.yml",
30
+ "examples/test/api_runner/001_create_ressource.yml",
31
+ "examples/test/api_runner/999_delete_ressource.yml",
32
+ "examples/test/api_runner/excludes.yml",
33
+ "features/apirunner.feature",
34
+ "features/step_definitions/apirunner_steps.rb",
35
+ "features/support/env.rb",
36
+ "lib/api_runner.rb",
37
+ "lib/apirunner.rb",
38
+ "lib/apirunner/railtie.rb",
39
+ "lib/expectation_matcher.rb",
40
+ "lib/http_client.rb",
41
+ "lib/tasks/api.rake",
42
+ "spec/.rspec",
43
+ "spec/expectation_matcher_spec.rb",
44
+ "spec/spec_helper.rb"
45
+ ]
46
+ s.homepage = %q{http://github.com/janroesner/apirunner}
47
+ s.require_paths = ["lib"]
48
+ s.rubygems_version = %q{1.3.7}
49
+ s.summary = %q{one-line summary of your gem}
50
+ s.test_files = [
51
+ "spec/expectation_matcher_spec.rb",
52
+ "spec/spec_helper.rb"
53
+ ]
54
+
55
+ if s.respond_to? :specification_version then
56
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
57
+ s.specification_version = 3
58
+
59
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
60
+ s.add_runtime_dependency(%q<nokogiri>, ["~> 1.4.3.1"])
61
+ s.add_runtime_dependency(%q<httparty>, ["~> 0.6.1"])
62
+ s.add_development_dependency(%q<rspec>, [">= 2.0.0.beta.19"])
63
+ s.add_development_dependency(%q<cucumber>, [">= 0"])
64
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
65
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.0.pre3"])
66
+ s.add_development_dependency(%q<rcov>, [">= 0"])
67
+ s.add_development_dependency(%q<rspec>, [">= 2.0.0.beta.19"])
68
+ s.add_development_dependency(%q<cucumber>, [">= 0"])
69
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
70
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.0.pre3"])
71
+ s.add_development_dependency(%q<rcov>, [">= 0"])
72
+ else
73
+ s.add_dependency(%q<nokogiri>, ["~> 1.4.3.1"])
74
+ s.add_dependency(%q<httparty>, ["~> 0.6.1"])
75
+ s.add_dependency(%q<rspec>, [">= 2.0.0.beta.19"])
76
+ s.add_dependency(%q<cucumber>, [">= 0"])
77
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
78
+ s.add_dependency(%q<jeweler>, ["~> 1.5.0.pre3"])
79
+ s.add_dependency(%q<rcov>, [">= 0"])
80
+ s.add_dependency(%q<rspec>, [">= 2.0.0.beta.19"])
81
+ s.add_dependency(%q<cucumber>, [">= 0"])
82
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
83
+ s.add_dependency(%q<jeweler>, ["~> 1.5.0.pre3"])
84
+ s.add_dependency(%q<rcov>, [">= 0"])
85
+ end
86
+ else
87
+ s.add_dependency(%q<nokogiri>, ["~> 1.4.3.1"])
88
+ s.add_dependency(%q<httparty>, ["~> 0.6.1"])
89
+ s.add_dependency(%q<rspec>, [">= 2.0.0.beta.19"])
90
+ s.add_dependency(%q<cucumber>, [">= 0"])
91
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
92
+ s.add_dependency(%q<jeweler>, ["~> 1.5.0.pre3"])
93
+ s.add_dependency(%q<rcov>, [">= 0"])
94
+ s.add_dependency(%q<rspec>, [">= 2.0.0.beta.19"])
95
+ s.add_dependency(%q<cucumber>, [">= 0"])
96
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
97
+ s.add_dependency(%q<jeweler>, ["~> 1.5.0.pre3"])
98
+ s.add_dependency(%q<rcov>, [">= 0"])
99
+ end
100
+ end
101
+
@@ -0,0 +1,15 @@
1
+ local:
2
+ protocol: http
3
+ host: localhost
4
+ port: 3000
5
+ namespace: api1v0
6
+ staging:
7
+ protocol: http
8
+ host: staging.moviepilot.dom
9
+ port: 8976
10
+ namespace: api1v0
11
+ production:
12
+ protocol: http
13
+ host: www.moviepilot.com
14
+ port: 80
15
+ namespace: api1v0
@@ -0,0 +1,25 @@
1
+ ---
2
+ - name: 'Create new User'
3
+ request:
4
+ path: '/users/duffyduck'
5
+ method: 'PUT'
6
+ body:
7
+ username: 'duffyduck'
8
+ response_expectation:
9
+ status_code: 201
10
+ headers:
11
+ Last-Modified: /.*/
12
+ body:
13
+ username: 'duffyduck'
14
+ fsk: "17"
15
+ - name: 'Update existing User - Update watchlist'
16
+ request:
17
+ path: '/users/duffyduck/watchlist'
18
+ method: 'PUT'
19
+ body:
20
+ - m367
21
+ - m73
22
+ response_expectation:
23
+ status_code: 204
24
+ body:
25
+
@@ -0,0 +1,8 @@
1
+ ---
2
+ - name: 'Delete User'
3
+ request:
4
+ path: '/users/duffyduck'
5
+ method: 'DELETE'
6
+ body: {}
7
+ response_expectation:
8
+ status_code: 202
@@ -0,0 +1,7 @@
1
+ local:
2
+ excludes:
3
+ - "content-length"
4
+ staging:
5
+ excludes:
6
+ - "Foo"
7
+ - "Bar"
@@ -0,0 +1,9 @@
1
+ Feature: something something
2
+ In order to something something
3
+ A user something something
4
+ something something something
5
+
6
+ Scenario: something something
7
+ Given inspiration
8
+ When I create a sweet new gem
9
+ Then everyone should see how awesome I am
File without changes
@@ -0,0 +1,13 @@
1
+ require 'bundler'
2
+ begin
3
+ Bundler.setup(:default, :development)
4
+ rescue Bundler::BundlerError => e
5
+ $stderr.puts e.message
6
+ $stderr.puts "Run `bundle install` to install missing gems"
7
+ exit e.status_code
8
+ end
9
+
10
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
11
+ require 'apirunner'
12
+
13
+ require 'spec/expectations'
data/lib/api_runner.rb ADDED
@@ -0,0 +1,84 @@
1
+ class ApiRunner
2
+ require 'yaml'
3
+ require 'expectation_matcher'
4
+ require 'http_client'
5
+
6
+ # initializes the object, loads environment, build base_uri
7
+ def initialize(env)
8
+ @http_client = HttpClient.new
9
+ @spec = []
10
+ @errors = []
11
+ @excludes = []
12
+ load_config(env)
13
+ load_excludes(env)
14
+ load_url_spec
15
+ @expectation = ExpectationMatcher.new(@excludes)
16
+ end
17
+
18
+ # checks servers availability and invokes test cases
19
+ def run
20
+ if server_is_available?
21
+ run_tests
22
+ @errors.try(:each_with_index) do |error, index|
23
+ puts("\n\nError (#{index+1}): #{error}")
24
+ end
25
+ else
26
+ puts("Server #{@host} seems to be unavailable!")
27
+ end
28
+ end
29
+
30
+ protected
31
+
32
+ # runs all testcases that are provided by the testclass an fills errors if there are any
33
+ def run_tests
34
+ @spec.each do |test_case|
35
+ response = send_request(test_case['request']['method'].downcase.to_sym, target_uri(test_case['request']['path']), {:body => test_case['request']['body'].to_json})
36
+ @expectation.test_types.each do |test_type|
37
+ test = @expectation.check(test_type, response, test_case)
38
+ if not test.succeeded
39
+ @errors << test.error
40
+ putc "F"
41
+ break
42
+ else
43
+ putc "." if test_type == @expectation.test_types.last
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ # sends http request and fetches response using the given http client
50
+ def send_request(method, uri, data)
51
+ @http_client.send_request(method, uri, data)
52
+ end
53
+
54
+ # builds target uri from base uri generated of host port and namespace as well as the ressource path
55
+ def target_uri(ressource_path)
56
+ "#{@protocol}://#{@host}:#{@port}/#{@namespace}" + ressource_path
57
+ end
58
+
59
+ # returns true if server is available
60
+ def server_is_available?
61
+ !@http_client.send_request(:get, "#{@protocol}://#{@host}:#{@port}", {:timeout => 5}).nil?
62
+ end
63
+
64
+ # loads environment config data from yaml file
65
+ def load_config(env)
66
+ config = YAML.load_file("config/api_runner.yml")
67
+ config[env.to_s].each { |key, value| instance_variable_set("@#{key}", value) }
68
+ end
69
+
70
+ # loads spec cases from yaml files
71
+ def load_url_spec
72
+ path = "test/api_runner/"
73
+ Dir.new(path).entries.each do |dir_entry|
74
+ @spec.push *YAML.load_file(path+dir_entry) if not (File.directory? dir_entry or dir_entry.match(/^\./) or dir_entry.match(/excludes/))
75
+ end
76
+ end
77
+
78
+ # loads and parses items that need to be excluded from the checks in certain environments
79
+ def load_excludes(env)
80
+ excludes_file = "test/api_runner/excludes.yml"
81
+ @excludes = YAML.load_file(excludes_file).detect{ |a| a.first == env.to_s }[1]["excludes"]
82
+ end
83
+ end
84
+
@@ -0,0 +1,11 @@
1
+ require 'apirunner'
2
+ require 'rails'
3
+
4
+ module Apirunner
5
+ class Railtie < Rails::Railtie
6
+
7
+ rake_tasks do
8
+ load "tasks/api.rake"
9
+ end
10
+ end
11
+ end
data/lib/apirunner.rb ADDED
@@ -0,0 +1,5 @@
1
+ APIRUNNER_ROOT=File.dirname(__FILE__) + "/.."
2
+ require 'api_runner'
3
+ module Apirunner
4
+ require 'apirunner/railtie' if defined?(Rails)
5
+ end
@@ -0,0 +1,165 @@
1
+ class ExpectationMatcher
2
+ require 'nokogiri'
3
+
4
+ def initialize(excludes=nil)
5
+ @test_types = [:response_code, :response_body_format, :response_headers, :response_body]
6
+ @excludes = excludes
7
+ end
8
+
9
+ # returns the available test types if this matcher class
10
+ def test_types
11
+ return @test_types
12
+ end
13
+
14
+ # dispatches incoming matching requests
15
+ def check(method, response, testcase)
16
+ self.send(method, response, testcase)
17
+ end
18
+
19
+ protected
20
+
21
+ # matches the given response code
22
+ def response_code(response, testcase)
23
+ result_struct = Struct.new(:succeeded, :error)
24
+ results = result_struct.new(:succeeded => true, :error => nil)
25
+ if not testcase['response_expectation']['status_code'] == response.code
26
+ results.succeeded = false
27
+ results.error = "testcase '#{testcase['name']}'\n expected response code --#{testcase['response_expectation']['status_code']}--\n got response code --#{response.code}--"
28
+ end
29
+ results
30
+ end
31
+
32
+ # checks the format of the given data of JSON conformity
33
+ # returns a structure containing return value and error if there is one
34
+ def response_body_format(response, testcase)
35
+ result_struct = Struct.new(:succeeded, :error)
36
+ results = result_struct.new(:succeeded => true, :error => nil)
37
+ if not valid_json?(response.body)
38
+ results.succeeded = false
39
+ results.error = "testcase '#{testcase['name']}'\n expected valid JSON in body\n got --#{response.body[1..400]}--"
40
+ end
41
+ results
42
+ end
43
+
44
+ # matches the given response header
45
+ def response_headers(response, testcase)
46
+ result_struct = Struct.new(:succeeded, :error)
47
+ results = result_struct.new(:succeeded => true, :error => nil)
48
+
49
+ testcase['response_expectation']['headers'].try(: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
+ results.succeeded = false
53
+ results.error = "testcase '#{testcase['name']}'\n 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
+ results.succeeded = false
58
+ results.error = "testcase '#{testcase['name']}'\n expected header identifier --#{header_name}-- to match --#{header_value}--\n got --#{response.headers[header_name]}--"
59
+ end
60
+ end
61
+ end
62
+ return results
63
+ end
64
+
65
+ # matches the given attributes and values against the ones from the response body
66
+ def response_body(response, testcase)
67
+ result_struct = Struct.new(:succeeded, :error)
68
+ results = result_struct.new(:succeeded => true, :error => nil)
69
+
70
+ expected_body_hash = testcase['response_expectation']['body']
71
+
72
+ # in case we have no body expectation we simply return success
73
+ return results if expected_body_hash.nil?
74
+
75
+ # in case the response body is nil or damaged we return an error
76
+ begin
77
+ responded_body_hash = JSON.parse(response.body)
78
+ rescue
79
+ results.succeeded = false
80
+ results.error = "testcase '#{testcase['name']}'\n expected response to have a body\n got raw body --#{response.body}-- which is nil or an unparseable hash"
81
+ return results
82
+ end
83
+
84
+ # else we build trees from both body structures...
85
+ expectation_tree = Nokogiri::XML(expected_body_hash.to_xml({ :indent => 0 }))
86
+ response_tree = Nokogiri::XML(responded_body_hash.to_xml({ :indent => 0 }))
87
+
88
+ # retrieve all the leafs pathes and match the leafs values using xpath
89
+ matcher_pathes_from(expectation_tree).each do |path|
90
+ expectation_node = expectation_tree.xpath(path).first
91
+ response_node = response_tree.xpath(path).first
92
+
93
+ # return error if response body does not have the expected entry
94
+ if response_node.nil?
95
+ results.succeeded = false
96
+ results.error = "testcase '#{testcase['name']}'\n expected body to have identifier --#{expectation_node.name}--\n got nil"
97
+ return results
98
+ end
99
+
100
+ # last but not least try the regex or direct match and return errors in case of any
101
+ if is_regex?(expectation_node.text)
102
+ if not (excluded?(expectation_node.name) or regex_matches?(expectation_node.text, response_node.text))
103
+ results.succeeded = false
104
+ results.error = "testcase '#{testcase['name']}'\n expected body identifier --#{expectation_node.name}-- to match regex --#{expectation_node.text}--\n got --#{response_node.text}--"
105
+ end
106
+ else
107
+ if not (excluded?(expectation_node.name) or string_matches?(expectation_node.text, response_node.text))
108
+ results.succeeded = false
109
+ results.error = "testcase '#{testcase['name']}'\n expected body identifier --#{expectation_node.name}-- to match --#{expectation_node.text}--\n got --#{response_node.text}--"
110
+ end
111
+ end
112
+ end
113
+ results
114
+ end
115
+
116
+ # recursively parses the tree and returns a set of relative pathes
117
+ # that can be used to match the both trees leafs
118
+ def matcher_pathes_from(node, pathes = nil)
119
+ pathes ||= []
120
+ if not node.children.blank?
121
+ node.children.each do |sub_node|
122
+ matcher_pathes_from(sub_node, pathes)
123
+ end
124
+ else
125
+ pathes << relative_path(node.parent.path)
126
+ end
127
+ pathes
128
+ end
129
+
130
+ # returns relative path for matching the target tree of the response body
131
+ # explicit array adressing is replaced by *
132
+ def relative_path(path)
133
+ path.gsub(/\/([^\/]+)\[\d+\]\//i,"/*/")
134
+ end
135
+
136
+ # returns true if given attributes is an excluded item that does not have to be evaluated in this environment
137
+ def excluded?(item)
138
+ @excludes.include?(item)
139
+ end
140
+
141
+ # returns true if given string seems to be a regular expression
142
+ def is_regex?(string)
143
+ string.to_s.match(/^\/.+\/$/)
144
+ end
145
+
146
+ # returns true if the given regular expression matches the given value
147
+ def regex_matches?(regex, value)
148
+ debugger
149
+ regex = Regexp.compile( regex.gsub(/^\//, '').gsub(/\/$/,'') )
150
+ !!value.to_s.match(regex)
151
+ end
152
+
153
+ # returns true if the given string exactly matches the given value
154
+ def string_matches?(string, value)
155
+ string.to_s == value.to_s
156
+ end
157
+
158
+ # parses output into JSON object
159
+ def valid_json?(response_body)
160
+ # responses may be nil, return true then
161
+ return true if response_body.blank?
162
+ # returns true if given response is valid json, else false
163
+ JSON.parse(response_body.to_s) rescue false
164
+ end
165
+ end
@@ -0,0 +1,22 @@
1
+ class HttpClient
2
+ require 'httparty'
3
+ include HTTParty
4
+
5
+ # sends http request with given method, uri and data and returns servers response
6
+ def send_request(method, uri, data=nil)
7
+ build_response(self.class.send(method, uri, data))
8
+ end
9
+
10
+ # returns struct containing response.code, headers, body and message
11
+ # this is only for easily interfaceing another http client
12
+ def build_response(raw_response)
13
+ response_struct = Struct.new(:code, :message, :headers, :body)
14
+ response = response_struct.new
15
+ response.code = raw_response.code
16
+ response.message = raw_response.message
17
+ response.headers = raw_response.headers
18
+ response.body = raw_response.body
19
+ response
20
+ end
21
+ end
22
+
@@ -0,0 +1,26 @@
1
+ config = YAML.load_file("#{Rails.root}/vendor/plugins/telekom_api/config/api_runner.yaml")
2
+
3
+ namespace :api do
4
+ namespace :run do
5
+ config.each_key do |env|
6
+ desc "runs a series of nessecary api calls and parses their response in environment #{env}"
7
+ task env.to_sym => :environment do
8
+ puts "Running API tests in environment #{env}"
9
+ api_runner = ApiRunner.new(env)
10
+ api_runner.run
11
+ puts "\nTestrun finished\n\n"
12
+ end
13
+ end
14
+ end
15
+ desc "generates configuration and a skeleton for apirunner tests as well as excludes"
16
+ task :scaffold do
17
+ FileUtils.mkdir_p( "test/api_runner" )
18
+ FileUtils.mkdir_p( "config" )
19
+ FileUtils.cp_r( "#{APIRUNNER_ROOT}/examples/config", ".")
20
+ FileUtils.cp_r( "#{APIRUNNER_ROOT}/examples/test", ".")
21
+ puts "created the following files:"
22
+ Dir.glob("#{APIRUNNER_ROOT}/examples/**/*").each do |file|
23
+ puts "\t#{file.gsub(/.*examples\//, '')}"
24
+ end
25
+ end
26
+ end
data/spec/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,13 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe 'ExpectationMatcher' do
4
+ describe 'regex_matches?' do
5
+ it 'should return true if given regex matches the given string' do
6
+ ExpectationMatcher.new.send(:regex_matches?, "/^\\d{2}$/", "12").should be_true
7
+ end
8
+ it 'should return false if the given regex does not match the given string' do
9
+ ExpectationMatcher.new.send(:regex_matches?, "/^\\d{2}$/", "133").should be_false
10
+ end
11
+ end
12
+ end
13
+
@@ -0,0 +1,21 @@
1
+ require 'bundler'
2
+ begin
3
+ Bundler.setup(:default, :development)
4
+ rescue Bundler::BundlerError => e
5
+ $stderr.puts e.message
6
+ $stderr.puts "Run `bundle install` to install missing gems"
7
+ exit e.status_code
8
+ end
9
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
10
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
11
+ require 'apirunner'
12
+ require 'rspec'
13
+ # require 'rspec/autorun'
14
+
15
+ # Requires supporting files with custom matchers and macros, etc,
16
+ # in ./support/ and its subdirectories.
17
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
18
+
19
+ RSpec.configure do |config|
20
+
21
+ end
metadata ADDED
@@ -0,0 +1,269 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apirunner
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 12
9
+ version: 0.0.12
10
+ platform: ruby
11
+ authors:
12
+ - moviepilot
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-09-20 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: nokogiri
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 1
29
+ - 4
30
+ - 3
31
+ - 1
32
+ version: 1.4.3.1
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: httparty
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ segments:
44
+ - 0
45
+ - 6
46
+ - 1
47
+ version: 0.6.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: rspec
53
+ requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ segments:
59
+ - 2
60
+ - 0
61
+ - 0
62
+ - beta
63
+ - 19
64
+ version: 2.0.0.beta.19
65
+ type: :development
66
+ prerelease: false
67
+ version_requirements: *id003
68
+ - !ruby/object:Gem::Dependency
69
+ name: cucumber
70
+ requirement: &id004 !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ type: :development
79
+ prerelease: false
80
+ version_requirements: *id004
81
+ - !ruby/object:Gem::Dependency
82
+ name: bundler
83
+ requirement: &id005 !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ~>
87
+ - !ruby/object:Gem::Version
88
+ segments:
89
+ - 1
90
+ - 0
91
+ - 0
92
+ version: 1.0.0
93
+ type: :development
94
+ prerelease: false
95
+ version_requirements: *id005
96
+ - !ruby/object:Gem::Dependency
97
+ name: jeweler
98
+ requirement: &id006 !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ segments:
104
+ - 1
105
+ - 5
106
+ - 0
107
+ - pre3
108
+ version: 1.5.0.pre3
109
+ type: :development
110
+ prerelease: false
111
+ version_requirements: *id006
112
+ - !ruby/object:Gem::Dependency
113
+ name: rcov
114
+ requirement: &id007 !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ segments:
120
+ - 0
121
+ version: "0"
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: *id007
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec
127
+ requirement: &id008 !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ segments:
133
+ - 2
134
+ - 0
135
+ - 0
136
+ - beta
137
+ - 19
138
+ version: 2.0.0.beta.19
139
+ type: :development
140
+ prerelease: false
141
+ version_requirements: *id008
142
+ - !ruby/object:Gem::Dependency
143
+ name: cucumber
144
+ requirement: &id009 !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ segments:
150
+ - 0
151
+ version: "0"
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: *id009
155
+ - !ruby/object:Gem::Dependency
156
+ name: bundler
157
+ requirement: &id010 !ruby/object:Gem::Requirement
158
+ none: false
159
+ requirements:
160
+ - - ~>
161
+ - !ruby/object:Gem::Version
162
+ segments:
163
+ - 1
164
+ - 0
165
+ - 0
166
+ version: 1.0.0
167
+ type: :development
168
+ prerelease: false
169
+ version_requirements: *id010
170
+ - !ruby/object:Gem::Dependency
171
+ name: jeweler
172
+ requirement: &id011 !ruby/object:Gem::Requirement
173
+ none: false
174
+ requirements:
175
+ - - ~>
176
+ - !ruby/object:Gem::Version
177
+ segments:
178
+ - 1
179
+ - 5
180
+ - 0
181
+ - pre3
182
+ version: 1.5.0.pre3
183
+ type: :development
184
+ prerelease: false
185
+ version_requirements: *id011
186
+ - !ruby/object:Gem::Dependency
187
+ name: rcov
188
+ requirement: &id012 !ruby/object:Gem::Requirement
189
+ none: false
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ segments:
194
+ - 0
195
+ version: "0"
196
+ type: :development
197
+ prerelease: false
198
+ version_requirements: *id012
199
+ description: longer description of your gem
200
+ email: developers@moviepilot.com
201
+ executables: []
202
+
203
+ extensions: []
204
+
205
+ extra_rdoc_files:
206
+ - LICENSE
207
+ - README.rdoc
208
+ files:
209
+ - .document
210
+ - .gitignore
211
+ - Gemfile
212
+ - Gemfile.lock
213
+ - LICENSE
214
+ - README.rdoc
215
+ - Rakefile
216
+ - VERSION
217
+ - apirunner.gemspec
218
+ - examples/config/api_runner.yml
219
+ - examples/test/api_runner/001_create_ressource.yml
220
+ - examples/test/api_runner/999_delete_ressource.yml
221
+ - examples/test/api_runner/excludes.yml
222
+ - features/apirunner.feature
223
+ - features/step_definitions/apirunner_steps.rb
224
+ - features/support/env.rb
225
+ - lib/api_runner.rb
226
+ - lib/apirunner.rb
227
+ - lib/apirunner/railtie.rb
228
+ - lib/expectation_matcher.rb
229
+ - lib/http_client.rb
230
+ - lib/tasks/api.rake
231
+ - spec/.rspec
232
+ - spec/expectation_matcher_spec.rb
233
+ - spec/spec_helper.rb
234
+ has_rdoc: true
235
+ homepage: http://github.com/janroesner/apirunner
236
+ licenses: []
237
+
238
+ post_install_message:
239
+ rdoc_options: []
240
+
241
+ require_paths:
242
+ - lib
243
+ required_ruby_version: !ruby/object:Gem::Requirement
244
+ none: false
245
+ requirements:
246
+ - - ">="
247
+ - !ruby/object:Gem::Version
248
+ hash: 897002952482873353
249
+ segments:
250
+ - 0
251
+ version: "0"
252
+ required_rubygems_version: !ruby/object:Gem::Requirement
253
+ none: false
254
+ requirements:
255
+ - - ">="
256
+ - !ruby/object:Gem::Version
257
+ segments:
258
+ - 0
259
+ version: "0"
260
+ requirements: []
261
+
262
+ rubyforge_project:
263
+ rubygems_version: 1.3.7
264
+ signing_key:
265
+ specification_version: 3
266
+ summary: one-line summary of your gem
267
+ test_files:
268
+ - spec/expectation_matcher_spec.rb
269
+ - spec/spec_helper.rb