openapi_first 3.4.2 → 4.0.0
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 +4 -4
- data/CHANGELOG.md +70 -0
- data/README.md +95 -42
- data/lib/openapi_first/builder.rb +87 -52
- data/lib/openapi_first/child_configuration.rb +0 -2
- data/lib/openapi_first/configuration.rb +0 -23
- data/lib/openapi_first/definition.rb +35 -2
- data/lib/openapi_first/failure.rb +5 -1
- data/lib/openapi_first/middlewares/request_validation.rb +1 -1
- data/lib/openapi_first/middlewares/response_validation.rb +1 -1
- data/lib/openapi_first/parameter/converter/array_converter.rb +42 -0
- data/lib/openapi_first/parameter/converter/object_converter.rb +60 -0
- data/lib/openapi_first/parameter/converter.rb +69 -0
- data/lib/openapi_first/parameter/unpackers.rb +132 -0
- data/lib/openapi_first/parameter.rb +70 -0
- data/lib/openapi_first/parameter_content_parsers.rb +55 -0
- data/lib/openapi_first/parameters_parser.rb +23 -0
- data/lib/openapi_first/query_string_parser.rb +93 -0
- data/lib/openapi_first/ref_resolver.rb +56 -3
- data/lib/openapi_first/request.rb +12 -18
- data/lib/openapi_first/request_body_parsers.rb +19 -16
- data/lib/openapi_first/request_headers.rb +27 -0
- data/lib/openapi_first/request_validator.rb +4 -1
- data/lib/openapi_first/response_header.rb +9 -0
- data/lib/openapi_first/response_parser.rb +3 -10
- data/lib/openapi_first/router.rb +27 -12
- data/lib/openapi_first/schema/hash.rb +0 -1
- data/lib/openapi_first/sinatra.rb +217 -0
- data/lib/openapi_first/test/configuration.rb +0 -34
- data/lib/openapi_first/test/coverage/html_reporter/context.rb +24 -17
- data/lib/openapi_first/test/coverage/html_reporter.css +214 -67
- data/lib/openapi_first/test/coverage/html_reporter.html.erb +39 -11
- data/lib/openapi_first/test/coverage/html_reporter.rb +11 -1
- data/lib/openapi_first/test/coverage/plan.rb +30 -10
- data/lib/openapi_first/test/coverage/request_task.rb +7 -2
- data/lib/openapi_first/test/coverage/response_task.rb +6 -1
- data/lib/openapi_first/test/coverage/route_task.rb +23 -1
- data/lib/openapi_first/test/coverage/skipped_summary.rb +22 -0
- data/lib/openapi_first/test/coverage/terminal_reporter.rb +23 -11
- data/lib/openapi_first/test/coverage.rb +9 -3
- data/lib/openapi_first/test.rb +69 -11
- data/lib/openapi_first/validators/multipart_request_body.rb +57 -0
- data/lib/openapi_first/validators/request_body.rb +20 -7
- data/lib/openapi_first/validators/request_parameters.rb +5 -4
- data/lib/openapi_first/version.rb +1 -1
- metadata +15 -23
- data/lib/openapi_first/header.rb +0 -9
|
@@ -38,30 +38,6 @@ module OpenapiFirst
|
|
|
38
38
|
:ignore_unknown_requests, :ignore_unknown_response_status, :minimum_coverage, :logger
|
|
39
39
|
attr_reader :report_coverage, :ignored_unknown_status
|
|
40
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
|
-
|
|
65
41
|
# Set ignored unknown status codes.
|
|
66
42
|
# @param [Array<Integer>] status Status codes that are okay not to cover in an OAD
|
|
67
43
|
def ignored_unknown_status=(status)
|
|
@@ -130,16 +106,6 @@ module OpenapiFirst
|
|
|
130
106
|
|
|
131
107
|
true
|
|
132
108
|
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
|
|
143
109
|
end
|
|
144
110
|
end
|
|
145
111
|
end
|
|
@@ -12,12 +12,16 @@ module OpenapiFirst
|
|
|
12
12
|
'API Coverage did not detect any API requests for the registered ' \
|
|
13
13
|
'API descriptions. Make sure to observe your application using OpenapiFirst::Test.'
|
|
14
14
|
|
|
15
|
-
attr_reader :coverage, :plans, :verbose
|
|
15
|
+
attr_reader :coverage, :plans, :verbose, :generated_at,
|
|
16
|
+
:skipped_requests_count, :skipped_responses_count
|
|
16
17
|
|
|
17
|
-
def initialize(coverage_result, verbose)
|
|
18
|
+
def initialize(coverage_result, verbose, generated_at: Time.now)
|
|
18
19
|
@coverage = coverage_result.coverage
|
|
19
20
|
@plans = coverage_result.plans
|
|
21
|
+
@skipped_requests_count = coverage_result.skipped_requests_count
|
|
22
|
+
@skipped_responses_count = coverage_result.skipped_responses_count
|
|
20
23
|
@verbose = verbose
|
|
24
|
+
@generated_at = generated_at.strftime('%Y-%m-%d %H:%M:%S %z')
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
# Helper for ERB rendering only — exposes this context's binding so the
|
|
@@ -26,14 +30,12 @@ module OpenapiFirst
|
|
|
26
30
|
binding
|
|
27
31
|
end
|
|
28
32
|
|
|
29
|
-
def
|
|
30
|
-
|
|
33
|
+
def any_skipped?
|
|
34
|
+
skipped_requests_count.positive? || skipped_responses_count.positive?
|
|
31
35
|
end
|
|
32
36
|
|
|
33
37
|
def visible_routes(plan)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
plan.routes.reject(&:finished?)
|
|
38
|
+
plan.routes
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
def any_request_made?(route)
|
|
@@ -41,27 +43,32 @@ module OpenapiFirst
|
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
def route_status(route)
|
|
44
|
-
return :
|
|
45
|
-
return :
|
|
46
|
+
return :skipped if route.skipped?
|
|
47
|
+
return :request_problem if route.tracked_requests.none?(&:finished?)
|
|
48
|
+
return :responses_problem if any_request_made?(route) && route.tracked_responses.any? { |r| !r.finished? }
|
|
46
49
|
|
|
47
50
|
:ok
|
|
48
51
|
end
|
|
49
52
|
|
|
53
|
+
def route_class(route)
|
|
54
|
+
return 'is-skipped' if route.skipped?
|
|
55
|
+
|
|
56
|
+
route.finished? ? 'is-covered' : 'is-uncovered'
|
|
57
|
+
end
|
|
58
|
+
|
|
50
59
|
def uncovered_responses_count(route)
|
|
51
|
-
route.
|
|
60
|
+
route.tracked_responses.count { |r| !r.finished? }
|
|
52
61
|
end
|
|
53
62
|
|
|
54
|
-
def request_items(route
|
|
63
|
+
def request_items(route)
|
|
64
|
+
return route.requests if route.skipped?
|
|
55
65
|
return [] unless any_request_made?(route) && route.requests.any?(&:content_type)
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
route.requests
|
|
58
68
|
end
|
|
59
69
|
|
|
60
|
-
def response_items(route
|
|
61
|
-
|
|
62
|
-
return route.responses if plan_verbose || route.responses.any? { |r| !r.finished? }
|
|
63
|
-
|
|
64
|
-
[]
|
|
70
|
+
def response_items(route)
|
|
71
|
+
route.responses
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
def h(text)
|
|
@@ -1,41 +1,162 @@
|
|
|
1
|
-
*,
|
|
1
|
+
*,
|
|
2
|
+
*::before,
|
|
3
|
+
*::after {
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
--font-size-base: 1rem;
|
|
9
|
+
--font-size-xs: calc(var(--font-size-base) * 0.75);
|
|
10
|
+
--font-size-sm: calc(var(--font-size-base) * 0.875);
|
|
11
|
+
--font-size-h1: calc(var(--font-size-base) * 1.6);
|
|
12
|
+
|
|
13
|
+
--bg-primary: #0f1117;
|
|
14
|
+
--bg-secondary: #161b27;
|
|
15
|
+
|
|
16
|
+
--border-default: #1e293b;
|
|
17
|
+
--border-active: #334155;
|
|
18
|
+
--border-warning: #3b3010;
|
|
19
|
+
|
|
20
|
+
--tree-indent: 1.25rem;
|
|
21
|
+
--tree-row-center: 0.9rem;
|
|
22
|
+
|
|
23
|
+
--text-heading: #f8fafc;
|
|
24
|
+
--text-primary: #e2e8f0;
|
|
25
|
+
--text-muted: #94a3b8;
|
|
26
|
+
|
|
27
|
+
--color-success: #4ade80;
|
|
28
|
+
--color-error: #f87171;
|
|
29
|
+
--color-info: #7dd3fc;
|
|
30
|
+
--color-warning: #fbbf24;
|
|
31
|
+
--color-skipped: #fb923c;
|
|
32
|
+
}
|
|
2
33
|
|
|
3
34
|
body {
|
|
4
|
-
font-family:
|
|
5
|
-
|
|
6
|
-
|
|
35
|
+
font-family:
|
|
36
|
+
ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
|
|
37
|
+
background: var(--bg-primary);
|
|
38
|
+
color: var(--text-primary);
|
|
7
39
|
margin: 0;
|
|
8
|
-
padding: 2rem;
|
|
40
|
+
padding: 1rem 2rem;
|
|
9
41
|
line-height: 1.6;
|
|
10
42
|
}
|
|
11
43
|
|
|
44
|
+
.page-header {
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: baseline;
|
|
47
|
+
justify-content: space-between;
|
|
48
|
+
gap: 1rem;
|
|
49
|
+
margin-block-end: 0.875rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.generated-at {
|
|
53
|
+
font-size: var(--font-size-xs);
|
|
54
|
+
color: var(--text-muted);
|
|
55
|
+
white-space: nowrap;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.page-title {
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
align-self: stretch;
|
|
62
|
+
margin: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
12
65
|
h1 {
|
|
13
|
-
font-size:
|
|
14
|
-
|
|
15
|
-
margin-block: 0 1rem;
|
|
16
|
-
color: #f8fafc;
|
|
66
|
+
font-size: var(--font-size-h1);
|
|
67
|
+
color: var(--text-heading);
|
|
17
68
|
|
|
18
69
|
& .pct {
|
|
19
|
-
font-size:
|
|
20
|
-
|
|
21
|
-
color: #94a3b8;
|
|
70
|
+
font-size: var(--font-size-sm);
|
|
71
|
+
color: var(--text-muted);
|
|
22
72
|
margin-inline-start: 0.5rem;
|
|
23
73
|
}
|
|
24
74
|
}
|
|
25
75
|
|
|
76
|
+
h2 {
|
|
77
|
+
margin: 0;
|
|
78
|
+
font-size: var(--font-size-sm);
|
|
79
|
+
color: var(--text-heading);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.api-identifier {
|
|
83
|
+
font-size: var(--font-size-xs);
|
|
84
|
+
color: var(--text-muted);
|
|
85
|
+
font-weight: normal;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.filters {
|
|
89
|
+
display: flex;
|
|
90
|
+
justify-content: flex-end;
|
|
91
|
+
gap: 1.25rem;
|
|
92
|
+
margin-block-end: 0.5rem;
|
|
93
|
+
font-size: var(--font-size-sm);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.filters .filter {
|
|
97
|
+
display: inline-flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
gap: 0.4rem;
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
color: var(--text-muted);
|
|
102
|
+
user-select: none;
|
|
103
|
+
|
|
104
|
+
& input {
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* CSS-only route filtering driven by the toolbar checkboxes. */
|
|
110
|
+
body:has(#filter-covered:not(:checked)) details.route.is-covered {
|
|
111
|
+
display: none;
|
|
112
|
+
}
|
|
113
|
+
body:has(#filter-uncovered:not(:checked)) details.route.is-uncovered {
|
|
114
|
+
display: none;
|
|
115
|
+
}
|
|
116
|
+
body:has(#filter-skipped-requests:not(:checked)) details.route.is-skipped {
|
|
117
|
+
display: none;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Hide a plan once none of its routes remain visible. */
|
|
121
|
+
body:has(#filter-covered:not(:checked))
|
|
122
|
+
details.plan:not(:has(details.route.is-uncovered)):not(
|
|
123
|
+
:has(details.route.is-skipped)
|
|
124
|
+
),
|
|
125
|
+
body:has(#filter-uncovered:not(:checked))
|
|
126
|
+
details.plan:not(:has(details.route.is-covered)):not(
|
|
127
|
+
:has(details.route.is-skipped)
|
|
128
|
+
),
|
|
129
|
+
body:has(#filter-skipped-requests:not(:checked))
|
|
130
|
+
details.plan:not(:has(details.route.is-covered)):not(
|
|
131
|
+
:has(details.route.is-uncovered)
|
|
132
|
+
),
|
|
133
|
+
body:has(#filter-covered:not(:checked)):has(#filter-uncovered:not(:checked))
|
|
134
|
+
details.plan:not(:has(details.route.is-skipped)),
|
|
135
|
+
body:has(#filter-covered:not(:checked)):has(
|
|
136
|
+
#filter-skipped-requests:not(:checked)
|
|
137
|
+
)
|
|
138
|
+
details.plan:not(:has(details.route.is-uncovered)),
|
|
139
|
+
body:has(#filter-uncovered:not(:checked)):has(
|
|
140
|
+
#filter-skipped-requests:not(:checked)
|
|
141
|
+
)
|
|
142
|
+
details.plan:not(:has(details.route.is-covered)),
|
|
143
|
+
body:has(#filter-covered:not(:checked)):has(
|
|
144
|
+
#filter-uncovered:not(:checked)
|
|
145
|
+
):has(#filter-skipped-requests:not(:checked))
|
|
146
|
+
details.plan {
|
|
147
|
+
display: none;
|
|
148
|
+
}
|
|
149
|
+
|
|
26
150
|
details.plan {
|
|
27
|
-
border: 1px solid
|
|
151
|
+
border: 1px solid var(--border-default);
|
|
28
152
|
padding: 1.25rem 1.5rem;
|
|
29
153
|
margin-block-end: 1.5rem;
|
|
30
|
-
background:
|
|
31
|
-
|
|
32
|
-
&[open] { border-color: #334155; }
|
|
154
|
+
background: var(--bg-secondary);
|
|
33
155
|
}
|
|
34
156
|
|
|
35
157
|
details.plan > summary {
|
|
36
|
-
font-size:
|
|
37
|
-
|
|
38
|
-
color: #cbd5e1;
|
|
158
|
+
font-size: var(--font-size-base);
|
|
159
|
+
color: var(--text-heading);
|
|
39
160
|
cursor: pointer;
|
|
40
161
|
list-style: none;
|
|
41
162
|
display: flex;
|
|
@@ -44,41 +165,50 @@ details.plan > summary {
|
|
|
44
165
|
user-select: none;
|
|
45
166
|
|
|
46
167
|
& .pct {
|
|
47
|
-
font-size:
|
|
48
|
-
|
|
49
|
-
color: #64748b;
|
|
168
|
+
font-size: var(--font-size-xs);
|
|
169
|
+
color: var(--text-muted);
|
|
50
170
|
text-transform: none;
|
|
51
171
|
letter-spacing: 0;
|
|
52
172
|
}
|
|
53
173
|
|
|
54
174
|
&::before {
|
|
55
|
-
content:
|
|
56
|
-
font-size:
|
|
57
|
-
color:
|
|
175
|
+
content: "▶";
|
|
176
|
+
font-size: var(--font-size-xs);
|
|
177
|
+
color: var(--text-muted);
|
|
58
178
|
transition: transform 0.15s ease;
|
|
59
179
|
align-self: center;
|
|
60
180
|
}
|
|
61
181
|
|
|
62
|
-
details[open] > &::before {
|
|
182
|
+
details[open] > &::before {
|
|
183
|
+
transform: rotate(90deg);
|
|
184
|
+
}
|
|
63
185
|
}
|
|
64
186
|
|
|
65
187
|
.plan-content {
|
|
66
|
-
margin-block-start:
|
|
188
|
+
margin-block-start: 0.75rem;
|
|
189
|
+
margin-inline-start: 0.25rem;
|
|
190
|
+
padding-inline-start: var(--tree-indent);
|
|
191
|
+
border-inline-start: 2px solid var(--border-default);
|
|
67
192
|
}
|
|
68
193
|
|
|
69
194
|
details.route {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
margin-block: 0.75rem;
|
|
195
|
+
margin-block: 0;
|
|
196
|
+
position: relative;
|
|
73
197
|
|
|
74
|
-
|
|
198
|
+
/* horizontal connector reaching left to the plan spine */
|
|
199
|
+
&::before {
|
|
200
|
+
content: "";
|
|
201
|
+
position: absolute;
|
|
202
|
+
left: calc(-1 * var(--tree-indent));
|
|
203
|
+
top: var(--tree-row-center);
|
|
204
|
+
width: calc(var(--tree-indent) - 0.25rem);
|
|
205
|
+
border-top: 2px solid var(--border-default);
|
|
206
|
+
}
|
|
75
207
|
}
|
|
76
208
|
|
|
77
209
|
details.route > summary {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
font-size: 0.85rem;
|
|
81
|
-
text-transform: uppercase;
|
|
210
|
+
color: var(--text-muted);
|
|
211
|
+
font-size: var(--font-size-sm);
|
|
82
212
|
letter-spacing: 0.04em;
|
|
83
213
|
cursor: pointer;
|
|
84
214
|
list-style: none;
|
|
@@ -89,27 +219,27 @@ details.route > summary {
|
|
|
89
219
|
user-select: none;
|
|
90
220
|
|
|
91
221
|
&::before {
|
|
92
|
-
content:
|
|
93
|
-
font-size:
|
|
94
|
-
color:
|
|
222
|
+
content: "▶";
|
|
223
|
+
font-size: var(--font-size-xs);
|
|
224
|
+
color: var(--text-muted);
|
|
95
225
|
transition: transform 0.15s ease;
|
|
96
226
|
}
|
|
97
227
|
|
|
98
|
-
details[open] > &::before {
|
|
228
|
+
details[open] > &::before {
|
|
229
|
+
transform: rotate(90deg);
|
|
230
|
+
}
|
|
99
231
|
}
|
|
100
232
|
|
|
101
233
|
.operation-label {
|
|
102
|
-
font-size:
|
|
103
|
-
|
|
104
|
-
color: #64748b;
|
|
234
|
+
font-size: var(--font-size-xs);
|
|
235
|
+
color: var(--text-muted);
|
|
105
236
|
text-transform: none;
|
|
106
237
|
letter-spacing: 0;
|
|
107
238
|
}
|
|
108
239
|
|
|
109
240
|
.request-status,
|
|
110
241
|
.response-summary {
|
|
111
|
-
font-size:
|
|
112
|
-
font-weight: 400;
|
|
242
|
+
font-size: var(--font-size-xs);
|
|
113
243
|
text-transform: none;
|
|
114
244
|
letter-spacing: 0;
|
|
115
245
|
}
|
|
@@ -121,59 +251,76 @@ details.route > summary {
|
|
|
121
251
|
ul.tasks {
|
|
122
252
|
list-style: none;
|
|
123
253
|
margin: 0;
|
|
124
|
-
padding:
|
|
254
|
+
padding-inline-start: var(--tree-indent);
|
|
255
|
+
padding-block: 0;
|
|
125
256
|
display: flex;
|
|
126
257
|
flex-direction: column;
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
ul.responses {
|
|
131
|
-
padding-inline-start: 1.5rem;
|
|
258
|
+
border-inline-start: 2px solid var(--border-default);
|
|
132
259
|
}
|
|
133
260
|
|
|
134
261
|
li {
|
|
135
|
-
font-size:
|
|
262
|
+
font-size: var(--font-size-sm);
|
|
136
263
|
padding: 0.2rem 0.4rem;
|
|
137
264
|
display: flex;
|
|
138
265
|
align-items: baseline;
|
|
139
266
|
gap: 0.5rem;
|
|
267
|
+
position: relative;
|
|
268
|
+
|
|
269
|
+
/* horizontal connector reaching left to the task spine */
|
|
270
|
+
&::before {
|
|
271
|
+
content: "";
|
|
272
|
+
position: absolute;
|
|
273
|
+
left: calc(-1 * var(--tree-indent));
|
|
274
|
+
top: var(--tree-row-center);
|
|
275
|
+
width: calc(var(--tree-indent) - 0.25rem);
|
|
276
|
+
border-top: 2px solid var(--border-default);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.covered {
|
|
281
|
+
color: var(--color-success);
|
|
140
282
|
}
|
|
141
283
|
|
|
142
|
-
.
|
|
284
|
+
.skipped {
|
|
285
|
+
color: var(--color-skipped);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.skipped-note,
|
|
289
|
+
.skipped-summary {
|
|
290
|
+
color: var(--color-skipped);
|
|
291
|
+
font-size: var(--font-size-xs);
|
|
292
|
+
}
|
|
143
293
|
|
|
144
|
-
.
|
|
145
|
-
|
|
146
|
-
|
|
294
|
+
.skipped-summary {
|
|
295
|
+
margin-inline-start: 0.75rem;
|
|
296
|
+
white-space: nowrap;
|
|
147
297
|
}
|
|
148
298
|
|
|
149
299
|
.status {
|
|
150
|
-
color:
|
|
151
|
-
font-weight: 600;
|
|
300
|
+
color: var(--text-primary);
|
|
152
301
|
min-width: 3ch;
|
|
153
302
|
}
|
|
154
303
|
|
|
155
304
|
.content-type {
|
|
156
|
-
color:
|
|
157
|
-
font-size:
|
|
305
|
+
color: var(--text-muted);
|
|
306
|
+
font-size: var(--font-size-xs);
|
|
158
307
|
}
|
|
159
308
|
|
|
160
309
|
.problem {
|
|
161
|
-
color:
|
|
162
|
-
font-size:
|
|
310
|
+
color: var(--color-error);
|
|
311
|
+
font-size: var(--font-size-xs);
|
|
163
312
|
}
|
|
164
313
|
|
|
165
314
|
.method {
|
|
166
|
-
color:
|
|
167
|
-
font-weight: 700;
|
|
315
|
+
color: var(--color-info);
|
|
168
316
|
}
|
|
169
317
|
|
|
170
318
|
.path {
|
|
171
|
-
color:
|
|
319
|
+
color: var(--text-primary);
|
|
172
320
|
}
|
|
173
321
|
|
|
174
322
|
.warning {
|
|
175
|
-
color:
|
|
176
|
-
|
|
177
|
-
border: 1px solid #3b3010;
|
|
323
|
+
color: var(--color-warning);
|
|
324
|
+
border: 1px solid var(--border-warning);
|
|
178
325
|
padding: 0.75rem 1rem;
|
|
179
326
|
}
|
|
@@ -6,41 +6,64 @@
|
|
|
6
6
|
<style><%= File.read(File.join(__dir__, 'html_reporter.css')) %></style>
|
|
7
7
|
</head>
|
|
8
8
|
<body>
|
|
9
|
-
<
|
|
9
|
+
<header class="page-header">
|
|
10
|
+
<h1 class="page-title">API Coverage: <%= coverage.round(1) %>%</h1>
|
|
11
|
+
<div>
|
|
12
|
+
<span class="generated-at">Generated at: <%= h generated_at %></span>
|
|
13
|
+
<% if any_skipped? -%>
|
|
14
|
+
<span class="skipped-summary"><%= skipped_requests_count %> request(s) and <%= skipped_responses_count %> response(s) skipped</span>
|
|
15
|
+
<% end -%>
|
|
16
|
+
<div class="filters">
|
|
17
|
+
<label class="filter"><input type="checkbox" id="filter-covered" checked> Covered operations</label>
|
|
18
|
+
<label class="filter"><input type="checkbox" id="filter-uncovered" checked> Uncovered operations</label>
|
|
19
|
+
<label class="filter"><input type="checkbox" id="filter-skipped-requests" checked> Skipped requests</label>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</header>
|
|
10
23
|
<% if plans.empty? -%>
|
|
11
24
|
<p class="warning"><%= NO_REQUESTS_WARNING %></p>
|
|
12
25
|
<% else -%>
|
|
13
26
|
<% plans.each do |plan| -%>
|
|
14
27
|
<details class="plan"<%= ' open' unless plan.done? %>>
|
|
15
28
|
<summary>
|
|
16
|
-
|
|
17
|
-
<span class="pct"><%= plan.
|
|
29
|
+
<h2><%= h plan.title || plan.api_identifier %> <span class="api-identifier"><%= h plan.api_identifier %></span></h2>
|
|
30
|
+
<span class="pct"><%= plan.coverage.round(1) %>%</span>
|
|
18
31
|
</summary>
|
|
19
32
|
<div class="plan-content">
|
|
20
|
-
<% plan_verbose = expand_plan?(plan) -%>
|
|
21
33
|
<% visible_routes(plan).each do |route| -%>
|
|
22
|
-
|
|
34
|
+
<% requests = request_items(route) -%>
|
|
35
|
+
<% responses = response_items(route) -%>
|
|
36
|
+
<details class="route <%= route_class(route) %>"<%= ' open' if any_request_made?(route) && !route.finished? %>>
|
|
23
37
|
<summary>
|
|
24
38
|
<span class="method"><%= h route.request_method.upcase %></span>
|
|
25
39
|
<span class="path"><%= h route.path %></span>
|
|
26
40
|
<% status = route_status(route) -%>
|
|
27
|
-
<% if status == :
|
|
28
|
-
<span class="request-status
|
|
41
|
+
<% if status == :skipped -%>
|
|
42
|
+
<span class="request-status skipped">Skipped</span>
|
|
43
|
+
<% elsif status == :request_problem -%>
|
|
44
|
+
<span class="request-status problem">❌ <span class="problem"><%= h explain_unfinished_request(route.tracked_requests.first) %></span></span>
|
|
29
45
|
<% elsif status == :responses_problem -%>
|
|
30
46
|
<span class="response-summary problem">⚠️ <%= uncovered_responses_count(route) %> response(s) not covered</span>
|
|
31
47
|
<% else -%>
|
|
32
48
|
<span class="request-status covered">✅</span>
|
|
33
49
|
<% end -%>
|
|
50
|
+
<% if route.skipped_responses.any? && status != :skipped -%>
|
|
51
|
+
<span class="response-summary skipped"><%= route.skipped_responses.count %> response(s) skipped</span>
|
|
52
|
+
<% end -%>
|
|
34
53
|
<% if (label = route.summary) -%>
|
|
35
54
|
<span class="operation-label"><%= h label %></span>
|
|
36
55
|
<% end -%>
|
|
37
56
|
</summary>
|
|
38
57
|
<div class="route-content">
|
|
39
|
-
<% requests = request_items(route, plan_verbose: plan_verbose) -%>
|
|
40
58
|
<% unless requests.empty? -%>
|
|
41
59
|
<ul class="tasks">
|
|
42
60
|
<% requests.each do |request| -%>
|
|
43
|
-
<% if request.
|
|
61
|
+
<% if request.skipped? -%>
|
|
62
|
+
<li class="skipped">
|
|
63
|
+
<% if request.content_type -%><span class="content-type"><%= h request.content_type %></span><% end -%>
|
|
64
|
+
<span class="skipped-note">Skipped, not covered by any test</span>
|
|
65
|
+
</li>
|
|
66
|
+
<% elsif request.finished? -%>
|
|
44
67
|
<li class="covered">
|
|
45
68
|
<span>✅</span>
|
|
46
69
|
<% if request.content_type -%><span class="content-type"><%= h request.content_type %></span><% end -%>
|
|
@@ -55,11 +78,16 @@
|
|
|
55
78
|
<% end -%>
|
|
56
79
|
</ul>
|
|
57
80
|
<% end -%>
|
|
58
|
-
<% responses = response_items(route, plan_verbose: plan_verbose) -%>
|
|
59
81
|
<% unless responses.empty? -%>
|
|
60
82
|
<ul class="tasks responses">
|
|
61
83
|
<% responses.each do |response| -%>
|
|
62
|
-
<% if response.
|
|
84
|
+
<% if response.skipped? -%>
|
|
85
|
+
<li class="skipped">
|
|
86
|
+
<span class="status"><%= h response.status %></span>
|
|
87
|
+
<% if response.content_type -%><span class="content-type"><%= h response.content_type %></span><% end -%>
|
|
88
|
+
<span class="skipped-note">Skipped</span>
|
|
89
|
+
</li>
|
|
90
|
+
<% elsif response.finished? -%>
|
|
63
91
|
<li class="covered">
|
|
64
92
|
<span>✅</span>
|
|
65
93
|
<span class="status"><%= h response.status %></span>
|
|
@@ -8,6 +8,8 @@ module OpenapiFirst
|
|
|
8
8
|
module Coverage
|
|
9
9
|
# Writes a self-contained HTML coverage report to a file.
|
|
10
10
|
class HtmlReporter
|
|
11
|
+
include SkippedSummary
|
|
12
|
+
|
|
11
13
|
def initialize(output: 'coverage/openapi_coverage.html', verbose: false, logger: Test.logger)
|
|
12
14
|
@output = output
|
|
13
15
|
@verbose = verbose
|
|
@@ -18,9 +20,17 @@ module OpenapiFirst
|
|
|
18
20
|
html = TEMPLATE.result(Context.new(coverage_result, @verbose).get_binding)
|
|
19
21
|
FileUtils.mkdir_p(File.dirname(@output))
|
|
20
22
|
File.write(@output, html)
|
|
21
|
-
|
|
23
|
+
coverage_result.plans.each do |plan|
|
|
24
|
+
next if plan.coverage >= 100
|
|
25
|
+
|
|
26
|
+
logger.info "API validation coverage for #{plan.api_identifier}: #{plan.coverage.round(4)}%"
|
|
27
|
+
end
|
|
28
|
+
log_skipped_summary(coverage_result)
|
|
29
|
+
logger.info "API coverage report written to #{@output}"
|
|
22
30
|
end
|
|
23
31
|
|
|
32
|
+
private attr_reader :logger
|
|
33
|
+
|
|
24
34
|
TEMPLATE_PATH = File.join(__dir__, 'html_reporter.html.erb')
|
|
25
35
|
TEMPLATE = ERB.new(File.read(TEMPLATE_PATH), trim_mode: '-')
|
|
26
36
|
TEMPLATE.filename = TEMPLATE_PATH
|
|
@@ -14,14 +14,13 @@ module OpenapiFirst
|
|
|
14
14
|
|
|
15
15
|
def self.for(oad, skip_response: nil, skip_route: nil)
|
|
16
16
|
plan = new(definition_key: oad.key, filepath: oad.filepath, title: oad.title)
|
|
17
|
-
routes
|
|
18
|
-
routes = routes.reject { |route| skip_route[route.path, route.request_method] } if skip_route
|
|
19
|
-
routes.each do |route|
|
|
20
|
-
responses = skip_response ? route.responses.reject(&skip_response) : route.responses
|
|
17
|
+
oad.routes.each do |route|
|
|
21
18
|
plan.add_route request_method: route.request_method,
|
|
22
19
|
path: route.path,
|
|
23
20
|
requests: route.requests,
|
|
24
|
-
responses:
|
|
21
|
+
responses: route.responses,
|
|
22
|
+
skipped: skip_route ? skip_route[route.path, route.request_method] : false,
|
|
23
|
+
skip_response:
|
|
25
24
|
end
|
|
26
25
|
plan
|
|
27
26
|
end
|
|
@@ -63,15 +62,36 @@ module OpenapiFirst
|
|
|
63
62
|
index.values
|
|
64
63
|
end
|
|
65
64
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
def skipped_requests
|
|
66
|
+
routes.flat_map(&:skipped_requests)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def skipped_responses
|
|
70
|
+
routes.flat_map(&:skipped_responses)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def skipped_requests_count
|
|
74
|
+
skipped_requests.count
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def skipped_responses_count
|
|
78
|
+
skipped_responses.count
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def add_route(request_method:, path:, requests:, responses:, skipped: false, skip_response: nil)
|
|
82
|
+
request_tasks = requests.to_a.map { |request| add_task RequestTask.new(request, skipped:) }
|
|
70
83
|
response_tasks = responses.to_a.map do |response|
|
|
71
|
-
|
|
84
|
+
add_task ResponseTask.new(response, skipped: skipped || (skip_response ? skip_response[response] : false))
|
|
72
85
|
end
|
|
73
86
|
@routes << RouteTask.new(path:, request_method:, requests: request_tasks, responses: response_tasks)
|
|
74
87
|
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def add_task(task)
|
|
92
|
+
index[task.key] = task unless task.skipped?
|
|
93
|
+
task
|
|
94
|
+
end
|
|
75
95
|
end
|
|
76
96
|
end
|
|
77
97
|
end
|