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,18 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api-tester/definition/fields/field'
2
4
 
3
5
  module ApiTester
6
+ # Class for defining numeric fields in contracts
4
7
  class NumberField < Field
5
- def initialize(name, default_value=5)
6
- super(name, default_value)
8
+ def initialize(name:, default: 5, required: false)
9
+ super name: name, default: default, required: required
7
10
  end
8
11
 
9
12
  def negative_boundary_values
10
13
  super +
14
+ [
15
+ 'string',
16
+ true,
17
+ false,
18
+ {}
19
+ ]
20
+ end
21
+
22
+ def good_cases
11
23
  [
12
- "string",
13
- true,
14
- false,
15
- {}
24
+ -1,
25
+ 0,
26
+ 1,
27
+ 100,
28
+ 9999,
29
+ 12_345_678_901_234_567_890
16
30
  ]
17
31
  end
18
32
  end
@@ -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)
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,24 +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 :headers
7
- attr_accessor :fields
8
+ attr_accessor :definition, :header_fields, :fields, :query_params
8
9
 
9
10
  def initialize
10
11
  self.fields = []
12
+ self.header_fields = []
13
+ self.query_params = []
11
14
  end
12
15
 
13
16
  def add_field(new_field)
14
- 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
23
+ self
24
+ end
25
+
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
15
32
  self
16
33
  end
17
34
 
18
35
  def payload
19
- response = Hash.new
20
- self.fields.each do |field|
21
- 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
22
41
  end
23
42
  response
24
43
  end
@@ -28,24 +47,47 @@ module ApiTester
28
47
  end
29
48
 
30
49
  def default_headers
31
- self.headers || {content_type: :json, accept: :json}
50
+ if header_fields == []
51
+ { content_type: :json, accept: :json }
52
+ else
53
+ headers
54
+ end
55
+ end
56
+
57
+ def headers
58
+ header_response = {}
59
+ header_fields.each do |header_field|
60
+ header_response[header_field.name] = header_field.default
61
+ end
62
+ header_response
32
63
  end
33
64
 
34
65
  def cases
35
- boundary_cases = Array.new
36
- self.fields.each do |field|
66
+ boundary_cases = []
67
+ fields.each do |field|
37
68
  field.negative_boundary_values.each do |value|
38
- bcase = BoundaryCase.new("Setting #{field.name} to #{value}", altered_payload(field.name, value), default_headers)
39
- 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
40
74
  end
41
75
  end
42
76
  boundary_cases
43
77
  end
44
78
 
45
- def altered_payload field_name, value
79
+ def altered_payload(field_name:, value:)
46
80
  body = payload
47
81
  body[field_name] = value
48
82
  body
49
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
50
92
  end
51
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,17 +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
15
+ headers = endpoint.methods[0].request.default_headers
10
16
  extras.each do |verb|
11
- verb_case = BoundaryCase.new("Verb check with #{verb} for #{endpoint.name}", {}, {})
12
- method = ApiTester::Method.new verb, ApiTester::Response.new, ApiTester::Request.new
13
- response = endpoint.call method, verb_case.payload, verb_case.headers
14
- 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
15
32
  reports.concat test.check
16
33
  end
17
34
  end
@@ -19,14 +36,24 @@ module ApiTester
19
36
  reports
20
37
  end
21
38
 
39
+ def self.supported_verbs
40
+ ApiTester::SupportedVerbs.all
41
+ end
42
+
22
43
  def self.order
23
44
  3
24
45
  end
25
46
  end
26
47
 
48
+ # Test template used for module
27
49
  class VerbClass < MethodCaseTest
28
- def initialize response, payload, expected_response, url, verb
29
- super response, payload, expected_response, url, verb, "VerbModule"
30
- 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
31
58
  end
32
59
  end
@@ -1,21 +1,30 @@
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
17
27
  end
18
-
19
28
  reports
20
29
  end
21
30
 
@@ -24,9 +33,15 @@ module ApiTester
24
33
  end
25
34
  end
26
35
 
36
+ # Test layout used by Format module
27
37
  class FormatTest < MethodCaseTest
28
- def initialize response, payload, expected_response, url, verb
29
- super response, payload, expected_response, url, verb, "FormatModule"
30
- 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
31
46
  end
32
47
  end