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.
- checksums.yaml +5 -5
- data/.github/dependabot.yml +15 -0
- data/.github/workflows/push.yml +39 -0
- data/.github/workflows/test.yml +31 -0
- data/.rubocop.yml +61 -0
- data/Gemfile +2 -0
- data/Guardfile +70 -0
- data/README.md +65 -61
- data/Rakefile +8 -3
- data/api-tester.gemspec +29 -24
- data/changelog.txt +10 -0
- data/lib/api-tester.rb +6 -3
- data/lib/api-tester/config.rb +16 -13
- data/lib/api-tester/definition/boundary_case.rb +4 -1
- data/lib/api-tester/definition/contract.rb +8 -3
- data/lib/api-tester/definition/endpoint.rb +32 -23
- data/lib/api-tester/definition/fields/array_field.rb +20 -19
- data/lib/api-tester/definition/fields/boolean_field.rb +12 -9
- data/lib/api-tester/definition/fields/email_field.rb +14 -11
- data/lib/api-tester/definition/fields/enum_field.rb +14 -11
- data/lib/api-tester/definition/fields/field.rb +46 -45
- data/lib/api-tester/definition/fields/number_field.rb +11 -8
- data/lib/api-tester/definition/fields/object_field.rb +34 -31
- data/lib/api-tester/definition/fields/plain_array_field.rb +25 -0
- data/lib/api-tester/definition/method.rb +7 -2
- data/lib/api-tester/definition/request.rb +43 -16
- data/lib/api-tester/definition/response.rb +29 -26
- data/lib/api-tester/method_case_test.rb +67 -53
- data/lib/api-tester/modules/extra_verbs.rb +29 -9
- data/lib/api-tester/modules/format.rb +23 -7
- data/lib/api-tester/modules/good_case.rb +25 -10
- data/lib/api-tester/modules/injection_module.rb +32 -17
- data/lib/api-tester/modules/required_fields.rb +51 -0
- data/lib/api-tester/modules/server_information.rb +13 -10
- data/lib/api-tester/modules/typo.rb +36 -13
- data/lib/api-tester/modules/unexpected_fields.rb +61 -0
- data/lib/api-tester/modules/unused_fields.rb +12 -6
- data/lib/api-tester/reporter/api_report.rb +24 -16
- data/lib/api-tester/reporter/missing_field_report.rb +12 -13
- data/lib/api-tester/reporter/report.rb +11 -8
- data/lib/api-tester/reporter/status_code_report.rb +9 -2
- data/lib/api-tester/test_helper.rb +6 -6
- data/lib/api-tester/util/response_evaluator.rb +70 -57
- data/lib/api-tester/util/supported_verbs.rb +8 -5
- data/lib/api-tester/version.rb +3 -1
- metadata +99 -25
- data/.travis.yml +0 -6
- 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
|
-
|
6
|
-
|
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
|
11
|
-
|
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
|
21
|
-
|
22
|
-
|
23
|
-
|
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,
|
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
|
-
|
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
|
44
|
-
super response
|
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
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'api-tester/reporter/missing_field_report'
|
2
4
|
|
3
5
|
module ApiTester
|
4
|
-
|
5
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
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
|
12
|
-
report = Report.new description,
|
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
|
14
21
|
end
|
15
22
|
|
16
|
-
def add_new_report
|
17
|
-
|
23
|
+
def add_new_report(report)
|
24
|
+
reports << report
|
18
25
|
end
|
19
26
|
|
20
|
-
def add_reports
|
21
|
-
|
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
|
26
|
-
puts
|
27
|
-
|
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
|
30
|
-
puts
|
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
|
-
|
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 :
|
7
|
+
attr_accessor :verb
|
6
8
|
attr_accessor :expected_field
|
7
|
-
attr_accessor :
|
9
|
+
attr_accessor :description
|
8
10
|
|
9
|
-
def initialize(
|
10
|
-
self.description = description
|
11
|
+
def initialize(url:, verb:, expected_field:, description:)
|
11
12
|
self.url = url
|
12
|
-
self.
|
13
|
+
self.verb = verb
|
13
14
|
self.expected_field = expected_field
|
14
|
-
self.
|
15
|
+
self.description = description
|
15
16
|
end
|
16
17
|
|
17
18
|
def print
|
18
|
-
puts "#{
|
19
|
-
puts "
|
20
|
-
puts " #{
|
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
|
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 "#{
|
19
|
-
puts " Requested #{
|
20
|
-
puts " #{
|
21
|
-
puts
|
22
|
-
puts
|
23
|
-
puts
|
24
|
-
puts " #{
|
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
|
9
|
-
super description
|
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
|
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
|
-
|
4
|
-
|
6
|
+
attr_accessor :response_body
|
7
|
+
attr_accessor :expected_response
|
5
8
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
9
|
+
def initialize(actual_body:, expected_fields:)
|
10
|
+
self.response_body = actual_body
|
11
|
+
self.expected_response = expected_fields
|
12
|
+
end
|
10
13
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
+
def response_field_array
|
15
|
+
field_array response_body
|
16
|
+
end
|
14
17
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
+
def expected_fields
|
19
|
+
expected_fields_hash.keys
|
20
|
+
end
|
18
21
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
+
def expected_fields_hash
|
33
|
+
expected_field_array expected_response.body
|
34
|
+
end
|
32
35
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
+
def extra_fields
|
37
|
+
response_field_array - expected_fields
|
38
|
+
end
|
36
39
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
+
def missing_fields
|
41
|
+
expected_fields - response_field_array
|
42
|
+
end
|
40
43
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
fields
|
71
|
-
|
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
|