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,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