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
|
@@ -3,37 +3,168 @@ require 'rack/mock'
|
|
|
3
3
|
require_relative 'dynamic_acceptance'
|
|
4
4
|
|
|
5
5
|
RSpec.describe DynamicDocumentationAcceptance do
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
let(:trace) { StringIO.new }
|
|
7
|
+
let(:request) do
|
|
8
|
+
Rack::MockRequest.new(DynamicDocumentationAPI::RequestTrace.new(DynamicDocumentationAPI.build.app, output: trace))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def receipt_snapshot
|
|
12
|
+
JSON.parse(request.get('/__acceptance_receipts').body)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def setup_panel(provider = nil, detail: :simple)
|
|
16
|
+
{ id: 'fixture-panel', group: provider ? "auth-#{provider}-examples" : 'init-examples', detail: }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def json_post(path, payload)
|
|
20
|
+
request.post(path, 'CONTENT_TYPE' => 'application/json', input: JSON.generate(payload))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'extracts the complete actual matrix with eight singleton and28 requested thorough panels' do
|
|
24
|
+
pages = described_class::PAGES.values.map do |query|
|
|
25
|
+
response = request.get("http://example.test/mount/v2/#{query}", 'HTTP_HOST' => 'example.test')
|
|
26
|
+
expect(response.status).to eq(200), response.body
|
|
27
|
+
response.body
|
|
28
|
+
end
|
|
29
|
+
panels = pages.flat_map { |body| described_class.extract(body) }
|
|
30
|
+
expect { described_class.validate_coverage!(panels) }.not_to raise_error
|
|
31
|
+
expect(panels.length).to eq(36)
|
|
32
|
+
expect(panels.count { |panel| panel[:detail] == :simple }).to eq(8)
|
|
33
|
+
expect(panels.count { |panel| panel[:detail] == :thorough }).to eq(28)
|
|
34
|
+
simple = pages.flat_map { |body| described_class.extract(body, detail: :simple) }
|
|
35
|
+
expect(simple.length).to eq(36)
|
|
36
|
+
expect(simple).to all(include(detail: :simple, requested_detail: :simple))
|
|
37
|
+
overlap = panels.map { |panel| panel[:id] } & simple.map { |panel| panel[:id] }
|
|
38
|
+
expect(overlap.length).to eq(8)
|
|
11
39
|
described_class::VARIANTS.each do |variant|
|
|
12
40
|
selected = panels.select { |panel| panel[:variant] == variant }
|
|
13
|
-
expect(selected.map { |panel| panel[:
|
|
14
|
-
expect(selected.count { |panel| panel[:id].start_with?('example-') }).to eq(2)
|
|
41
|
+
expect(selected.map { |panel| panel[:group] }.sort).to eq(described_class::GROUPS.sort)
|
|
15
42
|
expect(selected.last[:source]).to include('http://example.test/mount/')
|
|
16
43
|
expect(selected.last[:source]).not_to include('"')
|
|
44
|
+
expect(selected).to all(include(requested_detail: :thorough))
|
|
17
45
|
end
|
|
46
|
+
expect { described_class.validate_coverage!(panels.drop(1)) }.to raise_error(/Incomplete/)
|
|
47
|
+
expect { described_class.validate_coverage!(panels + [panels.first]) }.to raise_error(/Incomplete/)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'fails malformed missing duplicate or undeclared groups and selected code instead of skipping coverage' do
|
|
51
|
+
html = request.get('/mount/v2/', 'HTTP_HOST' => 'example.test').body
|
|
52
|
+
map = html.match(/data-example-details="([^"]+)"/)[1]
|
|
53
|
+
missing = JSON.parse(CGI.unescapeHTML(map)).except('javascript-callback')
|
|
54
|
+
variants = [
|
|
55
|
+
html.sub(/data-example-details="[^"]+"/, ''),
|
|
56
|
+
html.sub(map, CGI.escapeHTML('{broken')),
|
|
57
|
+
html.sub(map, CGI.escapeHTML(JSON.generate(missing))),
|
|
58
|
+
html.sub(map, CGI.escapeHTML('{"javascript-callback":["simple"],"javascript-callback":["simple"]}')),
|
|
59
|
+
html.sub(map, map.sub('simple', 'unknown')),
|
|
60
|
+
html.sub(/(data-client="javascript-callback"[^>]*data-detail=")simple"/, '\\1other"'),
|
|
61
|
+
html.sub(%r{(<details[^>]*data-client="javascript-callback"[^>]*>.*?</details>)}m) { |panel| panel * 2 },
|
|
62
|
+
html.sub(%r{(<details[^>]*data-client="javascript-callback"[^>]*>).*?</details>}m,
|
|
63
|
+
'\\1<p class="client-limitation">unsupported</p></details>')
|
|
64
|
+
]
|
|
65
|
+
variants.each_with_index do |invalid, index|
|
|
66
|
+
expect { described_class.extract(invalid) }.to raise_error(StandardError), "Malformed fixture #{index} was accepted"
|
|
67
|
+
end
|
|
68
|
+
group = html.match(%r{<div[^>]*data-example data-example-details=.*?</div>}m)[0]
|
|
69
|
+
expect { described_class.extract(html + group) }.to raise_error(/Duplicate example group/)
|
|
70
|
+
expect { described_class.extract(html, detail: :other) }.to raise_error(/Invalid requested/)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'requires authenticated mounted discovery for the correct provider on each independent run' do
|
|
74
|
+
before = receipt_snapshot
|
|
75
|
+
expect(request.options('/mount/v2/').status).to eq(200)
|
|
76
|
+
anonymous = receipt_snapshot
|
|
77
|
+
delta = described_class.receipt_delta(before, anonymous)
|
|
78
|
+
expect { described_class.verify_completion!(setup_panel, '', delta) }.not_to raise_error
|
|
79
|
+
expect { described_class.verify_completion!(setup_panel('basic'), '', delta) }.to raise_error(/did not complete/)
|
|
80
|
+
headers = { 'basic' => { 'HTTP_AUTHORIZATION' => "Basic #{['user:secret'].pack('m0')}" },
|
|
81
|
+
'token' => { 'HTTP_X_HAVEAPI_AUTH_TOKEN' => 'example-token' },
|
|
82
|
+
'oauth2' => { 'HTTP_X_FIXTURE_OAUTH2' => 'example-access-token' } }
|
|
83
|
+
headers.each do |provider, credentials|
|
|
84
|
+
before = receipt_snapshot
|
|
85
|
+
expect(request.options('/mount/v2/', credentials).status).to eq(200)
|
|
86
|
+
delta = described_class.receipt_delta(before, receipt_snapshot)
|
|
87
|
+
expect { described_class.verify_completion!(setup_panel(provider), '', delta) }.not_to raise_error unless provider == 'token'
|
|
88
|
+
expect { described_class.verify_completion!(setup_panel(provider), 'Discovery or authentication failed', delta) }
|
|
89
|
+
.to raise_error(/Failed callback/)
|
|
90
|
+
expect { described_class.verify_completion!(setup_panel(provider, detail: :thorough), '', delta) }
|
|
91
|
+
.to raise_error(provider == 'token' ? /Token challenge/ : /Missing completion output/)
|
|
92
|
+
expect { described_class.verify_completion!(setup_panel(provider), '', described_class.receipt_delta(receipt_snapshot, receipt_snapshot)) }
|
|
93
|
+
.to raise_error(/did not complete/)
|
|
94
|
+
end
|
|
95
|
+
before = receipt_snapshot
|
|
96
|
+
request.options('/mount/v2/', 'HTTP_AUTHORIZATION' => "Basic #{['user:wrong-secret'].pack('m0')}")
|
|
97
|
+
rejected = described_class.receipt_delta(before, receipt_snapshot)
|
|
98
|
+
expect { described_class.verify_completion!(setup_panel('basic'), '', rejected) }.to raise_error(/did not complete/)
|
|
99
|
+
expect(rejected['events']).to include(include('type' => 'authentication', 'provider' => 'basic', 'authenticated' => false))
|
|
100
|
+
expect(trace.string).not_to include('wrong-secret', 'secret', 'example-token', 'example-access-token', 'Authorization')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'requires request then verification then authenticated discovery inside each token-authentication program' do
|
|
104
|
+
before = receipt_snapshot
|
|
105
|
+
response = json_post('/mount/_auth/token/tokens', token: { user: 'user', password: 'secret', lifetime: 'fixed' })
|
|
106
|
+
expect(response.status).to eq(200), response.body
|
|
107
|
+
incomplete = described_class.receipt_delta(before, receipt_snapshot)
|
|
108
|
+
expect { described_class.verify_completion!(setup_panel('token'), '', incomplete) }.to raise_error(/did not complete/)
|
|
109
|
+
response = json_post('/mount/_auth/token/tokens/verify', token: { token: 'fixture-challenge', code: 'code' })
|
|
110
|
+
expect(response.status).to eq(200), response.body
|
|
111
|
+
request.options('/mount/v2/', 'HTTP_X_HAVEAPI_AUTH_TOKEN' => 'example-token')
|
|
112
|
+
delta = described_class.receipt_delta(before, receipt_snapshot)
|
|
113
|
+
expect { described_class.verify_completion!(setup_panel('token'), '', delta) }.not_to raise_error
|
|
114
|
+
expect { described_class.verify_completion!(setup_panel('token'), '', incomplete) }.to raise_error(/did not complete/)
|
|
115
|
+
before = receipt_snapshot
|
|
116
|
+
json_post('/mount/_auth/token/tokens/verify', token: { token: 'fixture-challenge', code: 'rejected-code' })
|
|
117
|
+
request.options('/mount/v2/', 'HTTP_X_HAVEAPI_AUTH_TOKEN' => 'example-token')
|
|
118
|
+
rejected = described_class.receipt_delta(before, receipt_snapshot)
|
|
119
|
+
expect { described_class.verify_completion!(setup_panel('token'), '', rejected) }.to raise_error(/Token challenge did not complete/)
|
|
120
|
+
expect(rejected['events']).to include(include('type' => 'token_verification_rejected'))
|
|
121
|
+
expect(JSON.generate(delta['events'])).not_to include('user', 'secret', 'example-token', 'code')
|
|
122
|
+
expect(trace.string).not_to include('user', 'secret', 'example-token', 'code', 'rejected-code')
|
|
18
123
|
end
|
|
19
124
|
|
|
20
125
|
it 'records real success and failure request values without source rewriting' do
|
|
21
|
-
|
|
126
|
+
before = receipt_snapshot
|
|
22
127
|
payload = { widget: { name: '', enabled: false, count: 0, nullable: nil, payload: DynamicDocumentationAPI::PAYLOAD } }
|
|
23
128
|
headers = {
|
|
24
129
|
'CONTENT_TYPE' => 'application/json',
|
|
25
|
-
'HTTP_AUTHORIZATION' => "Basic #{['
|
|
130
|
+
'HTTP_AUTHORIZATION' => "Basic #{['user:secret'].pack('m0')}",
|
|
26
131
|
input: JSON.generate(payload)
|
|
27
132
|
}
|
|
28
133
|
path = 'http://example.test/mount/v2/parents/7/children/name%20with%20space'
|
|
134
|
+
headers.delete('HTTP_AUTHORIZATION')
|
|
135
|
+
headers['HTTP_X_HAVEAPI_AUTH_TOKEN'] = 'example-token'
|
|
136
|
+
request.options('/mount/v2/', 'HTTP_X_HAVEAPI_AUTH_TOKEN' => 'example-token')
|
|
29
137
|
response = request.post(path, headers)
|
|
30
138
|
expect(response.status).to eq(200), response.body
|
|
31
139
|
expect(JSON.parse(response.body).fetch('status')).to be(true)
|
|
32
140
|
expect(DynamicDocumentationAPI.receipts.last[:input][:payload]).to eq(DynamicDocumentationAPI::PAYLOAD)
|
|
141
|
+
delta = described_class.receipt_delta(before, receipt_snapshot)
|
|
142
|
+
panel = { id: 'success-panel', group: described_class::GROUPS[-2], detail: :thorough }
|
|
143
|
+
expect { described_class.verify_completion!(panel, 'DOC_SUCCESS', delta) }.not_to raise_error
|
|
144
|
+
expect { described_class.verify_completion!(panel, '', delta) }.to raise_error(/Wrong result/)
|
|
145
|
+
expect { described_class.verify_completion!(panel, 'DOC_SUCCESS', delta.merge('actions' => delta['actions'] * 2)) }
|
|
146
|
+
.to raise_error(/did not execute once/)
|
|
147
|
+
before = receipt_snapshot
|
|
148
|
+
request.options('/mount/v2/', 'HTTP_X_HAVEAPI_AUTH_TOKEN' => 'example-token')
|
|
33
149
|
payload[:widget][:count] = -1
|
|
34
150
|
response = request.post(path, headers.merge(input: JSON.generate(payload)))
|
|
35
151
|
expect(response.status).to eq(400), response.body
|
|
36
152
|
expect(JSON.parse(response.body).fetch('message')).to eq('DOC_FAILURE')
|
|
153
|
+
delta = described_class.receipt_delta(before, receipt_snapshot)
|
|
154
|
+
panel[:group] = described_class::GROUPS.last
|
|
155
|
+
expect { described_class.verify_completion!(panel, 'DOC_FAILURE', delta) }.not_to raise_error
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it 'rejects malformed or rewound receipt snapshots and rejected token-login completion' do
|
|
159
|
+
before = receipt_snapshot
|
|
160
|
+
json_post('/mount/_auth/token/tokens', token: { user: 'user', password: 'wrong-secret', lifetime: 'fixed' })
|
|
161
|
+
after = receipt_snapshot
|
|
162
|
+
delta = described_class.receipt_delta(before, after)
|
|
163
|
+
expect(delta['events']).to include(include('type' => 'token_request_rejected'))
|
|
164
|
+
expect { described_class.verify_completion!(setup_panel('token'), '', delta) }.to raise_error(/did not complete/)
|
|
165
|
+
expect { described_class.receipt_delta(after, before) }.to raise_error(/changed before/)
|
|
166
|
+
expect { described_class.receipt_delta([], after) }.to raise_error(/Invalid fixture receipt/)
|
|
167
|
+
expect(trace.string).not_to include('wrong-secret', 'secret')
|
|
37
168
|
end
|
|
38
169
|
|
|
39
170
|
it 'preserves subprocess output before reporting a nonzero execution failure' do
|
|
@@ -46,20 +177,6 @@ RSpec.describe DynamicDocumentationAcceptance do
|
|
|
46
177
|
end
|
|
47
178
|
end
|
|
48
179
|
|
|
49
|
-
it 'preserves separate existing-token and challenge output before completion assertions' do
|
|
50
|
-
Dir.mktmpdir('haveapi-doc-output-') do |directory|
|
|
51
|
-
command = ['ruby', '-e', 'puts ENV.key?("HAVEAPI_TOKEN") ? "Authenticated" : "Discovery or authentication failed"']
|
|
52
|
-
path = File.join(directory, 'example-12.log')
|
|
53
|
-
challenge_path = File.join(directory, 'example-12-challenge.log')
|
|
54
|
-
result = described_class.run!(*command, chdir: directory, env: { 'HAVEAPI_TOKEN' => 'fixture-token' }, log_path: path)
|
|
55
|
-
challenge = described_class.run!(*command, chdir: directory, env: { 'HAVEAPI_TOKEN' => nil }, log_path: challenge_path)
|
|
56
|
-
expect(File.read(path)).to eq(result)
|
|
57
|
-
expect(File.read(challenge_path)).to eq(challenge)
|
|
58
|
-
expect(result).to include('Authenticated')
|
|
59
|
-
expect(challenge).to include('Discovery or authentication failed')
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
180
|
it 'traces failed mounted routes without headers, query strings or request bodies' do
|
|
64
181
|
output = StringIO.new
|
|
65
182
|
app = DynamicDocumentationAPI::RequestTrace.new(DynamicDocumentationAPI.build.app, output:)
|
|
@@ -29,8 +29,9 @@ module DynamicExampleFixture
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def token_description
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
request = { 'login-name' => { type: 'String' }, password: { type: 'String' }, lifetime: { type: 'String' } }
|
|
33
|
+
verify = { token: { type: 'String', required: true }, code: { type: 'String', required: true } }
|
|
34
|
+
{ resources: { token: { actions: { 'request' => { input: { parameters: request } },
|
|
35
|
+
'verify' => { input: { parameters: verify } } } } } }
|
|
35
36
|
end
|
|
36
37
|
end
|
|
@@ -39,9 +39,10 @@ RSpec.describe HaveAPI::ClientExamples::JsClient do
|
|
|
39
39
|
|
|
40
40
|
it 'renders complete syntax-valid initialization, authentication and hostile action programs' do
|
|
41
41
|
renderers.each do |renderer|
|
|
42
|
-
|
|
42
|
+
context = dynamic_context(detail: :thorough)
|
|
43
|
+
results = [renderer.render(:init, context:), renderer.render(:example, context:)]
|
|
43
44
|
%i[basic token oauth2].each do |method|
|
|
44
|
-
results << renderer.render(:auth, context
|
|
45
|
+
results << renderer.render(:auth, context:, auth_method: method, auth_description: token_description)
|
|
45
46
|
end
|
|
46
47
|
results.each do |result|
|
|
47
48
|
expect(result[:status]).to eq(:code), result.inspect
|
|
@@ -60,7 +61,7 @@ RSpec.describe HaveAPI::ClientExamples::JsClient do
|
|
|
60
61
|
expect(source).to include('["enabled"]: false', '["count"]: 0', '["nullable"]: null', '["__proto__"]:')
|
|
61
62
|
expect(source).to include('new\\"\\n</script>')
|
|
62
63
|
if renderer.id.end_with?('-async')
|
|
63
|
-
expect(source).to include('.resource("constructor").resource("resources").action(', 'result.value'
|
|
64
|
+
expect(source).to include('.resource("constructor").resource("resources").action(', 'result.value')
|
|
64
65
|
else
|
|
65
66
|
expect(source).to include('CallbackClient.findResource', 'CallbackClient.findAction', 'action.directInvoke', 'reply.message()', 'reply.envelope.errors')
|
|
66
67
|
end
|
|
@@ -72,14 +73,22 @@ RSpec.describe HaveAPI::ClientExamples::JsClient do
|
|
|
72
73
|
Dir.mktmpdir('haveapi-ts-docs-') do |directory|
|
|
73
74
|
files = []
|
|
74
75
|
[HaveAPI::ClientExamples::TsClient, HaveAPI::ClientExamples::TsAsyncClient].each_with_index do |renderer, index|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
results
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
%i[simple thorough].each do |detail|
|
|
77
|
+
context = dynamic_context(detail:)
|
|
78
|
+
results = [renderer.render(:init, context:), renderer.render(:example, context:)]
|
|
79
|
+
%i[basic token oauth2].each do |method|
|
|
80
|
+
results << renderer.render(:auth, context:, auth_method: method, auth_description: token_description)
|
|
81
|
+
end
|
|
82
|
+
results.each_with_index do |result, number|
|
|
83
|
+
source = result.fetch(:code)
|
|
84
|
+
if detail == :simple && number == 1
|
|
85
|
+
client = renderer.id.end_with?('-async') ? 'AsyncClient' : 'CallbackClient'
|
|
86
|
+
source = "import { #{client} } from \"haveapi-client\";\ndeclare const api: #{client};\n#{source}"
|
|
87
|
+
end
|
|
88
|
+
path = File.join(directory, "#{index}-#{detail}-#{number}.ts")
|
|
89
|
+
File.write(path, source)
|
|
90
|
+
files << path
|
|
91
|
+
end
|
|
83
92
|
end
|
|
84
93
|
end
|
|
85
94
|
options = { strict: true, noEmit: true, target: 'ES2020', module: 'ESNext', moduleResolution: 'node',
|
|
@@ -49,4 +49,73 @@ RSpec.describe HaveAPI::ClientExamples::DynamicHelpers do
|
|
|
49
49
|
expect(renderer.render(:example, context:)[:status]).to eq(:code)
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
|
+
|
|
53
|
+
it 'uses only public typed literals and advertised finite machine choices for token inputs' do
|
|
54
|
+
description = token_description
|
|
55
|
+
fields = description[:resources][:token][:actions]['request'][:input][:parameters]
|
|
56
|
+
fields[:user] = { type: 'String', required: true, default: 'PRIVATE_USER' }
|
|
57
|
+
fields[:password][:default] = 'PRIVATE_PASSWORD'
|
|
58
|
+
fields[:count] = { type: 'Integer', required: true, default: 99 }
|
|
59
|
+
fields[:enabled] = { type: 'Boolean', required: true, validators: { accept: { value: true, message: 'PRIVATE_PROSE' } } }
|
|
60
|
+
fields[:scope] = { type: 'String', required: true, validators: { include: { values: { 'all' => 'PRIVATE_LABEL' } } } }
|
|
61
|
+
fields[:resource] = { type: 'Resource', required: true }
|
|
62
|
+
fields[:timestamp] = { type: 'Datetime', required: true }
|
|
63
|
+
fields[:custom] = { type: 'Custom', required: true }
|
|
64
|
+
fields[:interval] = { type: 'Integer', required: true }
|
|
65
|
+
before = Marshal.dump(description)
|
|
66
|
+
renderers.each do |renderer|
|
|
67
|
+
result = renderer.render(:auth, context: dynamic_context, auth_method: :token, auth_description: description)
|
|
68
|
+
expect(result[:status]).to eq(:code), result.inspect
|
|
69
|
+
expect(result[:code]).to include('secret', 'user', 'fixed', 'all', '2026-01-01T00:00:00Z')
|
|
70
|
+
expect(result[:code]).not_to include('PRIVATE_', 'os.environ', 'credential(', 'HAVEAPI_', 'node:process')
|
|
71
|
+
end
|
|
72
|
+
expect(Marshal.dump(description)).to eq(before)
|
|
73
|
+
helper = HaveAPI::ClientExamples::JsClient.new('api.test', 'https://api.test/', '2')
|
|
74
|
+
inputs = helper.send(:auth_token_credentials, description)
|
|
75
|
+
expect(inputs).to include('user' => 'user', 'password' => 'secret', 'count' => 1, 'enabled' => true,
|
|
76
|
+
'scope' => 'all', 'resource' => 1, 'custom' => {}, 'interval' => 1, 'lifetime' => 'fixed')
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'limits unsupported required types and input shapes instead of guessing defaults or dropping fields' do
|
|
80
|
+
descriptions = []
|
|
81
|
+
description = token_description
|
|
82
|
+
description[:resources][:token][:actions]['request'][:input][:parameters][:hidden] = {
|
|
83
|
+
type: 'Unsupported', required: true, default: 'PRIVATE_DEFAULT'
|
|
84
|
+
}
|
|
85
|
+
descriptions << description
|
|
86
|
+
description = token_description
|
|
87
|
+
description[:resources][:token][:actions]['verify'][:input][:parameters][:hidden] = {
|
|
88
|
+
type: 'Unsupported', required: true, default: 'PRIVATE_DEFAULT'
|
|
89
|
+
}
|
|
90
|
+
descriptions << description
|
|
91
|
+
description = token_description
|
|
92
|
+
description[:resources][:token][:actions]['request'][:input][:layout] = :object_list
|
|
93
|
+
descriptions << description
|
|
94
|
+
renderers.each do |renderer|
|
|
95
|
+
descriptions.each do |desc|
|
|
96
|
+
result = renderer.render(:auth, context: dynamic_context, auth_method: :token, auth_description: desc)
|
|
97
|
+
expect(result).to include(status: :limitation)
|
|
98
|
+
expect(result.inspect).not_to include('PRIVATE_DEFAULT')
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'uses actual continuation action arguments and rejects unknown continuation or input names' do
|
|
104
|
+
renderer = HaveAPI::ClientExamples::JsClient.new('api.test', 'https://api.test/', '2')
|
|
105
|
+
options = renderer.token_login_options(token_description)
|
|
106
|
+
script = <<~JS
|
|
107
|
+
const assert = require('node:assert/strict');
|
|
108
|
+
const options = #{options};
|
|
109
|
+
assert.deepEqual(options.callback('verify', {code: {}}), {code: 'code'});
|
|
110
|
+
assert.throws(() => options.callback('missing', {code: {}}), /Unknown authentication continuation/);
|
|
111
|
+
assert.throws(() => options.callback('verify', {unexpected: {}}), /Unknown authentication input/);
|
|
112
|
+
JS
|
|
113
|
+
output, status = Open3.capture2e('node', '-e', script)
|
|
114
|
+
expect(status).to be_success, output
|
|
115
|
+
renderers.grep(Class).each do |client|
|
|
116
|
+
source = client.render(:auth, context: dynamic_context, auth_method: :token, auth_description: token_description).fetch(:code)
|
|
117
|
+
expect(source).not_to include('os.environ', 'HAVEAPI_', 'step.action')
|
|
118
|
+
expect(source).to include(client.code == :python ? 'step.next_action' : '(action, parameters)')
|
|
119
|
+
end
|
|
120
|
+
end
|
|
52
121
|
end
|
|
@@ -6,7 +6,7 @@ require_relative '../../test_support/dynamic_documentation_api'
|
|
|
6
6
|
RSpec.describe DynamicDocumentationAPI do
|
|
7
7
|
it 'uses real mounted descriptor routes for callback and async token challenges and nested actions' do
|
|
8
8
|
request = Rack::MockRequest.new(described_class.build.app)
|
|
9
|
-
headers = { 'HTTP_AUTHORIZATION' => "Basic #{['
|
|
9
|
+
headers = { 'HTTP_AUTHORIZATION' => "Basic #{['user:secret'].pack('m0')}" }
|
|
10
10
|
envelope = JSON.parse(request.options('http://example.test/mount/v2/', headers).body)
|
|
11
11
|
model = envelope.fetch('response')
|
|
12
12
|
token = model.fetch('authentication').fetch('token').fetch('resources').fetch('token').fetch('actions')
|
|
@@ -17,14 +17,16 @@ RSpec.describe DynamicDocumentationAPI do
|
|
|
17
17
|
post = lambda do |path, body, extra = {}|
|
|
18
18
|
response = request.post("http://example.test#{path}", extra.merge('CONTENT_TYPE' => 'application/json', input: JSON.generate(body)))
|
|
19
19
|
expect(response.status).to eq(200), response.body
|
|
20
|
-
JSON.parse(response.body)
|
|
20
|
+
payload = JSON.parse(response.body)
|
|
21
|
+
expect(payload.fetch('status')).to be(true), response.body
|
|
22
|
+
payload
|
|
21
23
|
end
|
|
22
24
|
challenge = post.call(token.fetch('request').fetch('path'), { token: {
|
|
23
|
-
user: '
|
|
25
|
+
user: 'user', password: 'secret', scope: 'all', lifetime: 'renewable_auto'
|
|
24
26
|
} })
|
|
25
|
-
completed = post.call(token.fetch('verify').fetch('path'), { token: { token: 'fixture-challenge', code: '
|
|
27
|
+
completed = post.call(token.fetch('verify').fetch('path'), { token: { token: 'fixture-challenge', code: 'code' } })
|
|
26
28
|
action_path = action.fetch('path').sub('{parent_id}', '7').sub('{widget_id}', 'name%20with%20space')
|
|
27
|
-
result = post.call(action_path, { widget: params }, { 'HTTP_X_HAVEAPI_AUTH_TOKEN' => '
|
|
29
|
+
result = post.call(action_path, { widget: params }, { 'HTTP_X_HAVEAPI_AUTH_TOKEN' => 'example-token' })
|
|
28
30
|
data = { envelope:, challenge:, completed:, result:, params:, action_path: }
|
|
29
31
|
script = <<~JS
|
|
30
32
|
const assert = require('node:assert/strict');
|
|
@@ -36,7 +38,7 @@ RSpec.describe DynamicDocumentationAPI do
|
|
|
36
38
|
async function check() {
|
|
37
39
|
const callback = new HaveAPI.CallbackClient('http://example.test/mount/', {version:'2'});
|
|
38
40
|
let ready = false;
|
|
39
|
-
callback.authenticate('token', {user:'
|
|
41
|
+
callback.authenticate('token', {user:'user', password:'secret', scope:'all', callback: () => ({code:'code'})},
|
|
40
42
|
(_, ok) => { ready = ok; });
|
|
41
43
|
// Parse actual discovery JSON inside the bundle's VM, matching the real XHR boundary.
|
|
42
44
|
assert.equal(requests[0].url, 'http://example.test/mount/v2/'); requests[0].respond(data.envelope);
|
|
@@ -47,7 +49,7 @@ RSpec.describe DynamicDocumentationAPI do
|
|
|
47
49
|
HaveAPI.CallbackClient.findAction(resource, 'new').directInvoke(7, 'name with space', {params:data.params});
|
|
48
50
|
assert.equal(requests[4].url, 'http://example.test' + data.action_path); requests[4].respond(data.result);
|
|
49
51
|
const api = new HaveAPI.AsyncClient('http://example.test/mount/', {version:'2'}); api.useDescription(model);
|
|
50
|
-
const authenticating = api.authenticate('token', {user:'
|
|
52
|
+
const authenticating = api.authenticate('token', {user:'user', password:'secret', scope:'all', onChallenge: () => ({code:'code'})});
|
|
51
53
|
await tick(); assert.equal(requests[5].url, 'http://example.test' + auth.request.path); requests[5].respond(data.challenge);
|
|
52
54
|
await tick(); assert.equal(requests[6].url, 'http://example.test' + auth.verify.path); requests[6].respond(data.completed);
|
|
53
55
|
await tick(); assert.equal(requests[7].url, 'http://example.test/mount/v2/'); requests[7].respond(data.envelope); await authenticating;
|
|
@@ -7,6 +7,19 @@ RSpec.describe HaveAPI::ClientExamples::PythonClient do
|
|
|
7
7
|
|
|
8
8
|
let(:renderers) { [described_class, HaveAPI::ClientExamples::PythonAsyncClient] }
|
|
9
9
|
|
|
10
|
+
it 'separates imports, executable sync code and top-level async definitions without blank padding' do
|
|
11
|
+
%i[simple thorough].each do |detail|
|
|
12
|
+
context = dynamic_context(detail:)
|
|
13
|
+
sync = described_class.render(:init, context:).fetch(:code)
|
|
14
|
+
async = HaveAPI::ClientExamples::PythonAsyncClient.render(:init, context:).fetch(:code)
|
|
15
|
+
expect(sync).to start_with('from haveapi_client import ')
|
|
16
|
+
expect(sync).to include(detail == :simple ? "Client\n\nwith Client(" : "HaveAPIError\n\ntry:\n")
|
|
17
|
+
expect(async).to start_with("import asyncio\n\nfrom haveapi_client import ")
|
|
18
|
+
expect(async).to include("\n\n\nasync def main() -> None:\n", "\n\n\nasyncio.run(main())\n")
|
|
19
|
+
expect([sync, async]).to all(satisfy { |source| !source.start_with?("\n") && !source.end_with?("\n\n") })
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
10
23
|
it 'keeps both variants available without a generated SDK profile' do
|
|
11
24
|
expect(renderers.map(&:id)).to eq(%w[python-sync python-async])
|
|
12
25
|
expect(renderers.map(&:order)).to eq([14, 15])
|
|
@@ -15,15 +28,28 @@ RSpec.describe HaveAPI::ClientExamples::PythonClient do
|
|
|
15
28
|
|
|
16
29
|
it 'renders complete parseable init, auth, action and async ownership' do
|
|
17
30
|
renderers.each do |renderer|
|
|
18
|
-
|
|
31
|
+
context = dynamic_context(detail: :thorough)
|
|
32
|
+
results = [renderer.render(:init, context:), renderer.render(:example, context:)]
|
|
19
33
|
%i[basic token oauth2].each do |method|
|
|
20
|
-
results << renderer.render(:auth, context
|
|
34
|
+
results << renderer.render(:auth, context:, auth_method: method, auth_description: token_description)
|
|
21
35
|
end
|
|
22
36
|
results.each do |result|
|
|
23
37
|
expect(result[:status]).to eq(:code), result.inspect
|
|
24
38
|
expect(result).to be_frozen
|
|
25
|
-
output, status = Open3.capture2e('python', '-c',
|
|
39
|
+
output, status = Open3.capture2e('python', '-c',
|
|
40
|
+
'import ast, json, sys; print(json.dumps([type(node).__name__ for node in ast.parse(sys.stdin.read()).body]))',
|
|
41
|
+
stdin_data: result[:code])
|
|
26
42
|
expect(status).to be_success, output
|
|
43
|
+
statements = JSON.parse(output)
|
|
44
|
+
expect(result[:code]).not_to include('__main__')
|
|
45
|
+
if renderer.id == 'python-async'
|
|
46
|
+
expect(statements.count('AsyncFunctionDef')).to eq(1)
|
|
47
|
+
expect(statements.last).to eq('Expr')
|
|
48
|
+
expect(result[:code]).to include("\nasyncio.run(main())\n")
|
|
49
|
+
else
|
|
50
|
+
expect(statements).to include('Try')
|
|
51
|
+
expect(statements).not_to include('FunctionDef', 'AsyncFunctionDef')
|
|
52
|
+
end
|
|
27
53
|
expect(result[:code]).to include('https://api.example.test/mount/', 'version="2"')
|
|
28
54
|
expect(result[:code]).to include('except ActionError', 'except HaveAPIError')
|
|
29
55
|
end
|
|
@@ -36,16 +62,42 @@ RSpec.describe HaveAPI::ClientExamples::PythonClient do
|
|
|
36
62
|
expect(source).to include('.resources["constructor"].resources["resources"].actions[',
|
|
37
63
|
'path_params={"parent_id": 7, "widget_id": "name/with space"}',
|
|
38
64
|
'"enabled": False', '"count": 0', '"nullable": None', '"__proto__":',
|
|
39
|
-
'print(response.data)'
|
|
65
|
+
'print(response.data)')
|
|
40
66
|
if renderer.id == 'python-async'
|
|
41
|
-
expect(source).to include('
|
|
67
|
+
expect(source).to include('response = await ', 'async function')
|
|
42
68
|
else
|
|
43
|
-
expect(source).to include('
|
|
69
|
+
expect(source).to include('response = api.resources')
|
|
44
70
|
expect(source).not_to include('await ')
|
|
45
71
|
end
|
|
46
72
|
end
|
|
47
73
|
end
|
|
48
74
|
|
|
75
|
+
it 'keeps simple setup and authentication parseable with only the imports they use' do
|
|
76
|
+
renderers.each do |renderer|
|
|
77
|
+
init = renderer.render(:init, context: dynamic_context).fetch(:code)
|
|
78
|
+
sources = [init]
|
|
79
|
+
%i[basic token oauth2].each do |method|
|
|
80
|
+
sources << renderer.render(:auth, context: dynamic_context, auth_method: method, auth_description: token_description).fetch(:code)
|
|
81
|
+
end
|
|
82
|
+
sources.each do |source|
|
|
83
|
+
output, status = Open3.capture2e('python', '-c', 'import ast, sys; ast.parse(sys.stdin.read())', stdin_data: source)
|
|
84
|
+
expect(status).to be_success, output
|
|
85
|
+
expect(source).not_to include('__main__', 'except ', 'ActionError', 'HaveAPIError', 'print("Authenticated")')
|
|
86
|
+
if renderer.id == 'python-async'
|
|
87
|
+
expect(source).to include('async def main()', "\nasyncio.run(main())\n")
|
|
88
|
+
else
|
|
89
|
+
expect(source).not_to include('def main', 'asyncio')
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
expect(init).not_to include('import os', 'import json')
|
|
93
|
+
basic = sources[1]
|
|
94
|
+
expect(basic).to include('BasicAuth("user", "secret")')
|
|
95
|
+
expect(basic).not_to include('import json')
|
|
96
|
+
expect(sources[2]).to include('begin_token_login', 'continuations', 'step.next_action')
|
|
97
|
+
expect(sources.join).not_to include('import os', 'import json', 'os.environ')
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
49
101
|
it 'retains list rows and exposes a limitation for an unnamespaced list' do
|
|
50
102
|
action = dynamic_action.merge(input: { layout: :hash_list, namespace: :widgets, parameters: { name: { type: 'String' } } })
|
|
51
103
|
context = dynamic_context(action:, sample: dynamic_sample.merge(request: [{ name: '' }, { name: 'second' }]))
|
|
@@ -69,7 +121,7 @@ RSpec.describe HaveAPI::ClientExamples::PythonClient do
|
|
|
69
121
|
it 'uses real token challenge APIs and advertised login fields without printing credentials' do
|
|
70
122
|
renderers.each do |renderer|
|
|
71
123
|
source = renderer.render(:auth, context: dynamic_context, auth_method: :token, auth_description: token_description).fetch(:code)
|
|
72
|
-
expect(source).to include('begin_token_login', 'step.input', 'step.submit(values)', '
|
|
124
|
+
expect(source).to include('begin_token_login', 'step.input', 'step.submit(values)', '"login-name": "login-name"')
|
|
73
125
|
expect(source).not_to include('print(step.token)', 'qwertyuiop')
|
|
74
126
|
end
|
|
75
127
|
end
|
|
@@ -81,15 +133,14 @@ RSpec.describe HaveAPI::ClientExamples::PythonClient do
|
|
|
81
133
|
parameters[:interval] = { type: 'Integer', required: false }
|
|
82
134
|
renderers.each do |renderer|
|
|
83
135
|
source = renderer.render(:auth, context: dynamic_context, auth_method: :token, auth_description: description).fetch(:code)
|
|
84
|
-
expect(source).to include('"lifetime":
|
|
136
|
+
expect(source).to include('"lifetime": "fixed"')
|
|
85
137
|
expect(source).not_to include('HAVEAPI_AUTH_interval', 'HAVEAPI_CHALLENGE_token', 'renewable_auto')
|
|
86
|
-
expect(source).to include('
|
|
87
|
-
'for name in step.input if name != "token"', 'step.submit(values)')
|
|
138
|
+
expect(source).to include('step.next_action', 'step.submit(values)', 'name not in values')
|
|
88
139
|
end
|
|
89
140
|
parameters[:interval][:required] = true
|
|
90
141
|
renderers.each do |renderer|
|
|
91
142
|
source = renderer.render(:auth, context: dynamic_context, auth_method: :token, auth_description: description).fetch(:code)
|
|
92
|
-
expect(source).to include('"interval":
|
|
143
|
+
expect(source).to include('"interval": 1')
|
|
93
144
|
end
|
|
94
145
|
end
|
|
95
146
|
|
|
@@ -7,6 +7,7 @@ RSpec.describe HaveAPI::ClientExamples::GoClient do
|
|
|
7
7
|
let(:adapter) do
|
|
8
8
|
contract = Class.new do
|
|
9
9
|
def render(**); end
|
|
10
|
+
def detail_levels_for(**); end
|
|
10
11
|
end
|
|
11
12
|
instance_double(contract, render: result)
|
|
12
13
|
end
|
|
@@ -22,6 +23,7 @@ RSpec.describe HaveAPI::ClientExamples::GoClient do
|
|
|
22
23
|
|
|
23
24
|
it 'registers the frozen Go ID and requires its configured profile' do
|
|
24
25
|
expect(described_class.id).to eq('go')
|
|
26
|
+
expect(described_class.label).to eq('Go API client')
|
|
25
27
|
expect(described_class.code).to eq(:go)
|
|
26
28
|
expect(described_class.order).to eq(16)
|
|
27
29
|
expect(described_class).to be_available(context)
|
|
@@ -29,6 +31,17 @@ RSpec.describe HaveAPI::ClientExamples::GoClient do
|
|
|
29
31
|
expect(described_class).not_to be_available(context.with(profile: Struct.new(:language).new(:rust)))
|
|
30
32
|
end
|
|
31
33
|
|
|
34
|
+
it 'delegates meaningful-detail queries with the existing rendered result and native mode' do
|
|
35
|
+
allow(adapter).to receive(:detail_levels_for).and_return([:simple].freeze)
|
|
36
|
+
expect(described_class.detail_levels_for(:init, context:, result:)).to eq([:simple])
|
|
37
|
+
expect(adapter).to have_received(:detail_levels_for).with(kind: :init, context:, result:, mode: :sync,
|
|
38
|
+
auth_method: nil, auth_description: nil)
|
|
39
|
+
allow(adapter).to receive(:detail_levels_for).and_return(%i[simple thorough].freeze)
|
|
40
|
+
expect(described_class.detail_levels_for(:example, context:, result:)).to eq(%i[simple thorough])
|
|
41
|
+
allow(adapter).to receive(:detail_levels_for).and_return([:thorough])
|
|
42
|
+
expect { described_class.detail_levels_for(:example, context:, result:) }.to raise_error(ArgumentError)
|
|
43
|
+
end
|
|
44
|
+
|
|
32
45
|
it 'delegates initialization without requiring the optional generator gem' do
|
|
33
46
|
expect(described_class.render(:init, context:)).to eq(result)
|
|
34
47
|
expect(adapter).to have_received(:render).with(kind: :init, context:, mode: :sync)
|
|
@@ -42,7 +55,7 @@ RSpec.describe HaveAPI::ClientExamples::GoClient do
|
|
|
42
55
|
end
|
|
43
56
|
|
|
44
57
|
it 'binds the selected filtered example and preserves plain limitation results' do
|
|
45
|
-
limitation = { status: :limitation, reason: 'This Go
|
|
58
|
+
limitation = { status: :limitation, reason: 'This Go API client input is unsupported.' }.freeze
|
|
46
59
|
allow(adapter).to receive(:render).and_return(limitation)
|
|
47
60
|
expect(described_class.render(:example, context:)).to eq(limitation)
|
|
48
61
|
expect(adapter).to have_received(:render) do |**arguments|
|