api-tester 0.3.1 → 1.1.2

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.
Files changed (54) 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/.rspec +1 -0
  7. data/.rubocop.yml +171 -0
  8. data/Gemfile +2 -0
  9. data/Guardfile +70 -0
  10. data/README.md +67 -63
  11. data/Rakefile +8 -3
  12. data/api-tester.gemspec +29 -23
  13. data/changelog.txt +19 -0
  14. data/lib/api-tester/config.rb +17 -15
  15. data/lib/api-tester/definition/boundary_case.rb +5 -4
  16. data/lib/api-tester/definition/contract.rb +10 -5
  17. data/lib/api-tester/definition/endpoint.rb +59 -32
  18. data/lib/api-tester/definition/fields/array_field.rb +26 -21
  19. data/lib/api-tester/definition/fields/boolean_field.rb +17 -7
  20. data/lib/api-tester/definition/fields/email_field.rb +28 -11
  21. data/lib/api-tester/definition/fields/enum_field.rb +19 -12
  22. data/lib/api-tester/definition/fields/field.rb +52 -45
  23. data/lib/api-tester/definition/fields/number_field.rb +20 -6
  24. data/lib/api-tester/definition/fields/object_field.rb +37 -30
  25. data/lib/api-tester/definition/fields/plain_array_field.rb +25 -0
  26. data/lib/api-tester/definition/method.rb +8 -5
  27. data/lib/api-tester/definition/request.rb +55 -13
  28. data/lib/api-tester/definition/response.rb +35 -25
  29. data/lib/api-tester/method_case_test.rb +68 -54
  30. data/lib/api-tester/modules/benchmark_module.rb +35 -0
  31. data/lib/api-tester/modules/extra_verbs.rb +37 -10
  32. data/lib/api-tester/modules/format.rb +23 -8
  33. data/lib/api-tester/modules/good_case.rb +25 -10
  34. data/lib/api-tester/modules/good_variations.rb +69 -0
  35. data/lib/api-tester/modules/injection_module.rb +44 -23
  36. data/lib/api-tester/modules/missing_resource.rb +64 -0
  37. data/lib/api-tester/modules/required_fields.rb +51 -0
  38. data/lib/api-tester/modules/server_information.rb +14 -12
  39. data/lib/api-tester/modules/typo.rb +39 -14
  40. data/lib/api-tester/modules/unexpected_fields.rb +61 -0
  41. data/lib/api-tester/modules/unused_fields.rb +13 -7
  42. data/lib/api-tester/reporter/api_report.rb +25 -16
  43. data/lib/api-tester/reporter/missing_field_report.rb +11 -15
  44. data/lib/api-tester/reporter/report.rb +12 -13
  45. data/lib/api-tester/reporter/response_time_report.rb +24 -0
  46. data/lib/api-tester/reporter/status_code_report.rb +10 -4
  47. data/lib/api-tester/test_helper.rb +8 -6
  48. data/lib/api-tester/util/response_evaluator.rb +84 -56
  49. data/lib/api-tester/util/supported_verbs.rb +8 -5
  50. data/lib/api-tester/version.rb +3 -1
  51. data/lib/api-tester.rb +6 -3
  52. metadata +117 -24
  53. data/.travis.yml +0 -6
  54. data/lib/api-tester/reporter/missing_response_field_report.rb +0 -21
data/api-tester.gemspec CHANGED
@@ -4,38 +4,44 @@ $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"
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 'pry', '~> 0.11'
37
+ spec.add_development_dependency 'rake', '~> 13.0.1'
38
+ spec.add_development_dependency 'require_all', '~>3.0.0'
39
+ spec.add_development_dependency 'rspec', '~> 3.0'
40
+ spec.add_development_dependency 'rubocop', '~> 1.31.0'
41
+ spec.add_development_dependency 'terminal-notifier', '~> 2.0.0'
42
+ spec.add_development_dependency 'terminal-notifier-guard', '~> 1.7.0'
43
+ spec.add_development_dependency 'webmock', '~> 3.4'
38
44
 
39
- spec.add_runtime_dependency "rest-client", "~> 2.0"
40
- spec.add_runtime_dependency "injection_vulnerability_library", "0.0.2"
45
+ spec.add_runtime_dependency 'injection_vulnerability_library', '0.1.3'
46
+ spec.add_runtime_dependency 'rest-client', '~> 2.0'
41
47
  end
data/changelog.txt CHANGED
@@ -1,3 +1,22 @@
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
+
11
+ 1.0.0
12
+
13
+ - Switching to using named variables to make future changes less breaky
14
+
15
+ 0.3.2
16
+
17
+ - Fixing some issues with how config adds modules
18
+ - Fixing README for same module issue as config
19
+
1
20
  0.3.1
2
21
 
3
22
  - Adding ability to change headers for request objects
@@ -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.new
25
- self.modules << GoodCase.new
26
- self.modules << Typo.new
27
- self.modules << UnusedFields.new
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.new
33
- self.modules << ExtraVerbs.new
34
- self.modules << GoodCase.new
35
- self.modules << Typo.new
36
- self.modules << UnusedFields.new
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,102 @@
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'
9
+ require 'pry'
6
10
 
7
11
  module ApiTester
12
+ # Class for defining and interacting with endpoints in a contract
8
13
  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
14
+ attr_accessor :name, :relative_url, :path_params, :methods, :test_helper, :bad_request_response, :not_allowed_response, :not_found_response, :longest_time, :required_headers
17
15
 
18
- def initialize name, url
19
- self.base_url = url
16
+ def initialize(name:, relative_url:)
17
+ self.relative_url = relative_url
20
18
  self.name = name
21
19
  self.methods = []
22
20
  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
21
+ self.longest_time = { time: 0 }
22
+ self.test_helper = ApiTester::TestHelper.new ''
23
+ self.bad_request_response = ApiTester::Response.new status_code: 400
24
+ self.not_allowed_response = ApiTester::Response.new status_code: 415
25
+ self.not_found_response = ApiTester::Response.new status_code: 404
26
+ self.required_headers = {}
27
+ end
28
+
29
+ def display_url
30
+ relative_url
27
31
  end
28
32
 
29
33
  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)
34
+ temp_url = relative_url.clone
35
+ path_params.each do |param|
36
+ value = test_helper.retrieve_param(param).to_s
37
+ temp_url = relative_url.sub "{#{param}}", value
33
38
  end
34
39
  temp_url
35
40
  end
36
41
 
37
- def default_call
38
- self.test_helper.before
39
- method_defaults = self.methods[0].default_request
40
- method_defaults[:url] = self.url
42
+ def default_call(base_url)
43
+ test_helper.before
44
+ method_defaults = methods[0].default_request
45
+ method_defaults[:url] = "#{base_url}#{url}"
41
46
  begin
42
- response = RestClient::Request.execute(method_defaults)
47
+ response = nil
48
+ time = Benchmark.measure {
49
+ response = RestClient::Request.execute(method_defaults)
50
+ }
51
+ if time.real > longest_time[:time] && longest_time[:time] > 0
52
+ longest_time[:time] = time.real
53
+ longest_time[:payload] = payload.to_json
54
+ longest_time[:verb] = method.verb
55
+ end
43
56
  rescue RestClient::ExceptionWithResponse => e
44
57
  response = e.response
45
58
  end
46
- self.test_helper.after
59
+ test_helper.after
47
60
  response
48
61
  end
49
62
 
50
- def call method, payload={}, headers={}
51
- self.test_helper.before
63
+ def call(base_url:, method:, query: '', payload: {}, headers: {})
64
+ test_helper.before
65
+ call_url = query ? "#{base_url}#{url}?#{query}" : "#{base_url}#{url}"
52
66
  begin
53
- response = RestClient::Request.execute(method: method.verb, url: self.url, payload: payload.to_json, headers: headers)
67
+ response = nil
68
+ time = Benchmark.measure {
69
+ response = RestClient::Request.execute(method: method.verb,
70
+ url: call_url,
71
+ payload: payload.to_json,
72
+ headers: headers)
73
+ }
74
+ if time.real > longest_time[:time]
75
+ longest_time[:time] = time.real
76
+ longest_time[:payload] = payload.to_json
77
+ longest_time[:verb] = method.verb
78
+ end
54
79
  rescue RestClient::ExceptionWithResponse => e
55
80
  response = e.response
56
81
  end
57
- self.test_helper.after
82
+ test_helper.after
58
83
  response
59
84
  end
60
85
 
61
- def add_method verb, response, request=Request.new()
62
- self.methods << ApiTester::Method.new(verb, response, request)
86
+ def add_method(verb:, response:, request: Request.new)
87
+ methods << ApiTester::Method.new(verb: verb,
88
+ response: response,
89
+ request: request)
63
90
  self
64
91
  end
65
92
 
66
- def add_path_param param
67
- self.path_params << param
93
+ def add_path_param(param)
94
+ path_params << param
68
95
  self
69
96
  end
70
97
 
71
98
  def verbs
72
- self.methods.map(&:verb)
99
+ methods.map(&:verb)
73
100
  end
74
101
  end
75
102
  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)
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, 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, 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, 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, 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, default_value="string"
9
- self.name = name
10
- self.default_value = default_value
11
- self.required = false
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