haveapi 0.31.1 → 0.32.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/.gitignore +2 -0
- data/benchmarks/documentation.rb +29 -6
- data/benchmarks/endpoints.rb +153 -24
- data/benchmarks/spec/client_examples_spec.rb +109 -37
- data/doc/client-examples.md +218 -45
- data/doc/documentation-oauth.md +157 -0
- data/doc/documentation-ui.md +192 -0
- data/doc/index.md +3 -6
- data/doc/performance.md +39 -12
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/authentication/chain.rb +19 -0
- data/lib/haveapi/authentication/oauth2/provider.rb +7 -1
- data/lib/haveapi/client_example.rb +143 -15
- data/lib/haveapi/client_examples/curl.rb +30 -20
- data/lib/haveapi/client_examples/dynamic_helpers.rb +1 -1
- data/lib/haveapi/client_examples/go_client.rb +9 -2
- data/lib/haveapi/client_examples/http.rb +9 -4
- data/lib/haveapi/client_examples/js_async_client.rb +7 -0
- data/lib/haveapi/client_examples/js_client.rb +47 -36
- data/lib/haveapi/client_examples/php_client.rb +161 -75
- data/lib/haveapi/client_examples/python_async_client.rb +3 -0
- data/lib/haveapi/client_examples/python_client.rb +61 -47
- data/lib/haveapi/client_examples/ruby_cli.rb +27 -18
- data/lib/haveapi/client_examples/ruby_client.rb +80 -31
- data/lib/haveapi/client_examples/rust_client.rb +20 -9
- data/lib/haveapi/client_examples/ts_async_client.rb +7 -0
- data/lib/haveapi/client_examples/ts_client.rb +3 -0
- data/lib/haveapi/client_examples/ts_generated_client.rb +10 -2
- data/lib/haveapi/documentation_context.rb +7 -2
- data/lib/haveapi/documentation_helpers.rb +223 -0
- data/lib/haveapi/documentation_oauth.rb +399 -0
- data/lib/haveapi/documentation_search.rb +225 -0
- data/lib/haveapi/documentation_search_index.rb +79 -0
- data/lib/haveapi/documentation_validators.rb +179 -0
- data/lib/haveapi/public/css/documentation.css +343 -0
- data/lib/haveapi/public/js/documentation.js +992 -0
- data/lib/haveapi/server.rb +144 -86
- data/lib/haveapi/shared_documentation_helpers.rb +54 -0
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/doc_layout.erb +16 -0
- data/lib/haveapi/views/doc_sidebars/create-client.erb +3 -3
- data/lib/haveapi/views/doc_sidebars/json-schema.erb +1 -1
- data/lib/haveapi/views/doc_sidebars/protocol.erb +5 -5
- data/lib/haveapi/views/documentation/search_form.erb +10 -0
- data/lib/haveapi/views/documentation/search_index.erb +1 -0
- data/lib/haveapi/views/documentation/search_results.erb +20 -0
- data/lib/haveapi/views/index.erb +2 -4
- data/lib/haveapi/views/main_layout.erb +12 -6
- data/lib/haveapi/views/shared_documentation.erb +12 -0
- data/lib/haveapi/views/shared_sidebar.erb +25 -0
- data/lib/haveapi/views/version_page/auth_body.erb +12 -33
- data/lib/haveapi/views/version_page/client_overview.erb +40 -0
- data/lib/haveapi/views/version_page/example_panels.erb +18 -0
- data/lib/haveapi/views/version_page/resource_body.erb +91 -132
- data/lib/haveapi/views/version_page/resource_directory.erb +14 -0
- data/lib/haveapi/views/version_page/usage.erb +7 -0
- data/lib/haveapi/views/version_page.erb +36 -117
- data/lib/haveapi/views/version_sidebar/auth_nav.erb +12 -16
- data/lib/haveapi/views/version_sidebar/auth_resource_nav.erb +19 -0
- data/lib/haveapi/views/version_sidebar/authentication_control.erb +26 -0
- data/lib/haveapi/views/version_sidebar/authentication_status.erb +3 -0
- data/lib/haveapi/views/version_sidebar/brand.erb +31 -0
- data/lib/haveapi/views/version_sidebar/resource_nav.erb +11 -20
- data/lib/haveapi/views/version_sidebar.erb +59 -48
- data/spec/authentication/basic_spec.rb +10 -10
- data/spec/authentication/oauth2_chain_spec.rb +72 -0
- data/spec/authentication/oauth2_token_validation_spec.rb +38 -0
- data/spec/client_examples/assets/documentation_test.cjs +817 -0
- data/spec/client_examples/context_performance_spec.rb +5 -4
- data/spec/client_examples/context_spec.rb +23 -2
- data/spec/client_examples/detail_spec.rb +175 -0
- data/spec/client_examples/dynamic_acceptance.md +59 -26
- data/spec/client_examples/dynamic_acceptance.rb +164 -44
- data/spec/client_examples/dynamic_acceptance_spec.rb +140 -23
- data/spec/client_examples/dynamic_fixture.rb +4 -3
- data/spec/client_examples/dynamic_js_spec.rb +20 -11
- data/spec/client_examples/dynamic_metadata_spec.rb +69 -0
- data/spec/client_examples/dynamic_mounted_paths_spec.rb +9 -7
- data/spec/client_examples/dynamic_python_spec.rb +62 -11
- data/spec/client_examples/go_spec.rb +14 -1
- data/spec/client_examples/presentation_spec.rb +247 -0
- data/spec/client_examples/profile_mount_spec.rb +40 -13
- data/spec/client_examples/rust_client_spec.rb +35 -1
- data/spec/client_examples/shell_spec.rb +268 -0
- data/spec/client_examples/syntax_spec.rb +192 -0
- data/spec/client_examples/ts_generated_client_spec.rb +15 -2
- data/spec/client_examples/typescript_profiles_spec.rb +1 -1
- data/spec/documentation/auth_filtering_spec.rb +9 -4
- data/spec/documentation/canonical_routes_spec.rb +89 -0
- data/spec/documentation/client_examples_spec.rb +39 -5
- data/spec/documentation/client_overview_spec.rb +145 -0
- data/spec/documentation/current_user_html_escaping_spec.rb +7 -2
- data/spec/documentation/example_variants_spec.rb +93 -0
- data/spec/documentation/fixtures/search_index.json +476 -0
- data/spec/documentation/identity_and_guides_spec.rb +130 -0
- data/spec/documentation/identity_context_spec.rb +180 -0
- data/spec/documentation/navigation_spec.rb +296 -0
- data/spec/documentation/oauth_fixture_spec.rb +39 -0
- data/spec/documentation/oauth_spec.rb +912 -0
- data/spec/documentation/oauth_transport_spec.rb +45 -0
- data/spec/documentation/performance_spec.rb +47 -6
- data/spec/documentation/reference_headings_spec.rb +190 -0
- data/spec/documentation/search_index_routes_spec.rb +202 -0
- data/spec/documentation/search_index_spec.rb +85 -0
- data/spec/documentation/search_routes_spec.rb +199 -0
- data/spec/documentation/search_spec.rb +134 -0
- data/spec/documentation/shared_pages_spec.rb +240 -0
- data/spec/documentation/validators_spec.rb +123 -0
- data/spec/documentation_spec.rb +3 -3
- data/spec/packaging_spec.rb +21 -2
- data/spec/performance/documentation_identity_fixture_spec.rb +47 -0
- data/spec/performance/documentation_shapes_spec.rb +97 -0
- data/spec/performance/json_activation_spec.rb +78 -1
- data/spec/performance/release_lineage_spec.rb +4 -3
- data/spec/performance/runner_spec.rb +1 -0
- data/test_support/all_client_performance_fixture.rb +6 -10
- data/test_support/documentation_oauth_fixture.rb +54 -0
- data/test_support/documentation_performance_fixture.rb +19 -1
- data/test_support/dynamic_documentation_api.rb +35 -10
- data/test_support/dynamic_python_login_spec.rb +3 -11
- metadata +52 -3
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'haveapi/documentation_validators'
|
|
3
|
+
|
|
4
|
+
describe HaveAPI::DocumentationValidators do
|
|
5
|
+
def render(name, options, type: nil)
|
|
6
|
+
described_class.render({ name => options }, type:)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it 'keeps every rule in a semantic subtable with scoped row headers' do
|
|
10
|
+
html = described_class.render({ present: { empty: false }, include: { values: [false, 0, ''] } })
|
|
11
|
+
expect(html).to include('<div class="docs-validators"><table class="docs-validator-table"><caption>Validation rules</caption>',
|
|
12
|
+
'<th scope="col">Rule</th><th scope="col">Details</th>',
|
|
13
|
+
'<tr class="docs-validator"><th scope="row">Present</th><td>',
|
|
14
|
+
'<tr class="docs-validator"><th scope="row">Include</th><td>')
|
|
15
|
+
expect(html.scan('<table').length).to eq(1)
|
|
16
|
+
expect(html.scan('<tr class="docs-validator">').length).to eq(2)
|
|
17
|
+
expect(html).not_to include('<h5>')
|
|
18
|
+
expect(described_class.render({})).to eq('')
|
|
19
|
+
expect(described_class.render(nil)).to eq('')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'explains required presence and its distinct empty-value modes' do
|
|
23
|
+
strict = render(:present, { empty: false, message: 'Required value' })
|
|
24
|
+
expect(strict).to include('Must be supplied and cannot be null.', 'Must not be empty;', 'non-whitespace', 'Required value')
|
|
25
|
+
expect(render(:present, { empty: true })).to include('Empty values are allowed.')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'renders inclusion choices as typed machine values with separate localized labels' do
|
|
29
|
+
values = { 'active' => 'Aktivní', 0 => 'Zero', false => 'Disabled', nil => 'Unset' }
|
|
30
|
+
html = render(:include, { values:, message: 'Select one' })
|
|
31
|
+
expect(html).to include('Allowed values:', '<code>active</code>: Aktivní', '<code>0</code>: Zero',
|
|
32
|
+
'<code>false</code>: Disabled', '<code>null</code>: Unset', 'Select one')
|
|
33
|
+
expect(html).not_to include('=>', '{"active"')
|
|
34
|
+
expect(html.index('Aktivní')).to be < html.index('Zero')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'lists array alternatives, reserved exclusions and empty sets honestly' do
|
|
38
|
+
expect(render(:include, { values: ['', false, 0] })).to include('<code></code>', '<code>false</code>', '<code>0</code>')
|
|
39
|
+
expect(render(:exclude, { values: ['root'] })).to include('Disallowed values:', '<code>root</code>')
|
|
40
|
+
expect(render(:exclude, { values: ['root'] })).not_to include('Allowed values:')
|
|
41
|
+
expect(render(:include, { values: [] })).to include('The set is empty.')
|
|
42
|
+
expect(render(:include, { values: {} })).to include('The set is empty.')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
%i[include exclude].each do |rule|
|
|
46
|
+
it "keeps #{rule} string contents including quotes and Unicode without adding presentation quotes" do
|
|
47
|
+
html = render(rule, { values: ['<tag>&"Žluťoučký"', "'quoted'", '', 'false', '0', 'null'] })
|
|
48
|
+
expect(html).to include('<ul class="docs-validator-values">', '<li><code><tag>&"Žluťoučký"</code></li>',
|
|
49
|
+
'<li><code>'quoted'</code></li>', '<li><code></code></li>',
|
|
50
|
+
'<li><code>false</code></li>', '<li><code>0</code></li>', '<li><code>null</code></li>')
|
|
51
|
+
expect(html).not_to include('<tag>', '&lt;', 'The set is empty.', '<code>"')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "retains #{rule} nonstring types and nested JSON quoting" do
|
|
55
|
+
html = render(rule, { values: [true, false, 0, 1.5, nil, ['a"b', ''], { 'nested' => ['<tag>', nil] }] })
|
|
56
|
+
expect(html).to include('<li><code>true</code></li>', '<li><code>false</code></li>', '<li><code>0</code></li>',
|
|
57
|
+
'<li><code>1.5</code></li>', '<li><code>null</code></li>',
|
|
58
|
+
'<li><code>["a\"b",""]</code></li>',
|
|
59
|
+
'<li><code>{"nested":["<tag>",null]}</code></li>')
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'keeps inclusion hash labels and empty string keys separate from empty sets' do
|
|
64
|
+
html = render(:include, { values: { '' => 'Empty label <&>', '"quoted"' => false, 'žluťoučký' => ['nested'] } })
|
|
65
|
+
expect(html).to include('<li><code></code>: Empty label <&></li>',
|
|
66
|
+
'<li><code>"quoted"</code>: <code>false</code></li>',
|
|
67
|
+
'<li><code>žluťoučký</code>: <code>["nested"]</code></li>')
|
|
68
|
+
expect(html).not_to include('The set is empty.', 'Empty label <&>', '&lt;')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'keeps acceptance and confirmation values distinct without coercion' do
|
|
72
|
+
[nil, false, 0, '0'].each do |value|
|
|
73
|
+
expect(render(:accept, { value: })).to include("Must equal <code>#{CGI.escapeHTML(JSON.generate(value))}</code>")
|
|
74
|
+
end
|
|
75
|
+
expect(render(:confirm, { parameter: :password, equal: true })).to include('Must equal parameter <code>password</code>')
|
|
76
|
+
expect(render(:confirm, { parameter: 'password', equal: false })).to include('Must differ from parameter <code>password</code>')
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'explains inclusive bounds, step origin, modulo and parity separately' do
|
|
80
|
+
html = render(:number, { min: 3, max: 10, step: 2, mod: 5, odd: true })
|
|
81
|
+
expect(html).to include('Minimum: <code>3</code> (inclusive)', 'Maximum: <code>10</code> (inclusive)',
|
|
82
|
+
'Step: <code>2</code>, measured from <code>3</code>',
|
|
83
|
+
'multiple of <code>5</code>, measured from zero', 'Must be odd.', 'Must be a number after parameter type conversion.')
|
|
84
|
+
expect(render(:number, { step: 2, even: true, odd: false })).to include('measured from <code>0</code>', 'Must be even.', 'Odd-only restriction: disabled.')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'describes character length for text and neutral length for unknown types' do
|
|
88
|
+
expect(render(:length, { min: 0, max: 12 }, type: 'String')).to include('Minimum length: <code>0</code> characters', 'Maximum length: <code>12</code> characters')
|
|
89
|
+
expect(render(:length, { equals: 2 }, type: 'Custom')).to include('Length must equal <code>2</code>.')
|
|
90
|
+
expect(render(:length, { equals: 2 }, type: 'Array')).to include('<code>2</code> items')
|
|
91
|
+
expect(render(:length, { equals: 2 }, type: 'Text')).not_to include('bytes')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'shows regex source, negation and descriptions without inventing flags' do
|
|
95
|
+
html = render(:format, { rx: '^[a-z]+$', match: false, description: 'Reserved spelling', message: 'Invalid spelling' })
|
|
96
|
+
expect(html).to include('Must not match this regular expression:', '<code>^[a-z]+$</code>', 'Reserved spelling', 'Validation error message:', 'Invalid spelling')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'labels application-specific rules without claiming generic enforcement' do
|
|
100
|
+
expect(render(:custom, 'Verified by the application')).to include('Application-specific rule: Verified by the application')
|
|
101
|
+
expect(render(:custom, 'Verified by the application')).not_to include('Must pass', 'automatically')
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'preserves unknown fields and unexpected shapes with a labelled lossless fallback' do
|
|
105
|
+
html = render('X-Extension', { 'nested' => { 'zero' => 0, 'false' => false, 'empty' => '', 'null' => nil } })
|
|
106
|
+
expect(html).to include('X-Extension', 'JSON:', '"zero": 0', '"false": false', '"empty": ""', '"null": null')
|
|
107
|
+
expect(render(:include, { values: 'unexpected', message: 'Kept' })).to include('Supplied validator metadata:', 'unexpected', 'Kept')
|
|
108
|
+
expect(render(:number, { min: 'not numeric', message: 'Kept' })).to include('Supplied validator metadata:', 'not numeric', 'Kept')
|
|
109
|
+
expect(render(:include, { values: [], extra: false })).to include('Additional details', CGI.escapeHTML({ extra: false }.inspect), 'Ruby value:')
|
|
110
|
+
expect(render(:include, { values: [:symbol] })).to include('Ruby value:', ':symbol')
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'escapes source labels messages and structured options once without mutation' do
|
|
114
|
+
payload = '<script>&"'
|
|
115
|
+
options = { values: { payload => payload }, message: payload, extra: { 'nested' => payload } }
|
|
116
|
+
before = Marshal.dump(options)
|
|
117
|
+
html = render(:include, options)
|
|
118
|
+
expect(html).not_to include('<script>', '&lt;')
|
|
119
|
+
expect(html).to include('<script>&', 'Validation error message:', 'Additional details')
|
|
120
|
+
expect(render(payload, { 'extra' => payload })).to include('<th scope="row"><script>&"</th>')
|
|
121
|
+
expect(Marshal.dump(options)).to eq(before)
|
|
122
|
+
end
|
|
123
|
+
end
|
data/spec/documentation_spec.rb
CHANGED
|
@@ -174,7 +174,7 @@ describe 'Documentation' do
|
|
|
174
174
|
expect(last_response.headers['Content-Type']).to include('text/html')
|
|
175
175
|
|
|
176
176
|
# servers/ruby/doc/index.md contains this heading
|
|
177
|
-
expect(last_response.body).to include('HaveAPI
|
|
177
|
+
expect(last_response.body).to include('HaveAPI internals')
|
|
178
178
|
end
|
|
179
179
|
|
|
180
180
|
it 'has online index doc page without a sidebar template' do
|
|
@@ -182,11 +182,11 @@ describe 'Documentation' do
|
|
|
182
182
|
|
|
183
183
|
expect(last_response.status).to eq(200)
|
|
184
184
|
expect(last_response.headers['Content-Type']).to include('text/html')
|
|
185
|
-
expect(last_response.body).to include('HaveAPI
|
|
185
|
+
expect(last_response.body).to include('HaveAPI internals')
|
|
186
186
|
end
|
|
187
187
|
|
|
188
188
|
it 'has online doc for every version' do
|
|
189
|
-
get '/v1
|
|
189
|
+
get '/v1/?resource=user'
|
|
190
190
|
expect(last_response.status).to eq(200)
|
|
191
191
|
expect(last_response.headers['Content-Type']).to include('text/html')
|
|
192
192
|
expect(last_response.body).to include('verified by MFA backend')
|
data/spec/packaging_spec.rb
CHANGED
|
@@ -55,6 +55,8 @@ describe HaveAPI::Server do
|
|
|
55
55
|
end
|
|
56
56
|
%w[
|
|
57
57
|
lib/haveapi/documentation_context.rb lib/haveapi/documentation_clients.rb
|
|
58
|
+
lib/haveapi/shared_documentation_helpers.rb
|
|
59
|
+
lib/haveapi/views/shared_sidebar.erb lib/haveapi/views/shared_documentation.erb
|
|
58
60
|
lib/haveapi/views/version_page/client_result.erb
|
|
59
61
|
lib/haveapi/public/js/highlight.pack.js lib/haveapi/public/js/highlight-LICENSE
|
|
60
62
|
].each do |name|
|
|
@@ -83,9 +85,9 @@ describe HaveAPI::Server do
|
|
|
83
85
|
server.use_version(1)
|
|
84
86
|
server.mount('/')
|
|
85
87
|
request = Rack::MockRequest.new(server.app)
|
|
86
|
-
responses = %w[/doc/json-schema /doc/protocol.md /doc/create-client.md /doc/client-examples.md /js/highlight-LICENSE].to_h do |path|
|
|
88
|
+
responses = %w[/ /doc /doc/index /doc/index.md /doc/readme /doc/json-schema /doc/protocol.md /doc/create-client.md /doc/client-examples.md /js/highlight-LICENSE].to_h do |path|
|
|
87
89
|
response = request.get(path, 'HTTP_HOST' => 'localhost')
|
|
88
|
-
[path, { status: response.status, type: response.content_type, body: response.body }]
|
|
90
|
+
[path, { status: response.status, type: response.content_type, cache_control: response['Cache-Control'], body: response.body }]
|
|
89
91
|
end
|
|
90
92
|
puts JSON.generate({
|
|
91
93
|
framework: HaveAPI::Server.instance_method(:mount).source_location.first,
|
|
@@ -117,6 +119,11 @@ describe HaveAPI::Server do
|
|
|
117
119
|
expect(File.realpath(result.fetch('framework'))).to eq(File.join(dir, 'lib/haveapi/server.rb'))
|
|
118
120
|
|
|
119
121
|
{
|
|
122
|
+
'/' => 'API versions',
|
|
123
|
+
'/doc' => 'Server-side implementation',
|
|
124
|
+
'/doc/index' => 'Server-side implementation',
|
|
125
|
+
'/doc/index.md' => 'Server-side implementation',
|
|
126
|
+
'/doc/readme' => 'Server-side implementation',
|
|
120
127
|
'/doc/json-schema' => 'Envelope',
|
|
121
128
|
'/doc/protocol.md' => 'Protocol definition',
|
|
122
129
|
'/doc/create-client.md' => 'Client definition',
|
|
@@ -127,6 +134,18 @@ describe HaveAPI::Server do
|
|
|
127
134
|
expect(response.fetch('status')).to eq(200), "Packaged route failed: #{path}"
|
|
128
135
|
expect(response.fetch('type')).to include('text/html') unless path == '/js/highlight-LICENSE'
|
|
129
136
|
expect(response.fetch('body')).to include(heading)
|
|
137
|
+
next if path == '/js/highlight-LICENSE'
|
|
138
|
+
|
|
139
|
+
expect(response.fetch('cache_control')).to eq('private, no-store')
|
|
140
|
+
expect(response.fetch('body')).to include('data-docs', '/js/documentation.js', '/css/documentation.css')
|
|
141
|
+
expect(response.fetch('body')).not_to include('/js/main.js', '/css/bootstrap')
|
|
142
|
+
end
|
|
143
|
+
%w[/doc /doc/index /doc/index.md /doc/readme].each do |path|
|
|
144
|
+
body = result.fetch('responses').fetch(path).fetch('body')
|
|
145
|
+
expect(body).to include('<title>HaveAPI internals · ', 'Server framework features',
|
|
146
|
+
'<pre><code class="ruby">', 'class User < ActiveRecord::Base')
|
|
147
|
+
menu = body.match(%r{<nav aria-label="Shared documentation">(.*?)</nav>}m)[1]
|
|
148
|
+
expect(menu).not_to include('href="/doc/readme"')
|
|
130
149
|
end
|
|
131
150
|
end
|
|
132
151
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require_relative '../../benchmarks/endpoints'
|
|
3
|
+
require_relative '../../test_support/documentation_performance_fixture'
|
|
4
|
+
|
|
5
|
+
describe HaveAPI::Performance::DocumentationScenario, '#call' do
|
|
6
|
+
def scenario(id, user = 'admin')
|
|
7
|
+
HaveAPI::Performance::DocumentationScenario.new(id:, dimensions: { size: 1, user: })
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'validates a real cookie token on every HTML page with stable role-specific bodies and counters' do
|
|
11
|
+
page = scenario('docs.html_resources')
|
|
12
|
+
page.reset!(iterations: 3)
|
|
13
|
+
responses = 3.times.map { page.call }
|
|
14
|
+
responses.each_with_index { |response, index| expect(page.validate!(response, index:)[:http_status]).to eq(200) }
|
|
15
|
+
expect(responses.map(&:body).uniq.length).to eq(1)
|
|
16
|
+
expect(responses.first.body).to include('<span class="docs-identity">admin</span>', 'name="csrf"')
|
|
17
|
+
fixture = page.instance_variable_get(:@server).fixture_documentation_oauth
|
|
18
|
+
expect(fixture.lookups).to eq(Array.new(3) { [1, 'documentation-admin'] })
|
|
19
|
+
expect(page.documentation_identity).to eq(mode: 'documentation_oauth_cookie', role: 'admin')
|
|
20
|
+
expect(page.counters[:operations]).to eq(Array.new(3) { { queries: 0, authorizations: 2, executions: 0 } })
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'keeps member role filtering and independent CSRF without accepting an admin overview' do
|
|
24
|
+
admin = scenario('docs.html_resources')
|
|
25
|
+
member = scenario('docs.html_resources', 'member')
|
|
26
|
+
[admin, member].each { |page| page.reset!(iterations: 1) }
|
|
27
|
+
administrative = admin.call
|
|
28
|
+
restricted = member.call
|
|
29
|
+
expect(member.validate!(restricted, index: 0)[:http_status]).to eq(200)
|
|
30
|
+
expect(restricted.body).to include('<span class="docs-identity">member</span>')
|
|
31
|
+
expect(restricted.body).not_to include('root-resource0-admin_only')
|
|
32
|
+
expect { member.validate_body!(administrative) }.to raise_error(HaveAPI::Performance::ContractError, /authorization/)
|
|
33
|
+
nonce = ->(response) { response.body.match(/name="csrf" value="([^"]+)"/)[1] }
|
|
34
|
+
expect(nonce.call(restricted)).not_to eq(nonce.call(administrative))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'retains ordinary Basic setup and counter behavior for documentation OPTIONS' do
|
|
38
|
+
page = scenario('docs.options')
|
|
39
|
+
page.reset!(iterations: 1)
|
|
40
|
+
response = page.call
|
|
41
|
+
expect(page.validate!(response, index: 0)[:http_status]).to eq(200)
|
|
42
|
+
expect(page.documentation_identity).to eq(mode: 'basic', role: 'admin')
|
|
43
|
+
expect(page.instance_variable_get(:@headers)).to include('HTTP_AUTHORIZATION')
|
|
44
|
+
expect(page.instance_variable_get(:@headers)).not_to include('HTTP_COOKIE')
|
|
45
|
+
expect(page.instance_variable_get(:@server)).not_to respond_to(:fixture_documentation_oauth)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require_relative '../../benchmarks/endpoints'
|
|
3
|
+
require_relative '../../benchmarks/documentation'
|
|
4
|
+
require_relative '../../test_support/documentation_performance_fixture'
|
|
5
|
+
|
|
6
|
+
describe HaveAPI::Performance::DocumentationScenario do
|
|
7
|
+
let(:scenario) { described_class.new(id: 'docs.html_resources', dimensions: { size: 2, user: 'admin' }) }
|
|
8
|
+
let(:overview) do
|
|
9
|
+
scenario.reset!(iterations: 1)
|
|
10
|
+
scenario.call
|
|
11
|
+
end
|
|
12
|
+
let(:error) { HaveAPI::Performance::ContractError }
|
|
13
|
+
|
|
14
|
+
def response_with(body)
|
|
15
|
+
Rack::MockResponse.new(200, { 'content-type' => 'text/html' }, body)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def replace_map(body)
|
|
19
|
+
body.sub(%r{(<script\b[^>]*data-legacy-anchors[^>]*>)(.*?)(</script>)}m) do
|
|
20
|
+
before, json, after = Regexp.last_match.captures
|
|
21
|
+
values = JSON.parse(json)
|
|
22
|
+
yield(values)
|
|
23
|
+
"#{before}#{JSON.generate(values)}#{after}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'accepts the actual overview and preserves authorization counters and raw identity' do
|
|
28
|
+
identity = scenario.validate!(overview, index: 0)
|
|
29
|
+
expect(identity[:page]).to include(shape: 'overview', actual_examples: 0, resources: 2, actions: 3)
|
|
30
|
+
expect(identity[:body_sha256]).to eq(Digest::SHA256.hexdigest(overview.body))
|
|
31
|
+
expect(overview.body).not_to include('PUBLIC_SAMPLE_', 'ADMIN_ONLY_REQUEST')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'accepts the strict legacy full-page contract without accepting an empty third shape' do
|
|
35
|
+
legacy = response_with('init-python-sync init-examples PUBLIC_SAMPLE_0 ADMIN_ONLY_REQUEST')
|
|
36
|
+
expect(scenario.validate_body!(legacy)[:page]).to include(shape: 'monolithic')
|
|
37
|
+
expect { scenario.validate_body!(response_with('<h1>Empty</h1>')) }.to raise_error(error, /panels/)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'requires directory navigation, canonical targets and real initialization code' do
|
|
41
|
+
bodies = [
|
|
42
|
+
overview.body.gsub('data-resource-directory-link', 'data-missing-directory-link'),
|
|
43
|
+
overview.body.sub('href="/benchmark/api/v1/?page=resources"', 'href="https://foreign.example/"'),
|
|
44
|
+
overview.body.sub('id="init-python-sync"', 'id="missing-python-sync"'),
|
|
45
|
+
overview.body.sub('id="init-python-sync-thorough"', 'id="missing-python-thorough"'),
|
|
46
|
+
overview.body.sub('id="init-javascript-async"', 'id="missing-javascript-async"')
|
|
47
|
+
]
|
|
48
|
+
bodies.each do |body|
|
|
49
|
+
expect { scenario.validate_body!(response_with(body)) }.to raise_error(error, /navigation|initialization/)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'rejects duplicate, malformed, wrong and mixed overview representations' do
|
|
54
|
+
bodies = [
|
|
55
|
+
overview.body.sub('data-page="overview"', 'data-page="resource"'),
|
|
56
|
+
overview.body.sub('data-docs data-page=', 'data-docs data-docs data-page='),
|
|
57
|
+
"#{overview.body}<div data-docs data-page=\"overview\"></div>",
|
|
58
|
+
overview.body.sub('data-page="overview"', 'data-page="overview" data-page="resource"'),
|
|
59
|
+
"#{overview.body}<div id=\"example-resource0-action0-0-examples\">PUBLIC_SAMPLE_0</div>"
|
|
60
|
+
]
|
|
61
|
+
bodies.each { |body| expect { scenario.validate_body!(response_with(body)) }.to raise_error(error, /overview/) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'rejects missing, wrong or foreign legacy destinations and wrong authorization roles' do
|
|
65
|
+
maps = [
|
|
66
|
+
->(values) { values.delete('root-resource1-action0') },
|
|
67
|
+
->(values) { values['root-resource1'] = '/benchmark/api/v1/?resource=resource0' },
|
|
68
|
+
->(values) { values['example-resource0-action0-'] = 'https://foreign.example/' },
|
|
69
|
+
->(values) { values.delete('root-resource0-admin_only') },
|
|
70
|
+
->(values) { values['auth-basic'] = 'https://foreign.example/' }
|
|
71
|
+
]
|
|
72
|
+
maps.each do |mutate|
|
|
73
|
+
body = replace_map(overview.body, &mutate)
|
|
74
|
+
expect { scenario.validate_body!(response_with(body)) }.to raise_error(error, /authorization|targets/)
|
|
75
|
+
end
|
|
76
|
+
member = described_class.new(id: 'docs.html_resources', dimensions: { size: 2, user: 'member' })
|
|
77
|
+
expect { member.validate_body!(overview) }.to raise_error(error, /authorization/)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'rejects malformed legacy JSON and private parameter or sample data in either role' do
|
|
81
|
+
body = overview.body.sub(%r{(<script[^>]*data-legacy-anchors[^>]*>).*?(</script>)}m, '\1[\2')
|
|
82
|
+
expect { scenario.validate_body!(response_with(body)) }.to raise_error(error, /map/)
|
|
83
|
+
%w[PUBLIC_SAMPLE_0 ADMIN_ONLY_DEFAULT ADMIN_ONLY_REQUEST ADMIN_ONLY_RESPONSE admin_field].each do |marker|
|
|
84
|
+
expect { scenario.validate_body!(response_with("#{overview.body}<p>#{marker}</p>")) }.to raise_error(error, /action data/)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'retains the optional benchmark root HTML lane for both explicit representations' do
|
|
89
|
+
benchmark = HaveAPI::DocumentationBenchmark.new([])
|
|
90
|
+
dimensions = { resource_count: 2 }
|
|
91
|
+
expect(benchmark.send(:html_output, overview, dimensions)[:status]).to eq(200)
|
|
92
|
+
legacy = response_with('init-examples PUBLIC_SAMPLE_0')
|
|
93
|
+
expect(benchmark.send(:html_output, legacy, dimensions)[:status]).to eq(200)
|
|
94
|
+
malformed = response_with(overview.body.sub('data-page="overview"', 'data-page="resource"'))
|
|
95
|
+
expect { benchmark.send(:html_output, malformed, dimensions) }.to raise_error(HaveAPI::DocumentationBenchmark::InvalidOutput)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -29,6 +29,83 @@ describe HaveAPI::Performance do
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
def with_console_versions(pinned: true)
|
|
33
|
+
with_newer_json do |directory, environment|
|
|
34
|
+
(pinned ? %w[0.9.4 99.0.0] : ['99.0.0']).each do |version|
|
|
35
|
+
library = File.join(directory, "gems/io-console-#{version}/lib/io")
|
|
36
|
+
FileUtils.mkdir_p(library)
|
|
37
|
+
File.write(File.join(library, 'console.rb'), "CONSOLE_ACTIVATION_CONTROL = #{version.inspect}\n")
|
|
38
|
+
spec = Gem::Specification.new do |gem|
|
|
39
|
+
gem.name = 'io-console'
|
|
40
|
+
gem.version = version
|
|
41
|
+
gem.summary = 'Owned worker activation control'
|
|
42
|
+
gem.authors = ['Fixture']
|
|
43
|
+
end
|
|
44
|
+
File.write(File.join(directory, "specifications/io-console-#{version}.gemspec"), spec.to_ruby)
|
|
45
|
+
end
|
|
46
|
+
yield directory, environment
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'activates the exact frozen console before source loading despite a newer visible version' do
|
|
51
|
+
with_console_versions do |_directory, environment|
|
|
52
|
+
program = <<~RUBY
|
|
53
|
+
require #{File.join(root, 'servers/ruby/benchmarks/endpoints.rb').inspect}
|
|
54
|
+
visible = Gem::Specification.find_all_by_name('io-console').map { |spec| spec.version.to_s }
|
|
55
|
+
begin
|
|
56
|
+
HaveAPI::Performance::Worker.new('benchmark' => true, 'source_root' => '/missing-worker-source').run
|
|
57
|
+
rescue Errno::ENOENT
|
|
58
|
+
puts JSON.generate(visible: visible, loaded: Gem.loaded_specs.fetch('io-console').version.to_s,
|
|
59
|
+
required: CONSOLE_ACTIVATION_CONTROL, pp_loaded: $LOADED_FEATURES.any? { |path| path.end_with?('/pp.rb') })
|
|
60
|
+
end
|
|
61
|
+
RUBY
|
|
62
|
+
output, errors, status = Open3.capture3(environment, RbConfig.ruby, '-e', program)
|
|
63
|
+
expect(status).to be_success, errors
|
|
64
|
+
result = JSON.parse(output)
|
|
65
|
+
expect(result.fetch('visible')).to include('99.0.0')
|
|
66
|
+
expect(result).to include('loaded' => '0.9.4', 'required' => '0.9.4', 'pp_loaded' => false)
|
|
67
|
+
budgets = JSON.parse(File.read(File.join(root, 'servers/ruby/benchmarks/performance_budgets.json')))
|
|
68
|
+
expect(budgets.fetch('environment').values.map { |value| value.fetch('gems').fetch('io-console').fetch('version') })
|
|
69
|
+
.to all(eq(result.fetch('loaded')))
|
|
70
|
+
lock = Bundler::LockfileParser.new(File.read(File.join(root, 'servers/ruby/benchmarks/Gemfile.lock')))
|
|
71
|
+
expect(lock.specs.find { |spec| spec.name == 'io-console' }.version.to_s).to eq(result.fetch('loaded'))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'leaves ordinary and non-Boolean worker console activation unchanged' do
|
|
76
|
+
with_console_versions(pinned: false) do |_directory, environment|
|
|
77
|
+
program = <<~RUBY
|
|
78
|
+
require #{File.join(root, 'servers/ruby/benchmarks/endpoints.rb').inspect}
|
|
79
|
+
before = Gem.loaded_specs['io-console']&.version&.to_s
|
|
80
|
+
[nil, false, 'true', 1].each do |flag|
|
|
81
|
+
HaveAPI::Performance::Worker.new('benchmark' => flag).send(:activate_benchmark_console)
|
|
82
|
+
end
|
|
83
|
+
puts JSON.generate(before: before, after: Gem.loaded_specs['io-console']&.version&.to_s,
|
|
84
|
+
required: defined?(CONSOLE_ACTIVATION_CONTROL))
|
|
85
|
+
RUBY
|
|
86
|
+
output, errors, status = Open3.capture3(environment, RbConfig.ruby, '-e', program)
|
|
87
|
+
expect(status).to be_success, errors
|
|
88
|
+
expect(JSON.parse(output)).to include('before' => nil, 'after' => nil, 'required' => nil)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'fails frozen workers clearly when the console version is missing or already conflicting' do
|
|
93
|
+
%w[missing conflicting].each do |control|
|
|
94
|
+
with_console_versions(pinned: control == 'conflicting') do |directory, environment|
|
|
95
|
+
program = <<~RUBY
|
|
96
|
+
require #{File.join(root, 'servers/ruby/benchmarks/endpoints.rb').inspect}
|
|
97
|
+
Gem.use_paths(#{directory.inspect}, [#{directory.inspect}])
|
|
98
|
+
gem 'io-console', '= 99.0.0' if #{control.inspect} == 'conflicting'
|
|
99
|
+
HaveAPI::Performance::Worker.new('benchmark' => true).send(:activate_benchmark_console)
|
|
100
|
+
RUBY
|
|
101
|
+
_output, errors, status = Open3.capture3(environment, RbConfig.ruby, '-e', program)
|
|
102
|
+
expect(status).not_to be_success
|
|
103
|
+
expect(errors).to include('io-console', '0.9.4')
|
|
104
|
+
expect(errors).to include(control == 'missing' ? 'Could not find' : 'already activated io-console-99.0.0')
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
32
109
|
it 'loads the native calibrated version in each public entry despite a newer visible gem' do
|
|
33
110
|
with_newer_json do |_directory, environment|
|
|
34
111
|
[comparison, File.join(root, 'utils/performance_ci.rb'), File.join(root, 'servers/ruby/benchmarks/endpoints.rb')].each do |entry|
|
|
@@ -76,7 +153,7 @@ describe HaveAPI::Performance do
|
|
|
76
153
|
_output, errors, status = Open3.capture3(environment, RbConfig.ruby, '-e', program)
|
|
77
154
|
expect(status).not_to be_success
|
|
78
155
|
expect(errors).to include('json', '2.21.2', '3.1.0')
|
|
79
|
-
expect(errors).to
|
|
156
|
+
expect(errors).to include('Could not find')
|
|
80
157
|
end
|
|
81
158
|
end
|
|
82
159
|
|
|
@@ -236,6 +236,7 @@ describe HaveAPI::Performance::Release do
|
|
|
236
236
|
File.write(File.join(harness, 'servers/ruby/Gemfile.lock'), "controlled server lock\n")
|
|
237
237
|
FileUtils.mkdir_p(File.join(harness, 'clients/rust'))
|
|
238
238
|
File.write(File.join(harness, 'clients/rust/Gemfile.lock'), "controlled Rust lock\n")
|
|
239
|
+
stub_const('HaveAPI::RustClient::VERSION', HaveAPI::VERSION) unless defined?(HaveAPI::RustClient::VERSION)
|
|
239
240
|
stub_const('HaveAPI::Performance::ROOT', harness)
|
|
240
241
|
ordinary = JSON.parse(JSON.generate(HaveAPI::Performance::Worker.inputs(root, 'quick')))
|
|
241
242
|
preparation = ordinary.fetch('json_preparation')
|
|
@@ -278,9 +279,9 @@ describe HaveAPI::Performance::Release do
|
|
|
278
279
|
end
|
|
279
280
|
|
|
280
281
|
def served_html
|
|
281
|
-
server = HaveAPI::DocumentationPerformanceFixture.build(resource_count: 1, actions_per_resource: 1)
|
|
282
|
-
response = Rack::MockRequest.new(server.app).get(
|
|
283
|
-
'HTTP_HOST' => 'example.test', '
|
|
282
|
+
server = HaveAPI::DocumentationPerformanceFixture.build(resource_count: 1, actions_per_resource: 1, documentation_oauth: true)
|
|
283
|
+
response = Rack::MockRequest.new(server.app).get("#{server.fixture_documentation_oauth.origin}/benchmark/api/v1/",
|
|
284
|
+
'HTTP_HOST' => 'example.test', 'HTTP_COOKIE' => server.fixture_documentation_cookie('admin'))
|
|
284
285
|
expect(response.status).to eq(200)
|
|
285
286
|
response.body
|
|
286
287
|
end
|
|
@@ -467,6 +467,7 @@ describe HaveAPI::Performance::Worker do
|
|
|
467
467
|
expect(retained['cases'].map { |item| item['iterations'] }).to all(eq(expensive ? 1 : 4))
|
|
468
468
|
expect(retained['cases'].map { |item| item['pilot']['baseline_batch_seconds'] }).to all(eq(expensive ? 2.0 : 1.0))
|
|
469
469
|
requests.each do |label, request|
|
|
470
|
+
expect(request['benchmark']).to be(true)
|
|
470
471
|
next if %w[pilot calibration].include?(label)
|
|
471
472
|
|
|
472
473
|
expect(request['counts'].values).to all(eq(expensive ? 1 : 4))
|
|
@@ -9,13 +9,7 @@ module HaveAPI
|
|
|
9
9
|
query_parameter '_auth_token'
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
def base_url
|
|
14
|
-
'http://example.test'
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def self.build(resources:)
|
|
12
|
+
def self.build(resources:, actions: 2)
|
|
19
13
|
definitions = Module.new
|
|
20
14
|
resources.times do |index|
|
|
21
15
|
resource = Class.new(HaveAPI::Resource)
|
|
@@ -23,7 +17,8 @@ module HaveAPI
|
|
|
23
17
|
resource.resource_name = "Resource#{index}"
|
|
24
18
|
resource.version 1
|
|
25
19
|
resource.route "resources#{index}"
|
|
26
|
-
%w[Public Protected].
|
|
20
|
+
names = %w[Public Protected] + (2...actions).map { |i| "Action#{i}" }
|
|
21
|
+
names.each { |name| add_action(resource, name) }
|
|
27
22
|
DocumentationPerformanceFixture.send(:add_admin_action, resource) if index == 0
|
|
28
23
|
end
|
|
29
24
|
server = HaveAPI::Server.new(definitions)
|
|
@@ -31,12 +26,13 @@ module HaveAPI
|
|
|
31
26
|
server.default_version = 1
|
|
32
27
|
server.auth_chain << DocumentationPerformanceFixture::BasicProvider
|
|
33
28
|
server.auth_chain << Authentication::Token.with_config(TokenConfig)
|
|
34
|
-
|
|
29
|
+
fixture = DocumentationPerformanceFixture.configure_documentation_oauth(server)
|
|
35
30
|
server.documentation_clients(1,
|
|
36
31
|
go: { import_path: 'example.test/performance/client' },
|
|
37
32
|
rust: { crate_name: 'performance_client' },
|
|
38
33
|
typescript: { package_name: 'performance-client' })
|
|
39
34
|
server.mount('/benchmark/api/')
|
|
35
|
+
DocumentationPerformanceFixture.attach_documentation_headers(server, fixture)
|
|
40
36
|
server
|
|
41
37
|
end
|
|
42
38
|
|
|
@@ -44,7 +40,7 @@ module HaveAPI
|
|
|
44
40
|
resource.define_action(name) do
|
|
45
41
|
route name.downcase
|
|
46
42
|
http_method :post
|
|
47
|
-
auth name
|
|
43
|
+
auth name != 'Public'
|
|
48
44
|
input(:hash) do
|
|
49
45
|
string :value
|
|
50
46
|
bool :enabled
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require 'digest'
|
|
2
|
+
require 'rack/response'
|
|
3
|
+
require 'haveapi'
|
|
4
|
+
|
|
5
|
+
# Real documentation cookies and provider lookup for specs and benchmarks.
|
|
6
|
+
class DocumentationOAuthFixture
|
|
7
|
+
CookieHandler = Struct.new(:response)
|
|
8
|
+
|
|
9
|
+
attr_reader :origin, :lookups
|
|
10
|
+
|
|
11
|
+
def self.configure(server, users:, origin: 'https://example.org', mount_path: '/')
|
|
12
|
+
fixture = new(users, origin)
|
|
13
|
+
fixture.configure(server, mount_path)
|
|
14
|
+
fixture
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(users, origin)
|
|
18
|
+
@users = users
|
|
19
|
+
@origin = origin
|
|
20
|
+
@lookups = []
|
|
21
|
+
@expires_at = Time.now.to_f + 3600
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def configure(server, mount_path)
|
|
25
|
+
fixture = self
|
|
26
|
+
config = Class.new(HaveAPI::Authentication::OAuth2::Config) do
|
|
27
|
+
define_method(:base_url) { fixture.origin }
|
|
28
|
+
define_method(:find_user_by_access_token) do |request, token|
|
|
29
|
+
fixture.find_user(@version, request, token)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
server.auth_chain << HaveAPI::Authentication::OAuth2.with_config(config)
|
|
33
|
+
server.documentation_oauth(
|
|
34
|
+
client_id: 'documentation-fixture', client_secret: 'documentation-fixture-client-secret',
|
|
35
|
+
session_secret: 'documentation-fixture-session-secret-' * 3,
|
|
36
|
+
redirect_uri: "#{origin}#{mount_path.chomp('/')}/_documentation/oauth/callback"
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def find_user(version, _request, token)
|
|
41
|
+
@lookups << [version, token]
|
|
42
|
+
@users.fetch(version, {})[token]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def header_for(server, version:, token:)
|
|
46
|
+
@users.fetch(version).fetch(token)
|
|
47
|
+
handler = CookieHandler.new(Rack::Response.new)
|
|
48
|
+
server.documentation_oauth.send(:write_cookie, handler, 'session', {
|
|
49
|
+
'token' => token, 'expires' => @expires_at,
|
|
50
|
+
'csrf' => Digest::SHA256.hexdigest("documentation-fixture-csrf:#{token}")
|
|
51
|
+
})
|
|
52
|
+
Array(handler.response.headers['set-cookie']).fetch(0).split(';', 2).first
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -14,7 +14,7 @@ module HaveAPI
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def self.build(resource_count: 4, actions_per_resource: 2, samples_per_action: 1,
|
|
17
|
-
parameters_per_action: 2, rust: false)
|
|
17
|
+
parameters_per_action: 2, rust: false, documentation_oauth: false)
|
|
18
18
|
dimensions = [resource_count, actions_per_resource, samples_per_action, parameters_per_action]
|
|
19
19
|
unless dimensions.all? { |value| value.is_a?(Integer) && value > 0 }
|
|
20
20
|
raise ArgumentError, 'Fixture dimensions must be positive integers'
|
|
@@ -41,10 +41,28 @@ module HaveAPI
|
|
|
41
41
|
if rust
|
|
42
42
|
server.documentation_clients(1, rust: { crate_name: 'documentation_performance_client' })
|
|
43
43
|
end
|
|
44
|
+
fixture = configure_documentation_oauth(server) if documentation_oauth
|
|
44
45
|
server.mount('/benchmark/api/')
|
|
46
|
+
attach_documentation_headers(server, fixture) if fixture
|
|
45
47
|
server
|
|
46
48
|
end
|
|
47
49
|
|
|
50
|
+
def self.configure_documentation_oauth(server)
|
|
51
|
+
require_relative 'documentation_oauth_fixture'
|
|
52
|
+
users = %w[admin member].to_h { |role| ["documentation-#{role}", User.new(role)] }
|
|
53
|
+
DocumentationOAuthFixture.configure(server, users: { 1 => users }, origin: 'https://example.test', mount_path: '/benchmark/api/')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.attach_documentation_headers(server, fixture)
|
|
57
|
+
# Mint once during setup. Each page still decrypts and validates the real
|
|
58
|
+
# token; deterministic per-role CSRF preserves repeated HTML body proof.
|
|
59
|
+
cookies = %w[admin member].to_h do |role|
|
|
60
|
+
[role, fixture.header_for(server, version: 1, token: "documentation-#{role}")]
|
|
61
|
+
end
|
|
62
|
+
server.define_singleton_method(:fixture_documentation_oauth) { fixture }
|
|
63
|
+
server.define_singleton_method(:fixture_documentation_cookie) { |role| cookies.fetch(role) }
|
|
64
|
+
end
|
|
65
|
+
|
|
48
66
|
def self.add_action(resource, index, samples, parameters)
|
|
49
67
|
resource.define_action("Action#{index}") do
|
|
50
68
|
route "actions#{index}"
|