api-tester 1.0.0 → 1.1.3

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 (53) 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/.rubocop.yml +171 -0
  7. data/Gemfile +2 -0
  8. data/Guardfile +70 -0
  9. data/README.md +65 -61
  10. data/Rakefile +8 -3
  11. data/api-tester.gemspec +28 -24
  12. data/changelog.txt +10 -0
  13. data/lib/api-tester/config.rb +17 -15
  14. data/lib/api-tester/definition/boundary_case.rb +5 -4
  15. data/lib/api-tester/definition/contract.rb +10 -5
  16. data/lib/api-tester/definition/endpoint.rb +58 -32
  17. data/lib/api-tester/definition/fields/array_field.rb +26 -21
  18. data/lib/api-tester/definition/fields/boolean_field.rb +17 -7
  19. data/lib/api-tester/definition/fields/email_field.rb +28 -11
  20. data/lib/api-tester/definition/fields/enum_field.rb +19 -12
  21. data/lib/api-tester/definition/fields/field.rb +52 -45
  22. data/lib/api-tester/definition/fields/number_field.rb +20 -6
  23. data/lib/api-tester/definition/fields/object_field.rb +37 -30
  24. data/lib/api-tester/definition/fields/plain_array_field.rb +25 -0
  25. data/lib/api-tester/definition/method.rb +8 -5
  26. data/lib/api-tester/definition/request.rb +43 -19
  27. data/lib/api-tester/definition/response.rb +35 -25
  28. data/lib/api-tester/method_case_test.rb +68 -54
  29. data/lib/api-tester/modules/benchmark_module.rb +35 -0
  30. data/lib/api-tester/modules/extra_verbs.rb +36 -10
  31. data/lib/api-tester/modules/format.rb +23 -7
  32. data/lib/api-tester/modules/good_case.rb +25 -10
  33. data/lib/api-tester/modules/good_variations.rb +69 -0
  34. data/lib/api-tester/modules/injection_module.rb +44 -23
  35. data/lib/api-tester/modules/missing_resource.rb +63 -0
  36. data/lib/api-tester/modules/required_fields.rb +51 -0
  37. data/lib/api-tester/modules/server_information.rb +14 -12
  38. data/lib/api-tester/modules/typo.rb +39 -14
  39. data/lib/api-tester/modules/unexpected_fields.rb +59 -0
  40. data/lib/api-tester/modules/unused_fields.rb +13 -7
  41. data/lib/api-tester/reporter/api_report.rb +25 -16
  42. data/lib/api-tester/reporter/missing_field_report.rb +11 -15
  43. data/lib/api-tester/reporter/report.rb +12 -13
  44. data/lib/api-tester/reporter/response_time_report.rb +24 -0
  45. data/lib/api-tester/reporter/status_code_report.rb +10 -4
  46. data/lib/api-tester/test_helper.rb +8 -6
  47. data/lib/api-tester/util/response_evaluator.rb +84 -56
  48. data/lib/api-tester/util/supported_verbs.rb +8 -5
  49. data/lib/api-tester/version.rb +3 -1
  50. data/lib/api-tester.rb +6 -3
  51. metadata +92 -27
  52. data/.travis.yml +0 -6
  53. data/lib/api-tester/reporter/missing_response_field_report.rb +0 -21
@@ -1,44 +1,51 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/definition/fields/field'
2
4
 
3
5
  module ApiTester
6
+ # Class for defining objects in a contract
4
7
  class ObjectField < Field
5
- attr_accessor :fields
8
+ attr_accessor :fields
6
9
 
7
- def initialize name:
8
- super name: name
9
- self.fields = []
10
- end
10
+ def initialize(name:, required: false, has_key: true)
11
+ super name: name, required: required, has_key: has_key
12
+ self.fields = []
13
+ end
11
14
 
12
- def with_field newField
13
- self.fields << newField
14
- self
15
- end
15
+ def with_field(new_field)
16
+ fields << new_field
17
+ self
18
+ end
16
19
 
17
- def has_subfields?
18
- true
19
- end
20
+ def subfields?
21
+ true
22
+ end
20
23
 
21
- def default_value
22
- obj = Hash.new
24
+ def type
25
+ 'object'
26
+ end
23
27
 
24
- self.fields.each do |field|
25
- obj[field.name] = field.default_value
26
- end
28
+ def default
29
+ obj = {}
27
30
 
28
- obj
31
+ fields.each do |field|
32
+ obj[field.name] = field.default
29
33
  end
30
34
 
31
- def negative_boundary_values
32
- super +
33
- [
34
- "string",
35
- [],
36
- 123,
37
- 1,
38
- 0,
39
- true,
40
- false
41
- ]
42
- end
35
+ obj
36
+ end
37
+
38
+ def negative_boundary_values
39
+ super +
40
+ [
41
+ 'string',
42
+ [],
43
+ 123,
44
+ 1,
45
+ 0,
46
+ true,
47
+ false
48
+ ]
49
+ end
43
50
  end
44
51
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'api-tester/definition/fields/field'
4
+
5
+ module ApiTester
6
+ # Class for defining plain arrays
7
+ class PlainArrayField < Field
8
+ def initialize(name:, default: [], required: false)
9
+ super name: name, default: default, required: required
10
+ end
11
+
12
+ def negative_boundary_values
13
+ super +
14
+ [
15
+ 'string',
16
+ 123,
17
+ 0,
18
+ 1,
19
+ {},
20
+ true,
21
+ false
22
+ ]
23
+ end
24
+ end
25
+ end
@@ -1,17 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiTester
4
+ # Class for defining methods as part of an endpoint
2
5
  class Method
3
- attr_accessor :request
4
- attr_accessor :expected_response
5
- attr_accessor :verb
6
+ attr_accessor :request, :expected_response, :verb
6
7
 
7
- def initialize verb, response, request
8
+ def initialize(verb:, response:, request:)
8
9
  self.verb = verb
9
10
  self.request = request
10
11
  self.expected_response = response
11
12
  end
12
13
 
13
14
  def default_request
14
- {:method => self.verb, :payload => request.default_payload, :headers => request.default_headers}
15
+ { method: verb,
16
+ payload: request.default_payload,
17
+ headers: request.default_headers }
15
18
  end
16
19
  end
17
20
  end
@@ -1,30 +1,43 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/definition/boundary_case'
2
4
 
3
5
  module ApiTester
6
+ # Class for defining requests in a contract
4
7
  class Request
5
- attr_accessor :definition
6
- attr_accessor :header_fields
7
- attr_accessor :fields
8
+ attr_accessor :definition, :header_fields, :fields, :query_params
8
9
 
9
10
  def initialize
10
11
  self.fields = []
11
12
  self.header_fields = []
13
+ self.query_params = []
12
14
  end
13
15
 
14
16
  def add_field(new_field)
15
- self.fields << new_field
17
+ fields << new_field
18
+ self
19
+ end
20
+
21
+ def add_query_param(new_query_param)
22
+ query_params << new_query_param
16
23
  self
17
24
  end
18
25
 
19
- def add_header_field new_header
20
- self.header_fields << new_header
26
+ def default_query
27
+ query_params.map { |param| "#{param.name}=#{param.default}" }.join('&')
28
+ end
29
+
30
+ def add_header_field(new_header)
31
+ header_fields << new_header
21
32
  self
22
33
  end
23
34
 
24
35
  def payload
25
- response = Hash.new
26
- self.fields.each do |field|
27
- response[field.name] = field.default_value
36
+ response = {}
37
+ fields.each do |field|
38
+ if field.required == true
39
+ response[field.name] = field.default
40
+ end
28
41
  end
29
42
  response
30
43
  end
@@ -34,36 +47,47 @@ module ApiTester
34
47
  end
35
48
 
36
49
  def default_headers
37
- if self.header_fields != []
38
- self.headers
50
+ if header_fields == []
51
+ { content_type: :json, accept: :json }
39
52
  else
40
- {content_type: :json, accept: :json}
53
+ headers
41
54
  end
42
55
  end
43
56
 
44
57
  def headers
45
58
  header_response = {}
46
- self.header_fields.each do |header|
47
- header_response[header.name] = header.default_value
59
+ header_fields.each do |header_field|
60
+ header_response[header_field.name] = header_field.default
48
61
  end
49
62
  header_response
50
63
  end
51
64
 
52
65
  def cases
53
- boundary_cases = Array.new
54
- self.fields.each do |field|
66
+ boundary_cases = []
67
+ fields.each do |field|
55
68
  field.negative_boundary_values.each do |value|
56
- bcase = BoundaryCase.new("Setting #{field.name} to #{value}", altered_payload(field.name, value), default_headers)
57
- boundary_cases.push(bcase)
69
+ bcase = BoundaryCase.new description: "Setting #{field.name} to #{value}",
70
+ payload: altered_payload(field_name: field.name,
71
+ value: value),
72
+ headers: default_headers
73
+ boundary_cases.push bcase
58
74
  end
59
75
  end
60
76
  boundary_cases
61
77
  end
62
78
 
63
- def altered_payload field_name, value
79
+ def altered_payload(field_name:, value:)
64
80
  body = payload
65
81
  body[field_name] = value
66
82
  body
67
83
  end
84
+
85
+ def altered_payload_with(fields)
86
+ body = payload
87
+ fields.each do |field|
88
+ body[field[:name]] = field[:value]
89
+ end
90
+ body
91
+ end
68
92
  end
69
93
  end
@@ -1,36 +1,46 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiTester
4
+ # Class for defining expected responses
2
5
  class Response
3
- attr_accessor :code
4
- attr_accessor :body
6
+ attr_accessor :code, :body
5
7
 
6
- def initialize(status_code=200)
7
- self.code = status_code
8
- self.body = []
9
- end
8
+ def initialize(status_code: 200)
9
+ self.code = status_code
10
+ self.body = []
11
+ end
10
12
 
11
- def add_field(new_field)
12
- self.body << new_field
13
- self
14
- end
13
+ def add_field(new_field)
14
+ body << new_field
15
+ self
16
+ end
15
17
 
16
- def to_s
17
- des = {}
18
- self.body.map do |f|
19
- des[f.name] = field_display f
20
- end
21
- des.to_json
18
+ def to_s
19
+ des = {}
20
+ body.map do |f|
21
+ if f.has_key
22
+ des[f.name] = field_display f
23
+ else
24
+ des = field_display f
25
+ end
22
26
  end
27
+ des.to_json
28
+ end
23
29
 
24
- def field_display field
25
- des = field.display_class
26
- if field.has_subfields?
27
- des = {}
28
- field.fields.map do |f|
29
- des[f.name] = field_display f
30
- end
31
- des.to_json
30
+ def field_display(field)
31
+ des = field.display_class
32
+ if field.subfields?
33
+ des = {}
34
+ field.fields.map do |f|
35
+ if f.has_key
36
+ des[f.name] = field_display f
37
+ else
38
+ des = field_display f
32
39
  end
33
- des
40
+ end
41
+ des.to_json
34
42
  end
43
+ des
44
+ end
35
45
  end
36
46
  end
@@ -1,69 +1,83 @@
1
- require 'api-tester/util/response_evaluator.rb'
1
+ # frozen_string_literal: true
2
+
3
+ require 'api-tester/util/response_evaluator'
2
4
 
3
5
  module ApiTester
6
+ # Class for testing methods
4
7
  class MethodCaseTest
5
- attr_accessor :expected_response
6
- attr_accessor :payload
7
- attr_accessor :response
8
- attr_accessor :reports
9
- attr_accessor :url
10
- attr_accessor :module_name
8
+ attr_accessor :expected_response, :payload, :response, :reports, :url, :module_name
11
9
 
12
- def initialize response, payload, expected_response, url, verb, module_name
13
- self.payload = payload
14
- self.response = response
15
- self.expected_response = expected_response
16
- self.reports = []
17
- self.url = "#{verb} #{url}"
18
- self.module_name = module_name
19
- end
10
+ def initialize(response:, payload:, expected_response:, url:, verb:, module_name:)
11
+ self.payload = payload
12
+ self.response = response
13
+ self.expected_response = expected_response
14
+ self.reports = []
15
+ self.url = "#{verb} #{url}"
16
+ self.module_name = module_name
17
+ end
20
18
 
21
- def response_code_report
22
- report = StatusCodeReport.new "#{module_name} - Incorrect response code", self.url, self.payload, self.expected_response.code, self.response.code
23
- self.reports << report
24
- nil
25
- end
19
+ def response_code_report
20
+ print 'F'
21
+ report = StatusCodeReport.new description: "#{module_name} - Incorrect response code",
22
+ url: url,
23
+ request: payload,
24
+ expected_status_code: expected_response.code,
25
+ actual_status_code: "#{response.code} : #{response.body}"
26
+ reports << report
27
+ nil
28
+ end
26
29
 
27
- def missing_field_report field
28
- report = Report.new "#{module_name} - Missing field #{field}", self.url, self.payload, self.expected_response, self.response
29
- self.reports << report
30
- nil
31
- end
30
+ def missing_field_report(field)
31
+ print 'F'
32
+ report = Report.new description: "#{module_name} - Missing field #{field}",
33
+ url: url,
34
+ request: payload,
35
+ expected_response: expected_response,
36
+ actual_response: response
37
+ reports << report
38
+ nil
39
+ end
32
40
 
33
- def extra_field_report field
34
- report = Report.new "#{module_name} - Found extra field #{field}", self.url, self.payload, self.expected_response, self.response
35
- self.reports << report
36
- nil
37
- end
41
+ def extra_field_report(field)
42
+ print 'F'
43
+ report = Report.new description: "#{module_name} - Found extra field #{field}",
44
+ url: url,
45
+ request: payload,
46
+ expected_response: expected_response,
47
+ actual_response: response
48
+ reports << report
49
+ nil
50
+ end
38
51
 
39
- def check
40
- if check_response_code
41
- evaluator = ApiTester::ResponseEvaluator.new json_parse(self.response.body), self.expected_response
42
- evaluator.missing_fields.map{|field| missing_field_report(field)}
43
- evaluator.extra_fields.map{|field| extra_field_report(field)}
44
- increment_fields evaluator.seen_fields
45
- end
46
- return self.reports
52
+ def check
53
+ if check_response_code
54
+ print '.'
55
+ evaluator = ApiTester::ResponseEvaluator.new actual_body: json_parse(response.body),
56
+ expected_fields: expected_response
57
+ evaluator.missing_fields.map { |field| missing_field_report(field) }
58
+ evaluator.extra_fields.map { |field| extra_field_report(field) }
59
+ increment_fields evaluator.seen_fields
47
60
  end
61
+ reports
62
+ end
48
63
 
49
- def check_response_code
50
- if response.code != expected_response.code
51
- response_code_report
52
- return false
53
- end
54
- return true
64
+ def check_response_code
65
+ if response && (response.code != expected_response.code)
66
+ print 'F'
67
+ response_code_report
68
+ return false
55
69
  end
70
+ true
71
+ end
56
72
 
57
- def increment_fields seen_fields
58
- seen_fields.each do |field|
59
- field.seen
60
- end
61
- end
73
+ def increment_fields(seen_fields)
74
+ seen_fields.each(&:seen)
75
+ end
62
76
 
63
- def json_parse body
64
- JSON.parse!(body)
65
- rescue JSON::ParserError
66
- body
67
- end
77
+ def json_parse(body)
78
+ JSON.parse!(body)
79
+ rescue JSON::ParserError
80
+ body
81
+ end
68
82
  end
69
83
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'api-tester/reporter/response_time_report'
4
+
5
+ module ApiTester
6
+ # Checks the response times collected during the test run
7
+ # Note: Needs at least one calling module, like GoodCase, to work
8
+ module BenchmarkModule
9
+ def self.go(contract)
10
+ reports = []
11
+
12
+ contract.endpoints.each do |endpoint|
13
+ longest_time = endpoint.longest_time
14
+ longest_time[:time] = longest_time[:time] * 1000.0 # Convert from seconds to ms
15
+ if longest_time[:time] > contract.max_time
16
+ print 'F'
17
+ reports << ResponseTimeReport.new(url: endpoint.url,
18
+ verb: longest_time[:verb],
19
+ payload: longest_time[:payload],
20
+ max_time: contract.max_time,
21
+ actual_time: longest_time[:time],
22
+ description: 'BenchmarkModule')
23
+ else
24
+ print '.'
25
+ end
26
+ end
27
+
28
+ reports
29
+ end
30
+
31
+ def self.order
32
+ 99
33
+ end
34
+ end
35
+ end
@@ -1,18 +1,34 @@
1
- require 'api-tester/util/supported_verbs'
1
+ # frozen_string_literal: true
2
+
3
+ require 'api-tester/util/supported_verbs'
4
+ require 'api-tester/definition/method'
5
+ require 'api-tester/method_case_test'
2
6
 
3
7
  module ApiTester
8
+ # Check verbs not explicitly defined in contract
4
9
  module ExtraVerbs
5
- def self.go contract
10
+ def self.go(contract)
6
11
  reports = []
7
12
 
8
13
  contract.endpoints.each do |endpoint|
9
- extras = ApiTester::SupportedVerbs.all - endpoint.verbs
14
+ extras = supported_verbs - endpoint.verbs
10
15
  headers = endpoint.methods[0].request.default_headers
11
16
  extras.each do |verb|
12
- verb_case = BoundaryCase.new("Verb check with #{verb} for #{endpoint.name}", {}, headers)
13
- method = ApiTester::Method.new verb, ApiTester::Response.new, ApiTester::Request.new
14
- response = endpoint.call method, verb_case.payload, verb_case.headers
15
- test = VerbClass.new response, verb_case.payload, endpoint.not_allowed_response, endpoint.url, verb
17
+ verb_case = BoundaryCase.new description: "Verb check with #{verb} for #{endpoint.name}",
18
+ payload: {},
19
+ headers: headers
20
+ method = ApiTester::Method.new verb: verb,
21
+ response: ApiTester::Response.new,
22
+ request: ApiTester::Request.new
23
+ response = endpoint.call base_url: contract.base_url,
24
+ method: method,
25
+ payload: verb_case.payload,
26
+ headers: verb_case.headers
27
+ test = VerbClass.new response: response,
28
+ payload: verb_case.payload,
29
+ expected_response: endpoint.not_allowed_response,
30
+ url: endpoint.url,
31
+ verb: verb
16
32
  reports.concat test.check
17
33
  end
18
34
  end
@@ -20,14 +36,24 @@ module ApiTester
20
36
  reports
21
37
  end
22
38
 
39
+ def self.supported_verbs
40
+ ApiTester::SupportedVerbs.all
41
+ end
42
+
23
43
  def self.order
24
44
  3
25
45
  end
26
46
  end
27
47
 
48
+ # Test template used for module
28
49
  class VerbClass < MethodCaseTest
29
- def initialize response, payload, expected_response, url, verb
30
- super response, payload, expected_response, url, verb, "VerbModule"
31
- end
50
+ def initialize(response:, payload:, expected_response:, url:, verb:)
51
+ super response: response,
52
+ payload: payload,
53
+ expected_response: expected_response,
54
+ url: url,
55
+ verb: verb,
56
+ module_name: 'VerbModule'
57
+ end
32
58
  end
33
59
  end
@@ -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 Format
6
- def self.go contract
7
+ # Checks the format constraints defined in contract
8
+ module Format
9
+ def self.go(contract)
7
10
  reports = []
8
11
  contract.endpoints.each do |endpoint|
9
12
  endpoint.methods.each do |method|
10
13
  cases = method.request.cases
11
14
  cases.each do |format_case|
12
- response = endpoint.call method, format_case.payload, format_case.headers
13
- test = FormatTest.new response, format_case.payload, endpoint.bad_request_response, endpoint.url, method.verb
15
+ response = endpoint.call base_url: contract.base_url,
16
+ method: method,
17
+ payload: format_case.payload,
18
+ headers: format_case.headers
19
+ test = FormatTest.new response: response,
20
+ payload: format_case.payload,
21
+ expected_response: endpoint.bad_request_response,
22
+ url: endpoint.url,
23
+ verb: method.verb
14
24
  reports.concat test.check
15
25
  end
16
26
  end
@@ -23,9 +33,15 @@ module ApiTester
23
33
  end
24
34
  end
25
35
 
36
+ # Test layout used by Format module
26
37
  class FormatTest < MethodCaseTest
27
- def initialize response, payload, expected_response, url, verb
28
- super response, payload, expected_response, url, verb, "FormatModule"
29
- end
38
+ def initialize(response:, payload:, expected_response:, url:, verb:)
39
+ super response: response,
40
+ payload: payload,
41
+ expected_response: expected_response,
42
+ url: url,
43
+ verb: verb,
44
+ module_name: 'FormatModule'
45
+ end
30
46
  end
31
47
  end
@@ -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