apivore 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,48 @@
1
+ require 'apivore/rspec_builder'
2
+ require 'apivore/rspec_matchers'
3
+
4
+ module Apivore
5
+ class Swagger < Hashie::Mash
6
+
7
+ def validate
8
+ case version
9
+ when '2.0'
10
+ schema = File.read(File.expand_path("../../data/swagger_2.0_schema.json", __FILE__))
11
+ else
12
+ raise "Unknown/unsupported Swagger version to validate against: #{version}"
13
+ end
14
+ JSON::Validator.fully_validate(schema, self)
15
+ end
16
+
17
+ def version
18
+ swagger
19
+ end
20
+
21
+ def base_path
22
+ self['basePath'] || ''
23
+ end
24
+
25
+ def each_response(&block)
26
+ paths.each do |path, path_data|
27
+ path_data.each do |verb, method_data|
28
+ raise "No responses found in swagger for path '#{path}', method #{verb}: #{method_data.inspect}" if method_data.responses.nil?
29
+ method_data.responses.each do |response_code, response_data|
30
+ schema_location = nil
31
+ if response_data.schema
32
+ schema_location = Fragment.new ['#', 'paths', path, verb, 'responses', response_code, 'schema']
33
+ end
34
+ block.call(path, verb, response_code, schema_location)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ # This is a workaround for json-schema's fragment validation which does not allow paths to contain forward slashes
42
+ # current json-schema attempts to split('/') on a string path to produce an array.
43
+ class Fragment < Array
44
+ def split(options = nil)
45
+ self
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,114 @@
1
+ require 'apivore/rspec_matchers'
2
+ require 'action_controller'
3
+ require 'action_dispatch'
4
+ require 'rspec/mocks'
5
+ require 'hashie'
6
+
7
+ module Apivore
8
+ module RspecBuilder
9
+ include Apivore::RspecMatchers
10
+ include ActionDispatch::Integration
11
+ include RSpec::Mocks::ExampleMethods
12
+
13
+ @@setups ||= {}
14
+
15
+ @@master_swagger_uri = nil
16
+
17
+ # Setup tests against a combination of path, method, and response.
18
+ # - *keys -> A combination of path, method, and/or response. Blank '' for base setup.
19
+ # - &block -> Code block to execute to setup the test. A hash of path subsitution parameters can be returned if required.
20
+ # All matching code blocks are executed, and substitution parameters are merged in order of specificity.
21
+ def apivore_setup(*keys, &block)
22
+ @@setups[keys.join] = block
23
+ end
24
+
25
+ def get_apivore_setup(path, method, response)
26
+ keys_to_search = [
27
+ '', # base setup key
28
+ response,
29
+ method,
30
+ path,
31
+ method + response,
32
+ path + response,
33
+ path + method,
34
+ path + method + response
35
+ ]
36
+ final_result = {}
37
+ keys_to_search.each do |k|
38
+ setup = @@setups[k]
39
+ if setup
40
+ result = instance_eval &setup
41
+ final_result.merge!(result) if result.is_a?(Hash)
42
+ end
43
+ end
44
+ final_result
45
+ end
46
+
47
+ def apivore_build_path(path, data)
48
+ path.scan(/\{([^\}]*)\}/).each do |param|
49
+ key = param.first
50
+ if data && data[key]
51
+ path = path.gsub "{#{key}}", data[key].to_s
52
+ else
53
+ raise URI::InvalidURIError, "No substitution data found for {#{key}} to test the path #{path}.\nAdd it via an:\n apivore_setup '<path>', '<method>', '<response>' do\n { '#{key}' => <value> }\n end\nblock in your specs.", caller
54
+ end
55
+ end
56
+ path + (data['_query_string'] ? "?#{data['_query_string']}" : '')
57
+ end
58
+
59
+ def apivore_check_consistency_with_swagger_at(uri, current_service = nil)
60
+ @@current_service = current_service
61
+ @@master_swagger_uri = uri
62
+ end
63
+
64
+ def apivore_swagger(swagger_path)
65
+ session = ActionDispatch::Integration::Session.new(Rails.application)
66
+ begin
67
+ session.get swagger_path
68
+ rescue
69
+ # TODO: make this fail inside rspec test execution rather than immediately raise an exception.
70
+ # ALSO, handle other scenarios where we can't get a response to generate tests, e.g 500s, invalid formats etc
71
+ raise "Unable to perform GET request for swagger json: #{swagger_path} - #{$!}."
72
+ end
73
+ Apivore::Swagger.new JSON.parse(session.response.body)
74
+ end
75
+
76
+ def validate(swagger_path)
77
+
78
+ describe "swagger documentation" do
79
+ before { get swagger_path }
80
+ subject { body }
81
+ it { should be_valid_swagger }
82
+ it { should have_models_for_all_get_endpoints }
83
+ if @@master_swagger_uri
84
+ req = Net::HTTP.get(@@master_swagger_uri, "/swagger.json")
85
+ master_swagger = JSON.parse(req)
86
+ it { should be_consistent_with_swagger_definitions master_swagger, @@current_service }
87
+ end
88
+ end
89
+
90
+ swagger = apivore_swagger(swagger_path)
91
+ swagger.each_response do |path, method, response_code, fragment|
92
+ describe "path #{path} method #{method} response #{response_code}" do
93
+ it "responds with the specified models" do
94
+ setup_data = get_apivore_setup(path, method, response_code)
95
+ full_path = apivore_build_path(swagger.base_path + path, setup_data)
96
+ # e.g., get(full_path)
97
+ begin
98
+ send(method, full_path, setup_data['_data'] || {}, setup_data['_headers'] || {})
99
+ rescue
100
+ raise "Unable to #{method} #{full_path} -- invalid response from server: #{$!}."
101
+ end
102
+ expect(response).to have_http_status(response_code), "expected #{response_code} array, got #{response.status}: #{response.body}"
103
+
104
+ if fragment
105
+ expect(response.body).to conform_to_the_documented_model_for(swagger, fragment)
106
+ end
107
+
108
+ end
109
+ end
110
+ end
111
+
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,79 @@
1
+ require 'json-schema'
2
+ require 'rspec/expectations'
3
+ require 'net/http'
4
+
5
+ module Apivore
6
+ module RspecMatchers
7
+ extend RSpec::Matchers::DSL
8
+ matcher :be_valid_swagger do |version|
9
+ match do |body|
10
+ @api_description = Swagger.new(JSON.parse(body))
11
+ @errors = @api_description.validate
12
+ @errors.empty?
13
+ end
14
+
15
+ failure_message do |body|
16
+ msg = "The document fails to validate as Swagger #{@api_description.version}:\n"
17
+ msg += @errors.join("\n")
18
+ end
19
+ end
20
+
21
+ matcher :have_models_for_all_get_endpoints do
22
+ match do |body|
23
+ @errors = []
24
+ swagger = Swagger.new(JSON.parse(body))
25
+ swagger.each_response do |path, method, response_code, schema|
26
+ if method == 'get' && !schema
27
+ @errors << "Unable to find a valid model for #{path} get #{response_code} response."
28
+ end
29
+ end
30
+ @errors.empty?
31
+ end
32
+
33
+ failure_message do
34
+ @errors.join("\n")
35
+ end
36
+ end
37
+
38
+ matcher :be_consistent_with_swagger_definitions do |master_swagger, current_service|
39
+
40
+ attr_reader :actual, :expected
41
+
42
+ def cleaned_definitions(definitions, current_service)
43
+ definitions.each do |key, definition_fields|
44
+ # We ignore definitions that are owned exclusively by the current_service
45
+ if [current_service] == definition_fields['x-services']
46
+ definitions[key] = nil
47
+ else
48
+ # 'x-services' is added by api.westfield.io when aggregating swagger docs
49
+ # Individual services will not have a 'x-services' property so we need to remove it to allow the comparison to pass
50
+ definitions[key] = definition_fields.except 'x-services'
51
+ end
52
+ end.select{ |_, value| !value.nil? }
53
+ end
54
+
55
+ match do |body|
56
+ our_swagger = JSON.parse(body)
57
+ master_definitions = cleaned_definitions(master_swagger["definitions"], current_service)
58
+ our_definitions = our_swagger["definitions"]
59
+ @actual = our_definitions.slice(*master_definitions.keys)
60
+ @expected = master_definitions.slice(*our_definitions.keys)
61
+ @actual == @expected
62
+ end
63
+
64
+ diffable
65
+ end
66
+
67
+ matcher :conform_to_the_documented_model_for do |swagger, fragment|
68
+ match do |body|
69
+ body = JSON.parse(body)
70
+ @errors = JSON::Validator.fully_validate(swagger, body, fragment: fragment)
71
+ @errors.empty?
72
+ end
73
+
74
+ failure_message do |body|
75
+ @errors.map { |e| e.sub("'#", "'#{path}#").gsub(/^The property|in schema.*$/,'') }.join("\n")
76
+ end
77
+ end
78
+ end
79
+ end
metadata ADDED
@@ -0,0 +1,203 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apivore
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Charles Horn
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json-schema
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.5.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.5.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec-expectations
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec-mocks
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: actionpack
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '4'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: hashie
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 3.3.1
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 3.3.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec-rails
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: activesupport
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: test-unit
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ description: Automatically tests your API using its Swagger description of end-points,
168
+ models, and query parameters.
169
+ email: charles.horn@gmail.com
170
+ executables: []
171
+ extensions: []
172
+ extra_rdoc_files: []
173
+ files:
174
+ - data/swagger_2.0_schema.json
175
+ - lib/apivore.rb
176
+ - lib/apivore/rspec_builder.rb
177
+ - lib/apivore/rspec_matchers.rb
178
+ homepage: http://github.com/westfieldlabs/apivore
179
+ licenses: []
180
+ metadata: {}
181
+ post_install_message:
182
+ rdoc_options: []
183
+ require_paths:
184
+ - lib
185
+ required_ruby_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ required_rubygems_version: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ requirements: []
196
+ rubyforge_project:
197
+ rubygems_version: 2.4.6
198
+ signing_key:
199
+ specification_version: 4
200
+ summary: Automatically tests your API using its Swagger description of end-points,
201
+ models, and query parameters.
202
+ test_files: []
203
+ has_rdoc: