haveapi 0.30.0 → 0.30.2
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 +1 -0
- data/Gemfile +1 -0
- data/benchmarks/documentation.rb +277 -0
- data/benchmarks/endpoints.rb +730 -0
- data/benchmarks/performance_budgets.json +1354 -0
- data/benchmarks/performance_compare.rb +579 -0
- data/doc/client-examples.md +233 -24
- data/doc/performance.md +272 -0
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/documentation_clients.rb +158 -35
- data/lib/haveapi/documentation_context.rb +42 -12
- data/lib/haveapi/documentation_schema.rb +73 -0
- data/lib/haveapi/server.rb +5 -0
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/version_page.erb +2 -2
- data/spec/client_examples/context_performance_spec.rb +72 -0
- data/spec/client_examples/context_spec.rb +206 -0
- data/spec/client_examples/dynamic_js_spec.rb +1 -0
- data/spec/client_examples/profile_mount_spec.rb +215 -0
- data/spec/client_examples/profiles_spec.rb +202 -32
- data/spec/client_examples/structural_schema_spec.rb +48 -0
- data/spec/documentation/client_examples_spec.rb +10 -0
- data/spec/documentation/performance_spec.rb +79 -0
- data/spec/performance/comparator_spec.rb +342 -0
- data/spec/performance/endpoint_contract_spec.rb +226 -0
- data/spec/performance/endpoint_scaling_spec.rb +82 -0
- data/spec/performance/event_baseline_spec.rb +279 -0
- data/spec/performance/json_activation_spec.rb +151 -0
- data/spec/performance/release_lineage_spec.rb +431 -0
- data/spec/performance/runner_spec.rb +476 -0
- data/spec/performance/sqlite_spec.rb +194 -0
- data/spec/performance/support/live_worker.rb +44 -0
- data/spec/performance/support/samples.rb +72 -0
- data/test_support/documentation_performance_fixture.rb +106 -0
- data/test_support/performance_endpoint_fixture.rb +350 -0
- data/test_support/performance_sqlite_fixture.rb +384 -0
- metadata +26 -3
|
@@ -3,22 +3,46 @@ require 'tmpdir'
|
|
|
3
3
|
|
|
4
4
|
describe HaveAPI::ClientExample::Profiles do
|
|
5
5
|
let(:profiles) { described_class.new }
|
|
6
|
+
let(:adapter) do
|
|
7
|
+
Class.new do
|
|
8
|
+
def resolve(**); end
|
|
9
|
+
def render(**); end
|
|
10
|
+
end.new
|
|
11
|
+
end
|
|
6
12
|
let(:directory) { Dir.mktmpdir('haveapi-doc-profiles-') }
|
|
7
|
-
let(:
|
|
8
|
-
let(:
|
|
9
|
-
|
|
13
|
+
let(:description) { { resources: {}, authentication: {}, meta: { namespace: '_meta' } } }
|
|
14
|
+
let(:server) { instance_double(HaveAPI::Server, versions: [1], root: '/', describe_version: description) }
|
|
15
|
+
|
|
16
|
+
def description_path
|
|
17
|
+
File.join(directory, 'description.json')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def config_path
|
|
21
|
+
File.join(directory, 'generator.toml')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def go_config
|
|
25
|
+
{ import_path: 'example.test/widget/client', module_version: 'v1.2.3', description_path:, generator_version: HaveAPI::VERSION }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def rust_config
|
|
29
|
+
{ snapshot_path: description_path, generator_config_path: config_path, generator_version: HaveAPI::VERSION }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def generator_config
|
|
33
|
+
{ 'crate_name' => 'widget-client', 'crate_version' => '1.2.3' }
|
|
10
34
|
end
|
|
11
|
-
let(:adapter) { Object.new }
|
|
12
35
|
|
|
13
36
|
before do
|
|
14
|
-
File.write(description_path, JSON.generate(
|
|
37
|
+
File.write(description_path, JSON.generate(description))
|
|
38
|
+
File.write(config_path, 'fixture config')
|
|
15
39
|
end
|
|
16
40
|
|
|
17
41
|
after { FileUtils.remove_entry(directory) }
|
|
18
42
|
|
|
19
|
-
def install_go_adapter
|
|
43
|
+
def install_go_adapter(version: HaveAPI::VERSION)
|
|
20
44
|
generator = Module.new
|
|
21
|
-
generator.const_set(:VERSION,
|
|
45
|
+
generator.const_set(:VERSION, version)
|
|
22
46
|
documentation = Class.new
|
|
23
47
|
generator.const_set(:Documentation, documentation)
|
|
24
48
|
stub_const('HaveAPI::GoClient', generator)
|
|
@@ -27,32 +51,89 @@ describe HaveAPI::ClientExample::Profiles do
|
|
|
27
51
|
documentation
|
|
28
52
|
end
|
|
29
53
|
|
|
30
|
-
|
|
31
|
-
|
|
54
|
+
def install_rust_adapter(factories: true)
|
|
55
|
+
generator = Module.new
|
|
56
|
+
generator.const_set(:VERSION, HaveAPI::VERSION)
|
|
57
|
+
documentation = Class.new
|
|
58
|
+
generator.const_set(:Documentation, documentation)
|
|
59
|
+
generator.const_set(:Generator, Class.new)
|
|
60
|
+
generator.const_set(:Model, Module.new)
|
|
61
|
+
generator.const_set(:Snapshot, Class.new)
|
|
62
|
+
stub_const('HaveAPI::RustClient', generator)
|
|
63
|
+
stub_const('Tomlrb', Module.new)
|
|
64
|
+
allow(Tomlrb).to receive(:load_file).with(config_path).and_return(generator_config)
|
|
65
|
+
allow(profiles).to receive(:require).with('haveapi/rust_client/documentation').and_return(true)
|
|
66
|
+
allow(documentation).to receive(:new).and_return(adapter)
|
|
67
|
+
if factories
|
|
68
|
+
allow(documentation).to receive_messages(from_description: adapter, from_snapshot: adapter)
|
|
69
|
+
allow(documentation).to receive(:validate_source) { |source| source }
|
|
70
|
+
allow(HaveAPI::RustClient::Generator).to receive(:validate_config)
|
|
71
|
+
allow(HaveAPI::RustClient::Model).to receive(:validate_package)
|
|
72
|
+
end
|
|
73
|
+
documentation
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def finalize
|
|
32
77
|
profiles.mount([1])
|
|
78
|
+
profiles.finalize(server)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'needs no optional generator or schema when profiles are absent' do
|
|
82
|
+
allow(profiles).to receive(:require).and_raise('Unexpected optional dependency')
|
|
83
|
+
finalize
|
|
33
84
|
expect(profiles).not_to have_received(:require)
|
|
85
|
+
expect(server).not_to have_received(:describe_version)
|
|
34
86
|
expect(profiles.for_version(1)).to eq({})
|
|
35
87
|
expect(profiles.for_version(1)).to be_frozen
|
|
36
88
|
end
|
|
37
89
|
|
|
38
|
-
it 'loads
|
|
90
|
+
it 'loads an explicit saved adapter once without rereading it during finalization' do
|
|
39
91
|
documentation = install_go_adapter
|
|
40
92
|
profiles.register(1, go: go_config)
|
|
41
|
-
profiles.
|
|
93
|
+
expect(profiles.for_version(1)).to be_empty
|
|
42
94
|
File.unlink(description_path)
|
|
95
|
+
finalize
|
|
43
96
|
profile = profiles.for_version(1).fetch(:go)
|
|
44
97
|
expect(profile).to have_attributes(language: :go, package_name: 'example.test/widget/client', package_version: 'v1.2.3')
|
|
45
98
|
expect(profile).to be_frozen
|
|
46
99
|
expect(profile.adapter).to equal(adapter)
|
|
47
|
-
expect(documentation).to have_received(:new).once.with(description:
|
|
100
|
+
expect(documentation).to have_received(:new).once.with(description: JSON.parse(JSON.generate(description)), import_path: 'example.test/widget/client', module_version: 'v1.2.3')
|
|
101
|
+
expect(server).not_to have_received(:describe_version)
|
|
48
102
|
expect(profile).not_to respond_to(:description_path)
|
|
49
103
|
end
|
|
50
104
|
|
|
105
|
+
it 'defaults the generator version and defers automatic Go without inventing an SDK version' do
|
|
106
|
+
documentation = install_go_adapter
|
|
107
|
+
config = { import_path: 'example.test/widget/client' }
|
|
108
|
+
profiles.register(1, go: config)
|
|
109
|
+
config[:import_path].replace('changed.test/client')
|
|
110
|
+
expect(documentation).not_to have_received(:new)
|
|
111
|
+
finalize
|
|
112
|
+
profile = profiles.for_version(1).fetch(:go)
|
|
113
|
+
expect(profile.package_name).to eq('example.test/widget/client')
|
|
114
|
+
expect(profile.package_version).to be_nil
|
|
115
|
+
expect(documentation).to have_received(:new).with(description: JSON.parse(JSON.generate(description)), import_path: 'example.test/widget/client', module_version: nil)
|
|
116
|
+
expect(server).to have_received(:describe_version).once
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'permits nil SDK versions and retains explicitly pinned generator versions' do
|
|
120
|
+
documentation = install_go_adapter(version: '0.29.0')
|
|
121
|
+
profiles.register(1, go: go_config.merge(module_version: nil, generator_version: '0.29.0'))
|
|
122
|
+
finalize
|
|
123
|
+
expect(profiles.for_version(1).fetch(:go).package_version).to be_nil
|
|
124
|
+
expect(documentation).to have_received(:new).with(description: anything, import_path: anything, module_version: nil)
|
|
125
|
+
end
|
|
126
|
+
|
|
51
127
|
it 'rejects unknown languages, malformed fields and unsafe package configuration' do
|
|
52
128
|
expect { profiles.register(1, python: {}) }.to raise_error(described_class::Invalid, /Unknown/)
|
|
53
129
|
expect { profiles.register(1, go: go_config.merge(unexpected: 'value')) }.to raise_error(described_class::Invalid, /fields/)
|
|
54
130
|
expect { profiles.register(1, go: go_config.merge(import_path: "example.test/\nsource")) }.to raise_error(described_class::Invalid, /package/)
|
|
55
|
-
|
|
131
|
+
['latest', '', 1, false].each do |value|
|
|
132
|
+
expect { profiles.register(1, go: go_config.merge(module_version: value)) }.to raise_error(described_class::Invalid, /package/)
|
|
133
|
+
end
|
|
134
|
+
expect { profiles.register(1, go: go_config.merge(generator_version: nil)) }.to raise_error(described_class::Invalid)
|
|
135
|
+
expect { profiles.register(1, go: go_config.merge(generator_version: 'latest')) }.to raise_error(described_class::Invalid, /generator version/)
|
|
136
|
+
expect { profiles.register(1, go: {}) }.to raise_error(described_class::Invalid, /package/)
|
|
56
137
|
end
|
|
57
138
|
|
|
58
139
|
it 'rejects missing adapters, mismatched generator versions and missing files at registration' do
|
|
@@ -63,33 +144,122 @@ describe HaveAPI::ClientExample::Profiles do
|
|
|
63
144
|
expect { profiles.register(1, go: go_config.merge(description_path: 'relative.json')) }.to raise_error(described_class::Invalid, /absolute local/)
|
|
64
145
|
end
|
|
65
146
|
|
|
66
|
-
it '
|
|
147
|
+
it 'checks the configured adapter interface' do
|
|
148
|
+
documentation = install_go_adapter
|
|
149
|
+
allow(documentation).to receive(:new).and_return(Object.new)
|
|
150
|
+
expect { profiles.register(1, go: go_config) }.to raise_error(described_class::Invalid, /adapter interface/)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it 'does not leave a partially registered language set after an invalid second profile' do
|
|
154
|
+
install_go_adapter
|
|
155
|
+
expect { profiles.register(1, go: go_config, rust: { unknown: 'field' }) }.to raise_error(described_class::Invalid)
|
|
156
|
+
profiles.register(1, go: go_config)
|
|
157
|
+
finalize
|
|
158
|
+
expect(profiles.for_version(1).keys).to eq([:go])
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'rejects duplicates and registration after mounting starts' do
|
|
67
162
|
install_go_adapter
|
|
68
163
|
profiles.register(1, go: go_config)
|
|
69
164
|
expect { profiles.register('1', go: go_config) }.to raise_error(described_class::Invalid, /Duplicate/)
|
|
70
|
-
expect { profiles.mount([2]) }.to raise_error(described_class::Invalid, /unavailable API version/)
|
|
71
165
|
profiles.mount([1])
|
|
72
166
|
expect { profiles.register(2, go: go_config) }.to raise_error(described_class::Invalid, /already mounted/)
|
|
167
|
+
profiles.finalize(server)
|
|
168
|
+
expect { profiles.finalize(server) }.to raise_error(described_class::Invalid, /not ready/)
|
|
73
169
|
end
|
|
74
170
|
|
|
75
|
-
it '
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
File.write(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
allow(profiles).to receive(:require).with('haveapi/rust_client/documentation').and_return(true)
|
|
89
|
-
allow(documentation).to receive(:new).and_return(adapter)
|
|
90
|
-
profiles.register(1, rust: { snapshot_path: description_path, generator_config_path: config_path, generator_version: '0.30.0' })
|
|
91
|
-
expect(documentation).to have_received(:new).with(snapshot: { 'api_version' => '2' }, config:)
|
|
171
|
+
it 'rejects profiles for an unavailable version' do
|
|
172
|
+
install_go_adapter
|
|
173
|
+
profiles.register(1, go: go_config)
|
|
174
|
+
expect { profiles.mount([2]) }.to raise_error(described_class::Invalid, /unavailable API version/)
|
|
175
|
+
expect(profiles.for_version(1)).to be_empty
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it 'preserves legacy Rust construction even without the new factories' do
|
|
179
|
+
documentation = install_rust_adapter(factories: false)
|
|
180
|
+
File.write(description_path, JSON.generate({ api_version: '1' }))
|
|
181
|
+
profiles.register(1, rust: rust_config)
|
|
182
|
+
finalize
|
|
183
|
+
expect(documentation).to have_received(:new).with(snapshot: { 'api_version' => '1' }, config: generator_config)
|
|
92
184
|
expect(profiles.for_version(1).fetch(:rust)).to have_attributes(package_name: 'widget-client', package_version: '1.2.3')
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it 'rejects a saved Rust snapshot for a different selected API version at mount' do
|
|
188
|
+
install_rust_adapter
|
|
189
|
+
File.write(description_path, JSON.generate({ api_version: '2' }))
|
|
190
|
+
profiles.register(1, rust: rust_config)
|
|
93
191
|
expect { profiles.mount([1]) }.to raise_error(described_class::Invalid, /snapshot API version mismatch/)
|
|
192
|
+
expect(profiles.for_version(1)).to be_empty
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'validates inline Rust identity and source before building from the mounted description' do
|
|
196
|
+
documentation = install_rust_adapter
|
|
197
|
+
source = { git: 'https://example.test/widget-client', rev: 'a' * 40 }
|
|
198
|
+
profiles.register(1, rust: { crate_name: 'widget-client', source: })
|
|
199
|
+
expect(HaveAPI::RustClient::Model).to have_received(:validate_package).with(crate_name: 'widget-client', crate_version: nil)
|
|
200
|
+
expect(documentation).to have_received(:validate_source).with(source)
|
|
201
|
+
expect(documentation).not_to have_received(:from_description)
|
|
202
|
+
finalize
|
|
203
|
+
expect(documentation).to have_received(:from_description).with(description: anything, api_version: '1', protocol_version: HaveAPI::PROTOCOL_VERSION,
|
|
204
|
+
mount_path: '/', crate_name: 'widget-client', crate_version: nil, source:)
|
|
205
|
+
expect(profiles.for_version(1).fetch(:rust).package_version).to be_nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it 'supports an inline Rust identity with a saved snapshot and optional explicit SDK version' do
|
|
209
|
+
documentation = install_rust_adapter
|
|
210
|
+
snapshot = { 'api_version' => '1' }
|
|
211
|
+
File.write(description_path, JSON.generate(snapshot))
|
|
212
|
+
profiles.register(1, rust: { snapshot_path: description_path, crate_name: 'widget-client', crate_version: '1.2.3' })
|
|
213
|
+
finalize
|
|
214
|
+
expect(documentation).to have_received(:from_snapshot).with(snapshot:, crate_name: 'widget-client', crate_version: '1.2.3', source: nil)
|
|
215
|
+
expect(server).not_to have_received(:describe_version)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it 'validates full legacy TOML before using it with an automatic Rust description' do
|
|
219
|
+
documentation = install_rust_adapter
|
|
220
|
+
snapshot = { 'api_version' => '1' }
|
|
221
|
+
allow(HaveAPI::RustClient::Snapshot).to receive(:from_description).and_return(snapshot)
|
|
222
|
+
profiles.register(1, rust: { generator_config_path: config_path })
|
|
223
|
+
expect(HaveAPI::RustClient::Generator).to have_received(:validate_config).with(generator_config)
|
|
224
|
+
finalize
|
|
225
|
+
expect(HaveAPI::RustClient::Snapshot).to have_received(:from_description).with(description: anything, api_version: '1', protocol_version: HaveAPI::PROTOCOL_VERSION,
|
|
226
|
+
mount_path: '/', source_url: 'https://haveapi.invalid/v1/', profile: 'server-documentation')
|
|
227
|
+
expect(documentation).to have_received(:new).with(snapshot:, config: generator_config)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it 'rejects automatic and inline modes when an older Rust generator lacks their factories' do
|
|
231
|
+
install_rust_adapter(factories: false)
|
|
232
|
+
expect { profiles.register(1, rust: { generator_config_path: config_path }) }.to raise_error(described_class::Invalid, /does not support/)
|
|
233
|
+
expect { profiles.register(1, rust: { crate_name: 'widget-client', snapshot_path: description_path }) }.to raise_error(described_class::Invalid, /does not support/)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it 'rejects conflicting Rust identities and unknown source profile fields' do
|
|
237
|
+
install_rust_adapter
|
|
238
|
+
expect { profiles.register(1, rust: rust_config.merge(crate_name: 'widget-client')) }.to raise_error(described_class::Invalid, /Conflicting/)
|
|
239
|
+
expect { profiles.register(1, rust: rust_config.merge(crate_version: nil)) }.to raise_error(described_class::Invalid, /Conflicting/)
|
|
240
|
+
expect { profiles.register(1, rust: { crate_name: 'widget-client', branch: 'main' }) }.to raise_error(described_class::Invalid, /fields/)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it 'captures one private structural schema per version shared by automatic profiles' do
|
|
244
|
+
go = install_go_adapter
|
|
245
|
+
rust = install_rust_adapter
|
|
246
|
+
profiles.register(1, go: { import_path: 'example.test/client' }, rust: { crate_name: 'widget-client' })
|
|
247
|
+
finalize
|
|
248
|
+
expect(server).to have_received(:describe_version).once do |context|
|
|
249
|
+
expect(context).to have_attributes(server:, version: 1, current_user: nil, endpoint: false, doc: true)
|
|
250
|
+
end
|
|
251
|
+
expect(go).to have_received(:new).with(description: anything, import_path: anything, module_version: nil)
|
|
252
|
+
expect(rust).to have_received(:from_description).with(description: anything, api_version: '1', protocol_version: anything, mount_path: '/', crate_name: anything, crate_version: nil, source: nil)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it 'publishes no partial profiles and hides private diagnostics when finalization fails' do
|
|
256
|
+
install_go_adapter
|
|
257
|
+
rust = install_rust_adapter
|
|
258
|
+
allow(rust).to receive(:from_description).and_raise('HIDDEN_SCHEMA_DEFAULT')
|
|
259
|
+
profiles.register(1, go: { import_path: 'example.test/client' }, rust: { crate_name: 'widget-client' })
|
|
260
|
+
profiles.mount([1])
|
|
261
|
+
expect { profiles.finalize(server) }.to raise_error(described_class::Invalid, 'Configured documentation client schema or configuration is invalid')
|
|
262
|
+
expect(profiles.for_version(1)).to be_empty
|
|
263
|
+
expect { profiles.register(1, go: go_config) }.to raise_error(described_class::Invalid, /already mounted/)
|
|
94
264
|
end
|
|
95
265
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe HaveAPI::ClientExample::Profiles do
|
|
4
|
+
let(:schema) { HaveAPI::ClientExample.const_get(:StructuralSchema) }
|
|
5
|
+
let(:data) { { description: 'data description', default: { examples: ['data example'] }, message: 'data message' } }
|
|
6
|
+
let(:parameter) do
|
|
7
|
+
{ type: 'Custom', default: data, description: 'private prose', label: 'private label',
|
|
8
|
+
validators: { include: { values: [data], message: 'private validation message' }, custom: data }, format: data }
|
|
9
|
+
end
|
|
10
|
+
let(:description) do
|
|
11
|
+
action = { path: '/v1/widgets/', input: { layout: :hash, parameters: { default: parameter } },
|
|
12
|
+
output: { parameters: {} }, meta: { global: { input: { parameters: { example: parameter } } } },
|
|
13
|
+
description: 'private action prose', examples: [{ request: data }] }
|
|
14
|
+
resource = { description: 'private resource prose', actions: { index: action }, resources: {} }
|
|
15
|
+
{ resources: { widget: resource }, authentication: { token: { description: 'private auth prose', resources: { token: resource } } } }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'copies structural locations and strips defaults and prose without changing the server description' do
|
|
19
|
+
before = JSON.generate(description)
|
|
20
|
+
result = schema.copy(description)
|
|
21
|
+
action = result.dig('resources', 'widget', 'actions', 'index')
|
|
22
|
+
expect(action).not_to have_key('description')
|
|
23
|
+
expect(action).not_to have_key('examples')
|
|
24
|
+
expect(result.dig('resources', 'widget')).not_to have_key('description')
|
|
25
|
+
expect(result.dig('authentication', 'token')).not_to have_key('description')
|
|
26
|
+
expect(action.dig('input', 'parameters', 'default')).not_to have_key('default')
|
|
27
|
+
expect(action.dig('meta', 'global', 'input', 'parameters', 'example')).not_to have_key('default')
|
|
28
|
+
expect(result.dig('authentication', 'token', 'resources', 'token', 'actions', 'index')).not_to have_key('examples')
|
|
29
|
+
expect(JSON.generate(description)).to eq(before)
|
|
30
|
+
expect(description).not_to be_frozen
|
|
31
|
+
expect(result).to be_frozen
|
|
32
|
+
expect(action.dig('input', 'parameters', 'default')).to be_frozen
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'retains parameter names, layouts, associations and arbitrary validator/Custom data verbatim' do
|
|
36
|
+
parameter[:resource] = %w[widget]
|
|
37
|
+
parameter[:choices] = { path: '/v1/widgets/', method: 'GET' }
|
|
38
|
+
result = schema.copy(description)
|
|
39
|
+
copied = result.dig('resources', 'widget', 'actions', 'index', 'input', 'parameters', 'default')
|
|
40
|
+
expect(copied).to include('type' => 'Custom', 'resource' => ['widget'], 'choices' => { 'path' => '/v1/widgets/', 'method' => 'GET' })
|
|
41
|
+
expect(copied['format']).to eq(JSON.parse(JSON.generate(data)))
|
|
42
|
+
expect(copied.dig('validators', 'include', 'values')).to eq([JSON.parse(JSON.generate(data))])
|
|
43
|
+
expect(copied.dig('validators', 'custom')).to eq(JSON.parse(JSON.generate(data)))
|
|
44
|
+
expect(copied.dig('validators', 'include')).not_to have_key('message')
|
|
45
|
+
expect(result.dig('resources', 'widget', 'actions', 'index', 'input', 'layout')).to eq('hash')
|
|
46
|
+
expect(result.dig('resources', 'widget', 'actions', 'index', 'path')).to eq('/v1/widgets/')
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -79,4 +79,14 @@ describe HaveAPI::ClientExample do
|
|
|
79
79
|
expect(selected.description[:resources]).to have_key('widget')
|
|
80
80
|
expect(selected.sample[:response]).to eq(name: 'documented widget')
|
|
81
81
|
end
|
|
82
|
+
|
|
83
|
+
it 'prints a package identity without inventing an omitted version and escapes it once' do
|
|
84
|
+
profile = HaveAPI::ClientExample::Profile.new(language: :go, package_name: 'example.test/<client>&', package_version: nil, adapter: Object.new)
|
|
85
|
+
allow(app.settings.api_server).to receive(:documentation_client_profiles).and_return({ go: profile })
|
|
86
|
+
allow(HaveAPI::ClientExamples::GoClient).to receive(:render).and_return({ status: :code, code: 'example' })
|
|
87
|
+
get '/v1/'
|
|
88
|
+
expect(last_response.status).to eq(200)
|
|
89
|
+
expect(last_response.body).to include('<code>example.test/<client>&</code></li>')
|
|
90
|
+
expect(last_response.body).not_to include('example.test/<client>', '&lt;', 'version = "latest"')
|
|
91
|
+
end
|
|
82
92
|
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'base64'
|
|
3
|
+
require_relative '../../test_support/documentation_performance_fixture'
|
|
4
|
+
|
|
5
|
+
describe HaveAPI::Server, '#mount' do
|
|
6
|
+
let(:server) do
|
|
7
|
+
HaveAPI::DocumentationPerformanceFixture.build(resource_count: 3, actions_per_resource: 2)
|
|
8
|
+
end
|
|
9
|
+
let(:contexts) { [] }
|
|
10
|
+
let(:help_inputs) { [] }
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
HaveAPI::ClientExample.clients.each do |client|
|
|
14
|
+
allow(client).to receive(:render).and_wrap_original do |original, kind, context:, **arguments|
|
|
15
|
+
contexts << context
|
|
16
|
+
original.call(kind, context:, **arguments)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
allow(server).to receive(:describe_version).and_wrap_original do |original, *arguments|
|
|
20
|
+
original.call(*arguments).tap do |help|
|
|
21
|
+
help_inputs << [help, Marshal.dump(help)]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def request_as(user)
|
|
27
|
+
contexts.clear
|
|
28
|
+
Rack::MockRequest.new(server.app).get(
|
|
29
|
+
'/benchmark/api/v1/', 'HTTP_HOST' => 'example.test',
|
|
30
|
+
'HTTP_AUTHORIZATION' => "Basic #{Base64.strict_encode64("#{user}:fixture-only")}"
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def expect_shared_request(response)
|
|
35
|
+
expect(response.status).to eq(200)
|
|
36
|
+
expect(response.body).to include('PUBLIC_SAMPLE_0', 'example-resource0-action0-0',
|
|
37
|
+
'init-javascript-callback', 'init-javascript-async',
|
|
38
|
+
'init-typescript-callback', 'init-typescript-async',
|
|
39
|
+
'init-python-sync', 'init-python-async')
|
|
40
|
+
expect(contexts.length).to be > 20
|
|
41
|
+
description = contexts.first.description
|
|
42
|
+
contexts.each { |context| expect(context.description).to equal(description) }
|
|
43
|
+
expect(description).to be_frozen
|
|
44
|
+
description
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'shares one description across real panels and isolates sequential admin/member/admin requests' do
|
|
48
|
+
first_admin = request_as('admin')
|
|
49
|
+
admin_description = expect_shared_request(first_admin)
|
|
50
|
+
privileged_markers = %w[ADMIN_ONLY_ACTION ADMIN_ONLY_DEFAULT ADMIN_ONLY_REQUEST ADMIN_ONLY_RESPONSE
|
|
51
|
+
ADMIN_ONLY_ACTION_SAMPLE admin_field admin_only]
|
|
52
|
+
privileged_markers.each { |marker| expect(first_admin.body).to include(marker) }
|
|
53
|
+
|
|
54
|
+
member = request_as('member')
|
|
55
|
+
member_description = expect_shared_request(member)
|
|
56
|
+
expect(member_description).not_to equal(admin_description)
|
|
57
|
+
privileged_markers.each { |marker| expect(member.body).not_to include(marker) }
|
|
58
|
+
expect(JSON.generate(contexts.map { |context| [context.description, context.action, context.sample] }))
|
|
59
|
+
.not_to match(/ADMIN_ONLY_|admin_field|admin_only/)
|
|
60
|
+
expect(member_description[:resources]['resource0'][:actions]).not_to have_key('admin_only')
|
|
61
|
+
selected = contexts.find { |context| context.action_name == 'action0' }
|
|
62
|
+
expect(selected.action[:input][:parameters]).not_to have_key(:admin_field)
|
|
63
|
+
expect(selected.sample[:request]).to include(enabled: false, count: 0, nullable: nil, value0: '')
|
|
64
|
+
|
|
65
|
+
last_admin = request_as('admin')
|
|
66
|
+
restored_description = expect_shared_request(last_admin)
|
|
67
|
+
expect(restored_description).not_to equal(admin_description)
|
|
68
|
+
expect(restored_description).not_to equal(member_description)
|
|
69
|
+
expect(restored_description).to eq(admin_description)
|
|
70
|
+
privileged_markers.each { |marker| expect(last_admin.body).to include(marker) }
|
|
71
|
+
|
|
72
|
+
help_inputs.each do |help, original|
|
|
73
|
+
expect(Marshal.dump(help)).to eq(original)
|
|
74
|
+
expect(help).not_to be_frozen
|
|
75
|
+
expect(help[:resources]).not_to be_frozen
|
|
76
|
+
expect(help[:resources]['resource0'][:actions]['action0'][:examples].first[:request]).not_to be_frozen
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|