api-tester 1.0.0 → 1.1.1

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 (48) hide show
  1. checksums.yaml +5 -5
  2. data/.github/dependabot.yml +15 -0
  3. data/.github/workflows/push.yml +39 -0
  4. data/.github/workflows/test.yml +31 -0
  5. data/.rubocop.yml +61 -0
  6. data/Gemfile +2 -0
  7. data/Guardfile +70 -0
  8. data/README.md +65 -61
  9. data/Rakefile +8 -3
  10. data/api-tester.gemspec +29 -24
  11. data/changelog.txt +10 -0
  12. data/lib/api-tester.rb +6 -3
  13. data/lib/api-tester/config.rb +16 -13
  14. data/lib/api-tester/definition/boundary_case.rb +4 -1
  15. data/lib/api-tester/definition/contract.rb +8 -3
  16. data/lib/api-tester/definition/endpoint.rb +32 -23
  17. data/lib/api-tester/definition/fields/array_field.rb +20 -19
  18. data/lib/api-tester/definition/fields/boolean_field.rb +12 -9
  19. data/lib/api-tester/definition/fields/email_field.rb +14 -11
  20. data/lib/api-tester/definition/fields/enum_field.rb +14 -11
  21. data/lib/api-tester/definition/fields/field.rb +46 -45
  22. data/lib/api-tester/definition/fields/number_field.rb +11 -8
  23. data/lib/api-tester/definition/fields/object_field.rb +34 -31
  24. data/lib/api-tester/definition/fields/plain_array_field.rb +25 -0
  25. data/lib/api-tester/definition/method.rb +7 -2
  26. data/lib/api-tester/definition/request.rb +43 -16
  27. data/lib/api-tester/definition/response.rb +29 -26
  28. data/lib/api-tester/method_case_test.rb +67 -53
  29. data/lib/api-tester/modules/extra_verbs.rb +29 -9
  30. data/lib/api-tester/modules/format.rb +23 -7
  31. data/lib/api-tester/modules/good_case.rb +25 -10
  32. data/lib/api-tester/modules/injection_module.rb +32 -17
  33. data/lib/api-tester/modules/required_fields.rb +51 -0
  34. data/lib/api-tester/modules/server_information.rb +13 -10
  35. data/lib/api-tester/modules/typo.rb +36 -13
  36. data/lib/api-tester/modules/unexpected_fields.rb +61 -0
  37. data/lib/api-tester/modules/unused_fields.rb +12 -6
  38. data/lib/api-tester/reporter/api_report.rb +24 -16
  39. data/lib/api-tester/reporter/missing_field_report.rb +12 -13
  40. data/lib/api-tester/reporter/report.rb +11 -8
  41. data/lib/api-tester/reporter/status_code_report.rb +9 -2
  42. data/lib/api-tester/test_helper.rb +6 -6
  43. data/lib/api-tester/util/response_evaluator.rb +70 -57
  44. data/lib/api-tester/util/supported_verbs.rb +8 -5
  45. data/lib/api-tester/version.rb +3 -1
  46. metadata +99 -25
  47. data/.travis.yml +0 -6
  48. data/lib/api-tester/reporter/missing_response_field_report.rb +0 -21
@@ -1,69 +1,83 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/util/response_evaluator.rb'
2
4
 
3
5
  module ApiTester
6
+ # Class for testing methods
4
7
  class MethodCaseTest
5
- attr_accessor :expected_response
6
- attr_accessor :payload
7
- attr_accessor :response
8
- attr_accessor :reports
9
- attr_accessor :url
10
- attr_accessor :module_name
8
+ attr_accessor :expected_response
9
+ attr_accessor :payload
10
+ attr_accessor :response
11
+ attr_accessor :reports
12
+ attr_accessor :url
13
+ attr_accessor :module_name
11
14
 
12
- def initialize response, payload, expected_response, url, verb, module_name
13
- self.payload = payload
14
- self.response = response
15
- self.expected_response = expected_response
16
- self.reports = []
17
- self.url = "#{verb} #{url}"
18
- self.module_name = module_name
19
- end
15
+ def initialize(response:, payload:, expected_response:, url:, verb:, module_name:)
16
+ self.payload = payload
17
+ self.response = response
18
+ self.expected_response = expected_response
19
+ self.reports = []
20
+ self.url = "#{verb} #{url}"
21
+ self.module_name = module_name
22
+ end
20
23
 
21
- def response_code_report
22
- report = StatusCodeReport.new "#{module_name} - Incorrect response code", self.url, self.payload, self.expected_response.code, self.response.code
23
- self.reports << report
24
- nil
25
- end
24
+ def response_code_report
25
+ report = StatusCodeReport.new description: "#{module_name} - Incorrect response code",
26
+ url: url,
27
+ request: payload,
28
+ expected_status_code: expected_response.code,
29
+ actual_status_code: "#{response.code} : #{response.body}"
30
+ reports << report
31
+ nil
32
+ end
26
33
 
27
- def missing_field_report field
28
- report = Report.new "#{module_name} - Missing field #{field}", self.url, self.payload, self.expected_response, self.response
29
- self.reports << report
30
- nil
31
- end
34
+ def missing_field_report(field)
35
+ report = Report.new description: "#{module_name} - Missing field #{field}",
36
+ url: url,
37
+ request: payload,
38
+ expected_response: expected_response,
39
+ actual_response: response
40
+ reports << report
41
+ nil
42
+ end
32
43
 
33
- def extra_field_report field
34
- report = Report.new "#{module_name} - Found extra field #{field}", self.url, self.payload, self.expected_response, self.response
35
- self.reports << report
36
- nil
37
- end
44
+ def extra_field_report(field)
45
+ report = Report.new description: "#{module_name} - Found extra field #{field}",
46
+ url: url,
47
+ request: payload,
48
+ expected_response: expected_response,
49
+ actual_response: response
50
+ reports << report
51
+ nil
52
+ end
38
53
 
39
- def check
40
- if check_response_code
41
- evaluator = ApiTester::ResponseEvaluator.new json_parse(self.response.body), self.expected_response
42
- evaluator.missing_fields.map{|field| missing_field_report(field)}
43
- evaluator.extra_fields.map{|field| extra_field_report(field)}
44
- increment_fields evaluator.seen_fields
45
- end
46
- return self.reports
54
+ def check
55
+ if check_response_code
56
+ evaluator = ApiTester::ResponseEvaluator.new actual_body: json_parse(response.body),
57
+ expected_fields: expected_response
58
+ evaluator.missing_fields.map { |field| missing_field_report(field) }
59
+ evaluator.extra_fields.map { |field| extra_field_report(field) }
60
+ increment_fields evaluator.seen_fields
47
61
  end
62
+ reports
63
+ end
48
64
 
49
- def check_response_code
50
- if response.code != expected_response.code
51
- response_code_report
52
- return false
53
- end
54
- return true
65
+ def check_response_code
66
+ if response.code != expected_response.code
67
+ response_code_report
68
+ return false
55
69
  end
70
+ true
71
+ end
56
72
 
57
- def increment_fields seen_fields
58
- seen_fields.each do |field|
59
- field.seen
60
- end
61
- end
73
+ def increment_fields(seen_fields)
74
+ seen_fields.each(&:seen)
75
+ end
62
76
 
63
- def json_parse body
64
- JSON.parse!(body)
65
- rescue JSON::ParserError
66
- body
67
- end
77
+ def json_parse(body)
78
+ JSON.parse!(body)
79
+ rescue JSON::ParserError
80
+ body
81
+ end
68
82
  end
69
83
  end
@@ -1,18 +1,32 @@
1
- require 'api-tester/util/supported_verbs'
1
+ # frozen_string_literal: true
2
+
3
+ require 'api-tester/util/supported_verbs'
2
4
 
3
5
  module ApiTester
6
+ # Check verbs not explicitly defined in contract
4
7
  module ExtraVerbs
5
- def self.go contract
8
+ def self.go(contract)
6
9
  reports = []
7
10
 
8
11
  contract.endpoints.each do |endpoint|
9
12
  extras = ApiTester::SupportedVerbs.all - endpoint.verbs
10
13
  headers = endpoint.methods[0].request.default_headers
11
14
  extras.each do |verb|
12
- verb_case = BoundaryCase.new("Verb check with #{verb} for #{endpoint.name}", {}, headers)
13
- method = ApiTester::Method.new verb, ApiTester::Response.new, ApiTester::Request.new
14
- response = endpoint.call method, verb_case.payload, verb_case.headers
15
- test = VerbClass.new response, verb_case.payload, endpoint.not_allowed_response, endpoint.url, verb
15
+ verb_case = BoundaryCase.new description: "Verb check with #{verb} for #{endpoint.name}",
16
+ payload: {},
17
+ headers: headers
18
+ method = ApiTester::Method.new verb: verb,
19
+ response: ApiTester::Response.new,
20
+ request: ApiTester::Request.new
21
+ response = endpoint.call base_url: contract.base_url,
22
+ method: method,
23
+ payload: verb_case.payload,
24
+ headers: verb_case.headers
25
+ test = VerbClass.new response: response,
26
+ payload: verb_case.payload,
27
+ expected_response: endpoint.not_allowed_response,
28
+ url: endpoint.url,
29
+ verb: verb
16
30
  reports.concat test.check
17
31
  end
18
32
  end
@@ -25,9 +39,15 @@ module ApiTester
25
39
  end
26
40
  end
27
41
 
42
+ # Test template used for module
28
43
  class VerbClass < MethodCaseTest
29
- def initialize response, payload, expected_response, url, verb
30
- super response, payload, expected_response, url, verb, "VerbModule"
31
- end
44
+ def initialize(response:, payload:, expected_response:, url:, verb:)
45
+ super response: response,
46
+ payload: payload,
47
+ expected_response: expected_response,
48
+ url: url,
49
+ verb: verb,
50
+ module_name: 'VerbModule'
51
+ end
32
52
  end
33
53
  end
@@ -1,16 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/reporter/status_code_report'
2
4
  require 'api-tester/method_case_test'
3
5
 
4
6
  module ApiTester
5
- class Format
6
- def self.go contract
7
+ # Checks the format constraints defined in contract
8
+ module Format
9
+ def self.go(contract)
7
10
  reports = []
8
11
  contract.endpoints.each do |endpoint|
9
12
  endpoint.methods.each do |method|
10
13
  cases = method.request.cases
11
14
  cases.each do |format_case|
12
- response = endpoint.call method, format_case.payload, format_case.headers
13
- test = FormatTest.new response, format_case.payload, endpoint.bad_request_response, endpoint.url, method.verb
15
+ response = endpoint.call base_url: contract.base_url,
16
+ method: method,
17
+ payload: format_case.payload,
18
+ headers: format_case.headers
19
+ test = FormatTest.new response: response,
20
+ payload: format_case.payload,
21
+ expected_response: endpoint.bad_request_response,
22
+ url: endpoint.url,
23
+ verb: method.verb
14
24
  reports.concat test.check
15
25
  end
16
26
  end
@@ -23,9 +33,15 @@ module ApiTester
23
33
  end
24
34
  end
25
35
 
36
+ # Test layout used by Format module
26
37
  class FormatTest < MethodCaseTest
27
- def initialize response, payload, expected_response, url, verb
28
- super response, payload, expected_response, url, verb, "FormatModule"
29
- end
38
+ def initialize(response:, payload:, expected_response:, url:, verb:)
39
+ super response: response,
40
+ payload: payload,
41
+ expected_response: expected_response,
42
+ url: url,
43
+ verb: verb,
44
+ module_name: 'FormatModule'
45
+ end
30
46
  end
31
47
  end
@@ -1,16 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/reporter/status_code_report'
2
4
  require 'api-tester/method_case_test'
3
5
 
4
6
  module ApiTester
5
- class GoodCase
6
- def self.go contract
7
+ # Checks the good case as defined in contract
8
+ module GoodCase
9
+ def self.go(contract)
7
10
  reports = []
8
11
 
9
12
  contract.endpoints.each do |endpoint|
10
13
  endpoint.methods.each do |method|
11
- default_case = BoundaryCase.new endpoint.url, method.request.default_payload, method.request.default_headers
12
- response = endpoint.call method, default_case.payload, default_case.headers
13
- test = GoodCaseTest.new response, endpoint.url, method
14
+ default_case = BoundaryCase.new description: contract.base_url + endpoint.url,
15
+ payload: method.request.default_payload,
16
+ headers: method.request.default_headers
17
+ response = endpoint.call base_url: contract.base_url,
18
+ method: method,
19
+ payload: default_case.payload,
20
+ headers: default_case.headers
21
+ test = GoodCaseTest.new response: response,
22
+ url: contract.base_url + endpoint.url,
23
+ method: method
14
24
  reports.concat test.check
15
25
  end
16
26
  end
@@ -18,14 +28,19 @@ module ApiTester
18
28
  end
19
29
 
20
30
  def self.order
21
- 1
31
+ 1
22
32
  end
23
33
  end
24
34
 
25
-
35
+ # Test layout used by module
26
36
  class GoodCaseTest < MethodCaseTest
27
- def initialize response, url, method
28
- super response, method.request.default_payload, method.expected_response, url, method.verb, "GoodCaseModule"
29
- end
37
+ def initialize(response:, url:, method:)
38
+ super response: response,
39
+ payload: method.request.default_payload,
40
+ expected_response: method.expected_response,
41
+ url: url,
42
+ verb: method.verb,
43
+ module_name: 'GoodCaseModule'
44
+ end
30
45
  end
31
46
  end
@@ -1,29 +1,39 @@
1
- require "injection_vulnerability_library"
1
+ # frozen_string_literal: true
2
+
3
+ require 'injection_vulnerability_library'
2
4
 
3
5
  module ApiTester
6
+ # Tests injection cases
4
7
  module InjectionModule
5
- def self.go contract
8
+ def self.go(contract)
6
9
  reports = []
7
10
  contract.endpoints.each do |endpoint|
8
11
  endpoint.methods.each do |method|
9
- reports.concat inject_payload endpoint, method
12
+ reports.concat inject_payload contract.base_url, endpoint, method
10
13
  end
11
14
  end
12
15
  reports
13
16
  end
14
17
 
15
- def self.inject_payload endpoint, method
18
+ def self.inject_payload(base_url, endpoint, method)
16
19
  reports = []
17
20
  sql_injections = InjectionVulnerabilityLibrary.sql_vulnerabilities
18
21
 
19
22
  method.request.fields.each do |field|
20
23
  sql_injections.each do |injection|
21
24
  injection_value = "#{field.default_value}#{injection}"
22
- payload = method.request.altered_payload(field.name, injection_value)
23
- response = endpoint.call method, payload, method.request.default_headers
24
- if(!check_response(response, endpoint)) then
25
- reports << InjectionReport.new("sql", endpoint.url, payload, response)
26
- end
25
+ payload = method.request.altered_payload field_name: field.name,
26
+ value: injection_value
27
+ response = endpoint.call base_url: base_url,
28
+ method: method,
29
+ payload: payload,
30
+ headers: method.request.default_headers
31
+ next if check_response(response, endpoint)
32
+
33
+ reports << InjectionReport.new('sql',
34
+ endpoint.url,
35
+ payload,
36
+ response)
27
37
  end
28
38
  end
29
39
 
@@ -34,22 +44,27 @@ module ApiTester
34
44
  response.code == 200 || check_error(response, endpoint)
35
45
  end
36
46
 
37
- def self.check_error response, endpoint
38
- evaluator = ApiTester::ResponseEvaluator.new response.body, endpoint.bad_request_response
47
+ def self.check_error(response, endpoint)
48
+ evaluator = ApiTester::ResponseEvaluator.new(
49
+ actual_body: response.body,
50
+ expected_fields: endpoint.bad_request_response
51
+ )
39
52
  missing_fields = evaluator.missing_fields
40
53
  extra_fields = evaluator.extra_fields
41
- response.code == endpoint.bad_request_response.code && missing_fields.size == 0 && extra_fields.size == 0
54
+ response.code == endpoint.bad_request_response.code &&
55
+ missing_fields.size.zero? && extra_fields.size.zero?
42
56
  end
43
57
  end
44
58
  end
45
59
 
60
+ # Report for InjectionModule
46
61
  class InjectionReport
47
62
  attr_accessor :injection_type
48
63
  attr_accessor :url
49
64
  attr_accessor :payload
50
65
  attr_accessor :response
51
66
 
52
- def initialize injection_type, url, payload, response
67
+ def initialize(injection_type, url, payload, response)
53
68
  self.injection_type = injection_type
54
69
  self.url = url
55
70
  self.payload = payload
@@ -57,10 +72,10 @@ class InjectionReport
57
72
  end
58
73
 
59
74
  def print
60
- puts "Found potential #{self.injection_type}: "
61
- puts " Requested #{self.url} with payload:"
62
- puts " #{self.payload}"
75
+ puts "Found potential #{injection_type}: "
76
+ puts " Requested #{url} with payload:"
77
+ puts " #{payload}"
63
78
  puts ' Received: '
64
- puts " #{self.response}"
79
+ puts " #{response}"
65
80
  end
66
81
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiTester
4
+ # Ensures the fields marked as required in contract are guarded
5
+ module RequiredFields
6
+ def self.go(contract)
7
+ reports = []
8
+ contract.endpoints.each do |endpoint|
9
+ endpoint.methods.each do |method|
10
+ request_def = method.request
11
+ required_fields = request_def.fields.keep_if(&:required)
12
+ combinations = (1..required_fields.size).flat_map { |size| required_fields.combination(size).to_a }
13
+ combinations.each do |remove_fields|
14
+ fields = remove_fields.map do |field|
15
+ { name: field.name, value: nil }
16
+ end
17
+ payload = request_def.altered_payload_with fields
18
+ response = endpoint.call base_url: contract.base_url,
19
+ method: method,
20
+ payload: payload,
21
+ headers: request_def.default_headers
22
+ test = RequiredFieldsTest.new response: response,
23
+ payload: payload,
24
+ expected_response: endpoint.bad_request_response,
25
+ url: endpoint.url,
26
+ verb: method.verb
27
+ reports.concat test.check
28
+ end
29
+ end
30
+ end
31
+
32
+ reports
33
+ end
34
+
35
+ def self.order
36
+ 5
37
+ end
38
+ end
39
+
40
+ # Test layout used for RequiredFieldsModule
41
+ class RequiredFieldsTest < MethodCaseTest
42
+ def initialize(response:, payload:, expected_response:, url:, verb:)
43
+ super response: response,
44
+ payload: payload,
45
+ expected_response: expected_response,
46
+ url: url,
47
+ verb: verb,
48
+ module_name: 'RequiredFieldsModule'
49
+ end
50
+ end
51
+ end
@@ -1,15 +1,17 @@
1
- require 'pry'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module ApiTester
4
+ # Module for ensuring the server isn't broadcasting information about itself
4
5
  module ServerInformation
5
- def self.go contract
6
+ def self.go(contract)
6
7
  reports = []
7
8
  endpoint = contract.endpoints[0]
8
- response = endpoint.default_call
9
+ response = endpoint.default_call contract.base_url
9
10
 
10
- [:server, :x_powered_by, :x_aspnetmvc_version, :x_aspnet_version].each do |server_key|
11
- if response.headers[server_key] then
12
- reports << ServerBroadcastReport.new(response.headers[server_key], server_key)
11
+ %i[server x_powered_by x_aspnetmvc_version x_aspnet_version].each do |key|
12
+ if response.headers[key]
13
+ reports << ServerBroadcastReport.new(response.headers[key],
14
+ key)
13
15
  end
14
16
  end
15
17
 
@@ -22,18 +24,19 @@ module ApiTester
22
24
  end
23
25
  end
24
26
 
27
+ # Report used by module
25
28
  class ServerBroadcastReport
26
29
  attr_accessor :server_info
27
30
  attr_accessor :server_key
28
31
 
29
- def initialize server_info, server_key
32
+ def initialize(server_info, server_key)
30
33
  self.server_info = server_info
31
34
  self.server_key = server_key
32
35
  end
33
36
 
34
37
  def print
35
- puts "Found server information being broadcast in headers:"
36
- puts " #{self.server_info}"
37
- puts " as #{self.server_key}"
38
+ puts 'Found server information being broadcast in headers:'
39
+ puts " #{server_info}"
40
+ puts " as #{server_key}"
38
41
  end
39
42
  end