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
|
@@ -25,6 +25,212 @@ describe HaveAPI::ClientExample::Context do
|
|
|
25
25
|
expect(child).to be_frozen
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
describe '#with' do
|
|
29
|
+
let(:fields) do
|
|
30
|
+
{
|
|
31
|
+
version: 1, host: 'api.test', base_url: 'https://api.test/mount/',
|
|
32
|
+
version_url: 'https://api.test/mount/v1/',
|
|
33
|
+
description: { 'resources' => { 'widget' => { 'label' => 'Widget' } } },
|
|
34
|
+
resource_path: [:widget, 2], resource: { 'label' => 'Widget' },
|
|
35
|
+
action_name: :show, action: { 'method' => 'GET' },
|
|
36
|
+
sample: { 'request' => { 'values' => [0, false, '', nil] } }, profile: Object.new
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
let(:parent) { described_class.new(**fields) }
|
|
40
|
+
let(:replacements) do
|
|
41
|
+
{
|
|
42
|
+
version: 2, host: 'other.test', base_url: 'https://other.test/api/',
|
|
43
|
+
version_url: 'https://other.test/api/v2/',
|
|
44
|
+
description: { 'resources' => { 'other' => { 'label' => 'Other' } } },
|
|
45
|
+
resource_path: [:other, 3], resource: { 'label' => 'Other' },
|
|
46
|
+
action_name: :update, action: { 'method' => 'POST' },
|
|
47
|
+
sample: { 'request' => { 'values' => [false, 0, nil, ''] } },
|
|
48
|
+
profile: Object.new
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'returns a distinct frozen context sharing every unchanged field' do
|
|
53
|
+
child = parent.with
|
|
54
|
+
|
|
55
|
+
expect(child).not_to equal(parent)
|
|
56
|
+
expect(child).to be_frozen
|
|
57
|
+
described_class::FIELDS.each do |field|
|
|
58
|
+
expect(child.public_send(field)).to equal(parent.public_send(field))
|
|
59
|
+
end
|
|
60
|
+
expect(child.description['resources']['widget']).to equal(parent.description['resources']['widget'])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
HaveAPI::ClientExample::Context::FIELDS.each do |field|
|
|
64
|
+
it "normalizes an explicit #{field} replacement like the constructor and shares other fields" do
|
|
65
|
+
changed = { field => replacements.fetch(field) }
|
|
66
|
+
child = parent.with(**changed)
|
|
67
|
+
constructor = described_class.new(**fields, **changed)
|
|
68
|
+
|
|
69
|
+
expect(child).to be_frozen
|
|
70
|
+
expect(child.public_send(field)).to eq(constructor.public_send(field))
|
|
71
|
+
(described_class::FIELDS - [field]).each do |unchanged|
|
|
72
|
+
expect(child.public_send(unchanged)).to equal(parent.public_send(unchanged))
|
|
73
|
+
end
|
|
74
|
+
expect(parent.public_send(field)).to eq(described_class.new(**fields).public_send(field))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'copies supplied nested containers and strings even when their outer hash is frozen' do
|
|
79
|
+
key = ['mutable key']
|
|
80
|
+
source = { key => [{ 'value' => 'safe' }] }.freeze
|
|
81
|
+
child = parent.with(description: source, resource: source, action: source, sample: source)
|
|
82
|
+
|
|
83
|
+
%i[description resource action sample].each do |field|
|
|
84
|
+
owned = child.public_send(field)
|
|
85
|
+
expect(owned).not_to equal(source)
|
|
86
|
+
expect(owned).to be_frozen
|
|
87
|
+
expect(owned.keys.first).not_to equal(key)
|
|
88
|
+
expect(owned.keys.first).to be_frozen
|
|
89
|
+
expect(owned.keys.first.first).to be_frozen
|
|
90
|
+
expect(owned.values.first).to be_frozen
|
|
91
|
+
expect(owned.values.first.first).to be_frozen
|
|
92
|
+
expect(owned.values.first.first.keys.first).to be_frozen
|
|
93
|
+
expect(owned.values.first.first['value']).to be_frozen
|
|
94
|
+
end
|
|
95
|
+
expect(key).not_to be_frozen
|
|
96
|
+
expect(key.first).not_to be_frozen
|
|
97
|
+
expect(source.values.first).not_to be_frozen
|
|
98
|
+
expect(source.values.first.first['value']).not_to be_frozen
|
|
99
|
+
key.first.replace('changed key')
|
|
100
|
+
source.values.first.first['value'].replace('changed')
|
|
101
|
+
%i[description resource action sample].each do |field|
|
|
102
|
+
expect(child.public_send(field)).to eq(['mutable key'] => [{ 'value' => 'safe' }])
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'detaches explicitly supplied strings and converts resource path elements without retaining them' do
|
|
107
|
+
text = +'value'
|
|
108
|
+
path = [text, :widget, 7]
|
|
109
|
+
child = parent.with(version: text, host: text, base_url: text, version_url: text,
|
|
110
|
+
resource_path: path, action_name: text)
|
|
111
|
+
|
|
112
|
+
%i[version host base_url version_url action_name].each do |field|
|
|
113
|
+
expect(child.public_send(field)).to eq('value')
|
|
114
|
+
expect(child.public_send(field)).not_to equal(text)
|
|
115
|
+
expect(child.public_send(field)).to be_frozen
|
|
116
|
+
end
|
|
117
|
+
expect(child.resource_path).to eq(%w[value widget 7])
|
|
118
|
+
expect(child.resource_path).to be_frozen
|
|
119
|
+
expect(child.resource_path).not_to equal(path)
|
|
120
|
+
expect(child.resource_path).to all(be_frozen)
|
|
121
|
+
expect(path).not_to be_frozen
|
|
122
|
+
expect(text).not_to be_frozen
|
|
123
|
+
text.replace('changed')
|
|
124
|
+
path << 'extra'
|
|
125
|
+
expect(child.version).to eq('value')
|
|
126
|
+
expect(child.resource_path).to eq(%w[value widget 7])
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'copies explicitly supplied owned values again instead of treating frozen data as trusted' do
|
|
130
|
+
child = parent.with(description: parent.description, resource_path: parent.resource_path,
|
|
131
|
+
sample: parent.sample)
|
|
132
|
+
|
|
133
|
+
%i[description resource_path sample].each do |field|
|
|
134
|
+
expect(child.public_send(field)).to eq(parent.public_send(field))
|
|
135
|
+
expect(child.public_send(field)).not_to equal(parent.public_send(field))
|
|
136
|
+
end
|
|
137
|
+
expect(child.description['resources']).not_to equal(parent.description['resources'])
|
|
138
|
+
expect(child.sample['request']['values']).not_to equal(parent.sample['request']['values'])
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it 'keeps parent, sibling and chained derivations detached from their supplied inputs' do
|
|
142
|
+
data = { request: { name: 'safe' } }
|
|
143
|
+
child = parent.with(sample: data)
|
|
144
|
+
sibling = parent.with(sample: data)
|
|
145
|
+
grandchild = child.with(action_name: :update)
|
|
146
|
+
fields[:description]['resources']['widget']['label'].replace('changed source')
|
|
147
|
+
data[:request][:name].replace('changed sample')
|
|
148
|
+
|
|
149
|
+
expect(parent.description['resources']['widget']['label']).to eq('Widget')
|
|
150
|
+
expect(child.description).to equal(parent.description)
|
|
151
|
+
expect(sibling.description).to equal(parent.description)
|
|
152
|
+
expect(grandchild.description).to equal(parent.description)
|
|
153
|
+
expect(parent.sample).to eq('request' => { 'values' => [0, false, '', nil] })
|
|
154
|
+
expect(child.sample).to eq(request: { name: 'safe' })
|
|
155
|
+
expect(sibling.sample).to eq(child.sample)
|
|
156
|
+
expect(sibling.sample).not_to equal(child.sample)
|
|
157
|
+
expect(grandchild.sample).to equal(child.sample)
|
|
158
|
+
expect(child.action_name).to eq('show')
|
|
159
|
+
expect(grandchild.action_name).to eq('update')
|
|
160
|
+
expect { child.sample[:request][:name].replace('forbidden') }.to raise_error(FrozenError)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it 'distinguishes omitted fields from explicit nil replacements' do
|
|
164
|
+
child = parent.with(description: nil, resource: nil, action_name: nil, action: nil, sample: nil, profile: nil)
|
|
165
|
+
|
|
166
|
+
%i[description resource action_name action sample profile].each do |field|
|
|
167
|
+
expect(child.public_send(field)).to be_nil
|
|
168
|
+
expect(parent.public_send(field)).not_to be_nil
|
|
169
|
+
end
|
|
170
|
+
expect(child.version).to equal(parent.version)
|
|
171
|
+
expect(parent.with(version: nil, host: nil, base_url: nil, version_url: nil))
|
|
172
|
+
.to have_attributes(version: '', host: '', base_url: '', version_url: '')
|
|
173
|
+
expect(context.with.action_name).to be_nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'shares arbitrary profiles and non-container leaves without freezing them' do
|
|
177
|
+
leaf = Object.new
|
|
178
|
+
profile = fields.fetch(:profile)
|
|
179
|
+
child = parent.with(description: { custom: leaf }, profile: leaf)
|
|
180
|
+
|
|
181
|
+
expect(parent.profile).to equal(profile)
|
|
182
|
+
expect(parent.with.profile).to equal(profile)
|
|
183
|
+
expect(profile).not_to be_frozen
|
|
184
|
+
expect(child.profile).to equal(leaf)
|
|
185
|
+
expect(child.description[:custom]).to equal(leaf)
|
|
186
|
+
expect(leaf).not_to be_frozen
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it 'continues rejecting invalid resource paths' do
|
|
190
|
+
expect { parent.with(resource_path: nil) }.to raise_error(NoMethodError)
|
|
191
|
+
expect { described_class.new(**fields, resource_path: nil) }.to raise_error(NoMethodError)
|
|
192
|
+
expect(parent.resource_path).to eq(%w[widget 2])
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'rejects unknown keywords before normalizing any supplied values' do
|
|
196
|
+
value = Object.new
|
|
197
|
+
allow(value).to receive(:to_s).and_call_original
|
|
198
|
+
|
|
199
|
+
expect { parent.with(version: value, unknown: true) }.to raise_error(ArgumentError, 'unknown keyword: :unknown')
|
|
200
|
+
expect { parent.with(unknown: true, other: false) }
|
|
201
|
+
.to raise_error(ArgumentError, 'unknown keywords: :unknown, :other')
|
|
202
|
+
expect { parent.with('version' => '2') }.to raise_error(ArgumentError, 'unknown keyword: "version"')
|
|
203
|
+
expect(value).not_to have_received(:to_s)
|
|
204
|
+
expect(parent).not_to respond_to(:unknown)
|
|
205
|
+
expect(parent.version).to eq('1')
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it 'keeps subclass constructors, extra keywords and constructor effects on every derivation' do
|
|
209
|
+
subclass = Class.new(described_class) do
|
|
210
|
+
attr_reader :marker, :initializations
|
|
211
|
+
|
|
212
|
+
def initialize(marker: :default, initializations: 0, **fields)
|
|
213
|
+
@marker = marker
|
|
214
|
+
@initializations = initializations + 1
|
|
215
|
+
super(**fields)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
original = subclass.new(**fields, marker: :original)
|
|
219
|
+
child = original.with(marker: :child, initializations: original.initializations)
|
|
220
|
+
defaulted = original.with
|
|
221
|
+
|
|
222
|
+
expect(child).to be_a(subclass)
|
|
223
|
+
expect(child).to be_frozen
|
|
224
|
+
expect(child.marker).to eq(:child)
|
|
225
|
+
expect(child.initializations).to eq(2)
|
|
226
|
+
expect(defaulted.marker).to eq(:default)
|
|
227
|
+
expect(original.marker).to eq(:original)
|
|
228
|
+
expect(original.initializations).to eq(1)
|
|
229
|
+
expect(child.description).to eq(original.description)
|
|
230
|
+
expect(child.description).not_to equal(original.description)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
28
234
|
it 'preserves legacy initialization constructors and wraps plain source' do
|
|
29
235
|
renderer = Class.new(HaveAPI::ClientExample) do
|
|
30
236
|
def initialize(host, base_url, version)
|
|
@@ -83,6 +83,7 @@ RSpec.describe HaveAPI::ClientExamples::JsClient do
|
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
85
|
options = { strict: true, noEmit: true, target: 'ES2020', module: 'ESNext', moduleResolution: 'node',
|
|
86
|
+
ignoreDeprecations: '6.0',
|
|
86
87
|
typeRoots: [File.join(root, 'clients/js/node_modules/@types')], types: ['node'],
|
|
87
88
|
baseUrl: directory, paths: { 'haveapi-client' => [File.join(root, 'clients/js/dist/types/haveapi')] } }
|
|
88
89
|
config = File.join(directory, 'tsconfig.json')
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'base64'
|
|
3
|
+
|
|
4
|
+
module ProfileMountSpec
|
|
5
|
+
User = Struct.new(:login)
|
|
6
|
+
|
|
7
|
+
module AuthenticationResources
|
|
8
|
+
class Session < HaveAPI::Resource
|
|
9
|
+
auth false
|
|
10
|
+
version :all
|
|
11
|
+
|
|
12
|
+
class Show < HaveAPI::Action
|
|
13
|
+
route ''
|
|
14
|
+
http_method :get
|
|
15
|
+
output(:hash) { string :id }
|
|
16
|
+
authorize { allow }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Provider < HaveAPI::Authentication::Basic::Provider
|
|
22
|
+
def resource_module
|
|
23
|
+
AuthenticationResources
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
protected
|
|
27
|
+
|
|
28
|
+
def find_user(_request, username, password)
|
|
29
|
+
User.new(username) if %w[member admin].include?(username) && password == 'pass'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Added < HaveAPI::Resource
|
|
34
|
+
auth false
|
|
35
|
+
version :all
|
|
36
|
+
|
|
37
|
+
class Index < HaveAPI::Action
|
|
38
|
+
route ''
|
|
39
|
+
http_method :get
|
|
40
|
+
output(:hash) { string :late }
|
|
41
|
+
authorize { allow }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe HaveAPI::Server, '#mount' do
|
|
47
|
+
api do
|
|
48
|
+
define_resource(:Widget) do
|
|
49
|
+
version :all
|
|
50
|
+
auth false
|
|
51
|
+
|
|
52
|
+
define_action(:Show) do
|
|
53
|
+
route '{widget_id}'
|
|
54
|
+
http_method :get
|
|
55
|
+
desc HaveAPI.message('profile_mount_spec.action')
|
|
56
|
+
input(:hash) do
|
|
57
|
+
string :visible
|
|
58
|
+
string :secret, default: 'PRIVATE_DEFAULT'
|
|
59
|
+
end
|
|
60
|
+
output(:hash) do
|
|
61
|
+
string :visible
|
|
62
|
+
string :secret
|
|
63
|
+
end
|
|
64
|
+
authorize do |user|
|
|
65
|
+
unless user.login == 'admin'
|
|
66
|
+
input blacklist: [:secret]
|
|
67
|
+
output blacklist: [:secret]
|
|
68
|
+
end
|
|
69
|
+
allow
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# rubocop:disable RSpec/NoExpectationExample
|
|
73
|
+
example 'visible sample' do
|
|
74
|
+
path_params 7
|
|
75
|
+
request({ visible: 'public sample' })
|
|
76
|
+
response({ visible: 'public response' })
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
example 'privileged sample' do
|
|
80
|
+
authorize { |user| user.login == 'admin' }
|
|
81
|
+
path_params 7
|
|
82
|
+
request({ secret: 'PRIVATE_SAMPLE' })
|
|
83
|
+
response({ secret: 'PRIVATE_RESPONSE' })
|
|
84
|
+
end
|
|
85
|
+
# rubocop:enable RSpec/NoExpectationExample
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
define_action(:Secret) do
|
|
89
|
+
route 'secret'
|
|
90
|
+
http_method :get
|
|
91
|
+
output(:hash) { string :admin_only }
|
|
92
|
+
authorize { |user| user.login == 'admin' ? allow : deny }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
let(:server) { described_class.new(get_opt(:api_module)) }
|
|
98
|
+
let(:descriptions) { [] }
|
|
99
|
+
let(:contexts) { [] }
|
|
100
|
+
let(:documentation) { Class.new }
|
|
101
|
+
let(:adapter) do
|
|
102
|
+
selected_contexts = contexts
|
|
103
|
+
Class.new do
|
|
104
|
+
define_method(:resolve) { |**| nil }
|
|
105
|
+
define_method(:render) do |kind:, context:, **|
|
|
106
|
+
selected_contexts << context
|
|
107
|
+
{ status: :code, code: JSON.generate(kind == :example ? context.sample : { url: context.version_url }) }
|
|
108
|
+
end
|
|
109
|
+
end.new
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
before do
|
|
113
|
+
generator = Module.new
|
|
114
|
+
generator.const_set(:VERSION, HaveAPI::VERSION)
|
|
115
|
+
generator.const_set(:Documentation, documentation)
|
|
116
|
+
stub_const('HaveAPI::GoClient', generator)
|
|
117
|
+
allow(registry).to receive(:require).with('haveapi/go_client/documentation').and_return(true)
|
|
118
|
+
allow(documentation).to receive(:new) do |description:, **|
|
|
119
|
+
descriptions << description
|
|
120
|
+
adapter
|
|
121
|
+
end
|
|
122
|
+
server.use_version([1, 2])
|
|
123
|
+
server.default_version = 1
|
|
124
|
+
server.auth_chain << ProfileMountSpec::Provider
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def registry
|
|
128
|
+
server.instance_variable_get(:@documentation_clients)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def register(version = 1)
|
|
132
|
+
server.documentation_clients(version, go: { import_path: 'example.test/widget/client' })
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def request_as(user, locale)
|
|
136
|
+
Rack::MockRequest.new(server.app).get(
|
|
137
|
+
'/mounted/api/v1/', 'HTTP_HOST' => 'example.org', 'HTTP_ACCEPT_LANGUAGE' => locale,
|
|
138
|
+
'HTTP_AUTHORIZATION' => "Basic #{Base64.strict_encode64("#{user}:pass")}"
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'captures the complete selected version after auth, action-state and post-mount routes exist' do
|
|
143
|
+
register
|
|
144
|
+
server.action_state = Object.new
|
|
145
|
+
server.connect_hook(:post_mount) do |_ret, api_server, _sinatra|
|
|
146
|
+
expect(api_server.documentation_client_profiles(1)).to be_empty
|
|
147
|
+
api_server.mount_resource('/mounted/api/v1/', 1, ProfileMountSpec::Added, api_server.routes[1][:resources])
|
|
148
|
+
{}
|
|
149
|
+
end
|
|
150
|
+
allow(server).to receive(:describe_version).and_call_original
|
|
151
|
+
server.mount('/mounted/api/')
|
|
152
|
+
expect(server).to have_received(:describe_version).once
|
|
153
|
+
expect(descriptions.size).to eq(1)
|
|
154
|
+
private_schema = descriptions.first
|
|
155
|
+
expect(private_schema['help']).to eq('/mounted/api/v1/')
|
|
156
|
+
expect(private_schema['resources'].keys).to include('widget', 'action_state', 'added')
|
|
157
|
+
expect(private_schema.dig('resources', 'widget', 'actions').keys).to include('show', 'secret')
|
|
158
|
+
expect(private_schema.dig('authentication', 'basic', 'resources')).to have_key('session')
|
|
159
|
+
expect(private_schema.dig('resources', 'widget', 'actions', 'show', 'input', 'parameters')).to have_key('secret')
|
|
160
|
+
expect(JSON.generate(private_schema)).not_to include('PRIVATE_DEFAULT', 'PRIVATE_SAMPLE', 'PRIVATE_RESPONSE')
|
|
161
|
+
expect(server.documentation_client_profiles(1).fetch(:go).package_version).to be_nil
|
|
162
|
+
expect(server.documentation_client_profiles(2)).to be_empty
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it 'captures each configured version once and does not initialize from requests' do
|
|
166
|
+
register(1)
|
|
167
|
+
register(2)
|
|
168
|
+
server.mount('/mounted/api/')
|
|
169
|
+
expect(descriptions.map { |description| description['help'] }).to eq(['/mounted/api/v1/', '/mounted/api/v2/'])
|
|
170
|
+
request_as('member', 'en')
|
|
171
|
+
request_as('admin', 'cs')
|
|
172
|
+
expect(descriptions.size).to eq(2)
|
|
173
|
+
expect { register }.to raise_error(HaveAPI::ClientExample::Profiles::Invalid, /already mounted/)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'keeps private schema separate from sequential users, locales, HTML and samples' do
|
|
177
|
+
register
|
|
178
|
+
server.available_locales = %i[en cs]
|
|
179
|
+
::I18n.backend.store_translations(:en, profile_mount_spec: { action: 'Visible English action' })
|
|
180
|
+
::I18n.backend.store_translations(:cs, profile_mount_spec: { action: 'Viditelna ceska akce' })
|
|
181
|
+
server.mount('/mounted/api/')
|
|
182
|
+
privileged = request_as('admin', 'en')
|
|
183
|
+
expect(privileged.status).to eq(200)
|
|
184
|
+
expect(privileged.body).to include('PRIVATE_SAMPLE', 'Visible English action')
|
|
185
|
+
contexts.clear
|
|
186
|
+
public_response = request_as('member', 'cs')
|
|
187
|
+
expect(public_response.status).to eq(200)
|
|
188
|
+
expect(public_response.body).to include('Viditelna ceska akce', 'init-go', 'example.test/widget/client')
|
|
189
|
+
expect(public_response.body).not_to include('PRIVATE_DEFAULT', 'PRIVATE_SAMPLE', 'PRIVATE_RESPONSE', 'admin_only', 'haveapi.invalid', 'StructuralSchema')
|
|
190
|
+
selected = contexts.find { |context| context.action_name == 'show' }
|
|
191
|
+
expect(selected.action.dig(:input, :parameters).keys.map(&:to_s)).to eq(['visible'])
|
|
192
|
+
expect(selected.description[:resources]['widget'][:actions]).not_to have_key('secret')
|
|
193
|
+
expect(selected.sample[:request]).to eq(visible: 'public sample')
|
|
194
|
+
expect(selected.action[:description]).to eq('Viditelna ceska akce')
|
|
195
|
+
expect(request_as('member', 'en').body).to include('Visible English action')
|
|
196
|
+
expect(descriptions.size).to eq(1)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'makes the server unusable and publishes no profiles after an adapter failure' do
|
|
200
|
+
register
|
|
201
|
+
allow(documentation).to receive(:new).and_raise('PRIVATE_SCHEMA_DIAGNOSTIC')
|
|
202
|
+
expect { server.mount('/') }.to raise_error(HaveAPI::ClientExample::Profiles::Invalid, 'Configured documentation client schema or configuration is invalid')
|
|
203
|
+
expect(server.app).to be_nil
|
|
204
|
+
expect(server.documentation_client_profiles(1)).to be_empty
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it 'makes the server unusable when a post-mount hook fails before finalization' do
|
|
208
|
+
register
|
|
209
|
+
server.connect_hook(:post_mount) { raise 'fixture mount failure' }
|
|
210
|
+
expect { server.mount('/') }.to raise_error(RuntimeError, 'fixture mount failure')
|
|
211
|
+
expect(documentation).not_to have_received(:new)
|
|
212
|
+
expect(server.app).to be_nil
|
|
213
|
+
expect(server.documentation_client_profiles(1)).to be_empty
|
|
214
|
+
end
|
|
215
|
+
end
|