openapi_first 3.3.1 → 3.4.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +1 -0
  4. data/lib/openapi_first/builder.rb +5 -2
  5. data/lib/openapi_first/configuration.rb +6 -0
  6. data/lib/openapi_first/definition.rb +43 -9
  7. data/lib/openapi_first/middlewares/request_validation.rb +21 -2
  8. data/lib/openapi_first/middlewares/response_validation.rb +2 -1
  9. data/lib/openapi_first/plugins/x_public.rb +29 -0
  10. data/lib/openapi_first/plugins.rb +44 -0
  11. data/lib/openapi_first/registry.rb +2 -2
  12. data/lib/openapi_first/request.rb +28 -15
  13. data/lib/openapi_first/request_body_parsers.rb +40 -8
  14. data/lib/openapi_first/request_validator.rb +5 -1
  15. data/lib/openapi_first/response.rb +2 -12
  16. data/lib/openapi_first/response_body_parsers.rb +2 -2
  17. data/lib/openapi_first/response_parser.rb +6 -3
  18. data/lib/openapi_first/response_validator.rb +4 -3
  19. data/lib/openapi_first/schema/hash.rb +1 -1
  20. data/lib/openapi_first/test/configuration.rb +45 -4
  21. data/lib/openapi_first/test/coverage/html_reporter/context.rb +89 -0
  22. data/lib/openapi_first/test/coverage/html_reporter.css +179 -0
  23. data/lib/openapi_first/test/coverage/html_reporter.html.erb +87 -0
  24. data/lib/openapi_first/test/coverage/html_reporter.rb +30 -0
  25. data/lib/openapi_first/test/coverage/plan.rb +4 -3
  26. data/lib/openapi_first/test/coverage/route_task.rb +5 -0
  27. data/lib/openapi_first/test/coverage/{terminal_formatter.rb → terminal_reporter.rb} +25 -33
  28. data/lib/openapi_first/test/coverage.rb +15 -7
  29. data/lib/openapi_first/test/logger.rb +17 -0
  30. data/lib/openapi_first/test/observe.rb +1 -1
  31. data/lib/openapi_first/test.rb +16 -6
  32. data/lib/openapi_first/validators/request_body.rb +3 -2
  33. data/lib/openapi_first/validators/request_parameters.rb +4 -4
  34. data/lib/openapi_first/validators/response_body.rb +2 -2
  35. data/lib/openapi_first/validators/response_headers.rb +4 -3
  36. data/lib/openapi_first/version.rb +1 -1
  37. data/lib/openapi_first.rb +8 -0
  38. metadata +11 -4
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'logger'
4
+
3
5
  module OpenapiFirst
4
6
  module Test
5
7
  # Helper class to setup tests
6
8
  class Configuration
7
9
  def initialize
8
10
  @minimum_coverage = 100
9
- @coverage_formatter = Coverage::TerminalFormatter
10
- @coverage_formatter_options = {}
11
+ @coverage_reporter = Coverage::HtmlReporter
12
+ @coverage_reporter_options = {}
11
13
  @skip_response_coverage = nil
12
14
  @skip_coverage = nil
13
15
  @response_raise_error = true
@@ -17,6 +19,7 @@ module OpenapiFirst
17
19
  @ignore_unknown_requests = false
18
20
  @ignore_request_error = nil
19
21
  @ignore_response_error = nil
22
+ @logger = Logger.new($stdout)
20
23
  end
21
24
 
22
25
  # Register OADs, but don't load them just yet
@@ -31,10 +34,34 @@ module OpenapiFirst
31
34
  Observe.observe(app, api:)
32
35
  end
33
36
 
34
- attr_accessor :coverage_formatter_options, :coverage_formatter, :response_raise_error,
35
- :ignore_unknown_requests, :ignore_unknown_response_status, :minimum_coverage
37
+ attr_accessor :coverage_reporter, :coverage_reporter_options, :response_raise_error,
38
+ :ignore_unknown_requests, :ignore_unknown_response_status, :minimum_coverage, :logger
36
39
  attr_reader :report_coverage, :ignored_unknown_status
37
40
 
41
+ # @deprecated Use {#coverage_reporter} instead.
42
+ def coverage_formatter
43
+ warn_coverage_formatter_deprecation
44
+ coverage_reporter
45
+ end
46
+
47
+ # @deprecated Use {#coverage_reporter=} instead.
48
+ def coverage_formatter=(value)
49
+ warn_coverage_formatter_deprecation
50
+ self.coverage_reporter = value
51
+ end
52
+
53
+ # @deprecated Use {#coverage_reporter_options} instead.
54
+ def coverage_formatter_options
55
+ warn_coverage_formatter_deprecation
56
+ coverage_reporter_options
57
+ end
58
+
59
+ # @deprecated Use {#coverage_reporter_options=} instead.
60
+ def coverage_formatter_options=(value)
61
+ warn_coverage_formatter_deprecation
62
+ self.coverage_reporter_options = value
63
+ end
64
+
38
65
  # Set ignored unknown status codes.
39
66
  # @param [Array<Integer>] status Status codes that are okay not to cover in an OAD
40
67
  def ignored_unknown_status=(status)
@@ -55,7 +82,9 @@ module OpenapiFirst
55
82
  # Ignore certain errors for certain requests
56
83
  # @param block A Proc that will be called with [OpenapiFirst::ValidatedRequest]
57
84
  def ignore_request_error(&block)
85
+ # :nocov:
58
86
  raise ArgumentError, 'You have to pass a block' unless block_given?
87
+ # :nocov:
59
88
 
60
89
  @ignore_request_error = block
61
90
  end
@@ -63,7 +92,9 @@ module OpenapiFirst
63
92
  # Ignore certain errors for certain responses
64
93
  # @param block A Proc that will be called with [OpenapiFirst::ValidatedResponse, Rack::Request]
65
94
  def ignore_response_error(&block)
95
+ # :nocov:
66
96
  raise ArgumentError, 'You have to pass a block' unless block_given?
97
+ # :nocov:
67
98
 
68
99
  @ignore_response_error = block
69
100
  end
@@ -99,6 +130,16 @@ module OpenapiFirst
99
130
 
100
131
  true
101
132
  end
133
+
134
+ private
135
+
136
+ def warn_coverage_formatter_deprecation
137
+ return if @coverage_formatter_warned
138
+
139
+ warn 'DEPRECATION WARNING: Test::Configuration#coverage_formatter(_options) is deprecated, ' \
140
+ 'use #coverage_reporter(_options) instead.'
141
+ @coverage_formatter_warned = true
142
+ end
102
143
  end
103
144
  end
104
145
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ module OpenapiFirst
6
+ module Test
7
+ module Coverage
8
+ class HtmlReporter
9
+ # Provides the binding and helper methods for the ERB template.
10
+ class Context
11
+ NO_REQUESTS_WARNING =
12
+ 'API Coverage did not detect any API requests for the registered ' \
13
+ 'API descriptions. Make sure to observe your application using OpenapiFirst::Test.'
14
+
15
+ attr_reader :coverage, :plans, :verbose
16
+
17
+ def initialize(coverage_result, verbose)
18
+ @coverage = coverage_result.coverage
19
+ @plans = coverage_result.plans
20
+ @verbose = verbose
21
+ end
22
+
23
+ # Helper for ERB rendering only — exposes this context's binding so the
24
+ # template can resolve helper methods and instance state.
25
+ def get_binding # rubocop:disable Naming/AccessorMethodName
26
+ binding
27
+ end
28
+
29
+ def expand_plan?(plan)
30
+ verbose || plan.done?
31
+ end
32
+
33
+ def visible_routes(plan)
34
+ return plan.routes if expand_plan?(plan)
35
+
36
+ plan.routes.reject(&:finished?)
37
+ end
38
+
39
+ def any_request_made?(route)
40
+ route.requests.any?(&:requested?)
41
+ end
42
+
43
+ def route_status(route)
44
+ return :request_problem if route.requests.none?(&:finished?)
45
+ return :responses_problem if any_request_made?(route) && route.responses.any? { |r| !r.finished? }
46
+
47
+ :ok
48
+ end
49
+
50
+ def uncovered_responses_count(route)
51
+ route.responses.count { |r| !r.finished? }
52
+ end
53
+
54
+ def request_items(route, plan_verbose:)
55
+ return [] unless any_request_made?(route) && route.requests.any?(&:content_type)
56
+
57
+ plan_verbose ? route.requests : route.requests.reject(&:finished?)
58
+ end
59
+
60
+ def response_items(route, plan_verbose:)
61
+ return [] unless plan_verbose || any_request_made?(route)
62
+ return route.responses if plan_verbose || route.responses.any? { |r| !r.finished? }
63
+
64
+ []
65
+ end
66
+
67
+ def h(text)
68
+ ERB::Util.html_escape(text)
69
+ end
70
+
71
+ def explain_unfinished_request(request)
72
+ return 'No requests tracked!' unless request.requested?
73
+ return if request.any_valid_request?
74
+
75
+ "All requests invalid! (#{request.last_error_message.inspect})"
76
+ end
77
+
78
+ def explain_unfinished_response(response, request_made: false)
79
+ unless response.responded?
80
+ return request_made ? 'No matching response tracked!' : 'No responses tracked!'
81
+ end
82
+
83
+ "All responses invalid! (#{response.last_error_message.inspect})" unless response.any_valid_response?
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,179 @@
1
+ *, *::before, *::after { box-sizing: border-box; }
2
+
3
+ body {
4
+ font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
5
+ background: #0f1117;
6
+ color: #e2e8f0;
7
+ margin: 0;
8
+ padding: 2rem;
9
+ line-height: 1.6;
10
+ }
11
+
12
+ h1 {
13
+ font-size: 1.1rem;
14
+ font-weight: 600;
15
+ margin-block: 0 1rem;
16
+ color: #f8fafc;
17
+
18
+ & .pct {
19
+ font-size: 0.95rem;
20
+ font-weight: 400;
21
+ color: #94a3b8;
22
+ margin-inline-start: 0.5rem;
23
+ }
24
+ }
25
+
26
+ details.plan {
27
+ border: 1px solid #1e293b;
28
+ padding: 1.25rem 1.5rem;
29
+ margin-block-end: 1.5rem;
30
+ background: #161b27;
31
+
32
+ &[open] { border-color: #334155; }
33
+ }
34
+
35
+ details.plan > summary {
36
+ font-size: 1rem;
37
+ font-weight: 600;
38
+ color: #cbd5e1;
39
+ cursor: pointer;
40
+ list-style: none;
41
+ display: flex;
42
+ align-items: baseline;
43
+ gap: 0.5rem;
44
+ user-select: none;
45
+
46
+ & .pct {
47
+ font-size: 0.8rem;
48
+ font-weight: 400;
49
+ color: #64748b;
50
+ text-transform: none;
51
+ letter-spacing: 0;
52
+ }
53
+
54
+ &::before {
55
+ content: '▶';
56
+ font-size: 0.6rem;
57
+ color: #475569;
58
+ transition: transform 0.15s ease;
59
+ align-self: center;
60
+ }
61
+
62
+ details[open] > &::before { transform: rotate(90deg); }
63
+ }
64
+
65
+ .plan-content {
66
+ margin-block-start: 1rem;
67
+ }
68
+
69
+ details.route {
70
+ border-inline-start: 2px solid #1e293b;
71
+ padding-inline-start: 1rem;
72
+ margin-block: 0.75rem;
73
+
74
+ &[open] { border-color: #334155; }
75
+ }
76
+
77
+ details.route > summary {
78
+ font-weight: 600;
79
+ color: #94a3b8;
80
+ font-size: 0.85rem;
81
+ text-transform: uppercase;
82
+ letter-spacing: 0.04em;
83
+ cursor: pointer;
84
+ list-style: none;
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 0.4rem;
88
+ padding-block: 0.2rem;
89
+ user-select: none;
90
+
91
+ &::before {
92
+ content: '▶';
93
+ font-size: 0.6rem;
94
+ color: #475569;
95
+ transition: transform 0.15s ease;
96
+ }
97
+
98
+ details[open] > &::before { transform: rotate(90deg); }
99
+ }
100
+
101
+ .operation-label {
102
+ font-size: 0.8rem;
103
+ font-weight: 400;
104
+ color: #64748b;
105
+ text-transform: none;
106
+ letter-spacing: 0;
107
+ }
108
+
109
+ .request-status,
110
+ .response-summary {
111
+ font-size: 0.8rem;
112
+ font-weight: 400;
113
+ text-transform: none;
114
+ letter-spacing: 0;
115
+ }
116
+
117
+ .route-content {
118
+ margin-block-start: 0.4rem;
119
+ }
120
+
121
+ ul.tasks {
122
+ list-style: none;
123
+ margin: 0;
124
+ padding: 0;
125
+ display: flex;
126
+ flex-direction: column;
127
+ gap: 0.2rem;
128
+ }
129
+
130
+ ul.responses {
131
+ padding-inline-start: 1.5rem;
132
+ }
133
+
134
+ li {
135
+ font-size: 0.875rem;
136
+ padding: 0.2rem 0.4rem;
137
+ display: flex;
138
+ align-items: baseline;
139
+ gap: 0.5rem;
140
+ }
141
+
142
+ .covered { color: #4ade80; }
143
+
144
+ .uncovered {
145
+ background: #1f1315;
146
+ font-weight: 500;
147
+ }
148
+
149
+ .status {
150
+ color: #e2e8f0;
151
+ font-weight: 600;
152
+ min-width: 3ch;
153
+ }
154
+
155
+ .content-type {
156
+ color: #64748b;
157
+ font-size: 0.8rem;
158
+ }
159
+
160
+ .problem {
161
+ color: #f87171;
162
+ font-size: 0.8rem;
163
+ }
164
+
165
+ .method {
166
+ color: #7dd3fc;
167
+ font-weight: 700;
168
+ }
169
+
170
+ .path {
171
+ color: #e2e8f0;
172
+ }
173
+
174
+ .warning {
175
+ color: #fbbf24;
176
+ background: #1c1a10;
177
+ border: 1px solid #3b3010;
178
+ padding: 0.75rem 1rem;
179
+ }
@@ -0,0 +1,87 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>OpenAPI API Coverage Report</title>
6
+ <style><%= File.read(File.join(__dir__, 'html_reporter.css')) %></style>
7
+ </head>
8
+ <body>
9
+ <h1>API Coverage <span class="pct"><%= coverage.round(1) %>%</span></h1>
10
+ <% if plans.empty? -%>
11
+ <p class="warning"><%= NO_REQUESTS_WARNING %></p>
12
+ <% else -%>
13
+ <% plans.each do |plan| -%>
14
+ <details class="plan"<%= ' open' unless plan.done? %>>
15
+ <summary>
16
+ <%= h plan.title || plan.api_identifier %>
17
+ <span class="pct"><%= plan.api_identifier %> · <%= plan.coverage.round(1) %>%</span>
18
+ </summary>
19
+ <div class="plan-content">
20
+ <% plan_verbose = expand_plan?(plan) -%>
21
+ <% visible_routes(plan).each do |route| -%>
22
+ <details class="route"<%= ' open' unless route.finished? %>>
23
+ <summary>
24
+ <span class="method"><%= h route.request_method.upcase %></span>
25
+ <span class="path"><%= h route.path %></span>
26
+ <% status = route_status(route) -%>
27
+ <% if status == :request_problem -%>
28
+ <span class="request-status problem">❌ <span class="problem"><%= h explain_unfinished_request(route.requests.first) %></span></span>
29
+ <% elsif status == :responses_problem -%>
30
+ <span class="response-summary problem">⚠️ <%= uncovered_responses_count(route) %> response(s) not covered</span>
31
+ <% else -%>
32
+ <span class="request-status covered">✅</span>
33
+ <% end -%>
34
+ <% if (label = route.summary) -%>
35
+ <span class="operation-label"><%= h label %></span>
36
+ <% end -%>
37
+ </summary>
38
+ <div class="route-content">
39
+ <% requests = request_items(route, plan_verbose: plan_verbose) -%>
40
+ <% unless requests.empty? -%>
41
+ <ul class="tasks">
42
+ <% requests.each do |request| -%>
43
+ <% if request.finished? -%>
44
+ <li class="covered">
45
+ <span>✅</span>
46
+ <% if request.content_type -%><span class="content-type"><%= h request.content_type %></span><% end -%>
47
+ </li>
48
+ <% else -%>
49
+ <li class="uncovered">
50
+ <span>❌</span>
51
+ <% if request.content_type -%><span class="content-type"><%= h request.content_type %></span><% end -%>
52
+ <span class="problem"><%= h explain_unfinished_request(request) %></span>
53
+ </li>
54
+ <% end -%>
55
+ <% end -%>
56
+ </ul>
57
+ <% end -%>
58
+ <% responses = response_items(route, plan_verbose: plan_verbose) -%>
59
+ <% unless responses.empty? -%>
60
+ <ul class="tasks responses">
61
+ <% responses.each do |response| -%>
62
+ <% if response.finished? -%>
63
+ <li class="covered">
64
+ <span>✅</span>
65
+ <span class="status"><%= h response.status %></span>
66
+ <% if response.content_type -%><span class="content-type"><%= h response.content_type %></span><% end -%>
67
+ </li>
68
+ <% else -%>
69
+ <li class="uncovered">
70
+ <span>❌</span>
71
+ <span class="status"><%= h response.status %></span>
72
+ <% if response.content_type -%><span class="content-type"><%= h response.content_type %></span><% end -%>
73
+ <span class="problem"><%= h explain_unfinished_response(response, request_made: any_request_made?(route)) %></span>
74
+ </li>
75
+ <% end -%>
76
+ <% end -%>
77
+ </ul>
78
+ <% end -%>
79
+ </div>
80
+ </details>
81
+ <% end -%>
82
+ </div>
83
+ </details>
84
+ <% end -%>
85
+ <% end -%>
86
+ </body>
87
+ </html>
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative 'html_reporter/context'
5
+
6
+ module OpenapiFirst
7
+ module Test
8
+ module Coverage
9
+ # Writes a self-contained HTML coverage report to a file.
10
+ class HtmlReporter
11
+ def initialize(output: 'coverage/openapi_coverage.html', verbose: false, logger: Test.logger)
12
+ @output = output
13
+ @verbose = verbose
14
+ @logger = logger
15
+ end
16
+
17
+ def report(coverage_result)
18
+ html = TEMPLATE.result(Context.new(coverage_result, @verbose).get_binding)
19
+ FileUtils.mkdir_p(File.dirname(@output))
20
+ File.write(@output, html)
21
+ @logger.info "API coverage report written to #{@output}"
22
+ end
23
+
24
+ TEMPLATE_PATH = File.join(__dir__, 'html_reporter.html.erb')
25
+ TEMPLATE = ERB.new(File.read(TEMPLATE_PATH), trim_mode: '-')
26
+ TEMPLATE.filename = TEMPLATE_PATH
27
+ end
28
+ end
29
+ end
30
+ end
@@ -13,7 +13,7 @@ module OpenapiFirst
13
13
  class UnknownRequestError < StandardError; end
14
14
 
15
15
  def self.for(oad, skip_response: nil, skip_route: nil)
16
- plan = new(definition_key: oad.key, filepath: oad.filepath)
16
+ plan = new(definition_key: oad.key, filepath: oad.filepath, title: oad.title)
17
17
  routes = oad.routes
18
18
  routes = routes.reject { |route| skip_route[route.path, route.request_method] } if skip_route
19
19
  routes.each do |route|
@@ -26,14 +26,15 @@ module OpenapiFirst
26
26
  plan
27
27
  end
28
28
 
29
- def initialize(definition_key:, filepath: nil)
29
+ def initialize(definition_key:, filepath: nil, title: nil)
30
30
  @routes = []
31
31
  @index = {}
32
32
  @api_identifier = filepath || definition_key
33
33
  @filepath = filepath
34
+ @title = title
34
35
  end
35
36
 
36
- attr_reader :api_identifier, :filepath, :routes
37
+ attr_reader :api_identifier, :filepath, :routes, :title
37
38
  private attr_reader :index
38
39
 
39
40
  def track_request(validated_request)
@@ -7,6 +7,11 @@ module OpenapiFirst
7
7
  def finished?
8
8
  requests.all?(&:finished?) && responses.all?(&:finished?)
9
9
  end
10
+
11
+ def summary
12
+ operation = requests.first&.request&.operation
13
+ operation&.[]('summary') || operation&.[]('description')
14
+ end
10
15
  end
11
16
  end
12
17
  end
@@ -3,38 +3,34 @@
3
3
  module OpenapiFirst
4
4
  module Test
5
5
  module Coverage
6
- # This is the default formatter
7
- class TerminalFormatter
8
- def initialize(verbose: false, focused: true)
6
+ # Reports coverage to a logger using ANSI-coloured lines.
7
+ class TerminalReporter
8
+ def initialize(verbose: false, focused: true, logger: Test.logger)
9
9
  @verbose = verbose
10
10
  @focused = focused && !verbose
11
+ @logger = logger
11
12
  end
12
13
 
13
14
  def format(coverage_result)
15
+ logger.warn 'DEPRECATION WARNING: TerminalReporter#format is deprecated, use #report instead.'
16
+ report(coverage_result)
17
+ end
18
+
19
+ def report(coverage_result)
14
20
  coverage = coverage_result.coverage
15
- @out = StringIO.new
16
21
  if coverage.zero?
17
- @out.puts 'API Coverage did not detect any API requests for the registered API descriptions. ' \
18
- 'Make sure to observe your application using OpenapiFirst::Test.'
22
+ logger.warn 'API Coverage did not detect any API requests for the registered API descriptions. ' \
23
+ 'Make sure to observe your application using OpenapiFirst::Test.'
19
24
  end
20
25
  coverage_result.plans.each { |plan| format_plan(plan) } if coverage.positive?
21
- @out.string
22
26
  end
23
27
 
24
- private attr_reader :out, :verbose, :focused
28
+ private attr_reader :out, :verbose, :focused, :logger
25
29
 
26
30
  private
27
31
 
28
- def puts(string)
29
- @out.puts(string)
30
- end
31
-
32
- def print(string)
33
- @out.print(string)
34
- end
35
-
36
32
  def format_plan(plan) # rubocop:disable Metrics/PerceivedComplexity
37
- puts ['', "API validation coverage for #{plan.api_identifier}: #{plan.coverage}%"]
33
+ logger.info "API validation coverage for #{plan.api_identifier}: #{plan.coverage}%"
38
34
  return if plan.done? && !verbose
39
35
 
40
36
  requested_routes_count = plan.routes.count { |route| route.requests.any?(&:requested?) }
@@ -54,9 +50,9 @@ module OpenapiFirst
54
50
  def format_requests(requests)
55
51
  requests.each do |request|
56
52
  if request.finished?
57
- puts green "✓ #{request_label(request)}"
53
+ log_success "✓ #{request_label(request)}"
58
54
  else
59
- puts red "❌ #{request_label(request)} – #{explain_unfinished_request(request)}"
55
+ log_error "❌ #{request_label(request)} – #{explain_unfinished_request(request)}"
60
56
  end
61
57
  end
62
58
  end
@@ -64,25 +60,13 @@ module OpenapiFirst
64
60
  def format_responses(responses)
65
61
  responses.each do |response|
66
62
  if response.finished?
67
- puts green " ✓ #{response_label(response)}" if verbose
63
+ log_success " ✓ #{response_label(response)}" if verbose
68
64
  else
69
- puts red " ❌ #{response_label(response)} – #{explain_unfinished_response(response)}"
65
+ log_error " ❌ #{response_label(response)} – #{explain_unfinished_response(response)}"
70
66
  end
71
67
  end
72
68
  end
73
69
 
74
- def green(text)
75
- "\e[32m#{text}\e[0m"
76
- end
77
-
78
- def red(text)
79
- "\e[31m#{text}\e[0m"
80
- end
81
-
82
- def orange(text)
83
- "\e[33m#{text}\e[0m"
84
- end
85
-
86
70
  def request_label(request)
87
71
  name = "#{request.request_method.upcase} #{request.path}" # TODO: add required query parameters?
88
72
  name << " (#{request.content_type})" if request.content_type
@@ -109,6 +93,14 @@ module OpenapiFirst
109
93
 
110
94
  "All responses invalid! (#{response.last_error_message.inspect})" unless response.any_valid_response?
111
95
  end
96
+
97
+ def log_success(msg)
98
+ logger.info "\e[32m#{msg}\e[0m"
99
+ end
100
+
101
+ def log_error(msg)
102
+ logger.error "\e[31m#{msg}\e[0m"
103
+ end
112
104
  end
113
105
  end
114
106
  end
@@ -12,7 +12,11 @@ module OpenapiFirst
12
12
  # to assess if all parts of the API description have been tested.
13
13
  # Currently it does not care about unknown requests that are not part of any API description.
14
14
  module Coverage
15
- autoload :TerminalFormatter, 'openapi_first/test/coverage/terminal_formatter'
15
+ autoload :TerminalReporter, 'openapi_first/test/coverage/terminal_reporter'
16
+ autoload :HtmlReporter, 'openapi_first/test/coverage/html_reporter'
17
+
18
+ # @deprecated Use {TerminalReporter} instead.
19
+ TerminalFormatter = TerminalReporter
16
20
 
17
21
  Result = Data.define(:plans, :coverage)
18
22
 
@@ -45,7 +49,11 @@ module OpenapiFirst
45
49
  # the coverage collection.
46
50
  # To make this work we need to keep arguments trivial, which is the reason the request
47
51
  # is wrapped in a CoveredRequest data object.
48
- tracker&.track_request(
52
+ # :nocov:
53
+ return unless tracker
54
+ # :nocov:
55
+
56
+ tracker.track_request(
49
57
  oad.key,
50
58
  CoveredRequest.new(
51
59
  key: request.request_definition.key,
@@ -61,7 +69,11 @@ module OpenapiFirst
61
69
  # the coverage collection.
62
70
  # To make this work we need to keep arguments trivial, which is the reason the response
63
71
  # is wrapped in a CoveredResponse data object.
64
- tracker&.track_response(
72
+ # :nocov:
73
+ return unless tracker
74
+ # :nocov:
75
+
76
+ tracker.track_response(
65
77
  oad.key,
66
78
  CoveredResponse.new(
67
79
  key: response.response_definition.key,
@@ -76,10 +88,6 @@ module OpenapiFirst
76
88
 
77
89
  private
78
90
 
79
- def current_run
80
- tracker.plans_by_key
81
- end
82
-
83
91
  # Returns all plans (Plan) that were registered for this run
84
92
  def plans
85
93
  tracker&.plans || []
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module OpenapiFirst
6
+ module Test
7
+ # Logger to output coverage reports and such
8
+ class Logger < ::Logger
9
+ def initialize(*)
10
+ super
11
+ self.formatter = proc { |_severity, _time, _progname, msg|
12
+ "#{msg}\n"
13
+ }
14
+ end
15
+ end
16
+ end
17
+ end