api-validator 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in api-validator.gemspec
4
+ gemspec
5
+
6
+ gem 'json-pointer', :git => 'git://github.com/tent/json-pointer-ruby.git', :branch => 'master'
data/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2013 Apollic Software, LLC. All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are
5
+ met:
6
+
7
+ * Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above
10
+ copyright notice, this list of conditions and the following disclaimer
11
+ in the documentation and/or other materials provided with the
12
+ distribution.
13
+ * Neither the name of Apollic Software, LLC nor the names of its
14
+ contributors may be used to endorse or promote products derived from
15
+ this software without specific prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,80 @@
1
+ # ApiValidator [![Build Status](https://travis-ci.org/tent/api-validator.png)](https://travis-ci.org/tent/api-validator)
2
+
3
+ Framework for integration testing an API server.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'api-validator'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install api-validator
18
+
19
+ ## Usage
20
+
21
+ ```ruby
22
+ class MyValidation < ApiValidator::Spec
23
+
24
+ def create_resource
25
+ # ...
26
+ set(:resource, data)
27
+ end
28
+
29
+ def fudge_resource
30
+ # ...
31
+ set(:resource, fudged_data)
32
+ end
33
+
34
+ describe "GET /posts/{entity}/{id}" do
35
+ shared_examples :not_found do
36
+ expect_response(:schema => :error, :status => 404) do
37
+ # ...
38
+ response
39
+ end
40
+ end
41
+
42
+ context "when resource exists", :before => :create_resource do
43
+ context "when authorized to read all posts" do
44
+ authorize!(:server => :remote, :scopes => %w[ read_posts ], :read_types => %w[ all ])
45
+
46
+ expect_response(:schema => :post, :status => 200) do
47
+ expect_headers({ 'Content-Type' => /json\b/ })
48
+ expect_properties(:entity => get(:resource, :entity), :id => get(:resource, :id))
49
+ expect_schema(:post_content, "/content")
50
+
51
+ # ...
52
+ response
53
+ end
54
+ end
55
+
56
+ context "when authorize to write resource but not read" do
57
+ authorize!(:server => :remote, :scopes => %w[ write_posts ], :write_types => %w[ all ])
58
+ behaves_as :not_found
59
+ end
60
+
61
+ context "when not authorized" do
62
+ behaves_as :not_found
63
+ end
64
+ end
65
+
66
+ context "when resource does not exist", :before => :fudge_resource do
67
+ behaves_as :not_found
68
+ end
69
+ end
70
+
71
+ end
72
+ ```
73
+
74
+ ## Contributing
75
+
76
+ 1. Fork it
77
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
78
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
79
+ 4. Push to the branch (`git push origin my-new-feature`)
80
+ 5. Create new Pull Request
@@ -0,0 +1,8 @@
1
+ require 'bundler/setup'
2
+ require 'bundler/gem_tasks'
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec) do |spec|
6
+ spec.pattern = 'spec/**/*_spec.rb'
7
+ end
8
+ task :default => :spec
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'api-validator/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "api-validator"
8
+ gem.version = ApiValidator::VERSION
9
+ gem.authors = ["Jesse Stuart"]
10
+ gem.email = ["jesse@jessestuart.ca"]
11
+ gem.description = %q{ }
12
+ gem.summary = %q{ }
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_runtime_dependency 'json-pointer'
21
+
22
+ gem.add_development_dependency 'rspec', '~> 2.11'
23
+ gem.add_development_dependency 'mocha', '~> 0.13'
24
+ gem.add_development_dependency "bundler", "~> 1.3"
25
+ gem.add_development_dependency "rake"
26
+ gem.add_development_dependency "faraday"
27
+ end
@@ -0,0 +1,19 @@
1
+ require 'api-validator/version'
2
+
3
+ module ApiValidator
4
+
5
+ require 'api-validator/mixins'
6
+
7
+ require 'api-validator/assertion'
8
+ require 'api-validator/base'
9
+ require 'api-validator/json_schemas'
10
+ require 'api-validator/json_schema'
11
+ require 'api-validator/json'
12
+ require 'api-validator/json'
13
+ require 'api-validator/header'
14
+ require 'api-validator/status'
15
+
16
+ require 'api-validator/response_expectation'
17
+ require 'api-validator/spec'
18
+
19
+ end
@@ -0,0 +1,32 @@
1
+
2
+ module ApiValidator
3
+ class Assertion
4
+
5
+ attr_reader :value, :path, :type, :format
6
+ def initialize(path, value, options = {})
7
+ @path, @value, @type, @format = path, value, options.delete(:type), options.delete(:format)
8
+ end
9
+
10
+ def to_hash(options = {})
11
+ _h = { :op => "test", :path => path, :value => stringified_value }
12
+ _h.delete(:value) if type && value.nil?
13
+ _h[:type] = "regexp" if Regexp === value
14
+ _h[:type] = type if type
15
+ _h[:format] = format if format
16
+ _h
17
+ end
18
+
19
+ def stringified_value
20
+ if Regexp === value
21
+ regex = value.to_s.
22
+ sub(%r|\(\?-mix:(.*)\)|) { $1 }.
23
+ gsub("\\A", "^").
24
+ gsub("\\Z", "$")
25
+ "/#{regex}/"
26
+ else
27
+ value
28
+ end
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,47 @@
1
+ require 'uri'
2
+
3
+ module ApiValidator
4
+ class Base
5
+
6
+ def initialize(expected)
7
+ @expected = expected
8
+ initialize_assertions(expected)
9
+ end
10
+
11
+ def assertions
12
+ @assertions ||= []
13
+ end
14
+
15
+ def validate(response)
16
+ {
17
+ :assertions => assertions.map(&:to_hash)
18
+ }
19
+ end
20
+
21
+ private
22
+
23
+ def assertion_valid?(assertion, actual)
24
+ value = assertion.value
25
+ case value
26
+ when Regexp
27
+ value.match(actual.to_s)
28
+ when Numeric
29
+ (Numeric === actual) && (value == actual)
30
+ else
31
+ value == actual
32
+ end
33
+ end
34
+
35
+ def assertion_format_valid?(assertion, actual)
36
+ return true unless format = assertion.format
37
+ case format
38
+ when 'uri'
39
+ uri = URI(actual)
40
+ uri.scheme && uri.host
41
+ end
42
+ rescue URI::InvalidURIError, ArgumentError
43
+ false
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,77 @@
1
+ module ApiValidator
2
+ class Header < Base
3
+
4
+ def self.named_expectations
5
+ @named_expectations ||= {}
6
+ end
7
+
8
+ def self.register(name, expected)
9
+ named_expectations[name] = expected
10
+ end
11
+
12
+ def validate(response)
13
+ compiled_assertions = compile_assertions(response)
14
+ response_headers = response.env[:response_headers]
15
+ _failed_assertions = failed_assertions(compiled_assertions, response_headers)
16
+ super.merge(
17
+ :assertions => compiled_assertions.map(&:to_hash),
18
+ :key => :response_headers,
19
+ :failed_assertions => _failed_assertions.map(&:to_hash),
20
+ :diff => diff(response_headers, _failed_assertions).map(&:to_hash),
21
+ :valid => _failed_assertions.empty?
22
+ )
23
+ end
24
+
25
+ private
26
+
27
+ NoSuchExpectationError = Class.new(StandardError)
28
+ def initialize_assertions(expected)
29
+ unless Hash === expected
30
+ name = expected
31
+ unless expected = self.class.named_expectations[name]
32
+ raise NoSuchExpectationError.new("Expected #{name.inspect} to be registered with #{self.class.name}!")
33
+ end
34
+ end
35
+
36
+ @assertions = expected.inject([]) do |memo, (header, value)|
37
+ memo << Assertion.new("/#{header}", value)
38
+ end
39
+ end
40
+
41
+ def compile_assertions(response)
42
+ assertions.map do |assertion|
43
+ if Proc === assertion.value
44
+ Assertion.new(assertion.path, assertion.value.call(response))
45
+ else
46
+ assertion
47
+ end
48
+ end
49
+ end
50
+
51
+ def failed_assertions(assertions, actual)
52
+ assertions.select do |assertion|
53
+ header = key_from_path(assertion.path)
54
+ !assertion_valid?(assertion, actual[header])
55
+ end
56
+ end
57
+
58
+ def diff(actual, _failed_assertions)
59
+ _failed_assertions.map do |assertion|
60
+ header = key_from_path(assertion.path)
61
+ assertion = assertion.to_hash
62
+ if actual.has_key?(header)
63
+ assertion[:op] = "replace"
64
+ assertion[:current_value] = actual[header]
65
+ else
66
+ assertion[:op] = "add"
67
+ end
68
+ assertion
69
+ end
70
+ end
71
+
72
+ def key_from_path(path)
73
+ path.slice(1, path.length) # remove prefixed "/"
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,56 @@
1
+ module ApiValidator
2
+ class Json < Base
3
+
4
+ def validate(response)
5
+ response_body = response.body.respond_to?(:to_hash) ? response.body.to_hash : response.body
6
+ _failed_assertions = failed_assertions(response_body)
7
+ super.merge(
8
+ :key => :response_body,
9
+ :failed_assertions => _failed_assertions.map(&:to_hash),
10
+ :diff => diff(response_body, _failed_assertions),
11
+ :valid => _failed_assertions.empty?
12
+ )
13
+ end
14
+
15
+ private
16
+
17
+ def initialize_assertions(expected, path = "")
18
+ case expected
19
+ when Hash
20
+ expected.each_pair do |key, val|
21
+ item_path = [path, key].join("/")
22
+ initialize_assertions(val, item_path)
23
+ end
24
+ when Array
25
+ expected.each_with_index do |val, index|
26
+ item_path = [path, index].join("/")
27
+ initialize_assertions(val, item_path)
28
+ end
29
+ else
30
+ assertions << Assertion.new(path, expected)
31
+ end
32
+ end
33
+
34
+ def failed_assertions(actual)
35
+ assertions.select do |assertion|
36
+ pointer = JsonPointer.new(actual, assertion.path)
37
+ !pointer.exists? || !assertion_valid?(assertion, pointer.value)
38
+ end
39
+ end
40
+
41
+ def diff(actual, _failed_assertions)
42
+ _failed_assertions.map do |assertion|
43
+ pointer = JsonPointer.new(actual, assertion.path)
44
+ assertion = assertion.to_hash
45
+ if pointer.exists?
46
+ assertion[:op] = "replace"
47
+ assertion[:current_value] = pointer.value
48
+ else
49
+ assertion[:op] = "add"
50
+ end
51
+ assertion
52
+ end
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,228 @@
1
+ require 'json-pointer'
2
+
3
+ module ApiValidator
4
+ class JsonSchema < Base
5
+
6
+ SchemaNotFoundError = Class.new(StandardError)
7
+
8
+ attr_reader :root_path
9
+ def initialize(expected, root_path = nil)
10
+ @expected, @root_path = expected, root_path
11
+
12
+ if Hash === expected
13
+ schema = expected
14
+ else
15
+ unless schema = JsonSchemas[expected]
16
+ raise SchemaNotFoundError.new("Unable to locate schema: #{expected.inspect}")
17
+ end
18
+ end
19
+
20
+ @schema = schema
21
+
22
+ initialize_assertions(schema)
23
+ end
24
+
25
+ def validate(response)
26
+ response_body = response.body.respond_to?(:to_hash) ? response.body.to_hash : response.body
27
+ _failed_assertions = failed_assertions(response_body)
28
+ _diff = diff(response_body, _failed_assertions)
29
+ super.merge(
30
+ :key => :response_body,
31
+ :failed_assertions => _failed_assertions.map(&:to_hash),
32
+ :diff => _diff,
33
+ :valid => _diff.empty?
34
+ )
35
+ end
36
+
37
+ def initialize_assertions(schema, parent_path = "")
38
+ parent_path = root_path if root_path && parent_path == ""
39
+ (schema["properties"] || {}).each_pair do |key, val|
40
+ next unless val["required"] == true
41
+ path = [parent_path, key].join("/")
42
+ assertions << Assertion.new(path, nil, :type => val["type"])
43
+ if val["type"] == "object"
44
+ initialize_assertions(val, path)
45
+ end
46
+ end
47
+ end
48
+
49
+ def failed_assertions(actual)
50
+ assertions.select do |assertion|
51
+ pointer = JsonPointer.new(actual, assertion.path)
52
+ !pointer.exists? || !assertion_valid?(assertion, pointer.value)
53
+ end
54
+ end
55
+
56
+ def diff(actual, _failed_assertions)
57
+ _diff = _failed_assertions.inject([]) do |memo, assertion|
58
+ pointer = JsonPointer.new(actual, assertion.path)
59
+ if !pointer.exists?
60
+ assertion = assertion.to_hash
61
+ actual_value = nil
62
+ assertion[:op] = "add"
63
+ assertion[:value] = value_for_schema_type(assertion[:type], actual_value)
64
+ assertion[:message] = wrong_type_message(assertion[:type], schema_type(actual_value))
65
+ memo << assertion
66
+ end
67
+ memo
68
+ end
69
+
70
+ _diff + schema_diff(@schema, actual)
71
+ end
72
+
73
+ def schema_diff(schema, actual, parent_path = "")
74
+ properties = schema["properties"]
75
+
76
+ return [] unless Hash === actual
77
+
78
+ if root_path && parent_path == ""
79
+ pointer = JsonPointer.new(actual, root_path)
80
+ return [] unless pointer.exists?
81
+ actual = pointer.value
82
+
83
+ parent_path = root_path
84
+ end
85
+
86
+ actual.inject([]) do |memo, (key, val)|
87
+ path = [parent_path, key].join("/")
88
+ if property = properties[key.to_s]
89
+ schema_property_diff(property, val, path) do |diff_item|
90
+ memo << diff_item
91
+ end
92
+ elsif schema['additionalProperties'] == false
93
+ memo << { :op => "remove", :path => path }
94
+ end
95
+ memo
96
+ end
97
+ end
98
+
99
+ def schema_property_diff(property, actual, path, &block)
100
+ assertion = Assertion.new(path, nil, :type => property["type"], :format => property["format"])
101
+
102
+ if !assertion_valid?(assertion, actual)
103
+ yield({
104
+ :op => "replace",
105
+ :path => path,
106
+ :value => value_for_schema_type(assertion.type, actual),
107
+ :current_value => actual,
108
+ :type => assertion.type,
109
+ :message => wrong_type_message(assertion.type, schema_type(actual))
110
+ })
111
+ elsif !assertion_format_valid?(assertion, actual)
112
+ yield({
113
+ :op => "replace",
114
+ :path => path,
115
+ :value => value_for_schema_format(assertion.format, actual),
116
+ :current_value => actual,
117
+ :type => assertion.type,
118
+ :format => assertion.format,
119
+ :message => wrong_format_message(assertion.format)
120
+ })
121
+ elsif (property["type"] == "object") && (Hash === property["properties"])
122
+ schema_diff(property, actual, path).each { |d| yield(d) }
123
+ elsif (property['type'] == 'array') && (Hash === property['items'])
124
+ array_property = property['items']
125
+ actual.each_with_index do |val, index|
126
+ schema_property_diff(array_property, val, path + "/#{index}", &block)
127
+ end
128
+ end
129
+ end
130
+
131
+ def wrong_type_message(expected_type, actual_type)
132
+ "expected type #{expected_type}, got #{actual_type}"
133
+ end
134
+
135
+ def wrong_format_message(expected_format)
136
+ "expected #{expected_format} format"
137
+ end
138
+
139
+ def assertion_valid?(assertion, actual)
140
+ type = assertion.type
141
+ case type
142
+ when "array"
143
+ Array === actual
144
+ when "boolean"
145
+ (TrueClass === actual) || (FalseClass === actual)
146
+ when "integer"
147
+ Fixnum === actual
148
+ when "number"
149
+ Numeric === actual
150
+ when "null"
151
+ NilClass === actual
152
+ when "object"
153
+ Hash === actual
154
+ when "string"
155
+ String === actual
156
+ else
157
+ super
158
+ end
159
+ end
160
+
161
+ def schema_type(value)
162
+ case value
163
+ when Array
164
+ "array"
165
+ when TrueClass, FalseClass
166
+ "boolean"
167
+ when Fixnum
168
+ "integer"
169
+ when Numeric
170
+ "number"
171
+ when NilClass
172
+ "null"
173
+ when Hash
174
+ "object"
175
+ when String
176
+ "string"
177
+ else
178
+ "unknown"
179
+ end
180
+ end
181
+
182
+ def value_for_schema_type(type, value)
183
+ klass = class_for_schema_type(type)
184
+ if klass == Array
185
+ value.respond_to?(:to_a) ? value.to_a : Array.new
186
+ elsif klass == [TrueClass, FalseClass]
187
+ !!value
188
+ elsif klass == Fixnum
189
+ value.respond_to?(:to_i) ? value.to_i : 0
190
+ elsif klass == Numeric
191
+ value.respond_to?(:to_f) ? value.to_f : 0.0
192
+ elsif klass == Hash
193
+ value.respond_to?(:to_hash) ? value.to_hash : Hash.new
194
+ elsif klass == String
195
+ value.respond_to?(:to_s) ? value.to_s : ""
196
+ else
197
+ nil
198
+ end
199
+ end
200
+
201
+ def value_for_schema_format(format, value)
202
+ case format
203
+ when 'uri'
204
+ "https://example.com"
205
+ end
206
+ end
207
+
208
+ def class_for_schema_type(type)
209
+ case type
210
+ when "array"
211
+ Array
212
+ when "boolean"
213
+ [TrueClass, FalseClass]
214
+ when "integer"
215
+ Fixnum
216
+ when "number"
217
+ Numeric
218
+ when "null"
219
+ NilClass
220
+ when "object"
221
+ Hash
222
+ when "string"
223
+ String
224
+ end
225
+ end
226
+
227
+ end
228
+ end