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.
- checksums.yaml +7 -0
- data/data/swagger_2.0_schema.json +1513 -0
- data/lib/apivore.rb +48 -0
- data/lib/apivore/rspec_builder.rb +114 -0
- data/lib/apivore/rspec_matchers.rb +79 -0
- metadata +203 -0
data/lib/apivore.rb
ADDED
@@ -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:
|