api-tester 1.0.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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,35 +1,52 @@
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
12
  contract.endpoints.each do |endpoint|
10
- allowances(endpoint).each do |verbs|
11
- reports.concat check_typo_url(endpoint)
13
+ allowances(endpoint).each do
14
+ reports.concat check_typo_url(contract.base_url, endpoint)
12
15
  end
13
16
  end
14
17
 
15
18
  reports
16
19
  end
17
20
 
18
- def self.check_typo_url endpoint
21
+ def self.check_typo_url(base_url, endpoint)
19
22
  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
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
24
37
 
25
- test = TypoClass.new response, typo_case.payload, endpoint.not_found_response, bad_url, ApiTester::SupportedVerbs::GET
38
+ test = TypoClass.new response,
39
+ typo_case.payload,
40
+ endpoint.not_found_response,
41
+ bad_url,
42
+ ApiTester::SupportedVerbs::GET
26
43
  test.check
27
44
  end
28
45
 
29
46
  def self.allowances(endpoint)
30
47
  allowances = []
31
48
  endpoint.methods.each do |method|
32
- allowances << method.verb
49
+ allowances << method.verb
33
50
  end
34
51
  allowances.uniq
35
52
  end
@@ -39,9 +56,15 @@ module ApiTester
39
56
  end
40
57
  end
41
58
 
59
+ # Test layout for TypoModule
42
60
  class TypoClass < MethodCaseTest
43
- def initialize response, payload, expected_response, url, verb
44
- super response, payload, expected_response, url, verb, "TypoModule"
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'
45
68
  end
46
69
  end
47
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
@@ -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
13
  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
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,36 @@ 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
+ if reports.size.zero?
35
+ puts 'No issues found'
36
+ else
37
+ puts "Issues discovered: #{reports.size}"
38
+ reports.each do |report|
28
39
  report.print
29
- puts "\n"
30
- puts "\n"
40
+ puts '\n'
41
+ puts '\n'
31
42
  end
32
- puts ""
33
- puts "Issues discovered: #{self.reports.size}"
34
- else
35
- puts "No issues found"
43
+ puts "Total issues: #{reports.size}"
36
44
  end
37
45
  end
38
46
  end
@@ -1,25 +1,24 @@
1
- module ApiReport
1
+ # frozen_string_literal: true
2
+
3
+ module ApiTester
4
+ # Report used for when response is missing a field
2
5
  class MissingFieldReport
3
- attr_accessor :description
4
6
  attr_accessor :url
5
- attr_accessor :request
7
+ attr_accessor :verb
6
8
  attr_accessor :expected_field
7
- attr_accessor :actual_response
9
+ attr_accessor :description
8
10
 
9
- def initialize(description, url, request, expected_field)
10
- self.description = description
11
+ def initialize(url:, verb:, expected_field:, description:)
11
12
  self.url = url
12
- self.request = request
13
+ self.verb = verb
13
14
  self.expected_field = expected_field
14
- self.actual_response = ''
15
+ self.description = description
15
16
  end
16
17
 
17
18
  def print
18
- puts "#{self.description}: "
19
- puts " Requested #{self.url} with payload:"
20
- puts " #{self.request}"
21
- puts ' Missing field: '
22
- puts " #{self.expected_field}"
19
+ puts "#{description}:"
20
+ puts " #{verb} #{url} is missing response field:"
21
+ puts " #{expected_field}"
23
22
  end
24
23
  end
25
24
  end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiTester
4
+ # Standard report format for differing responses
2
5
  class Report
3
6
  attr_accessor :description
4
7
  attr_accessor :url
@@ -6,7 +9,7 @@ module ApiTester
6
9
  attr_accessor :expected_response
7
10
  attr_accessor :actual_response
8
11
 
9
- def initialize description, url, request, expected_response, actual_response
12
+ def initialize(description:, url:, request:, expected_response:, actual_response:)
10
13
  self.description = description
11
14
  self.url = url
12
15
  self.request = request
@@ -15,13 +18,13 @@ module ApiTester
15
18
  end
16
19
 
17
20
  def print
18
- puts "#{self.description}: "
19
- puts " Requested #{self.url} with payload:"
20
- puts " #{self.request.to_json}"
21
- puts " Expecting: "
22
- puts " " + self.expected_response.to_s
23
- puts " Receiving: "
24
- puts " #{self.actual_response}"
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}"
25
28
  end
26
29
  end
27
30
  end
@@ -1,12 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/reporter/report'
2
4
 
3
5
  module ApiTester
6
+ # Report for when status code is different than expected
4
7
  class StatusCodeReport < Report
5
8
  attr_accessor :expected_status_code
6
9
  attr_accessor :actual_status_code
7
10
 
8
- def initialize description, url, request, expected_status_code, actual_status_code
9
- super description, url, request, expected_status_code, actual_status_code
11
+ def initialize(description:, url:, request:, expected_status_code:, actual_status_code:)
12
+ super description: description,
13
+ url: url,
14
+ request: request,
15
+ expected_response: expected_status_code,
16
+ actual_response: actual_status_code
10
17
  self.expected_status_code = expected_status_code
11
18
  self.actual_status_code = actual_status_code
12
19
  end
@@ -1,12 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiTester
4
+ # Interface for when things need to be done before or after an api call
2
5
  class TestHelper
3
- def before
4
- end
6
+ def before; end
5
7
 
6
- def retrieve_param key
7
- end
8
+ def retrieve_param(key); end
8
9
 
9
- def after
10
- end
10
+ def after; end
11
11
  end
12
12
  end
@@ -1,75 +1,88 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiTester
4
+ # Class for evaluating responses against what is expected
2
5
  class ResponseEvaluator
3
- attr_accessor :response_body
4
- attr_accessor :expected_response
6
+ attr_accessor :response_body
7
+ attr_accessor :expected_response
5
8
 
6
- def initialize(actual_response_body, expected_response_fields)
7
- self.response_body = actual_response_body
8
- self.expected_response = expected_response_fields
9
- end
9
+ def initialize(actual_body:, expected_fields:)
10
+ self.response_body = actual_body
11
+ self.expected_response = expected_fields
12
+ end
10
13
 
11
- def response_field_array
12
- field_array self.response_body
13
- end
14
+ def response_field_array
15
+ field_array response_body
16
+ end
14
17
 
15
- def expected_fields
16
- expected_fields_hash.keys
17
- end
18
+ def expected_fields
19
+ expected_fields_hash.keys
20
+ end
18
21
 
19
- def seen_fields
20
- seen = []
21
- fields = response_field_array - extra_fields
22
- expected = expected_fields_hash
23
- fields.each do |field_key|
24
- seen << expected[field_key]
25
- end
26
- seen
22
+ def seen_fields
23
+ seen = []
24
+ fields = response_field_array - extra_fields
25
+ expected = expected_fields_hash
26
+ fields.each do |field_key|
27
+ seen << expected[field_key]
27
28
  end
29
+ seen
30
+ end
28
31
 
29
- def expected_fields_hash
30
- expected_field_array self.expected_response.body
31
- end
32
+ def expected_fields_hash
33
+ expected_field_array expected_response.body
34
+ end
32
35
 
33
- def extra_fields
34
- response_field_array - expected_fields
35
- end
36
+ def extra_fields
37
+ response_field_array - expected_fields
38
+ end
36
39
 
37
- def missing_fields
38
- expected_fields - response_field_array
39
- end
40
+ def missing_fields
41
+ expected_fields - response_field_array
42
+ end
40
43
 
41
- def expected_field_array expected_fields
42
- fields = {}
43
- expected_fields.each do |field|
44
- fields[field.name] = field
45
- fields = fields.merge inner_expected_field(field.fields, field.name)
46
- end
47
- fields
44
+ def expected_field_array(expected_fields)
45
+ fields = {}
46
+ expected_fields.each do |field|
47
+ fields[field.name] = field
48
+ fields = fields.merge inner_expected_field(expected_fields: field.fields,
49
+ name: field.name)
48
50
  end
51
+ fields
52
+ end
49
53
 
50
- def inner_expected_field expected_fields, name
51
- fields = {}
52
- expected_fields.each do |field|
53
- inner_name = "#{name}.#{field.name}"
54
- fields[inner_name] = field
55
- fields = fields.merge inner_expected_field(field.fields, inner_name)
56
- end
57
- fields
54
+ def inner_expected_field(expected_fields:, name:)
55
+ fields = {}
56
+ expected_fields.each do |field|
57
+ inner_name = "#{name}.#{field.name}"
58
+ fields[inner_name] = field
59
+ fields = fields.merge inner_expected_field(expected_fields: field.fields,
60
+ name: inner_name)
58
61
  end
62
+ fields
63
+ end
64
+
65
+ def field_array(object)
66
+ fields = []
59
67
 
60
- def field_array object
61
- fields = []
62
- object.each do |key, value|
63
- if(value)
64
- fields << key.to_s
65
- fields.concat(field_array(value).map{|i| "#{key}.#{i}"})
66
- else
67
- fields.concat(field_array(key))
68
- end
69
- end
70
- fields
71
- rescue NoMethodError
72
- fields
68
+ object.each do |key, value|
69
+ if key.respond_to?('each')
70
+ fields.concat(field_array(key))
71
+ elsif value == nil || value == 0 || value == false
72
+ fields << key.to_s
73
+ fields.concat(field_array(value).map { |i| "#{key}.#{i}" })
74
+ elsif value.to_s[0] == '[' && value.to_s[-1] == ']' && !value.to_s.include?('=>')
75
+ fields << key.to_s
76
+ elsif value
77
+ fields << key.to_s
78
+ fields.concat(field_array(value).map { |i| "#{key}.#{i}" })
79
+ else
80
+ fields.concat(field_array(key))
81
+ end
73
82
  end
83
+ fields
84
+ rescue NoMethodError
85
+ fields
86
+ end
74
87
  end
75
88
  end