mini-apivore 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []