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
@@ -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.display_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
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'api-tester/reporter/status_code_report'
4
+ require 'api-tester/method_case_test'
5
+
6
+ module ApiTester
7
+ # Checks the good case as defined in contract
8
+ module GoodVariations
9
+ def self.go(contract)
10
+ reports = []
11
+
12
+ contract.endpoints.each do |endpoint|
13
+ endpoint.methods.each do |method|
14
+ method.request.fields.each do |field|
15
+ field.good_cases.each do |value|
16
+ payload = method.request.default_payload
17
+ payload[field.name] = value
18
+ call = BoundaryCase.new description: contract.base_url + endpoint.display_url,
19
+ payload: payload,
20
+ headers: method.request.default_headers
21
+ response = endpoint.call base_url: contract.base_url,
22
+ method: method,
23
+ payload: payload,
24
+ headers: call.headers
25
+ test = GoodVariationTest.new response: response,
26
+ url: contract.base_url + endpoint.url,
27
+ method: method
28
+ reports.concat test.check
29
+ end
30
+ end
31
+ method.request.query_params.each do |field|
32
+ field.good_cases.each do |value|
33
+ payload = method.request.default_payload
34
+ payload[field.name] = value
35
+ call = BoundaryCase.new description: contract.base_url + endpoint.display_url,
36
+ payload: payload,
37
+ headers: method.request.default_headers
38
+ response = endpoint.call base_url: contract.base_url,
39
+ method: method,
40
+ payload: payload,
41
+ headers: call.headers
42
+ test = GoodVariationTest.new response: response,
43
+ url: contract.base_url + endpoint.url,
44
+ method: method
45
+ reports.concat test.check
46
+ end
47
+ end
48
+ end
49
+ end
50
+ reports
51
+ end
52
+
53
+ def self.order
54
+ 1
55
+ end
56
+ end
57
+
58
+ # Test layout used by module
59
+ class GoodVariationTest < MethodCaseTest
60
+ def initialize(response:, url:, method:)
61
+ super response: response,
62
+ payload: method.request.default_payload,
63
+ expected_response: method.expected_response,
64
+ url: url,
65
+ verb: method.verb,
66
+ module_name: 'GoodVariationsModule'
67
+ end
68
+ end
69
+ 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
- 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
24
+ injection_value = "#{field.default}#{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)
27
37
  end
28
38
  end
29
39
 
@@ -31,25 +41,36 @@ module ApiTester
31
41
  end
32
42
 
33
43
  def self.check_response(response, endpoint)
34
- response.code == 200 || check_error(response, endpoint)
44
+ if response.code == 200 || check_error(response, endpoint)
45
+ print '.'
46
+ return true
47
+ end
48
+ print 'F'
49
+ false
35
50
  end
36
51
 
37
- def self.check_error response, endpoint
38
- evaluator = ApiTester::ResponseEvaluator.new response.body, endpoint.bad_request_response
52
+ def self.check_error(response, endpoint)
53
+ evaluator = ApiTester::ResponseEvaluator.new(
54
+ actual_body: response.body,
55
+ expected_fields: endpoint.bad_request_response
56
+ )
39
57
  missing_fields = evaluator.missing_fields
40
58
  extra_fields = evaluator.extra_fields
41
- response.code == endpoint.bad_request_response.code && missing_fields.size == 0 && extra_fields.size == 0
59
+ response.code == endpoint.bad_request_response.code &&
60
+ missing_fields.size.zero? && extra_fields.size.zero?
61
+ end
62
+
63
+ def self.order
64
+ 5
42
65
  end
43
66
  end
44
67
  end
45
68
 
69
+ # Report for InjectionModule
46
70
  class InjectionReport
47
- attr_accessor :injection_type
48
- attr_accessor :url
49
- attr_accessor :payload
50
- attr_accessor :response
71
+ attr_accessor :injection_type, :url, :payload, :response
51
72
 
52
- def initialize injection_type, url, payload, response
73
+ def initialize(injection_type, url, payload, response)
53
74
  self.injection_type = injection_type
54
75
  self.url = url
55
76
  self.payload = payload
@@ -57,10 +78,10 @@ class InjectionReport
57
78
  end
58
79
 
59
80
  def print
60
- puts "Found potential #{self.injection_type}: "
61
- puts " Requested #{self.url} with payload:"
62
- puts " #{self.payload}"
81
+ puts "Found potential #{injection_type}: "
82
+ puts " Requested #{url} with payload:"
83
+ puts " #{payload}"
63
84
  puts ' Received: '
64
- puts " #{self.response}"
85
+ puts " #{response}"
65
86
  end
66
87
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'api-tester/reporter/status_code_report'
4
+ require 'api-tester/util/supported_verbs'
5
+ require 'pry'
6
+
7
+ module ApiTester
8
+ # Module checking various not found scenarios
9
+ module MissingResource
10
+ def self.go(contract)
11
+ reports = []
12
+
13
+ contract.endpoints.each do |endpoint|
14
+ endpoint.path_params.each do |path_param|
15
+ bad_resource = endpoint.relative_url.gsub("{#{path_param}}", 'gibberish')
16
+
17
+ bad_endpoint = ApiTester::Endpoint.new name: 'Bad Resource',
18
+ relative_url: bad_resource
19
+ method = ApiTester::Method.new verb: ApiTester::SupportedVerbs::GET,
20
+ response: ApiTester::Response.new(
21
+ status_code: 200
22
+ ),
23
+ request: ApiTester::Request.new
24
+ response = bad_endpoint.call base_url: contract.base_url + bad_resource,
25
+ method: method,
26
+ payload: {},
27
+ headers: contract.required_headers
28
+ test = MissingResourceTest.new response,
29
+ {},
30
+ endpoint.not_found_response,
31
+ bad_resource,
32
+ ApiTester::SupportedVerbs::GET
33
+ test.check
34
+ end
35
+ end
36
+
37
+ reports
38
+ end
39
+
40
+ def self.allowed_verbs(endpoint)
41
+ allowances = []
42
+ endpoint.methods.each do |method|
43
+ allowances << method.verb
44
+ end
45
+ allowances.uniq
46
+ end
47
+
48
+ def self.order
49
+ 4
50
+ end
51
+ end
52
+
53
+ # Test layout for Missing Resource
54
+ class MissingResourceTest < MethodCaseTest
55
+ def initialize(response, payload, expected_response, url, verb)
56
+ super response: response,
57
+ payload: payload,
58
+ expected_response: expected_response,
59
+ url: url,
60
+ verb: verb,
61
+ module_name: 'Missing Resource'
62
+ end
63
+ end
64
+ 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,18 @@ module ApiTester
22
24
  end
23
25
  end
24
26
 
27
+ # Report used by module
25
28
  class ServerBroadcastReport
26
- attr_accessor :server_info
27
- attr_accessor :server_key
29
+ attr_accessor :server_info, :server_key
28
30
 
29
- def initialize server_info, server_key
31
+ def initialize(server_info, server_key)
30
32
  self.server_info = server_info
31
33
  self.server_key = server_key
32
34
  end
33
35
 
34
36
  def print
35
- puts "Found server information being broadcast in headers:"
36
- puts " #{self.server_info}"
37
- puts " as #{self.server_key}"
37
+ puts 'Found server information being broadcast in headers:'
38
+ puts " #{server_info}"
39
+ puts " as #{server_key}"
38
40
  end
39
41
  end
@@ -1,35 +1,54 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/reporter/status_code_report'
2
4
  require 'api-tester/util/supported_verbs'
3
5
 
4
6
  module ApiTester
5
- class Typo
6
- def self.go contract
7
+ # Module checking various not found scenarios
8
+ module Typo
9
+ def self.go(contract)
7
10
  reports = []
8
11
 
9
- contract.endpoints.each do |endpoint|
10
- allowances(endpoint).each do |verbs|
11
- reports.concat check_typo_url(endpoint)
12
+ # Filtering out endpoints with ids since not a better way to check this
13
+ # Need to redesign system to handle this better
14
+ contract.endpoints.reject { |e| e.relative_url.include?('{') }.each do |endpoint|
15
+ allowances(endpoint).each do
16
+ reports.concat check_typo_url(contract.base_url, endpoint)
12
17
  end
13
18
  end
14
19
 
15
20
  reports
16
21
  end
17
22
 
18
- def self.check_typo_url endpoint
23
+ def self.check_typo_url(base_url, endpoint)
19
24
  bad_url = "#{endpoint.url}gibberishadsfasdf"
20
- bad_endpoint = ApiTester::Endpoint.new "Bad URL", bad_url
21
- typo_case = BoundaryCase.new("Typo URL check", {}, {})
22
- method = ApiTester::Method.new ApiTester::SupportedVerbs::GET, ApiTester::Response.new(200), ApiTester::Request.new
23
- response = bad_endpoint.call method, typo_case.payload, typo_case.headers
25
+ bad_endpoint = ApiTester::Endpoint.new name: 'Bad URL',
26
+ relative_url: bad_url
27
+ typo_case = BoundaryCase.new description: 'Typo URL check',
28
+ payload: {},
29
+ headers: {}
30
+ method = ApiTester::Method.new verb: ApiTester::SupportedVerbs::GET,
31
+ response: ApiTester::Response.new(
32
+ status_code: 200
33
+ ),
34
+ request: ApiTester::Request.new
35
+ response = bad_endpoint.call base_url: base_url,
36
+ method: method,
37
+ payload: typo_case.payload,
38
+ headers: typo_case.headers
24
39
 
25
- test = TypoClass.new response, typo_case.payload, endpoint.not_found_response, bad_url, ApiTester::SupportedVerbs::GET
40
+ test = TypoClass.new response,
41
+ typo_case.payload,
42
+ endpoint.not_found_response,
43
+ bad_url,
44
+ ApiTester::SupportedVerbs::GET
26
45
  test.check
27
46
  end
28
47
 
29
48
  def self.allowances(endpoint)
30
49
  allowances = []
31
50
  endpoint.methods.each do |method|
32
- allowances << method.verb
51
+ allowances << method.verb
33
52
  end
34
53
  allowances.uniq
35
54
  end
@@ -39,9 +58,15 @@ module ApiTester
39
58
  end
40
59
  end
41
60
 
61
+ # Test layout for TypoModule
42
62
  class TypoClass < MethodCaseTest
43
- def initialize response, payload, expected_response, url, verb
44
- super response, payload, expected_response, url, verb, "TypoModule"
63
+ def initialize(response, payload, expected_response, url, verb)
64
+ super response: response,
65
+ payload: payload,
66
+ expected_response: expected_response,
67
+ url: url,
68
+ verb: verb,
69
+ module_name: 'TypoModule'
45
70
  end
46
71
  end
47
72
  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
@@ -1,16 +1,22 @@
1
- require 'api-tester/reporter/missing_response_field_report'
1
+ # frozen_string_literal: true
2
+
3
+ require 'api-tester/reporter/missing_field_report'
2
4
 
3
5
  module ApiTester
4
- class UnusedFields
5
- def self.go contract
6
+ # Ensures all fields defined in contract are returned during test suite
7
+ module UnusedFields
8
+ def self.go(contract)
6
9
  reports = []
7
10
 
8
11
  contract.endpoints.each do |endpoint|
9
12
  endpoint.methods.each do |method|
10
- method.expected_response.body.each do |field|
11
- if field.is_seen == 0
12
- reports << MissingResponseFieldReport.new(endpoint.url, method.verb, field.name, "UnusedFieldsModule")
13
- end
13
+ method.expected_response.body.filter(&:has_key).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')
14
20
  end
15
21
  end
16
22
  end
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/reporter/report'
2
4
 
3
5
  module ApiTester
6
+ # class for dealing with reports generated by the modules during test suite
4
7
  class ApiReport
5
8
  attr_accessor :reports
6
9
 
@@ -8,31 +11,37 @@ module ApiTester
8
11
  self.reports = []
9
12
  end
10
13
 
11
- def add_new url, request, expected_response, actual_response, description="A case"
12
- report = Report.new description, url, request, expected_response, actual_response
13
- self.reports << report
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
14
21
  end
15
22
 
16
- def add_new_report report
17
- self.reports << report
23
+ def add_new_report(report)
24
+ reports << report
18
25
  end
19
26
 
20
- def add_reports reports
21
- self.reports.concat reports
27
+ def add_reports(reports)
28
+ reports.each do |report|
29
+ add_new_report(report)
30
+ end
22
31
  end
23
32
 
24
33
  def print
25
- if self.reports.size > 0
26
- puts "Issues discovered: #{self.reports.size}"
27
- self.reports.each do |report|
34
+ puts ''
35
+ if reports.size.zero?
36
+ puts 'No issues found'
37
+ else
38
+ puts "Issues discovered: #{reports.size}"
39
+ reports.each do |report|
28
40
  report.print
29
- puts "\n"
30
- puts "\n"
41
+ puts ''
42
+ puts ''
31
43
  end
32
- puts ""
33
- puts "Issues discovered: #{self.reports.size}"
34
- else
35
- puts "No issues found"
44
+ puts "Total issues: #{reports.size}"
36
45
  end
37
46
  end
38
47
  end