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.
@@ -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,7 @@
1
+ module ApiValidator
2
+ module Mixins
3
+
4
+ require 'api-validator/mixins/deep_merge'
5
+
6
+ end
7
+ 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,3 @@
1
+ module ApiValidator
2
+ VERSION = "0.0.1"
3
+ 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