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,268 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
require_relative 'dynamic_fixture'
|
|
7
|
+
|
|
8
|
+
RSpec.describe HaveAPI::ClientExamples do
|
|
9
|
+
include DynamicExampleFixture
|
|
10
|
+
|
|
11
|
+
let(:action) do
|
|
12
|
+
{ method: 'POST', path: '/widgets/{widget_id}', auth: false,
|
|
13
|
+
input: { layout: :hash, namespace: :widget, parameters: { name: { type: 'String' } } },
|
|
14
|
+
output: { layout: :hash, namespace: :widget, parameters: {} } }
|
|
15
|
+
end
|
|
16
|
+
let(:sample) { { path_params: [7], request: { name: 'ordinary' }, response: {}, status: true } }
|
|
17
|
+
|
|
18
|
+
def renderer(type, base_url: 'https://docs.example/api/', version: '1', resource_path: ['widget'], action_name: 'update', contract: action)
|
|
19
|
+
type.new('docs.example', base_url, version, resource_path, {}, action_name, contract)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def capture_arguments(source, directory)
|
|
23
|
+
bin = File.join(directory, 'bin')
|
|
24
|
+
FileUtils.mkdir_p(bin)
|
|
25
|
+
%w[haveapi-cli curl].each do |name|
|
|
26
|
+
executable = File.join(bin, name)
|
|
27
|
+
File.write(executable, "#!/bin/sh\nprintf '%s\\0' \"$@\" >> \"$CAPTURE\"\n")
|
|
28
|
+
File.chmod(0o755, executable)
|
|
29
|
+
end
|
|
30
|
+
capture = File.join(directory, 'arguments')
|
|
31
|
+
FileUtils.rm_f(capture)
|
|
32
|
+
stdout, stderr, status = Open3.capture3(
|
|
33
|
+
{ 'PATH' => "#{bin}:#{ENV.fetch('PATH')}", 'CAPTURE' => capture },
|
|
34
|
+
'sh', '-c', source.delete_prefix('$ ')
|
|
35
|
+
)
|
|
36
|
+
expect(status).to be_success, "#{stdout}\n#{stderr}"
|
|
37
|
+
File.binread(capture).force_encoding(Encoding::UTF_8).split("\0", -1)[0...-1]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
[HaveAPI::ClientExamples::RubyCli, HaveAPI::ClientExamples::Curl].each do |type|
|
|
41
|
+
it "keeps the reported #{type.label} response substitution inert in the whole transcript", :aggregate_failures do
|
|
42
|
+
Dir.mktmpdir('haveapi-shell-response-') do |directory|
|
|
43
|
+
marker = File.join(directory, 'response-executed')
|
|
44
|
+
payload = "$(printf response-confirmed > #{marker})"
|
|
45
|
+
contract = action.merge(output: action[:output].merge(parameters: { name: { type: 'String' } }))
|
|
46
|
+
example = sample.merge(request: {}, response: { name: payload }, message: 'ok', errors: nil)
|
|
47
|
+
context = dynamic_context(base_url: 'https://docs.example/api/', version: '1', resource_path: ['widget'],
|
|
48
|
+
action_name: 'show', action: contract, sample: example)
|
|
49
|
+
source = type.render(:example, context:).fetch(:code)
|
|
50
|
+
argv = capture_arguments(source, directory)
|
|
51
|
+
expect(File.exist?(marker)).to be(false)
|
|
52
|
+
expect(source).to include(payload)
|
|
53
|
+
if type == HaveAPI::ClientExamples::RubyCli
|
|
54
|
+
expect(argv).to eq(['-u', 'https://docs.example/api/', '--version', '1', 'widget', 'show', '7'])
|
|
55
|
+
else
|
|
56
|
+
expect(argv).to eq(['--request', 'POST', '--data-binary', JSON.generate(widget: {}), 'https://docs.example/api/widgets/7'])
|
|
57
|
+
expect(JSON.parse(argv.fetch(3))).to eq('widget' => {})
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'keeps complete success and failure transcripts inert at both direct details without changing request bytes' do
|
|
64
|
+
Dir.mktmpdir('haveapi-shell-transcripts-') do |directory|
|
|
65
|
+
marker = File.join(directory, 'response-executed')
|
|
66
|
+
hostile = "$(touch #{marker})\n`touch #{marker}`\r\n; touch #{marker}\r& echo '雪' \"$value\"\\\ntrailing\\"
|
|
67
|
+
request = "request\r\n雪'\"\\$(touch #{marker})"
|
|
68
|
+
parameters = { name: { type: 'String', label: hostile }, zero: { type: 'Integer' }, empty: { type: 'String' },
|
|
69
|
+
enabled: { type: 'Boolean' }, nullable: { type: 'Custom' }, structured: { type: 'Custom' } }
|
|
70
|
+
contract = action.merge(output: action[:output].merge(parameters:))
|
|
71
|
+
values = { name: hostile, zero: 0, empty: '', enabled: false, nullable: nil, structured: { list: [false, 0, nil] } }
|
|
72
|
+
examples = [sample.merge(request: { name: request }, response: values, message: 'ok', errors: nil),
|
|
73
|
+
sample.merge(request: { name: request }, response: [values, values], message: 'ok', errors: nil),
|
|
74
|
+
sample.merge(request: { name: request }, response: {}, status: false, message: hostile, errors: { hostile => [hostile, 'retained diagnostic'] })]
|
|
75
|
+
[HaveAPI::ClientExamples::RubyCli, HaveAPI::ClientExamples::Curl].each do |type|
|
|
76
|
+
%i[simple thorough].each do |detail|
|
|
77
|
+
examples.each do |example|
|
|
78
|
+
context = dynamic_context(base_url: 'https://docs.example/api/', version: '1', resource_path: ['widget'],
|
|
79
|
+
action_name: 'show', action: contract, sample: example, detail:)
|
|
80
|
+
source = type.render(:example, context:).fetch(:code)
|
|
81
|
+
argv = capture_arguments(source, directory)
|
|
82
|
+
expect(File.exist?(marker)).to be(false)
|
|
83
|
+
expect(source).to end_with("\n")
|
|
84
|
+
if type == HaveAPI::ClientExamples::RubyCli
|
|
85
|
+
expect(argv).to eq(['-u', 'https://docs.example/api/', '--version', '1', 'widget', 'show', '7', '--', '--name', request])
|
|
86
|
+
display = source.split("\n# ", 2).fetch(1)
|
|
87
|
+
expect("# #{display}".lines).to all(start_with('# '))
|
|
88
|
+
expect(display).not_to include("\r")
|
|
89
|
+
expect(display).to include("$(touch #{marker})", "`touch #{marker}`", "; touch #{marker}", 'trailing\\')
|
|
90
|
+
expect(display).to include('Action failed:', 'Errors:', 'retained diagnostic') unless example[:status]
|
|
91
|
+
expect(display).to include('0', 'list') if example[:status]
|
|
92
|
+
else
|
|
93
|
+
expect(argv).to eq(['--request', 'POST', '--data-binary', JSON.generate(widget: { name: request }), 'https://docs.example/api/widgets/7'])
|
|
94
|
+
expect(JSON.parse(argv.fetch(3))).to eq('widget' => { 'name' => request })
|
|
95
|
+
response = source.split("\n\n", 2).fetch(1)
|
|
96
|
+
expect(response.lines).to all(start_with('# '))
|
|
97
|
+
decoded = response.lines.map { |line| line.delete_prefix('# ') }.join
|
|
98
|
+
envelope = { status: example[:status], message: example[:message], response: { widget: example[:response] }, errors: example[:errors] }
|
|
99
|
+
expect(JSON.parse(decoded)).to eq(JSON.parse(JSON.generate(envelope)))
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'preserves direct curl JSON, raw HTTP and inherited shell response composition' do
|
|
108
|
+
example = sample.merge(response: { name: "line\r\n雪$(echo retained)" }, http_status: 200, message: 'ok', errors: nil)
|
|
109
|
+
curl = renderer(HaveAPI::ClientExamples::Curl)
|
|
110
|
+
expect(JSON.parse(curl.response(example))).to eq('status' => true, 'message' => 'ok',
|
|
111
|
+
'response' => { 'widget' => { 'name' => example[:response][:name] } }, 'errors' => nil)
|
|
112
|
+
expect(curl.response(example)).not_to start_with('# ')
|
|
113
|
+
http = renderer(HaveAPI::ClientExamples::Http)
|
|
114
|
+
expect(http.request(example)).to start_with("POST /widgets/7 HTTP/1.1\nHost: docs.example\nContent-Type: application/json\n\n")
|
|
115
|
+
expect(http.response(example)).to start_with("HTTP/1.1 200 OK\nContent-Type: application/json;charset=utf-8\n")
|
|
116
|
+
context = dynamic_context(action:, sample: example)
|
|
117
|
+
[HaveAPI::ClientExamples::RubyCli, HaveAPI::ClientExamples::Curl].each do |type|
|
|
118
|
+
expect(Class.new(type).render(:example, context:)).to eq(type.render(:example, context:))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'keeps the reported CLI request-value breakout literal' do
|
|
123
|
+
Dir.mktmpdir('haveapi-shell-') do |directory|
|
|
124
|
+
marker = File.join(directory, 'request-value-executed')
|
|
125
|
+
payload = "'; printf request-value-confirmed > #{marker}; #'"
|
|
126
|
+
source = renderer(HaveAPI::ClientExamples::RubyCli).example(sample.merge(request: { name: payload }))
|
|
127
|
+
argv = capture_arguments(source, directory)
|
|
128
|
+
expect(File.exist?(marker)).to be(false)
|
|
129
|
+
expect(argv).to eq(['-u', 'https://docs.example/api/', '--version', '1', 'widget', 'update', '7', '--', '--name', payload])
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'keeps the reported CLI base-URL separator literal' do
|
|
134
|
+
Dir.mktmpdir('haveapi-shell-') do |directory|
|
|
135
|
+
marker = File.join(directory, 'base-url-executed')
|
|
136
|
+
base_url = "https://docs.example/api; printf host-confirmed > #{marker}; #"
|
|
137
|
+
argv = capture_arguments(renderer(HaveAPI::ClientExamples::RubyCli, base_url:).init, directory)
|
|
138
|
+
expect(File.exist?(marker)).to be(false)
|
|
139
|
+
expect(argv).to eq(['-u', base_url, '--version', '1'])
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'keeps the reported curl JSON substitution literal' do
|
|
144
|
+
Dir.mktmpdir('haveapi-shell-') do |directory|
|
|
145
|
+
marker = File.join(directory, 'request-value-executed')
|
|
146
|
+
payload = "$(printf request-value-confirmed > #{marker})"
|
|
147
|
+
argv = capture_arguments(renderer(HaveAPI::ClientExamples::Curl).request(sample.merge(request: { name: payload })), directory)
|
|
148
|
+
expect(File.exist?(marker)).to be(false)
|
|
149
|
+
expect(argv).to eq(['--request', 'POST', '--data-binary', JSON.generate(widget: { name: payload }), 'https://docs.example/api/widgets/7'])
|
|
150
|
+
expect(JSON.parse(argv.fetch(3))).to eq('widget' => { 'name' => payload })
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'keeps the reported curl URL quote breakout literal' do
|
|
155
|
+
Dir.mktmpdir('haveapi-shell-') do |directory|
|
|
156
|
+
marker = File.join(directory, 'base-url-executed')
|
|
157
|
+
base_url = "https://docs.example/'$(printf host-confirmed > #{marker})'/api/"
|
|
158
|
+
argv = capture_arguments(renderer(HaveAPI::ClientExamples::Curl, base_url:).request(sample), directory)
|
|
159
|
+
expect(File.exist?(marker)).to be(false)
|
|
160
|
+
expect(argv).to eq(['--request', 'POST', '--data-binary', JSON.generate(widget: { name: 'ordinary' }), "#{base_url}widgets/7"])
|
|
161
|
+
expect(JSON.parse(argv.fetch(3))).to eq('widget' => { 'name' => 'ordinary' })
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it 'preserves CLI argument order, Boolean flags and existing to_s values across shell metacharacters' do
|
|
166
|
+
Dir.mktmpdir('haveapi-shell-') do |directory|
|
|
167
|
+
marker = File.join(directory, 'unintended')
|
|
168
|
+
hostile = "space\tline\n雪'\"\\$value;`touch #{marker}`$(touch #{marker})"
|
|
169
|
+
values = { 'odd_name' => hostile, empty: '', zero: 0, nil_value: nil,
|
|
170
|
+
list: [false, 0, nil], object: { nested: false }, enabled_flag: true, disabled_flag: false,
|
|
171
|
+
at_file: '@example', escaped_at: '@@example' }
|
|
172
|
+
parameters = values.to_h { |key, _| [key, { type: 'String' }] }
|
|
173
|
+
parameters[:enabled_flag] = parameters[:disabled_flag] = { type: 'Boolean' }
|
|
174
|
+
contract = action.merge(input: action[:input].merge(parameters:))
|
|
175
|
+
generator = renderer(HaveAPI::ClientExamples::RubyCli, base_url: hostile, version: hostile,
|
|
176
|
+
resource_path: [hostile, 'nested'], action_name: hostile, contract:)
|
|
177
|
+
request = sample.merge(path_params: [hostile, '', 0, nil], request: values)
|
|
178
|
+
argv = capture_arguments(generator.example(request), directory)
|
|
179
|
+
expect(argv).to eq(['-u', hostile, '--version', hostile, "#{hostile}.nested", hostile, hostile, '', '0', '', '--',
|
|
180
|
+
'--odd-name', hostile, '--empty', '', '--zero', '0', '--nil-value', '',
|
|
181
|
+
'--list', values[:list].to_s, '--object', values[:object].to_s,
|
|
182
|
+
'--enabled-flag', '--no-disabled_flag', '--at-file', '@example', '--escaped-at', '@@example'])
|
|
183
|
+
expect(File.exist?(marker)).to be(false)
|
|
184
|
+
malicious_option = "option'; touch #{marker}; #"
|
|
185
|
+
contract[:input][:parameters][malicious_option] = { type: 'String' }
|
|
186
|
+
argv = capture_arguments(generator.example(sample.merge(request: { malicious_option => hostile })), directory)
|
|
187
|
+
expect(argv.last(3)).to eq(['--', "--#{malicious_option.tr('_', '-')}", hostile])
|
|
188
|
+
expect(File.exist?(marker)).to be(false)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it 'preserves curl methods, paths and complete JSON values without shell evaluation' do
|
|
193
|
+
Dir.mktmpdir('haveapi-shell-') do |directory|
|
|
194
|
+
marker = File.join(directory, 'unintended')
|
|
195
|
+
hostile = "\t\n雪'\"\\$value;`touch #{marker}`$(touch #{marker})"
|
|
196
|
+
values = { name: hostile, empty: '', enabled: false, zero: 0, nullable: nil,
|
|
197
|
+
nested: { list: [false, 0, nil, '', hostile] } }
|
|
198
|
+
contract = action.merge(method: hostile, path: "/widgets/{widget_id}#{hostile}")
|
|
199
|
+
argv = capture_arguments(renderer(HaveAPI::ClientExamples::Curl, contract:).request(sample.merge(path_params: [hostile], request: values)), directory)
|
|
200
|
+
expect(argv).to eq(['--request', hostile, '--data-binary', JSON.generate(widget: values),
|
|
201
|
+
"https://docs.example/api/widgets/#{hostile}#{hostile}"])
|
|
202
|
+
expect(JSON.parse(argv.fetch(3))).to eq(JSON.parse(JSON.generate(widget: values)))
|
|
203
|
+
expect(File.exist?(marker)).to be(false)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it 'keeps curl GET query construction and body semantics intact' do
|
|
208
|
+
Dir.mktmpdir('haveapi-shell-') do |directory|
|
|
209
|
+
values = { name: 'a &雪', enabled: false, zero: 0, nullable: nil }
|
|
210
|
+
argv = capture_arguments(renderer(HaveAPI::ClientExamples::Curl, contract: action.merge(method: 'GET')).request(sample.merge(request: values)), directory)
|
|
211
|
+
expect(argv).to eq(['--request', 'GET', '--data-binary', JSON.generate(widget: values),
|
|
212
|
+
'https://docs.example/api/widgets/7?name=a+%26%E9%9B%AA&enabled=false&zero=0&nullable='])
|
|
213
|
+
expect(JSON.parse(argv.fetch(3))).to eq(JSON.parse(JSON.generate(widget: values)))
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'encodes curl initialization and auth URLs, token headers and login values once' do
|
|
218
|
+
Dir.mktmpdir('haveapi-shell-') do |directory|
|
|
219
|
+
marker = File.join(directory, 'unintended')
|
|
220
|
+
hostile = "\n雪'\"\\$value;`touch #{marker}`$(touch #{marker})"
|
|
221
|
+
generator = renderer(HaveAPI::ClientExamples::Curl, base_url: "https://docs.example/#{hostile}/", version: hostile)
|
|
222
|
+
base = generator.base_url
|
|
223
|
+
expect(capture_arguments(generator.init, directory)).to eq(['--request', 'OPTIONS', generator.version_url])
|
|
224
|
+
basic = generator.auth(:basic, {})
|
|
225
|
+
first = basic.split("\n# Password: secret", 2).first.split('$ ', 2).last
|
|
226
|
+
last = basic.split('# Password given on the command line', 2).last.split('$ ', 2).last
|
|
227
|
+
expect(capture_arguments("$ #{first}", directory)).to eq(['--request', 'OPTIONS', '--user', 'user', base])
|
|
228
|
+
expect(capture_arguments("$ #{last}", directory)).to eq(['--request', 'OPTIONS', '--user', 'user:secret', base])
|
|
229
|
+
description = { http_header: hostile, resources: { token: { actions: { request: { input: { parameters: {
|
|
230
|
+
hostile => { type: 'String', required: true }, lifetime: { type: 'String', required: true }
|
|
231
|
+
} } } } } } }
|
|
232
|
+
token = generator.auth(:token, description)
|
|
233
|
+
acquire, reuse = token.split("\n\n# Use a previously acquired token", 2)
|
|
234
|
+
acquire = acquire.split('$ ', 2).last
|
|
235
|
+
reuse = reuse.split('$ ', 2).last
|
|
236
|
+
expect(capture_arguments("$ #{acquire}", directory)).to eq(['--request', 'POST', '--header', 'Content-Type: application/json',
|
|
237
|
+
'--data-binary', JSON.generate(token: { hostile => hostile, lifetime: 'fixed' }),
|
|
238
|
+
File.join(base, '_auth', 'token', 'tokens')])
|
|
239
|
+
expect(capture_arguments("$ #{reuse}", directory)).to eq(['--request', 'OPTIONS', '--header', "#{hostile}: example-token", base])
|
|
240
|
+
expect(File.exist?(marker)).to be(false)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'encodes CLI Basic and token command spans without changing prompts or token options' do
|
|
245
|
+
Dir.mktmpdir('haveapi-shell-') do |directory|
|
|
246
|
+
marker = File.join(directory, 'unintended')
|
|
247
|
+
hostile = "\n雪'\"\\$value;`touch #{marker}`$(touch #{marker})"
|
|
248
|
+
generator = renderer(HaveAPI::ClientExamples::RubyCli, base_url: hostile, version: hostile)
|
|
249
|
+
basic = generator.auth(:basic, {})
|
|
250
|
+
first, last = basic.split("\n\n# If username", 2)
|
|
251
|
+
first = first.split('$ ', 2).last
|
|
252
|
+
last = last.split('$ ', 2).last.split("\n# Password: secret", 2).first
|
|
253
|
+
prefix = ['-u', hostile, '--version', hostile]
|
|
254
|
+
expect(capture_arguments("$ #{first}", directory)).to eq(prefix + %w[--auth basic --user user --password secret])
|
|
255
|
+
expect(capture_arguments("$ #{last}", directory)).to eq(prefix + %w[--auth basic --user user])
|
|
256
|
+
description = { resources: { token: { actions: { request: { input: { parameters: {
|
|
257
|
+
hostile => { type: 'String', required: true }, lifetime: { type: 'String', required: true }, password: { type: 'String' }
|
|
258
|
+
} } } } } } }
|
|
259
|
+
token = generator.auth(:token, description)
|
|
260
|
+
first = token.split("\n# Password: secret", 2).first.split('$ ', 2).last
|
|
261
|
+
last = token.split('# nor password and be authenticated', 2).last.split('$ ', 2).last
|
|
262
|
+
expect(capture_arguments("$ #{first}", directory)).to eq(prefix + ['--auth', 'token', "--#{hostile}", hostile, '--lifetime', 'fixed', '--save', 'user', 'current'])
|
|
263
|
+
expect(capture_arguments("$ #{last}", directory)).to eq(prefix + %w[user current])
|
|
264
|
+
expect(token).to include('# Password: secret')
|
|
265
|
+
expect(File.exist?(marker)).to be(false)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
@@ -9,6 +9,7 @@ require 'haveapi/client_examples/http'
|
|
|
9
9
|
require 'haveapi/client_examples/js_client'
|
|
10
10
|
require 'haveapi/client_examples/php_client'
|
|
11
11
|
require 'haveapi/client_examples/ruby_client'
|
|
12
|
+
require 'haveapi/client'
|
|
12
13
|
|
|
13
14
|
describe HaveAPI::ClientExamples do
|
|
14
15
|
let(:action) do
|
|
@@ -70,6 +71,14 @@ describe HaveAPI::ClientExamples do
|
|
|
70
71
|
end
|
|
71
72
|
end
|
|
72
73
|
|
|
74
|
+
def execute_ruby(source)
|
|
75
|
+
Tempfile.create(['haveapi-handler-', '.rb']) do |file|
|
|
76
|
+
file.write(source)
|
|
77
|
+
file.flush
|
|
78
|
+
load file.path
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
73
82
|
it 'generates valid JavaScript' do
|
|
74
83
|
result = HaveAPI::ClientExamples::JsClient.new(*client_args).example(sample)
|
|
75
84
|
source = result.is_a?(Hash) ? result.fetch(:code) : result
|
|
@@ -98,6 +107,18 @@ describe HaveAPI::ClientExamples do
|
|
|
98
107
|
expect(status).to be_success, output
|
|
99
108
|
end
|
|
100
109
|
|
|
110
|
+
it 'round-trips PHP data literals through plain and Thorough source without changing control bytes' do
|
|
111
|
+
renderer = HaveAPI::ClientExamples::PhpClient.new(*client_args)
|
|
112
|
+
values = ['', '$user', "quote'\"\\", "line\n indented\r\n\t$value\\\x00\x1f\x7f ž", false, nil, 0]
|
|
113
|
+
body = "echo json_encode(#{renderer.value(values)});"
|
|
114
|
+
[body, renderer.with_error_handling(body)].each do |source|
|
|
115
|
+
output, status = Open3.capture2e('php', stdin_data: "<?php\n#{source}")
|
|
116
|
+
expect(status).to be_success, output
|
|
117
|
+
expect(JSON.parse(output)).to eq(values)
|
|
118
|
+
end
|
|
119
|
+
expect(renderer.value('public literal')).to eq("'public literal'")
|
|
120
|
+
end
|
|
121
|
+
|
|
101
122
|
it 'generates shell-parseable curl requests' do
|
|
102
123
|
source = HaveAPI::ClientExamples::Curl.new(*client_args).request(sample)
|
|
103
124
|
status, output = syntax_check(%w[bash -n], source, '.sh')
|
|
@@ -131,4 +152,175 @@ describe HaveAPI::ClientExamples do
|
|
|
131
152
|
expect(content_length).to eq(response_body.bytesize)
|
|
132
153
|
expect(JSON.parse(response_body)).to include('status' => true)
|
|
133
154
|
end
|
|
155
|
+
|
|
156
|
+
it 'runs the Ruby handlers with actual client exception classes and propagates unknown errors' do
|
|
157
|
+
context = HaveAPI::ClientExample::Context.new(version: '1', host: 'api.test', base_url: 'https://api.test/',
|
|
158
|
+
version_url: 'https://api.test/v1/', description: {},
|
|
159
|
+
resource_path: ['widget'], action_name: 'update', action:, sample:, detail: :thorough)
|
|
160
|
+
client_type = Class.new(HaveAPI::Client::Client) { attr_accessor :widget }
|
|
161
|
+
client = instance_double(client_type, setup: nil, authenticate: nil)
|
|
162
|
+
allow(HaveAPI::Client).to receive(:new).and_return(client)
|
|
163
|
+
failure_action = Object.new
|
|
164
|
+
def failure_action.client_message(_key, **values)
|
|
165
|
+
values[:message] || 'validation failure'
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def failure_action.name
|
|
169
|
+
'update'
|
|
170
|
+
end
|
|
171
|
+
response = HaveAPI::Client::Response.new(failure_action, { status: false, message: 'API failure', errors: { count: ['bad'] } })
|
|
172
|
+
errors = [HaveAPI::Client::ValidationError.new(failure_action, { count: ['bad'] }),
|
|
173
|
+
HaveAPI::Client::ActionFailed.new(response),
|
|
174
|
+
HaveAPI::Client::Authentication::AuthenticationFailed.new('authentication failure'),
|
|
175
|
+
HaveAPI::Client::ProtocolError.new('protocol failure'), RestClient::Exception.new,
|
|
176
|
+
JSON::ParserError.new('invalid JSON')]
|
|
177
|
+
source = HaveAPI::ClientExamples::RubyClient.render(:init, context:).fetch(:code)
|
|
178
|
+
errors.each do |error|
|
|
179
|
+
allow(client).to receive(:setup).and_raise(error)
|
|
180
|
+
expect { execute_ruby(source) }.to output(/./).to_stderr
|
|
181
|
+
end
|
|
182
|
+
expect(client).to have_received(:setup).exactly(errors.length).times
|
|
183
|
+
allow(client).to receive(:setup).and_return(nil)
|
|
184
|
+
allow(client).to receive(:authenticate).and_raise(errors[2])
|
|
185
|
+
auth = HaveAPI::ClientExamples::RubyClient.render(:auth, context:, auth_method: :basic, auth_description: {}).fetch(:code)
|
|
186
|
+
expect { execute_ruby(auth) }.to output(/authentication failure/).to_stderr
|
|
187
|
+
allow(client).to receive(:setup).and_return(nil)
|
|
188
|
+
widget_type = Class.new do
|
|
189
|
+
def update(*); end
|
|
190
|
+
end
|
|
191
|
+
widget = instance_double(widget_type)
|
|
192
|
+
allow(client).to receive(:widget).and_return(widget)
|
|
193
|
+
allow(widget).to receive(:update).and_raise(errors[0])
|
|
194
|
+
action_source = HaveAPI::ClientExamples::RubyClient.render(:example, context:).fetch(:code)
|
|
195
|
+
expect { execute_ruby(action_source) }.to output(/validation failure/).to_stderr
|
|
196
|
+
allow(client).to receive(:setup).and_raise(RuntimeError, 'unknown dependency failure')
|
|
197
|
+
expect { execute_ruby(source) }.to raise_error(RuntimeError, 'unknown dependency failure')
|
|
198
|
+
expect(source.index('ValidationError')).to be < source.index('ActionFailed')
|
|
199
|
+
expect(source).to include('error.response.is_a?(HaveAPI::Client::Response)')
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it 'runs PHP handlers with real framework error objects and rethrows unknown dependency exceptions' do
|
|
203
|
+
root = File.expand_path('../../../..', __dir__)
|
|
204
|
+
renderer = HaveAPI::ClientExamples::PhpClient.new(*client_args)
|
|
205
|
+
prefix = <<~PHP
|
|
206
|
+
<?php
|
|
207
|
+
require #{renderer.value(File.join(root, 'clients/php/src/Client/Exception/Base.php'))};
|
|
208
|
+
require #{renderer.value(File.join(root, 'clients/php/src/Client/Exception/ValidationError.php'))};
|
|
209
|
+
require #{renderer.value(File.join(root, 'clients/php/src/Client/Exception/ActionFailed.php'))};
|
|
210
|
+
require #{renderer.value(File.join(root, 'clients/php/src/Client/Exception/AuthenticationFailed.php'))};
|
|
211
|
+
require #{renderer.value(File.join(root, 'clients/php/src/Client/Exception/ProtocolError.php'))};
|
|
212
|
+
require #{renderer.value(File.join(root, 'clients/php/src/Client/Response.php'))};
|
|
213
|
+
$wire = (object) ['code' => 400, 'body' => (object) ['status' => false, 'message' => 'API failure', 'errors' => ['count' => ['bad']]]];
|
|
214
|
+
$response = new \\HaveAPI\\Client\\Response(null, $wire, 0);
|
|
215
|
+
PHP
|
|
216
|
+
errors = [
|
|
217
|
+
'new \\HaveAPI\\Client\\Exception\\ValidationError("update", ["count" => ["bad"]])',
|
|
218
|
+
'new \\HaveAPI\\Client\\Exception\\ActionFailed($response, "API failure")',
|
|
219
|
+
'new \\HaveAPI\\Client\\Exception\\AuthenticationFailed("authentication failure")',
|
|
220
|
+
'new \\HaveAPI\\Client\\Exception\\ProtocolError("protocol failure")'
|
|
221
|
+
]
|
|
222
|
+
errors.each do |error|
|
|
223
|
+
script = prefix + "$failure = #{error};\n" + renderer.with_error_handling('throw $failure;')
|
|
224
|
+
output, status = Open3.capture2e('php', stdin_data: script)
|
|
225
|
+
expect(status).to be_success, output
|
|
226
|
+
expect(output).not_to be_empty
|
|
227
|
+
end
|
|
228
|
+
script = "#{prefix}$failure = new \\RuntimeException('unknown dependency');\ntry {\n" \
|
|
229
|
+
"#{renderer.with_error_handling('throw $failure;')}" \
|
|
230
|
+
"} catch (\\RuntimeException $error) { echo 'UNKNOWN_PROPAGATED'; }\n"
|
|
231
|
+
output, status = Open3.capture2e('php', stdin_data: script)
|
|
232
|
+
expect(status).to be_success, output
|
|
233
|
+
expect(output).to include('UNKNOWN_PROPAGATED')
|
|
234
|
+
context = HaveAPI::ClientExample::Context.new(version: '1', host: 'api.test', base_url: 'https://api.test/',
|
|
235
|
+
version_url: 'https://api.test/v1/', description: {}, detail: :thorough)
|
|
236
|
+
source = HaveAPI::ClientExamples::PhpClient.render(:auth, context:, auth_method: :oauth2, auth_description: {}).fetch(:code)
|
|
237
|
+
status, output = syntax_check(%w[php -l], "<?php\n#{source}", '.php')
|
|
238
|
+
expect(status).to be_success, output
|
|
239
|
+
expect(source).to include('session_start()', 'requestAuthorizationCode()', 'requestAccessToken()', "$_SESSION['access_token']")
|
|
240
|
+
expect(source).not_to include('Throwable', 'getResponse()->http_status')
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it 'keeps Ruby token-login literals truthy through the actual runtime option filter' do
|
|
244
|
+
fields = { user: { type: 'String', required: true }, password: { type: 'String', required: true },
|
|
245
|
+
enabled: { type: 'Boolean', required: true },
|
|
246
|
+
custom: { type: 'Custom', nullable: true, required: false,
|
|
247
|
+
validators: { include: { values: [nil, 'public-choice'] } } } }
|
|
248
|
+
continuation = { confirmation: { type: 'Boolean', required: true, validators: { accept: { value: false } } },
|
|
249
|
+
empty: { type: 'Custom', nullable: true, required: false, validators: { accept: { value: nil } } } }
|
|
250
|
+
description = { resources: { token: { actions: { request: { input: { parameters: fields } },
|
|
251
|
+
verify: { input: { parameters: continuation } } } } } }
|
|
252
|
+
definitions = HaveAPI::Params.new(:input, nil)
|
|
253
|
+
definitions.layout = :hash
|
|
254
|
+
definitions.namespace = false
|
|
255
|
+
definitions.custom(:custom, nullable: true, required: false, default: 'public-choice', fill: true, choices: [nil, 'public-choice'])
|
|
256
|
+
expect(definitions.params.first).to be_optional
|
|
257
|
+
expect(definitions.params.first).to be_nullable
|
|
258
|
+
expect(definitions.validate({ custom: nil })).to eq(custom: nil)
|
|
259
|
+
expect(definitions.validate({})).to eq(custom: 'public-choice')
|
|
260
|
+
before = Marshal.dump(description)
|
|
261
|
+
renderer = HaveAPI::ClientExamples::RubyClient.new(*client_args)
|
|
262
|
+
options = renderer.send(:auth_token_credentials, description).transform_keys(&:to_sym)
|
|
263
|
+
expect(options).to include(enabled: true, custom: 'public-choice')
|
|
264
|
+
finite = Marshal.load(Marshal.dump(description))
|
|
265
|
+
finite[:resources][:token][:actions][:request][:input][:parameters][:enabled][:validators] = { include: { values: [false, true] } }
|
|
266
|
+
expect(renderer.send(:auth_token_credentials, finite)).to include('enabled' => true)
|
|
267
|
+
provider = HaveAPI::Client::Authentication::Token.allocate
|
|
268
|
+
provider.instance_variable_set(:@opts, options)
|
|
269
|
+
provider.instance_variable_set(:@desc, description)
|
|
270
|
+
allow(provider).to receive(:login_step).and_return(:done)
|
|
271
|
+
provider.send(:request_token)
|
|
272
|
+
expect(provider).to have_received(:login_step).with(:request, include(enabled: true, custom: 'public-choice'))
|
|
273
|
+
expect(renderer.send(:auth_token_continuations, description).fetch('verify')).to eq('confirmation' => false, 'empty' => nil)
|
|
274
|
+
expect(Marshal.dump(description)).to eq(before)
|
|
275
|
+
php = HaveAPI::ClientExamples::PhpClient.new(*client_args)
|
|
276
|
+
expect(php.send(:auth_token_credentials, description)).to include('enabled' => false, 'custom' => 'public-choice')
|
|
277
|
+
root = File.expand_path('../../../..', __dir__)
|
|
278
|
+
script = <<~PHP
|
|
279
|
+
<?php
|
|
280
|
+
require #{php.value(File.join(root, 'clients/php/src/Client/Authentication/Base.php'))};
|
|
281
|
+
require #{php.value(File.join(root, 'clients/php/src/Client/Authentication/Token.php'))};
|
|
282
|
+
new class(#{php.value(php.send(:auth_token_credentials, description))}) extends \\HaveAPI\\Client\\Authentication\\Token {
|
|
283
|
+
public function __construct($options) { $this->opts = $options; $this->requestToken(); }
|
|
284
|
+
protected function getRequestCredentials() { return ["user", "password", "enabled", "custom"]; }
|
|
285
|
+
protected function runAuthentication($action, $input) { echo json_encode($input); }
|
|
286
|
+
};
|
|
287
|
+
PHP
|
|
288
|
+
output, status = Open3.capture2e('php', stdin_data: script)
|
|
289
|
+
expect(status).to be_success, output
|
|
290
|
+
expect(JSON.parse(output)).to include('enabled' => false, 'custom' => 'public-choice')
|
|
291
|
+
# The same real PHP method loses explicit null before the request boundary.
|
|
292
|
+
dropped_script = script.sub(php.value(php.send(:auth_token_credentials, description)),
|
|
293
|
+
php.value({ 'user' => 'user', 'password' => 'secret', 'enabled' => false, 'custom' => nil }))
|
|
294
|
+
dropped_output, dropped_status = Open3.capture2e('php', stdin_data: dropped_script)
|
|
295
|
+
expect(dropped_status).to be_success, dropped_output
|
|
296
|
+
dropped = JSON.parse(dropped_output)
|
|
297
|
+
expect(dropped).to include('enabled' => false)
|
|
298
|
+
expect(dropped).not_to have_key('custom')
|
|
299
|
+
|
|
300
|
+
js = HaveAPI::ClientExamples::JsClient.new(*client_args)
|
|
301
|
+
expect(js.send(:auth_token_credentials, description)).to include('enabled' => false, 'custom' => nil)
|
|
302
|
+
expect(HaveAPI::ClientExamples::PythonClient.new(*client_args).send(:auth_token_credentials, description))
|
|
303
|
+
.to include('enabled' => false, 'custom' => nil)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
it 'limits required Ruby false and optional nullable Ruby/PHP null-only login choices' do
|
|
307
|
+
ruby = HaveAPI::ClientExamples::RubyClient
|
|
308
|
+
php = HaveAPI::ClientExamples::PhpClient
|
|
309
|
+
boolean = { type: 'Boolean', required: true, validators: { accept: { value: false } } }
|
|
310
|
+
custom = { type: 'Custom', nullable: true, required: false, validators: { include: { values: [nil] } } }
|
|
311
|
+
context = HaveAPI::ClientExample::Context.new(version: '1', host: 'api.test', base_url: 'https://api.test/',
|
|
312
|
+
version_url: 'https://api.test/v1/', description: {})
|
|
313
|
+
null_acceptance = custom.merge(validators: { accept: { value: nil } })
|
|
314
|
+
[[ruby, boolean], [ruby, custom], [php, custom], [ruby, null_acceptance], [php, null_acceptance],
|
|
315
|
+
[ruby, boolean.merge(required: false)]].each do |renderer, parameter|
|
|
316
|
+
description = { resources: { token: { actions: { request: { input: { parameters: { required_input: parameter } } } } } } }
|
|
317
|
+
%i[simple thorough].each do |detail|
|
|
318
|
+
result = renderer.render(:auth, context: context.with(detail:), auth_method: :token, auth_description: description)
|
|
319
|
+
expect(result).to include(status: :limitation)
|
|
320
|
+
expect(result[:reason]).to include('cannot send')
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
fields = { resources: { token: { actions: { request: { input: { parameters: { enabled: boolean } } } } } } }
|
|
324
|
+
expect(php.render(:auth, context:, auth_method: :token, auth_description: fields)[:status]).to eq(:code)
|
|
325
|
+
end
|
|
134
326
|
end
|
|
@@ -2,6 +2,7 @@ require 'spec_helper'
|
|
|
2
2
|
|
|
3
3
|
class TypeScriptDocumentationAdapterFixture
|
|
4
4
|
def render(kind:, context:, mode:, auth_method: nil, auth_description: nil); end
|
|
5
|
+
def detail_levels_for(kind:, context:, result:, mode:, auth_method: nil, auth_description: nil); end
|
|
5
6
|
end
|
|
6
7
|
|
|
7
8
|
describe HaveAPI::ClientExamples::TsGeneratedClient do
|
|
@@ -17,12 +18,24 @@ describe HaveAPI::ClientExamples::TsGeneratedClient do
|
|
|
17
18
|
|
|
18
19
|
it 'adds a separate generated variant while preserving dynamic TypeScript identifiers' do
|
|
19
20
|
expect(described_class.id).to eq('typescript-generated')
|
|
20
|
-
expect(described_class.label).to eq('TypeScript
|
|
21
|
+
expect(described_class.label).to eq('TypeScript API client')
|
|
21
22
|
expect(described_class.code).to eq(:typescript)
|
|
22
23
|
expect(HaveAPI::ClientExamples::TsClient.id).to eq('typescript-callback')
|
|
23
24
|
expect(HaveAPI::ClientExamples::TsAsyncClient.id).to eq('typescript-async')
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
it 'delegates meaningful-detail queries with the rendered result, authentication and native mode' do
|
|
28
|
+
result = { status: :code, code: 'API client' }.freeze
|
|
29
|
+
allow(adapter).to receive(:detail_levels_for).and_return([:simple].freeze)
|
|
30
|
+
expect(described_class.detail_levels_for(:auth, context:, result:, auth_method: :basic, auth_description: {})).to eq([:simple])
|
|
31
|
+
expect(adapter).to have_received(:detail_levels_for).with(kind: :auth, context:, result:, mode: :async,
|
|
32
|
+
auth_method: :basic, auth_description: {})
|
|
33
|
+
allow(adapter).to receive(:detail_levels_for).and_return(%i[simple thorough].freeze)
|
|
34
|
+
expect(described_class.detail_levels_for(:example, context:, result:)).to eq(%i[simple thorough])
|
|
35
|
+
allow(adapter).to receive(:detail_levels_for).and_return(nil)
|
|
36
|
+
expect { described_class.detail_levels_for(:example, context:, result:) }.to raise_error(ArgumentError)
|
|
37
|
+
end
|
|
38
|
+
|
|
26
39
|
it 'is available only for a matching optional profile' do
|
|
27
40
|
expect(described_class.available?(nil)).to be(false)
|
|
28
41
|
expect(described_class.available?(context.with(profile: nil))).to be(false)
|
|
@@ -42,7 +55,7 @@ describe HaveAPI::ClientExamples::TsGeneratedClient do
|
|
|
42
55
|
|
|
43
56
|
it 'binds only the supplied authorized sample and preserves limitations' do
|
|
44
57
|
sample = { request: { label: 'public' } }
|
|
45
|
-
result = { status: :limitation, reason: 'The saved
|
|
58
|
+
result = { status: :limitation, reason: 'The saved API client does not represent this sample.' }.freeze
|
|
46
59
|
allow(adapter).to receive(:render).and_return(result)
|
|
47
60
|
renderer = described_class.new('example.test', 'https://example.test/', '1.0').with_context(context)
|
|
48
61
|
expect(renderer.example(sample)).to eq(result)
|
|
@@ -58,7 +58,7 @@ describe HaveAPI::ClientExample::Profiles do
|
|
|
58
58
|
expect(server).to have_received(:describe_version).once
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
it 'loads a saved
|
|
61
|
+
it 'loads a saved API client once at registration with explicit package and generator versions' do
|
|
62
62
|
File.write(snapshot_path, JSON.generate(api_version: '1.0'))
|
|
63
63
|
profiles.register('1.0', typescript: { package_name: 'widget-client', package_version: '1.2.3-beta.1+build.4',
|
|
64
64
|
snapshot_path:, generator_version: HaveAPI::VERSION })
|
|
@@ -278,15 +278,20 @@ describe DocAuthFilteringSpec do
|
|
|
278
278
|
end
|
|
279
279
|
|
|
280
280
|
it 'does not retain privileged sample data across HTML documentation requests' do
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
require_relative '../../test_support/documentation_oauth_fixture'
|
|
282
|
+
@api = HaveAPI::Server.new(get_opt(:api_module))
|
|
283
|
+
@api.use_version(1)
|
|
284
|
+
users = %w[admin user].to_h { |name| [name, DocAuthFilteringSpec::User.new(1, name)] }
|
|
285
|
+
fixture = DocumentationOAuthFixture.configure(@api, users: { 1 => users })
|
|
286
|
+
@api.mount('/')
|
|
287
|
+
get "#{fixture.origin}/v1/?resource=secure", {}, 'HTTP_COOKIE' => fixture.header_for(@api, version: 1, token: 'admin')
|
|
283
288
|
expect(last_response.status).to eq(200)
|
|
284
289
|
expect(last_response.body).to include('ADMIN_ONLY_RESULT')
|
|
285
290
|
|
|
286
|
-
|
|
287
|
-
get '/v1/'
|
|
291
|
+
get "#{fixture.origin}/v1/?resource=secure", {}, 'HTTP_COOKIE' => fixture.header_for(@api, version: 1, token: 'user')
|
|
288
292
|
expect(last_response.status).to eq(200)
|
|
289
293
|
expect(last_response.body).not_to include('ADMIN_ONLY_RESULT')
|
|
294
|
+
expect(fixture.lookups).to eq([[1, 'admin'], [1, 'user']])
|
|
290
295
|
end
|
|
291
296
|
end
|
|
292
297
|
end
|