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.
- checksums.yaml +7 -0
- data/data/draft04_schema.json +150 -0
- data/data/swagger_2.0_schema.json +1591 -0
- data/lib/mini_apivore.rb +74 -0
- data/lib/mini_apivore/declarative.rb +26 -0
- data/lib/mini_apivore/fragment.rb +11 -0
- data/lib/mini_apivore/http_codes.rb +9 -0
- data/lib/mini_apivore/swagger.rb +52 -0
- data/lib/mini_apivore/swagger_checker.rb +114 -0
- data/lib/mini_apivore/validation.rb +133 -0
- data/lib/mini_apivore/version.rb +5 -0
- metadata +127 -0
data/lib/mini_apivore.rb
ADDED
@@ -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,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
|
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: []
|