apivore 0.0.2

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.
@@ -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: