api-tester 0.1.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 (79) 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/.rspec +1 -0
  6. data/.rubocop.yml +61 -0
  7. data/Gemfile +2 -0
  8. data/Guardfile +70 -0
  9. data/README.md +106 -74
  10. data/Rakefile +8 -3
  11. data/api-tester.gemspec +31 -23
  12. data/changelog.txt +35 -0
  13. data/lib/api-tester.rb +15 -0
  14. data/lib/api-tester/config.rb +43 -0
  15. data/lib/api-tester/definition/boundary_case.rb +16 -0
  16. data/lib/api-tester/definition/contract.rb +20 -0
  17. data/lib/api-tester/definition/endpoint.rb +84 -0
  18. data/lib/api-tester/definition/fields/array_field.rb +47 -0
  19. data/lib/api-tester/definition/fields/boolean_field.rb +23 -0
  20. data/lib/api-tester/definition/fields/email_field.rb +25 -0
  21. data/lib/api-tester/definition/fields/enum_field.rb +32 -0
  22. data/lib/api-tester/definition/fields/field.rb +50 -0
  23. data/lib/api-tester/definition/fields/number_field.rb +22 -0
  24. data/lib/api-tester/definition/fields/object_field.rb +47 -0
  25. data/lib/api-tester/definition/fields/plain_array_field.rb +25 -0
  26. data/lib/api-tester/definition/method.rb +22 -0
  27. data/lib/api-tester/definition/request.rb +96 -0
  28. data/lib/api-tester/definition/response.rb +39 -0
  29. data/lib/api-tester/method_case_test.rb +83 -0
  30. data/lib/api-tester/modules/extra_verbs.rb +53 -0
  31. data/lib/api-tester/modules/format.rb +47 -0
  32. data/lib/api-tester/modules/good_case.rb +46 -0
  33. data/lib/api-tester/modules/injection_module.rb +81 -0
  34. data/lib/api-tester/modules/required_fields.rb +51 -0
  35. data/lib/api-tester/modules/server_information.rb +42 -0
  36. data/lib/api-tester/modules/typo.rb +70 -0
  37. data/lib/api-tester/modules/unexpected_fields.rb +61 -0
  38. data/lib/api-tester/modules/unused_fields.rb +31 -0
  39. data/lib/api-tester/reporter/api_report.rb +47 -0
  40. data/lib/api-tester/reporter/missing_field_report.rb +24 -0
  41. data/lib/api-tester/reporter/report.rb +30 -0
  42. data/lib/api-tester/reporter/status_code_report.rb +21 -0
  43. data/lib/api-tester/test_helper.rb +12 -0
  44. data/lib/api-tester/util/response_evaluator.rb +88 -0
  45. data/lib/api-tester/util/supported_verbs.rb +39 -0
  46. data/lib/api-tester/version.rb +5 -0
  47. metadata +159 -42
  48. data/.travis.yml +0 -6
  49. data/lib/tester.rb +0 -7
  50. data/lib/tester/api_tester.rb +0 -50
  51. data/lib/tester/definition/api_contract.rb +0 -13
  52. data/lib/tester/definition/api_method.rb +0 -11
  53. data/lib/tester/definition/boundary_case.rb +0 -11
  54. data/lib/tester/definition/endpoint.rb +0 -57
  55. data/lib/tester/definition/fields/array_field.rb +0 -44
  56. data/lib/tester/definition/fields/boolean_field.rb +0 -18
  57. data/lib/tester/definition/fields/email_field.rb +0 -20
  58. data/lib/tester/definition/fields/enum_field.rb +0 -27
  59. data/lib/tester/definition/fields/field.rb +0 -47
  60. data/lib/tester/definition/fields/number_field.rb +0 -17
  61. data/lib/tester/definition/fields/object_field.rb +0 -42
  62. data/lib/tester/definition/request.rb +0 -49
  63. data/lib/tester/definition/response.rb +0 -34
  64. data/lib/tester/method_case_test.rb +0 -67
  65. data/lib/tester/modules/extra_verbs.rb +0 -25
  66. data/lib/tester/modules/format.rb +0 -26
  67. data/lib/tester/modules/good_case.rb +0 -29
  68. data/lib/tester/modules/module.rb +0 -18
  69. data/lib/tester/modules/typo.rb +0 -41
  70. data/lib/tester/modules/unused_fields.rb +0 -22
  71. data/lib/tester/reporter/api_report.rb +0 -33
  72. data/lib/tester/reporter/missing_field_report.rb +0 -23
  73. data/lib/tester/reporter/missing_response_field_report.rb +0 -19
  74. data/lib/tester/reporter/report.rb +0 -25
  75. data/lib/tester/reporter/status_code_report.rb +0 -12
  76. data/lib/tester/test_helper.rb +0 -10
  77. data/lib/tester/util/response_evaluator.rb +0 -73
  78. data/lib/tester/util/supported_verbs.rb +0 -34
  79. data/lib/tester/version.rb +0 -3
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'injection_vulnerability_library'
4
+
5
+ module ApiTester
6
+ # Tests injection cases
7
+ module InjectionModule
8
+ def self.go(contract)
9
+ reports = []
10
+ contract.endpoints.each do |endpoint|
11
+ endpoint.methods.each do |method|
12
+ reports.concat inject_payload contract.base_url, endpoint, method
13
+ end
14
+ end
15
+ reports
16
+ end
17
+
18
+ def self.inject_payload(base_url, endpoint, method)
19
+ reports = []
20
+ sql_injections = InjectionVulnerabilityLibrary.sql_vulnerabilities
21
+
22
+ method.request.fields.each do |field|
23
+ sql_injections.each do |injection|
24
+ injection_value = "#{field.default_value}#{injection}"
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)
37
+ end
38
+ end
39
+
40
+ reports
41
+ end
42
+
43
+ def self.check_response(response, endpoint)
44
+ response.code == 200 || check_error(response, endpoint)
45
+ end
46
+
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
+ )
52
+ missing_fields = evaluator.missing_fields
53
+ extra_fields = evaluator.extra_fields
54
+ response.code == endpoint.bad_request_response.code &&
55
+ missing_fields.size.zero? && extra_fields.size.zero?
56
+ end
57
+ end
58
+ end
59
+
60
+ # Report for InjectionModule
61
+ class InjectionReport
62
+ attr_accessor :injection_type
63
+ attr_accessor :url
64
+ attr_accessor :payload
65
+ attr_accessor :response
66
+
67
+ def initialize(injection_type, url, payload, response)
68
+ self.injection_type = injection_type
69
+ self.url = url
70
+ self.payload = payload
71
+ self.response = response
72
+ end
73
+
74
+ def print
75
+ puts "Found potential #{injection_type}: "
76
+ puts " Requested #{url} with payload:"
77
+ puts " #{payload}"
78
+ puts ' Received: '
79
+ puts " #{response}"
80
+ end
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
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiTester
4
+ # Module for ensuring the server isn't broadcasting information about itself
5
+ module ServerInformation
6
+ def self.go(contract)
7
+ reports = []
8
+ endpoint = contract.endpoints[0]
9
+ response = endpoint.default_call contract.base_url
10
+
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)
15
+ end
16
+ end
17
+
18
+ reports
19
+ end
20
+
21
+ def self.order
22
+ 10
23
+ end
24
+ end
25
+ end
26
+
27
+ # Report used by module
28
+ class ServerBroadcastReport
29
+ attr_accessor :server_info
30
+ attr_accessor :server_key
31
+
32
+ def initialize(server_info, server_key)
33
+ self.server_info = server_info
34
+ self.server_key = server_key
35
+ end
36
+
37
+ def print
38
+ puts 'Found server information being broadcast in headers:'
39
+ puts " #{server_info}"
40
+ puts " as #{server_key}"
41
+ end
42
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'api-tester/reporter/status_code_report'
4
+ require 'api-tester/util/supported_verbs'
5
+
6
+ module ApiTester
7
+ # Module checking various not found scenarios
8
+ module Typo
9
+ def self.go(contract)
10
+ reports = []
11
+
12
+ contract.endpoints.each do |endpoint|
13
+ allowances(endpoint).each do
14
+ reports.concat check_typo_url(contract.base_url, endpoint)
15
+ end
16
+ end
17
+
18
+ reports
19
+ end
20
+
21
+ def self.check_typo_url(base_url, endpoint)
22
+ bad_url = "#{endpoint.url}gibberishadsfasdf"
23
+ bad_endpoint = ApiTester::Endpoint.new name: 'Bad URL',
24
+ relative_url: bad_url
25
+ typo_case = BoundaryCase.new description: 'Typo URL check',
26
+ payload: {},
27
+ headers: {}
28
+ method = ApiTester::Method.new verb: ApiTester::SupportedVerbs::GET,
29
+ response: ApiTester::Response.new(
30
+ status_code: 200
31
+ ),
32
+ request: ApiTester::Request.new
33
+ response = bad_endpoint.call base_url: base_url,
34
+ method: method,
35
+ payload: typo_case.payload,
36
+ headers: typo_case.headers
37
+
38
+ test = TypoClass.new response,
39
+ typo_case.payload,
40
+ endpoint.not_found_response,
41
+ bad_url,
42
+ ApiTester::SupportedVerbs::GET
43
+ test.check
44
+ end
45
+
46
+ def self.allowances(endpoint)
47
+ allowances = []
48
+ endpoint.methods.each do |method|
49
+ allowances << method.verb
50
+ end
51
+ allowances.uniq
52
+ end
53
+
54
+ def self.order
55
+ 4
56
+ end
57
+ end
58
+
59
+ # Test layout for TypoModule
60
+ class TypoClass < MethodCaseTest
61
+ def initialize(response, payload, expected_response, url, verb)
62
+ super response: response,
63
+ payload: payload,
64
+ expected_response: expected_response,
65
+ url: url,
66
+ verb: verb,
67
+ module_name: 'TypoModule'
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pry'
4
+
5
+ module ApiTester
6
+ # Module checking nothing shows up in response which is not defined in contract
7
+ module UnexpectedFields
8
+ def self.go(contract)
9
+ reports = []
10
+
11
+ contract.endpoints.each do |endpoint|
12
+ endpoint.methods.each do |method|
13
+ default_case = BoundaryCase.new description: endpoint.url,
14
+ payload: method.request.default_payload,
15
+ headers: method.request.default_headers
16
+ response = endpoint.call base_url: contract.base_url,
17
+ method: method,
18
+ payload: default_case.payload,
19
+ headers: default_case.headers
20
+ test = UnexpectedFieldsTest.new response, endpoint.url, method
21
+ reports.concat test.check
22
+ end
23
+ end
24
+ reports
25
+ end
26
+
27
+ def self.order
28
+ 90
29
+ end
30
+ end
31
+
32
+ # Test layout for UnexpectedFields module
33
+ class UnexpectedFieldsTest < MethodCaseTest
34
+ def initialize(response, url, method)
35
+ super response: response,
36
+ payload: method.request.default_payload,
37
+ expected_response: method.expected_response,
38
+ url: url,
39
+ verb: method.verb,
40
+ module_name: 'UnexpectedFieldsModule'
41
+ end
42
+
43
+ def check
44
+ evaluator = ApiTester::ResponseEvaluator.new actual_body: json_parse(response.body),
45
+ expected_fields: expected_response
46
+ response_fields = evaluator.response_field_array
47
+ expected_fields = evaluator.expected_fields
48
+ increment_fields evaluator.seen_fields
49
+ extra = response_fields - expected_fields
50
+ extra.each do |extra_field|
51
+ report = Report.new description: "UnexpectedFieldsModule - Found unexpected field #{extra_field}",
52
+ url: url,
53
+ request: payload,
54
+ expected_response: expected_response,
55
+ actual_response: response
56
+ reports << report
57
+ end
58
+ reports
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'api-tester/reporter/missing_field_report'
4
+
5
+ module ApiTester
6
+ # Ensures all fields defined in contract are returned during test suite
7
+ module UnusedFields
8
+ def self.go(contract)
9
+ reports = []
10
+
11
+ contract.endpoints.each do |endpoint|
12
+ endpoint.methods.each do |method|
13
+ method.expected_response.body.each do |field|
14
+ next unless field.is_seen.zero?
15
+
16
+ reports << MissingFieldReport.new(url: endpoint.url,
17
+ verb: method.verb,
18
+ expected_field: field.name,
19
+ description: 'UnusedFieldsModule')
20
+ end
21
+ end
22
+ end
23
+
24
+ reports
25
+ end
26
+
27
+ def self.order
28
+ 99
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'api-tester/reporter/report'
4
+
5
+ module ApiTester
6
+ # class for dealing with reports generated by the modules during test suite
7
+ class ApiReport
8
+ attr_accessor :reports
9
+
10
+ def initialize
11
+ self.reports = []
12
+ end
13
+
14
+ def add_new(url:, request:, expected_response:, actual_response:, description: 'case')
15
+ report = Report.new description,
16
+ url,
17
+ request,
18
+ expected_response,
19
+ actual_response
20
+ reports << report
21
+ end
22
+
23
+ def add_new_report(report)
24
+ reports << report
25
+ end
26
+
27
+ def add_reports(reports)
28
+ reports.each do |report|
29
+ add_new_report(report)
30
+ end
31
+ end
32
+
33
+ def print
34
+ if reports.size.zero?
35
+ puts 'No issues found'
36
+ else
37
+ puts "Issues discovered: #{reports.size}"
38
+ reports.each do |report|
39
+ report.print
40
+ puts '\n'
41
+ puts '\n'
42
+ end
43
+ puts "Total issues: #{reports.size}"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiTester
4
+ # Report used for when response is missing a field
5
+ class MissingFieldReport
6
+ attr_accessor :url
7
+ attr_accessor :verb
8
+ attr_accessor :expected_field
9
+ attr_accessor :description
10
+
11
+ def initialize(url:, verb:, expected_field:, description:)
12
+ self.url = url
13
+ self.verb = verb
14
+ self.expected_field = expected_field
15
+ self.description = description
16
+ end
17
+
18
+ def print
19
+ puts "#{description}:"
20
+ puts " #{verb} #{url} is missing response field:"
21
+ puts " #{expected_field}"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiTester
4
+ # Standard report format for differing responses
5
+ class Report
6
+ attr_accessor :description
7
+ attr_accessor :url
8
+ attr_accessor :request
9
+ attr_accessor :expected_response
10
+ attr_accessor :actual_response
11
+
12
+ def initialize(description:, url:, request:, expected_response:, actual_response:)
13
+ self.description = description
14
+ self.url = url
15
+ self.request = request
16
+ self.expected_response = expected_response
17
+ self.actual_response = actual_response
18
+ end
19
+
20
+ def print
21
+ puts "#{description}: "
22
+ puts " Requested #{url} with payload:"
23
+ puts " #{request.to_json}"
24
+ puts ' Expecting: '
25
+ puts ' ' + expected_response.to_s
26
+ puts ' Receiving: '
27
+ puts " #{actual_response}"
28
+ end
29
+ end
30
+ end