mini-apivore 0.1.0

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,74 @@
1
+ require 'json-schema'
2
+ require 'mini_apivore/version'
3
+ require 'mini_apivore/fragment'
4
+ require 'mini_apivore/swagger'
5
+ require 'mini_apivore/swagger_checker'
6
+ require 'mini_apivore/validation'
7
+ require 'mini_apivore/http_codes'
8
+
9
+ module MiniApivore
10
+ SWAGGER_CHECKERS = {}
11
+ #----- Module globals -----------------
12
+ def self.runnable_list; @@runnable_list ||= [] end
13
+ def self.all_test_ran?; runnable_list.empty? end
14
+
15
+ def self.prepare_untested_errors
16
+ errors = []
17
+ SWAGGER_CHECKERS.each do |cls, chkr|
18
+ chkr.untested_mappings.each do |path, methods|
19
+ methods.each do |method, codes|
20
+ codes.each do |code, _|
21
+ errors << "#{method} #{path} is untested for response code #{code} in test class #{cls.to_s}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ errors
27
+ end
28
+
29
+ def self.included(base)
30
+ base.extend ClassMethods
31
+ base.include MiniApivore::Validation
32
+ end
33
+
34
+ #---- class methods -----------
35
+ module ClassMethods
36
+
37
+ def init_swagger( swagger_path )
38
+ SWAGGER_CHECKERS[self] ||= MiniApivore::SwaggerChecker.instance_for(swagger_path)
39
+ end
40
+
41
+ def runnable_methods
42
+ super | ['final_test']
43
+ end
44
+
45
+ def test(name, &block )
46
+ super( name, &block ).tap{ |sym| MiniApivore.runnable_list << "#{to_s}::#{sym}" }
47
+ end
48
+
49
+ def swagger_checker;
50
+ SWAGGER_CHECKERS[self]
51
+ end
52
+ end
53
+
54
+ #----- Minitest callback -----------
55
+ def teardown
56
+ super
57
+ MiniApivore.runnable_list.delete( "#{self.class.to_s}::#{@NAME}" )
58
+ end
59
+
60
+ #----- test for untested routes ---------
61
+ def final_test
62
+ return unless MiniApivore.all_test_ran?
63
+
64
+ @errors = MiniApivore.prepare_untested_errors
65
+ assert( @errors.empty?, @errors.join("\n") )
66
+
67
+ # preventing duplicate execution
68
+ MiniApivore.runnable_list << "#{self.class.to_s}::#{__method__}_runned"
69
+ end
70
+
71
+
72
+ end
73
+
74
+
@@ -0,0 +1,26 @@
1
+ require "mini_apivore/version"
2
+
3
+ module ActiveSupport
4
+ module Testing
5
+ module Declarative
6
+ # Helper to define a test method using a String. Under the hood, it replaces
7
+ # spaces with underscores and defines the test method.
8
+ #
9
+ # test "verify something" do
10
+ # ...
11
+ # end
12
+ def test(name, &block)
13
+ test_name = "test_#{name.gsub(/\s+/, '_')}".to_sym
14
+ defined = method_defined? test_name
15
+ raise "#{test_name} is already defined in #{self}" if defined
16
+ if block_given?
17
+ define_method(test_name, &block)
18
+ else
19
+ define_method(test_name) do
20
+ flunk "No implementation provided for #{name}"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end unless defined?(ActiveSupport)
@@ -0,0 +1,11 @@
1
+ require "mini_apivore/version"
2
+
3
+ module MiniApivore
4
+ # This is a workaround for json-schema's fragment validation which does not allow paths to contain forward slashes
5
+ # current json-schema attempts to split('/') on a string path to produce an array.
6
+ class Fragment < Array
7
+ def split(options = nil)
8
+ self
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ require "mini_apivore/version"
2
+
3
+ module MiniApivore
4
+ NOT_FOUND = 404
5
+ NOT_AUTHORIZED = 401
6
+ FORBIDDEN = 403 # Access denied
7
+ UNPROCESSABLE_ENTITY = 422
8
+ OK = 200
9
+ end
@@ -0,0 +1,52 @@
1
+ require "mini_apivore/version"
2
+ require 'hashie'
3
+
4
+
5
+ module MiniApivore
6
+ class Swagger < Hashie::Mash
7
+ NONVERB_PATH_ITEMS = %q(parameters)
8
+
9
+ def validate
10
+ case version
11
+ when '2.0'
12
+ schema = File.read(File.expand_path("../../../data/swagger_2.0_schema.json", __FILE__))
13
+ else
14
+ raise "Unknown/unsupported Swagger version to validate against: #{version}"
15
+ end
16
+ JSON::Validator.fully_validate(schema, self)
17
+ end
18
+
19
+ def version
20
+ swagger
21
+ end
22
+
23
+ def base_path
24
+ self['basePath'] || ''
25
+ end
26
+
27
+ def each_response(&block)
28
+ paths.each do |path, path_data|
29
+ next if vendor_specific_tag? path
30
+ path_data.each do |verb, method_data|
31
+ next if NONVERB_PATH_ITEMS.include?(verb)
32
+ next if vendor_specific_tag? verb
33
+ if method_data.responses.nil?
34
+ raise "No responses found in swagger for path '#{path}', " \
35
+ "verb #{verb}: #{method_data.inspect}"
36
+ end
37
+ method_data.responses.each do |response_code, response_data|
38
+ schema_location = nil
39
+ if response_data.schema
40
+ schema_location = Fragment.new ['#', 'paths', path, verb, 'responses', response_code, 'schema']
41
+ end
42
+ block.call(path, verb, response_code, schema_location)
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ def vendor_specific_tag? tag
49
+ tag =~ /\Ax-.*/
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,114 @@
1
+ require "mini_apivore/version"
2
+
3
+ module MiniApivore
4
+ class SwaggerChecker
5
+ PATH_TO_CHECKER_MAP = {}
6
+
7
+ def self.instance_for(path)
8
+ PATH_TO_CHECKER_MAP[path] ||= new(path)
9
+ end
10
+
11
+ def has_path?(path)
12
+ mappings.has_key?(path)
13
+ end
14
+
15
+ def has_method_at_path?(path, verb)
16
+ mappings[path].has_key?(verb)
17
+ end
18
+
19
+ def has_response_code_for_path?(path, verb, code)
20
+ mappings[path][verb].has_key?(code.to_s)
21
+ end
22
+
23
+ def response_codes_for_path(path, verb)
24
+ mappings[path][verb].keys.join(", ")
25
+ end
26
+
27
+ def has_matching_document_for(path, verb, code, body)
28
+ JSON::Validator.fully_validate(
29
+ swagger, body, fragment: fragment(path, verb, code)
30
+ )
31
+ end
32
+
33
+ def fragment(path, verb, code)
34
+ path_fragment = mappings[path][verb.to_s][code.to_s]
35
+ path_fragment.dup unless path_fragment.nil?
36
+ end
37
+
38
+ def remove_tested_end_point_response(path, verb, code)
39
+ return if untested_mappings[path].nil? ||
40
+ untested_mappings[path][verb].nil?
41
+ untested_mappings[path][verb].delete(code.to_s)
42
+ if untested_mappings[path][verb].size == 0
43
+ untested_mappings[path].delete(verb)
44
+ if untested_mappings[path].size == 0
45
+ untested_mappings.delete(path)
46
+ end
47
+ end
48
+ end
49
+
50
+ def base_path
51
+ @swagger.base_path
52
+ end
53
+
54
+ def response=(response)
55
+ @response = response
56
+ end
57
+
58
+ attr_reader :response, :swagger, :swagger_path
59
+
60
+ def untested_mappings; @untested_mappings end
61
+ def untested_mappings=( other ); @untested_mappings = other end
62
+
63
+ private
64
+
65
+ attr_reader :mappings
66
+
67
+ def initialize(swagger_path)
68
+ @swagger_path = swagger_path
69
+ load_swagger_doc!
70
+ validate_swagger!
71
+ setup_mappings!
72
+ end
73
+
74
+ # сюда можно поставить замену для загрузки из файла данных, а не из рельс.
75
+ def load_swagger_doc!
76
+ @swagger = MiniApivore::Swagger.new(fetch_swagger!)
77
+ end
78
+
79
+ def fetch_swagger!
80
+ if File.exist?( swagger_path )
81
+ JSON.parse( File.read(swagger_path) )
82
+ else
83
+ session = ActionDispatch::Integration::Session.new(Rails.application)
84
+ begin
85
+ session.get(swagger_path)
86
+ rescue
87
+ fail "Unable to perform GET request for swagger json: #{swagger_path} - #{$!}."
88
+ end
89
+ JSON.parse(session.response.body)
90
+ end
91
+ end
92
+
93
+ def validate_swagger!
94
+ errors = swagger.validate
95
+ unless errors.empty?
96
+ msg = "The document fails to validate as Swagger #{swagger.version}:\n"
97
+ msg += errors.join("\n")
98
+ fail msg
99
+ end
100
+ end
101
+
102
+ def setup_mappings!
103
+ @mappings = {}
104
+ @swagger.each_response do |path, verb, response_code, fragment|
105
+ @mappings[path] ||= {}
106
+ @mappings[path][verb] ||= {}
107
+ raise "duplicate" unless @mappings[path][verb][response_code].nil?
108
+ @mappings[path][verb][response_code] = fragment
109
+ end
110
+
111
+ self.untested_mappings = JSON.parse(JSON.generate(@mappings))
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,133 @@
1
+ require "mini_apivore/version"
2
+
3
+ module MiniApivore
4
+ module Validation # former Validator
5
+
6
+ class IndHash < Hash
7
+ include Hashie::Extensions::MergeInitializer
8
+ include Hashie::Extensions::IndifferentAccess
9
+ end
10
+
11
+ def prepare_action_env(verb, path, expected_response_code, params = {})
12
+ @errors = []
13
+ @verb = verb.to_s
14
+ @path = path.to_s
15
+ @params = IndHash.new(params)
16
+ @expected_response_code = expected_response_code.to_i
17
+ end
18
+
19
+ def swagger_checker; self.class.swagger_checker end
20
+
21
+ def check_route( verb, path, expected_response_code, params = {} )
22
+ prepare_action_env( verb, path, expected_response_code, params )
23
+ assert( match?, failure_message )
24
+ end
25
+
26
+ def match?
27
+ #pre_checks
28
+ check_request_path
29
+
30
+ # request
31
+ unless has_errors?
32
+ send(
33
+ @verb,
34
+ *action_dispatch_request_args(
35
+ full_path,
36
+ params: @params['_data'] || {},
37
+ headers: @params['_headers'] || {}
38
+ )
39
+ )
40
+
41
+ #post_checks
42
+ check_status_code
43
+ check_response_is_valid unless has_errors?
44
+
45
+
46
+ if has_errors? && response.body.length > 0
47
+ @errors << "\nResponse body:\n #{JSON.pretty_generate(JSON.parse(response.body))}"
48
+ end
49
+
50
+ swagger_checker.remove_tested_end_point_response(
51
+ @path, @verb, @expected_response_code
52
+ )
53
+ end
54
+ !has_errors?
55
+ end
56
+
57
+ def check_request_path
58
+ if !swagger_checker.has_path?(@path)
59
+ @errors << "Swagger doc: #{swagger_checker.swagger_path} does not have"\
60
+ " a documented @path for #{@path}"
61
+ elsif !swagger_checker.has_method_at_path?(@path, @verb)
62
+ @errors << "Swagger doc: #{swagger_checker.swagger_path} does not have"\
63
+ " a documented @path for #{@verb} #{@path}"
64
+ elsif !swagger_checker.has_response_code_for_path?(@path, @verb, @expected_response_code)
65
+ @errors << "Swagger doc: #{swagger_checker.swagger_path} does not have"\
66
+ " a documented response code of #{@expected_response_code} at @path"\
67
+ " #{@verb} #{@path}. "\
68
+ "\n Available response codes: #{swagger_checker.response_codes_for_path(@path, @verb)}"
69
+ elsif @verb == "get" && swagger_checker.fragment(@path, @verb, @expected_response_code).nil?
70
+ @errors << "Swagger doc: #{swagger_checker.swagger_path} missing"\
71
+ " response model for get request with #{@path} for code"\
72
+ " #{@expected_response_code}"
73
+ end
74
+ end
75
+
76
+ def full_path
77
+ apivore_build_path(swagger_checker.base_path + @path, @params)
78
+ end
79
+
80
+ def apivore_build_path(path, data)
81
+ path.scan(/\{([^\}]*)\}/).each do |param|
82
+ key = param.first
83
+ dkey = data && ( data[key] || data[key.to_sym] )
84
+ if dkey
85
+ path = path.gsub "{#{key}}", dkey.to_s
86
+ else
87
+ raise URI::InvalidURIError, "No substitution data found for {#{key}}"\
88
+ " to test the path #{path}.", caller
89
+ end
90
+ end
91
+ path + (data['_query_string'] ? "?#{data['_query_string'].to_param}" : '')
92
+ end
93
+
94
+ def has_errors?; !@errors.empty? end
95
+
96
+ def failure_message; @errors.join(" ") end
97
+
98
+ def check_status_code
99
+ if response.status != @expected_response_code
100
+ @errors << "Path #{@path} did not respond with expected status code."\
101
+ " Expected #{@expected_response_code} got #{response.status}"\
102
+ end
103
+ end
104
+
105
+ def check_response_is_valid
106
+ swagger_errors = swagger_checker.has_matching_document_for(
107
+ @path, @verb, response.status, response_body
108
+ )
109
+ unless swagger_errors.empty?
110
+ @errors.concat(
111
+ swagger_errors.map do |e|
112
+ e.sub("'#", "'#{full_path}#").gsub(
113
+ /^The property|in schema.*$/,''
114
+ )
115
+ end
116
+ )
117
+ end
118
+ end
119
+
120
+ def response_body
121
+ JSON.parse(response.body) if response.body && !response.body.empty?
122
+ end
123
+
124
+ def action_dispatch_request_args(path, params: {}, headers: {})
125
+ if defined?(ActionPack) && ActionPack::VERSION::MAJOR >= 5
126
+ [path, params: params, headers: headers]
127
+ else
128
+ [path, params, headers]
129
+ end
130
+ end
131
+ end
132
+
133
+ end
@@ -0,0 +1,5 @@
1
+ module Mini
2
+ module Apivore
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mini-apivore
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - alekseyl
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-11-02 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'
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'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: hashie
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.3'
83
+ description: " Provides a tool for testing your application api against your swagger
84
+ schema "
85
+ email:
86
+ - leshchuk@gmail.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - data/draft04_schema.json
92
+ - data/swagger_2.0_schema.json
93
+ - lib/mini_apivore.rb
94
+ - lib/mini_apivore/declarative.rb
95
+ - lib/mini_apivore/fragment.rb
96
+ - lib/mini_apivore/http_codes.rb
97
+ - lib/mini_apivore/swagger.rb
98
+ - lib/mini_apivore/swagger_checker.rb
99
+ - lib/mini_apivore/validation.rb
100
+ - lib/mini_apivore/version.rb
101
+ homepage: https://github.com/alekseyl/mini-apivore
102
+ licenses:
103
+ - MIT
104
+ metadata:
105
+ allowed_push_host: https://rubygems.org
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ - data
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 2.6.13
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: Minitest adaptation of an apivore gem
127
+ test_files: []