api-validator 0.0.1
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.
- data/.gitignore +17 -0
- data/Gemfile +6 -0
- data/LICENSE +27 -0
- data/README.md +80 -0
- data/Rakefile +8 -0
- data/api-validator.gemspec +27 -0
- data/lib/api-validator.rb +19 -0
- data/lib/api-validator/assertion.rb +32 -0
- data/lib/api-validator/base.rb +47 -0
- data/lib/api-validator/header.rb +77 -0
- data/lib/api-validator/json.rb +56 -0
- data/lib/api-validator/json_schema.rb +228 -0
- data/lib/api-validator/json_schemas.rb +20 -0
- data/lib/api-validator/mixins.rb +7 -0
- data/lib/api-validator/mixins/deep_merge.rb +28 -0
- data/lib/api-validator/response_expectation.rb +90 -0
- data/lib/api-validator/response_expectation/results.rb +62 -0
- data/lib/api-validator/spec.rb +153 -0
- data/lib/api-validator/spec/results.rb +28 -0
- data/lib/api-validator/status.rb +37 -0
- data/lib/api-validator/version.rb +3 -0
- data/spec/header_validator_spec.rb +108 -0
- data/spec/json_schema_validator_spec.rb +463 -0
- data/spec/json_validator_spec.rb +174 -0
- data/spec/response_expectation_results_spec.rb +105 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/spec_results_spec.rb +52 -0
- data/spec/spec_spec.rb +196 -0
- data/spec/status_validator_spec.rb +46 -0
- data/spec/support/hash_wrapper.rb +50 -0
- data/spec/support/shared_examples/shared_example_declaration.rb +8 -0
- data/spec/support/shared_examples/shared_example_lookup.rb +5 -0
- data/spec/support/shared_examples/validation_declaration.rb +65 -0
- data/spec/support/validator_shared_examples.rb +21 -0
- metadata +188 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
module ApiValidator
|
2
|
+
class JsonSchemas
|
3
|
+
|
4
|
+
def self.register(schema_name, schema)
|
5
|
+
schemas[schema_name.to_s] = schema
|
6
|
+
end
|
7
|
+
class << self
|
8
|
+
alias []= register
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.schemas
|
12
|
+
@schemas ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.[](schema_name)
|
16
|
+
schemas[schema_name.to_s]
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ApiValidator
|
2
|
+
module Mixins
|
3
|
+
|
4
|
+
module DeepMerge
|
5
|
+
def deep_merge!(hash, *others)
|
6
|
+
others.each do |other|
|
7
|
+
other.each_pair do |key, val|
|
8
|
+
if hash.has_key?(key)
|
9
|
+
next if hash[key] == val
|
10
|
+
case val
|
11
|
+
when Hash
|
12
|
+
deep_merge!(hash[key], val)
|
13
|
+
when Array
|
14
|
+
hash[key].concat(val)
|
15
|
+
when FalseClass
|
16
|
+
# false always wins
|
17
|
+
hash[key] = val
|
18
|
+
end
|
19
|
+
else
|
20
|
+
hash[key] = val
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module ApiValidator
|
2
|
+
class ResponseExpectation
|
3
|
+
|
4
|
+
require 'api-validator/response_expectation/results'
|
5
|
+
|
6
|
+
attr_accessor :status_validator
|
7
|
+
def initialize(validator, options = {}, &block)
|
8
|
+
@validator, @block = validator, block
|
9
|
+
initialize_headers(options.delete(:headers))
|
10
|
+
initialize_status(options.delete(:status))
|
11
|
+
initialize_schema(options.delete(:schema))
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize_headers(expected_headers)
|
15
|
+
return unless expected_headers
|
16
|
+
self.header_validators << ApiValidator::Header.new(expected_headers)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize_status(expected_status)
|
20
|
+
return unless expected_status
|
21
|
+
self.status_validator = ApiValidator::Status.new(expected_status)
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize_schema(expected_schema)
|
25
|
+
return unless expected_schema
|
26
|
+
schema_validators << ApiValidator::JsonSchema.new(expected_schema)
|
27
|
+
end
|
28
|
+
|
29
|
+
def json_validators
|
30
|
+
@json_validators ||= []
|
31
|
+
end
|
32
|
+
|
33
|
+
def schema_validators
|
34
|
+
@schema_validators ||= []
|
35
|
+
end
|
36
|
+
|
37
|
+
def header_validators
|
38
|
+
@header_validators ||= []
|
39
|
+
end
|
40
|
+
|
41
|
+
def response_filters
|
42
|
+
@response_filters ||= []
|
43
|
+
end
|
44
|
+
|
45
|
+
def expectations
|
46
|
+
[status_validator].compact + header_validators + schema_validators + json_validators
|
47
|
+
end
|
48
|
+
|
49
|
+
def expect_properties(properties)
|
50
|
+
json_validators << ApiValidator::Json.new(properties)
|
51
|
+
end
|
52
|
+
|
53
|
+
def expect_schema(expected_schema, path=nil)
|
54
|
+
schema_validators << ApiValidator::JsonSchema.new(expected_schema, path)
|
55
|
+
end
|
56
|
+
|
57
|
+
def expect_headers(expected_headers)
|
58
|
+
header_validators << ApiValidator::Header.new(expected_headers)
|
59
|
+
end
|
60
|
+
|
61
|
+
def expect_post_type(type_uri)
|
62
|
+
response_filters << proc { |response| response.env['expected_post_type'] = type_uri }
|
63
|
+
type_uri
|
64
|
+
end
|
65
|
+
|
66
|
+
def run
|
67
|
+
return unless @block
|
68
|
+
response = instance_eval(&@block)
|
69
|
+
Results.new(response, validate(response))
|
70
|
+
end
|
71
|
+
|
72
|
+
def validate(response)
|
73
|
+
response_filters.each { |filter| filter.call(response) }
|
74
|
+
expectations.map { |expectation| expectation.validate(response) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def respond_to_method_missing?(method)
|
78
|
+
@validator.respond_to?(method)
|
79
|
+
end
|
80
|
+
|
81
|
+
def method_missing(method, *args, &block)
|
82
|
+
if respond_to_method_missing?(method)
|
83
|
+
@validator.send(method, *args, &block)
|
84
|
+
else
|
85
|
+
super
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module ApiValidator
|
2
|
+
class ResponseExpectation
|
3
|
+
|
4
|
+
class Results
|
5
|
+
include Mixins::DeepMerge
|
6
|
+
|
7
|
+
attr_reader :response, :results
|
8
|
+
def initialize(response, results)
|
9
|
+
@response, @results = response, results
|
10
|
+
end
|
11
|
+
|
12
|
+
def as_json(options = {})
|
13
|
+
res = results.inject(Hash.new) do |memo, result|
|
14
|
+
result = result.dup
|
15
|
+
deep_merge!((memo[result.delete(:key)] ||= Hash.new), result)
|
16
|
+
memo
|
17
|
+
end
|
18
|
+
|
19
|
+
merge_diffs!(res)
|
20
|
+
|
21
|
+
{
|
22
|
+
:expected => res,
|
23
|
+
:actual => {
|
24
|
+
:request_headers => response.env[:request_headers],
|
25
|
+
:request_body => response.env[:request_body],
|
26
|
+
:request_path => response.env[:url].path,
|
27
|
+
:request_params => parse_params(response.env[:url]),
|
28
|
+
:request_url => response.env[:url].to_s,
|
29
|
+
:request_method => response.env[:method].to_s.upcase,
|
30
|
+
|
31
|
+
:response_headers => response.headers,
|
32
|
+
:response_body => response.body,
|
33
|
+
:response_status => response.status
|
34
|
+
}
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def parse_params(uri)
|
41
|
+
return unless uri.query
|
42
|
+
uri.query.split('&').inject({}) do |params, part|
|
43
|
+
key, value = part.split('=')
|
44
|
+
params[key] = value
|
45
|
+
params
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def merge_diffs!(expectation_results)
|
50
|
+
expectation_results.each_pair do |key, results|
|
51
|
+
results[:diff] = results[:diff].inject({}) do |memo, diff|
|
52
|
+
(memo[diff[:path]] ||= []) << diff
|
53
|
+
memo
|
54
|
+
end.inject([]) do |memo, (path, diffs)|
|
55
|
+
memo << diffs.sort_by { |d| d[:value].to_s.size * -1 }.first
|
56
|
+
end.sort_by { |d| d[:path].split("/").size }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module ApiValidator
|
2
|
+
class Spec
|
3
|
+
|
4
|
+
require 'api-validator/spec/results'
|
5
|
+
|
6
|
+
module SharedClassAndInstanceMethods
|
7
|
+
def shared_examples
|
8
|
+
@shared_examples ||= begin
|
9
|
+
if self.respond_to?(:superclass) && self.superclass.respond_to?(:shared_examples)
|
10
|
+
self.superclass.shared_examples || Hash.new
|
11
|
+
else
|
12
|
+
Hash.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def shared_example(name, &block)
|
18
|
+
self.shared_examples[name] = block
|
19
|
+
end
|
20
|
+
|
21
|
+
def validations
|
22
|
+
@validations ||= []
|
23
|
+
end
|
24
|
+
|
25
|
+
def describe(name, options = {}, &block)
|
26
|
+
validation = self.new(name, options.merge(:parent => self), &block)
|
27
|
+
self.validations << validation
|
28
|
+
validation
|
29
|
+
end
|
30
|
+
alias context describe
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
include SharedClassAndInstanceMethods
|
35
|
+
end
|
36
|
+
include SharedClassAndInstanceMethods
|
37
|
+
|
38
|
+
def self.run
|
39
|
+
validations.inject(Results.new(self.new(''), [])) do |memo, validation|
|
40
|
+
results = validation.run
|
41
|
+
memo.merge!(results)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_reader :parent, :name, :pending
|
46
|
+
def initialize(name, options = {}, &block)
|
47
|
+
@parent = options.delete(:parent)
|
48
|
+
@parent = nil if @parent == self.class
|
49
|
+
@name = name
|
50
|
+
|
51
|
+
initialize_before_hooks(options.delete(:before))
|
52
|
+
|
53
|
+
if block_given?
|
54
|
+
instance_eval(&block)
|
55
|
+
else
|
56
|
+
@pending = true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize_before_hooks(hooks)
|
61
|
+
Array(hooks).each do |method_name_or_block|
|
62
|
+
if method_name_or_block.respond_to?(:call)
|
63
|
+
self.before_hooks << method_name_or_block
|
64
|
+
elsif respond_to?(method_name_or_block)
|
65
|
+
self.before_hooks << method(method_name_or_block)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def before_hooks
|
71
|
+
@before_hooks ||= []
|
72
|
+
end
|
73
|
+
|
74
|
+
def new(*args, &block)
|
75
|
+
self.class.new(*args, &block)
|
76
|
+
end
|
77
|
+
|
78
|
+
def find_shared_example(name)
|
79
|
+
ref = self
|
80
|
+
begin
|
81
|
+
if block = ref.shared_examples[name]
|
82
|
+
return block
|
83
|
+
end
|
84
|
+
end while ref = ref.parent
|
85
|
+
self.class.shared_examples[name]
|
86
|
+
end
|
87
|
+
|
88
|
+
BehaviourNotFoundError = Class.new(StandardError)
|
89
|
+
def behaves_as(name)
|
90
|
+
block = find_shared_example(name)
|
91
|
+
raise BehaviourNotFoundError.new("Behaviour #{name.inspect} could not be found") unless block
|
92
|
+
instance_eval(&block)
|
93
|
+
end
|
94
|
+
|
95
|
+
def expectations
|
96
|
+
@expectations ||= []
|
97
|
+
end
|
98
|
+
|
99
|
+
def expect_response(options = {}, &block)
|
100
|
+
expectation = ResponseExpectation.new(self, options, &block)
|
101
|
+
self.expectations << expectation
|
102
|
+
expectation
|
103
|
+
end
|
104
|
+
|
105
|
+
def cache
|
106
|
+
@cache ||= Hash.new
|
107
|
+
end
|
108
|
+
|
109
|
+
def get(path)
|
110
|
+
if Symbol === path
|
111
|
+
path = "/#{path}"
|
112
|
+
end
|
113
|
+
|
114
|
+
pointer = JsonPointer.new(cache, path, :symbolize_keys => true)
|
115
|
+
unless pointer.exists?
|
116
|
+
return parent ? parent.get(path) : nil
|
117
|
+
end
|
118
|
+
val = pointer.value
|
119
|
+
Proc === val ? val.call : val
|
120
|
+
end
|
121
|
+
|
122
|
+
def set(path, val=nil, &block)
|
123
|
+
if Symbol === path
|
124
|
+
path = "/#{path}"
|
125
|
+
end
|
126
|
+
|
127
|
+
pointer = JsonPointer.new(cache, path, :symbolize_keys => true)
|
128
|
+
pointer.value = block_given? ? block : val
|
129
|
+
val
|
130
|
+
end
|
131
|
+
|
132
|
+
def run
|
133
|
+
before_hooks.each do |hook|
|
134
|
+
if hook.respond_to?(:receiver) && hook.receiver == self
|
135
|
+
# It's a method
|
136
|
+
hook.call
|
137
|
+
else
|
138
|
+
# It's a block
|
139
|
+
instance_eval(&hook)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
results = self.expectations.inject([]) do |memo, expectation|
|
144
|
+
memo << expectation.run
|
145
|
+
end
|
146
|
+
|
147
|
+
self.validations.inject(Results.new(self, results)) do |memo, validation|
|
148
|
+
memo.merge!(validation.run)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ApiValidator
|
2
|
+
class Spec
|
3
|
+
|
4
|
+
class Results
|
5
|
+
include Mixins::DeepMerge
|
6
|
+
|
7
|
+
attr_reader :name, :results
|
8
|
+
def initialize(validator, expectations_results)
|
9
|
+
@name = validator.name
|
10
|
+
@results = {
|
11
|
+
@name => {
|
12
|
+
:results => expectations_results
|
13
|
+
}
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def merge!(other)
|
18
|
+
deep_merge!(results[name], other.results)
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def as_json(options = {})
|
23
|
+
results
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ApiValidator
|
2
|
+
class Status < Base
|
3
|
+
|
4
|
+
def validate(response)
|
5
|
+
response_status = response.status
|
6
|
+
_failed_assertions = failed_assertions(response_status)
|
7
|
+
super.merge(
|
8
|
+
:key => :response_status,
|
9
|
+
:failed_assertions => _failed_assertions.map(&:to_hash),
|
10
|
+
:diff => diff(response_status, _failed_assertions),
|
11
|
+
:valid => _failed_assertions.empty?
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def initialize_assertions(expected)
|
18
|
+
@assertions = [ Assertion.new("", expected) ]
|
19
|
+
end
|
20
|
+
|
21
|
+
def failed_assertions(actual)
|
22
|
+
assertions.select do |assertion|
|
23
|
+
!assertion_valid?(assertion, actual)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def diff(actual, _failed_assertions)
|
28
|
+
_failed_assertions.map do |assertion|
|
29
|
+
assertion = assertion.to_hash
|
30
|
+
assertion[:op] = "replace"
|
31
|
+
assertion[:current_value] = actual
|
32
|
+
assertion
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'faraday'
|
3
|
+
|
4
|
+
require 'support/validator_shared_examples'
|
5
|
+
|
6
|
+
describe ApiValidator::Header do
|
7
|
+
let(:env) { HashWrapper.new(:status => 200, :response_headers => {}, :token => 'foobar', :body => '') }
|
8
|
+
let(:response) { Faraday::Response.new(env) }
|
9
|
+
let(:validator) { stub(:everything) }
|
10
|
+
let(:instance) { described_class.new(expected) }
|
11
|
+
let(:expectation_key) { :response_headers }
|
12
|
+
|
13
|
+
let(:res) { instance.validate(response) }
|
14
|
+
|
15
|
+
describe "#validate" do
|
16
|
+
let(:expected) do
|
17
|
+
{
|
18
|
+
"Count" => /\A\d+\Z/,
|
19
|
+
"Token" => lambda { |response| response.env['token'] },
|
20
|
+
"Say Hello" => "Hello Tent!"
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:expected_assertions) {
|
25
|
+
[
|
26
|
+
{ :op => "test", :path => "/Count", :value => "/^\\d+$/", :type => "regexp" },
|
27
|
+
{ :op => "test", :path => "/Token", :value => env[:token] },
|
28
|
+
{ :op => "test", :path => "/Say Hello", :value => "Hello Tent!" }
|
29
|
+
]
|
30
|
+
}
|
31
|
+
|
32
|
+
context "when expectation fails" do
|
33
|
+
context "when expectation is a Regexp" do
|
34
|
+
it_behaves_like "a validator #validate method"
|
35
|
+
|
36
|
+
before do
|
37
|
+
env.response_headers = {
|
38
|
+
"Count" => "NaN",
|
39
|
+
"Token" => env[:token],
|
40
|
+
"Say Hello" => "Hello Tent!"
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
let(:expected_diff) { [{ :op => "replace", :path => "/Count", :value => "/^\\d+$/", :current_value => "NaN", :type => "regexp" }] }
|
45
|
+
let(:expected_failed_assertions) { [expected_assertions.first] }
|
46
|
+
end
|
47
|
+
|
48
|
+
context "when expectation is a lambda" do
|
49
|
+
it_behaves_like "a validator #validate method"
|
50
|
+
|
51
|
+
before do
|
52
|
+
env.response_headers = {
|
53
|
+
"Count" => "185",
|
54
|
+
"Token" => "baz",
|
55
|
+
"Say Hello" => "Hello Tent!"
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
let(:expected_diff) { [{ :op => "replace", :path => "/Token", :value => env[:token], :current_value => "baz" }] }
|
60
|
+
let(:expected_failed_assertions) { [expected_assertions[1]] }
|
61
|
+
end
|
62
|
+
|
63
|
+
context "when expectation is a String" do
|
64
|
+
it_behaves_like "a validator #validate method"
|
65
|
+
|
66
|
+
before do
|
67
|
+
env.response_headers = {
|
68
|
+
"Count" => "185",
|
69
|
+
"Token" => env[:token],
|
70
|
+
"Say Hello" => "No, I won't do it!"
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
let(:expected_diff) { [{ :op => "replace", :path => "/Say Hello", :value => "Hello Tent!", :current_value => "No, I won't do it!" }] }
|
75
|
+
let(:expected_failed_assertions) { [expected_assertions.last] }
|
76
|
+
end
|
77
|
+
|
78
|
+
context "when header missing" do
|
79
|
+
it_behaves_like "a validator #validate method"
|
80
|
+
|
81
|
+
before do
|
82
|
+
env.response_headers = {
|
83
|
+
"Count" => "185",
|
84
|
+
"Token" => env[:token]
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
let(:expected_diff) { [{ :op => "add", :path => "/Say Hello", :value => "Hello Tent!" }] }
|
89
|
+
let(:expected_failed_assertions) { [expected_assertions.last] }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "when expectation passes" do
|
94
|
+
it_behaves_like "a validator #validate method"
|
95
|
+
|
96
|
+
before do
|
97
|
+
env.response_headers = {
|
98
|
+
"Count" => "198",
|
99
|
+
"Token" => env[:token],
|
100
|
+
"Say Hello" => "Hello Tent!"
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
let(:expected_diff) { [] }
|
105
|
+
let(:expected_failed_assertions) { [] }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|