api-tester 1.0.0 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +5 -5
  2. data/.github/dependabot.yml +15 -0
  3. data/.github/workflows/dependabot.yml +29 -0
  4. data/.github/workflows/push.yml +39 -0
  5. data/.github/workflows/test.yml +31 -0
  6. data/.rubocop.yml +171 -0
  7. data/Gemfile +2 -0
  8. data/Guardfile +70 -0
  9. data/README.md +65 -61
  10. data/Rakefile +8 -3
  11. data/api-tester.gemspec +28 -24
  12. data/changelog.txt +10 -0
  13. data/lib/api-tester/config.rb +17 -15
  14. data/lib/api-tester/definition/boundary_case.rb +5 -4
  15. data/lib/api-tester/definition/contract.rb +10 -5
  16. data/lib/api-tester/definition/endpoint.rb +58 -32
  17. data/lib/api-tester/definition/fields/array_field.rb +26 -21
  18. data/lib/api-tester/definition/fields/boolean_field.rb +17 -7
  19. data/lib/api-tester/definition/fields/email_field.rb +28 -11
  20. data/lib/api-tester/definition/fields/enum_field.rb +19 -12
  21. data/lib/api-tester/definition/fields/field.rb +52 -45
  22. data/lib/api-tester/definition/fields/number_field.rb +20 -6
  23. data/lib/api-tester/definition/fields/object_field.rb +37 -30
  24. data/lib/api-tester/definition/fields/plain_array_field.rb +25 -0
  25. data/lib/api-tester/definition/method.rb +8 -5
  26. data/lib/api-tester/definition/request.rb +43 -19
  27. data/lib/api-tester/definition/response.rb +35 -25
  28. data/lib/api-tester/method_case_test.rb +68 -54
  29. data/lib/api-tester/modules/benchmark_module.rb +35 -0
  30. data/lib/api-tester/modules/extra_verbs.rb +36 -10
  31. data/lib/api-tester/modules/format.rb +23 -7
  32. data/lib/api-tester/modules/good_case.rb +25 -10
  33. data/lib/api-tester/modules/good_variations.rb +69 -0
  34. data/lib/api-tester/modules/injection_module.rb +44 -23
  35. data/lib/api-tester/modules/missing_resource.rb +63 -0
  36. data/lib/api-tester/modules/required_fields.rb +51 -0
  37. data/lib/api-tester/modules/server_information.rb +14 -12
  38. data/lib/api-tester/modules/typo.rb +39 -14
  39. data/lib/api-tester/modules/unexpected_fields.rb +59 -0
  40. data/lib/api-tester/modules/unused_fields.rb +13 -7
  41. data/lib/api-tester/reporter/api_report.rb +25 -16
  42. data/lib/api-tester/reporter/missing_field_report.rb +11 -15
  43. data/lib/api-tester/reporter/report.rb +12 -13
  44. data/lib/api-tester/reporter/response_time_report.rb +24 -0
  45. data/lib/api-tester/reporter/status_code_report.rb +10 -4
  46. data/lib/api-tester/test_helper.rb +8 -6
  47. data/lib/api-tester/util/response_evaluator.rb +84 -56
  48. data/lib/api-tester/util/supported_verbs.rb +8 -5
  49. data/lib/api-tester/version.rb +3 -1
  50. data/lib/api-tester.rb +6 -3
  51. metadata +92 -27
  52. data/.travis.yml +0 -6
  53. data/lib/api-tester/reporter/missing_response_field_report.rb +0 -21
data/api-tester.gemspec CHANGED
@@ -4,39 +4,43 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'api-tester/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "api-tester"
7
+ spec.name = 'api-tester'
8
8
  spec.version = ApiTester::VERSION
9
- spec.authors = ["arane"]
10
- spec.email = ["arane9@gmail.com"]
9
+ spec.authors = ['arane']
10
+ spec.email = ['arane9@gmail.com']
11
11
 
12
- spec.summary = %q{Tool to help test APIs}
13
- spec.description = %q{Tool to test APIs which will eventually do boundary testing and other sorts of testing automatically given a contract}
14
- spec.homepage = "https://github.com/araneforseti/api-tester"
15
- spec.license = "MIT"
12
+ spec.summary = 'Tool to help test APIs'
13
+ spec.description = 'Tool to test APIs which will eventually do boundary testing and other sorts of testing automatically given a contract'
14
+ spec.homepage = 'https://github.com/araneforseti/api-tester'
15
+ spec.license = 'MIT'
16
16
 
17
17
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
18
  # to allow pushing to a single host or delete this section to allow pushing to any host.
19
- if spec.respond_to?(:metadata)
20
- spec.metadata['allowed_push_host'] = 'https://rubygems.org/'
21
- else
22
- raise "RubyGems 2.0 or newer is required to protect against " \
23
- "public gem pushes."
24
- end
19
+ # if spec.respond_to?(:metadata)
20
+ # spec.metadata['allowed_push_host'] = 'https://rubygems.org/'
21
+ # else
22
+ # raise 'RubyGems 2.0 or newer is required to protect against ' \
23
+ # 'public gem pushes.'
24
+ # end
25
25
 
26
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
26
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
27
27
  f.match(%r{^(test|spec|features)/})
28
28
  end
29
- spec.bindir = "exe"
29
+ spec.bindir = 'exe'
30
30
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
- spec.require_paths = ["lib"]
31
+ spec.require_paths = ['lib']
32
32
 
33
- spec.add_development_dependency "bundler", "~> 1.13"
34
- spec.add_development_dependency "rake", "~> 10.0"
35
- spec.add_development_dependency "rspec", "~> 3.0"
36
- spec.add_development_dependency "webmock", "~> 3.4"
37
- spec.add_development_dependency "pry", "~> 0.11"
38
- spec.add_development_dependency "require_all", "~>2.0.0"
33
+ spec.add_development_dependency 'bundler'
34
+ spec.add_development_dependency 'bundler-audit', '~>0.9.0'
35
+ spec.add_development_dependency 'guard-rspec', '~> 4.7.3'
36
+ spec.add_development_dependency 'rake', '~> 13.0.1'
37
+ spec.add_development_dependency 'require_all', '~>3.0.0'
38
+ spec.add_development_dependency 'rspec', '~> 3.0'
39
+ spec.add_development_dependency 'rubocop', '~> 1.31.0'
40
+ spec.add_development_dependency 'terminal-notifier', '~> 2.0.0'
41
+ spec.add_development_dependency 'terminal-notifier-guard', '~> 1.7.0'
42
+ spec.add_development_dependency 'webmock', '~> 3.4'
39
43
 
40
- spec.add_runtime_dependency "rest-client", "~> 2.0"
41
- spec.add_runtime_dependency "injection_vulnerability_library", "0.0.2"
44
+ spec.add_runtime_dependency 'injection_vulnerability_library', '0.1.3'
45
+ spec.add_runtime_dependency 'rest-client', '~> 2.0'
42
46
  end
data/changelog.txt CHANGED
@@ -1,3 +1,13 @@
1
+ 2.0.0
2
+
3
+ - Added Required Fields module
4
+ - Added Unexpected Fields module
5
+ - Adding query params to definition
6
+ - Moved base_url to contract
7
+ - Changed endpoints to be relative urls
8
+ - Removed missing field report as it was unused
9
+ - Changing everything with more than one parameter to named parameters
10
+
1
11
  1.0.0
2
12
 
3
13
  - Switching to using named variables to make future changes less breaky
@@ -1,39 +1,41 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/reporter/api_report'
2
4
 
3
5
  module ApiTester
6
+ # Config class for changing how the tool operates
4
7
  class Config
5
- attr_accessor :reporter
6
- attr_accessor :modules
8
+ attr_accessor :reporter, :modules
7
9
 
8
- def initialize reporter=ApiTester::ApiReport.new
10
+ def initialize(reporter: ApiTester::ApiReport.new)
9
11
  self.reporter = reporter
10
12
  self.modules = []
11
13
  end
12
14
 
13
- def with_reporter reporter
15
+ def with_reporter(reporter)
14
16
  self.reporter = reporter
15
17
  self
16
18
  end
17
19
 
18
- def with_module new_module
19
- self.modules << new_module
20
+ def with_module(new_module)
21
+ modules << new_module
20
22
  self
21
23
  end
22
24
 
23
25
  def with_default_modules
24
- self.modules << Format
25
- self.modules << GoodCase
26
- self.modules << Typo
27
- self.modules << UnusedFields
26
+ modules << Format
27
+ modules << GoodCase
28
+ modules << Typo
29
+ modules << UnusedFields
28
30
  self
29
31
  end
30
32
 
31
33
  def with_all_modules
32
- self.modules << Format
33
- self.modules << ExtraVerbs
34
- self.modules << GoodCase
35
- self.modules << Typo
36
- self.modules << UnusedFields
34
+ modules << Format
35
+ modules << ExtraVerbs
36
+ modules << GoodCase
37
+ modules << Typo
38
+ modules << UnusedFields
37
39
  self
38
40
  end
39
41
  end
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiTester
4
+ # Holds data necessary for tests
2
5
  class BoundaryCase
3
- attr_accessor :payload
4
- attr_accessor :headers
5
- attr_accessor :description
6
+ attr_accessor :payload, :headers, :description
6
7
 
7
- def initialize description, payload, headers
8
+ def initialize(description:, payload:, headers:)
8
9
  self.description = description
9
10
  self.payload = payload
10
11
  self.headers = headers
@@ -1,15 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiTester
4
+ # Class to define the whole contract
2
5
  class Contract
3
- attr_accessor :name
4
- attr_accessor :endpoints
6
+ attr_accessor :name, :endpoints, :base_url, :max_time, :required_headers
5
7
 
6
- def initialize name
8
+ def initialize(name:, base_url:, max_time: 500)
7
9
  self.name = name
8
10
  self.endpoints = []
11
+ self.base_url = base_url
12
+ self.max_time = max_time
13
+ self.required_headers = {}
9
14
  end
10
15
 
11
- def add_endpoint endpoint
12
- self.endpoints << endpoint
16
+ def add_endpoint(endpoint)
17
+ endpoints << endpoint
13
18
  end
14
19
  end
15
20
  end
@@ -1,75 +1,101 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/definition/response'
2
4
  require 'api-tester/definition/method'
3
5
  require 'api-tester/test_helper'
6
+ require 'benchmark'
4
7
  require 'rest-client'
5
8
  require 'json'
6
9
 
7
10
  module ApiTester
11
+ # Class for defining and interacting with endpoints in a contract
8
12
  class Endpoint
9
- attr_accessor :name
10
- attr_accessor :base_url
11
- attr_accessor :path_params
12
- attr_accessor :methods
13
- attr_accessor :test_helper
14
- attr_accessor :bad_request_response
15
- attr_accessor :not_allowed_response
16
- attr_accessor :not_found_response
13
+ attr_accessor :name, :relative_url, :path_params, :methods, :test_helper, :bad_request_response, :not_allowed_response, :not_found_response, :longest_time, :required_headers
17
14
 
18
- def initialize name, url
19
- self.base_url = url
15
+ def initialize(name:, relative_url:)
16
+ self.relative_url = relative_url
20
17
  self.name = name
21
18
  self.methods = []
22
19
  self.path_params = []
23
- self.test_helper = ApiTester::TestHelper.new
24
- self.bad_request_response = ApiTester::Response.new 400
25
- self.not_allowed_response = ApiTester::Response.new 415
26
- self.not_found_response = ApiTester::Response.new 404
20
+ self.longest_time = { time: 0 }
21
+ self.test_helper = ApiTester::TestHelper.new ''
22
+ self.bad_request_response = ApiTester::Response.new status_code: 400
23
+ self.not_allowed_response = ApiTester::Response.new status_code: 415
24
+ self.not_found_response = ApiTester::Response.new status_code: 404
25
+ self.required_headers = {}
26
+ end
27
+
28
+ def display_url
29
+ relative_url
27
30
  end
28
31
 
29
32
  def url
30
- temp_url = self.base_url
31
- self.path_params.each do |param|
32
- temp_url.sub! "{#{param}}", self.test_helper.retrieve_param(param)
33
+ temp_url = relative_url.clone
34
+ path_params.each do |param|
35
+ value = test_helper.retrieve_param(param).to_s
36
+ temp_url = relative_url.sub "{#{param}}", value
33
37
  end
34
38
  temp_url
35
39
  end
36
40
 
37
- def default_call
38
- self.test_helper.before
39
- method_defaults = self.methods[0].default_request
40
- method_defaults[:url] = self.url
41
+ def default_call(base_url)
42
+ test_helper.before
43
+ method_defaults = methods[0].default_request
44
+ method_defaults[:url] = "#{base_url}#{url}"
41
45
  begin
42
- response = RestClient::Request.execute(method_defaults)
46
+ response = nil
47
+ time = Benchmark.measure {
48
+ response = RestClient::Request.execute(method_defaults)
49
+ }
50
+ if time.real > longest_time[:time] && longest_time[:time] > 0
51
+ longest_time[:time] = time.real
52
+ longest_time[:payload] = payload.to_json
53
+ longest_time[:verb] = method.verb
54
+ end
43
55
  rescue RestClient::ExceptionWithResponse => e
44
56
  response = e.response
45
57
  end
46
- self.test_helper.after
58
+ test_helper.after
47
59
  response
48
60
  end
49
61
 
50
- def call method, payload={}, headers={}
51
- self.test_helper.before
62
+ def call(base_url:, method:, query: '', payload: {}, headers: {})
63
+ test_helper.before
64
+ call_url = query ? "#{base_url}#{url}?#{query}" : "#{base_url}#{url}"
52
65
  begin
53
- response = RestClient::Request.execute(method: method.verb, url: self.url, payload: payload.to_json, headers: headers)
66
+ response = nil
67
+ time = Benchmark.measure {
68
+ response = RestClient::Request.execute(method: method.verb,
69
+ url: call_url,
70
+ payload: payload.to_json,
71
+ headers: headers)
72
+ }
73
+ if time.real > longest_time[:time]
74
+ longest_time[:time] = time.real
75
+ longest_time[:payload] = payload.to_json
76
+ longest_time[:verb] = method.verb
77
+ end
54
78
  rescue RestClient::ExceptionWithResponse => e
55
79
  response = e.response
56
80
  end
57
- self.test_helper.after
81
+ test_helper.after
58
82
  response
59
83
  end
60
84
 
61
- def add_method verb, response, request=Request.new()
62
- self.methods << ApiTester::Method.new(verb, response, request)
85
+ def add_method(verb:, response:, request: Request.new)
86
+ methods << ApiTester::Method.new(verb: verb,
87
+ response: response,
88
+ request: request)
63
89
  self
64
90
  end
65
91
 
66
- def add_path_param param
67
- self.path_params << param
92
+ def add_path_param(param)
93
+ path_params << param
68
94
  self
69
95
  end
70
96
 
71
97
  def verbs
72
- self.methods.map(&:verb)
98
+ methods.map(&:verb)
73
99
  end
74
100
  end
75
101
  end
@@ -1,46 +1,51 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/definition/fields/field'
2
4
 
3
5
  module ApiTester
6
+ # Class used for defining array fields
4
7
  class ArrayField < Field
5
8
  attr_accessor :fields
6
9
 
7
- def initialize name:
8
- super name: name
10
+ def initialize(name:, required: false, has_key: true)
11
+ super name: name, required: required, has_key: has_key
9
12
  self.fields = []
10
13
  end
11
14
 
12
- def with_field newField
13
- self.fields << newField
15
+ def with_field(new_field)
16
+ fields << new_field
14
17
  self
15
18
  end
16
19
 
17
- def has_subfields?
20
+ def subfields?
18
21
  true
19
22
  end
20
23
 
21
- def default_value
22
- if self.fields.size == 0
23
- return []
24
- end
24
+ def type
25
+ 'array'
26
+ end
27
+
28
+ def default
29
+ return [] if fields.size.zero?
25
30
 
26
- obj = Hash.new
27
- self.fields.each do |field|
28
- obj[field.name] = field.default_value
31
+ obj = {}
32
+ fields.each do |field|
33
+ obj[field.name] = field.default
29
34
  end
30
35
  [obj]
31
36
  end
32
37
 
33
38
  def negative_boundary_values
34
39
  super +
35
- [
36
- "string",
37
- 123,
38
- 0,
39
- 1,
40
- true,
41
- false,
42
- {}
43
- ]
40
+ [
41
+ 'string',
42
+ 123,
43
+ 0,
44
+ 1,
45
+ true,
46
+ false,
47
+ {}
48
+ ]
44
49
  end
45
50
  end
46
51
  end
@@ -1,19 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/definition/fields/field'
2
4
 
3
5
  module ApiTester
6
+ # Class for defining booleans in contract
4
7
  class BooleanField < Field
5
- def initialize name:, default_value: true
6
- super name: name, default_value: default_value
8
+ def initialize(name:, default: true, required: false)
9
+ super name: name, default: default, required: required
7
10
  end
8
11
 
9
12
  def negative_boundary_values
10
13
  super +
14
+ [
15
+ 'string',
16
+ 123,
17
+ 0,
18
+ 1,
19
+ {}
20
+ ]
21
+ end
22
+
23
+ def good_cases
11
24
  [
12
- "string",
13
- 123,
14
- 0,
15
- 1,
16
- {}
25
+ true,
26
+ false
17
27
  ]
18
28
  end
19
29
  end
@@ -1,22 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
1
5
  require 'api-tester/definition/fields/field'
2
6
 
3
7
  module ApiTester
8
+ # Class for defining email fields in contract
4
9
  class EmailField < Field
5
- def initialize name:, default_value: "test@test.com"
6
- super name: name, default_value: default_value
10
+ attr_accessor :randomize
11
+
12
+ def initialize(name:, default: 'test@test.com', required: false, randomize: false)
13
+ super name: name, default: default, required: required
14
+ self.randomize = randomize
15
+ end
16
+
17
+ def default
18
+ # Since many APIs have unique email checks, this allows us to generate hopefully unique emails
19
+ if randomize
20
+ "test#{SecureRandom.hex(10)}@test.com"
21
+ else
22
+ super
23
+ end
7
24
  end
8
25
 
9
26
  def negative_boundary_values
10
27
  super +
11
- [
12
- "string",
13
- 123,
14
- 1,
15
- 0,
16
- true,
17
- false,
18
- {}
19
- ]
28
+ [
29
+ 'string',
30
+ 123,
31
+ 1,
32
+ 0,
33
+ true,
34
+ false,
35
+ {}
36
+ ]
20
37
  end
21
38
  end
22
39
  end
@@ -1,14 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/definition/fields/field'
2
4
 
3
5
  module ApiTester
6
+ # Class for defining enumerators
4
7
  class EnumField < Field
5
8
  attr_accessor :acceptable_values
6
9
 
7
- def initialize name:, acceptable_values:, default_value: nil
8
- if default_value
9
- super name: name, default_value: default_value
10
+ def initialize(name:, acceptable_values:, default: nil, required: false)
11
+ if default
12
+ super name: name, default: default, required: required
10
13
  else
11
- super name: name, default_value: acceptable_values[0]
14
+ super name: name, default: acceptable_values[0], required: required
12
15
  end
13
16
 
14
17
  self.acceptable_values = acceptable_values
@@ -16,14 +19,18 @@ module ApiTester
16
19
 
17
20
  def negative_boundary_values
18
21
  super +
19
- [
20
- 123,
21
- 0,
22
- 1,
23
- true,
24
- false,
25
- {}
26
- ]
22
+ [
23
+ 123,
24
+ 0,
25
+ 1,
26
+ true,
27
+ false,
28
+ {}
29
+ ]
30
+ end
31
+
32
+ def good_cases
33
+ acceptable_values
27
34
  end
28
35
  end
29
36
  end
@@ -1,49 +1,56 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiTester
4
+ # Base class for field definitions
2
5
  class Field
3
- attr_accessor :name
4
- attr_accessor :default_value
5
- attr_accessor :required
6
- attr_accessor :is_seen
7
-
8
- def initialize name:, required:false, default_value:"string"
9
- self.name = name
10
- self.default_value = default_value
11
- self.required = required
12
- self.is_seen = 0
13
- end
14
-
15
- def is_required
16
- self.required = true
17
- self
18
- end
19
-
20
- def is_not_required
21
- self.required = false
22
- self
23
- end
24
-
25
- def has_subfields?
26
- false
27
- end
28
-
29
- def fields
30
- []
31
- end
32
-
33
- def negative_boundary_values
34
- cases = []
35
- if self.required
36
- cases << nil
37
- end
38
- cases
39
- end
40
-
41
- def seen
42
- self.is_seen += 1
43
- end
44
-
45
- def display_class
46
- self.class
47
- end
6
+ attr_accessor :name, :default, :required, :is_seen, :has_key
7
+
8
+ def initialize(name:, required: false, has_key: true, default: 'string')
9
+ self.name = name
10
+ self.default = default
11
+ self.required = required
12
+ self.is_seen = 0
13
+ self.has_key = has_key
14
+ end
15
+
16
+ def type
17
+ 'field'
18
+ end
19
+
20
+ def is_required
21
+ self.required = true
22
+ self
23
+ end
24
+
25
+ def is_not_required
26
+ self.required = false
27
+ self
28
+ end
29
+
30
+ def subfields?
31
+ false
32
+ end
33
+
34
+ def fields
35
+ []
36
+ end
37
+
38
+ def negative_boundary_values
39
+ cases = []
40
+ cases << nil if required
41
+ cases
42
+ end
43
+
44
+ def good_cases
45
+ []
46
+ end
47
+
48
+ def seen
49
+ self.is_seen += 1
50
+ end
51
+
52
+ def display_class
53
+ self.class
54
+ end
48
55
  end
49
56
  end
@@ -1,18 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/definition/fields/field'
2
4
 
3
5
  module ApiTester
6
+ # Class for defining numeric fields in contracts
4
7
  class NumberField < Field
5
- def initialize name:, default_value: 5
6
- super name: name, default_value: default_value
8
+ def initialize(name:, default: 5, required: false)
9
+ super name: name, default: default, required: required
7
10
  end
8
11
 
9
12
  def negative_boundary_values
10
13
  super +
14
+ [
15
+ 'string',
16
+ true,
17
+ false,
18
+ {}
19
+ ]
20
+ end
21
+
22
+ def good_cases
11
23
  [
12
- "string",
13
- true,
14
- false,
15
- {}
24
+ -1,
25
+ 0,
26
+ 1,
27
+ 100,
28
+ 9999,
29
+ 12_345_678_901_234_567_890
16
30
  ]
17
31
  end
18
32
  end