api-tester 0.3.1 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
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