haveapi 0.32.1 → 0.32.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 +3 -8
- data/README.md +3 -422
- data/Rakefile +17 -7
- data/benchmarks/performance_compare.rb +27 -6
- data/doc/cs/client-examples.md +535 -0
- data/doc/cs/create-client.md +118 -0
- data/doc/cs/json-schema.html +1182 -0
- data/doc/cs/protocol.md +595 -0
- data/doc/cs/readme.md +432 -0
- data/doc/cs/typed-input-validation.md +122 -0
- data/doc/{client-examples.md → en/client-examples.md} +20 -10
- data/doc/{documentation-ui.md → en/documentation-ui.md} +42 -6
- data/doc/{performance.md → en/performance.md} +11 -0
- data/doc/{protocol.md → en/protocol.md} +2 -2
- data/doc/en/readme.md +423 -0
- data/haveapi.gemspec +2 -2
- data/lib/haveapi/client_example.rb +18 -5
- data/lib/haveapi/client_examples/curl.rb +5 -5
- data/lib/haveapi/client_examples/dynamic_helpers.rb +10 -10
- data/lib/haveapi/client_examples/go_client.rb +1 -1
- data/lib/haveapi/client_examples/http.rb +4 -4
- data/lib/haveapi/client_examples/js_client.rb +10 -10
- data/lib/haveapi/client_examples/php_client.rb +24 -25
- data/lib/haveapi/client_examples/python_client.rb +11 -11
- data/lib/haveapi/client_examples/ruby_cli.rb +6 -9
- data/lib/haveapi/client_examples/ruby_client.rb +24 -16
- data/lib/haveapi/client_examples/rust_client.rb +1 -1
- data/lib/haveapi/client_examples/ts_generated_client.rb +1 -1
- data/lib/haveapi/documentation_context.rb +14 -2
- data/lib/haveapi/documentation_helpers.rb +92 -15
- data/lib/haveapi/documentation_locale.rb +65 -0
- data/lib/haveapi/documentation_oauth.rb +12 -12
- data/lib/haveapi/documentation_search.rb +5 -5
- data/lib/haveapi/documentation_validators.rb +43 -33
- data/lib/haveapi/locales/cs.yml +468 -0
- data/lib/haveapi/locales/en.yml +467 -0
- data/lib/haveapi/public/css/documentation.css +8 -4
- data/lib/haveapi/public/js/documentation.js +73 -20
- data/lib/haveapi/server.rb +32 -47
- data/lib/haveapi/shared_documentation_helpers.rb +53 -5
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/doc_layout.erb +3 -2
- data/lib/haveapi/views/doc_sidebars/create-client.erb +10 -10
- data/lib/haveapi/views/doc_sidebars/json-schema.erb +2 -2
- data/lib/haveapi/views/doc_sidebars/protocol.erb +24 -24
- data/lib/haveapi/views/documentation/search_form.erb +5 -4
- data/lib/haveapi/views/documentation/search_results.erb +6 -6
- data/lib/haveapi/views/index.erb +3 -3
- data/lib/haveapi/views/main_layout.erb +4 -4
- data/lib/haveapi/views/shared_documentation.erb +8 -5
- data/lib/haveapi/views/shared_sidebar.erb +11 -11
- data/lib/haveapi/views/version_page/auth_body.erb +7 -7
- data/lib/haveapi/views/version_page/client_auth.erb +1 -1
- data/lib/haveapi/views/version_page/client_example.erb +1 -1
- data/lib/haveapi/views/version_page/client_init.erb +1 -1
- data/lib/haveapi/views/version_page/client_overview.erb +7 -5
- data/lib/haveapi/views/version_page/example_panels.erb +5 -2
- data/lib/haveapi/views/version_page/resource_body.erb +24 -24
- data/lib/haveapi/views/version_page/resource_directory.erb +2 -2
- data/lib/haveapi/views/version_page/usage.erb +5 -5
- data/lib/haveapi/views/version_page.erb +11 -11
- data/lib/haveapi/views/version_sidebar/auth_nav.erb +2 -2
- data/lib/haveapi/views/version_sidebar/authentication_control.erb +8 -8
- data/lib/haveapi/views/version_sidebar/authentication_status.erb +1 -1
- data/lib/haveapi/views/version_sidebar/brand.erb +16 -5
- data/lib/haveapi/views/version_sidebar.erb +33 -18
- data/spec/client_examples/assets/documentation_test.cjs +107 -9
- data/spec/client_examples/comment_capture.rb +72 -0
- data/spec/client_examples/comments_spec.rb +162 -0
- data/spec/client_examples/context_spec.rb +18 -2
- data/spec/client_examples/detail_spec.rb +68 -0
- data/spec/documentation/client_overview_spec.rb +60 -0
- data/spec/documentation/example_selection_spec.rb +192 -0
- data/spec/documentation/language_settings_spec.rb +132 -0
- data/spec/documentation/locale_spec.rb +367 -0
- data/spec/documentation/navigation_spec.rb +61 -3
- data/spec/documentation/oauth_spec.rb +42 -0
- data/spec/documentation/search_index_routes_spec.rb +8 -4
- data/spec/documentation/search_spec.rb +1 -1
- data/spec/documentation/shared_pages_spec.rb +63 -2
- data/spec/documentation/validators_spec.rb +25 -0
- data/spec/extensions/exception_mailer_spec.rb +2 -2
- data/spec/packaging_spec.rb +68 -9
- data/spec/performance/event_baseline_spec.rb +1 -1
- data/spec/performance/source_versions_spec.rb +111 -2
- metadata +24 -11
- /data/doc/{create-client.md → en/create-client.md} +0 -0
- /data/doc/{documentation-oauth.md → en/documentation-oauth.md} +0 -0
- /data/doc/{json-schema.html → en/json-schema.html} +0 -0
- /data/doc/{typed-input-validation.md → en/typed-input-validation.md} +0 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'cgi'
|
|
3
|
+
require 'ripper'
|
|
4
|
+
require 'open3'
|
|
5
|
+
|
|
6
|
+
RSpec.describe HaveAPI::DocumentationLocale do
|
|
7
|
+
api do
|
|
8
|
+
define_resource(:Thing) do
|
|
9
|
+
version 1
|
|
10
|
+
auth false
|
|
11
|
+
desc HaveAPI.message('documentation_locale_spec.resource')
|
|
12
|
+
define_action(:Show) do
|
|
13
|
+
route ''
|
|
14
|
+
http_method :get
|
|
15
|
+
input { string :query }
|
|
16
|
+
output { string :language }
|
|
17
|
+
authorize { allow }
|
|
18
|
+
def exec
|
|
19
|
+
{ language: ::I18n.locale.to_s }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
let(:server) do
|
|
26
|
+
HaveAPI::Server.new(get_opt(:api_module)).tap do |api|
|
|
27
|
+
api.use_version(1)
|
|
28
|
+
api.default_locale = :cs
|
|
29
|
+
api.locale_header = 'X-API-Language'
|
|
30
|
+
api.parameter_i18n_scope = 'documentation_locale_spec'
|
|
31
|
+
api.locale { :cs }
|
|
32
|
+
api.mount('/api/')
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
let(:app) { server.app }
|
|
36
|
+
|
|
37
|
+
before do
|
|
38
|
+
::I18n.backend.store_translations(:en, documentation_locale_spec: {
|
|
39
|
+
resource: 'English resource',
|
|
40
|
+
attributes: { query: { label: 'English query' } }
|
|
41
|
+
})
|
|
42
|
+
::I18n.backend.store_translations(:cs, documentation_locale_spec: {
|
|
43
|
+
resource: 'Český prostředek',
|
|
44
|
+
attributes: { query: { label: 'Český dotaz' } }
|
|
45
|
+
})
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '#documentation_t' do
|
|
49
|
+
let(:helper) { Object.new.extend(described_class) }
|
|
50
|
+
|
|
51
|
+
it 'preserves translations, interpolation and defaults across repeated locale changes' do
|
|
52
|
+
::I18n.backend.store_translations(:en, haveapi: { documentation: {
|
|
53
|
+
locale_spec_present: 'English %{name}', locale_spec_fallback: 'Fallback %{name}'
|
|
54
|
+
} })
|
|
55
|
+
::I18n.backend.store_translations(:cs, haveapi: { documentation: { locale_spec_present: 'Česky %{name}' } })
|
|
56
|
+
%i[en cs en].each do |locale|
|
|
57
|
+
::I18n.with_locale(locale) do
|
|
58
|
+
expected = locale == :en ? 'English <&>' : 'Česky <&>'
|
|
59
|
+
expect(helper.documentation_t('locale_spec_present', name: '<&>')).to eq(expected)
|
|
60
|
+
expect(helper.documentation_t('locale_spec_present', default: 'Ignored', name: '<&>')).to eq(expected)
|
|
61
|
+
expect(helper.documentation_t('locale_spec_fallback', name: 'input')).to eq('Fallback input')
|
|
62
|
+
expect(helper.documentation_t('locale_spec_absent', default: 'Default %{name}', name: 'input')).to eq('Default input')
|
|
63
|
+
expect(helper.documentation_t('locale_spec_absent')).to be_nil
|
|
64
|
+
expect(helper.documentation_t('browser')).to eq(::I18n.t('haveapi.documentation.browser'))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'reuses the resolved English fallback without translating the same key twice' do
|
|
70
|
+
allow(::I18n).to receive(:t).and_call_original
|
|
71
|
+
::I18n.with_locale(:en) do
|
|
72
|
+
expect(helper.documentation_t('labels.language')).to eq('Language')
|
|
73
|
+
end
|
|
74
|
+
expect(::I18n).to have_received(:t).with('haveapi.documentation.labels.language', locale: :en, default: nil).once
|
|
75
|
+
expect(::I18n).to have_received(:t).once
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
context 'with a documentation render scope' do
|
|
79
|
+
let(:helper) do
|
|
80
|
+
Class.new do
|
|
81
|
+
attr_accessor :scope_locale
|
|
82
|
+
|
|
83
|
+
def settings = self
|
|
84
|
+
def api_server = self
|
|
85
|
+
def documentation_locale = scope_locale
|
|
86
|
+
def activate_locale(locale) = (::I18n.locale = locale)
|
|
87
|
+
def add_vary_header(_header); end
|
|
88
|
+
end.include(described_class).new.tap { |instance| instance.scope_locale = :en }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
before do
|
|
92
|
+
::I18n.backend.store_translations(:en, haveapi: { documentation: {
|
|
93
|
+
memo_static: 'English label', memo_interpolated: 'Hello %{name}',
|
|
94
|
+
memo_fallback: 'English fallback', memo_hash: { leaf: 'Leaf' }, memo_number: 7
|
|
95
|
+
} })
|
|
96
|
+
::I18n.backend.store_translations(:cs, haveapi: { documentation: { memo_static: 'Český štítek' } })
|
|
97
|
+
allow(::I18n).to receive(:t).and_call_original
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'memoizes static strings by active locale, including explicit nil defaults and English fallback' do
|
|
101
|
+
helper.with_documentation_locale(persist: false) do
|
|
102
|
+
%i[en cs en].each do |locale|
|
|
103
|
+
::I18n.with_locale(locale) do
|
|
104
|
+
expected = locale == :en ? 'English label' : 'Český štítek'
|
|
105
|
+
expect(helper.documentation_t('memo_static', default: nil)).to eq(expected)
|
|
106
|
+
expect(helper.documentation_t('memo_static')).to eq(expected)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
::I18n.with_locale(:cs) do
|
|
110
|
+
2.times { expect(helper.documentation_t('memo_fallback')).to eq('English fallback') }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
expect(::I18n).to have_received(:t).with('haveapi.documentation.memo_static', locale: :en, default: nil).twice
|
|
115
|
+
expect(::I18n).to have_received(:t).with('haveapi.documentation.memo_static', default: 'English label').once
|
|
116
|
+
expect(::I18n).to have_received(:t).with('haveapi.documentation.memo_fallback', locale: :en, default: nil).once
|
|
117
|
+
expect(::I18n).to have_received(:t).with('haveapi.documentation.memo_fallback', default: 'English fallback').once
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'isolates caller mutation of both the initial result and later hit copies' do
|
|
121
|
+
helper.with_documentation_locale(persist: false) do
|
|
122
|
+
first = helper.documentation_t('memo_static')
|
|
123
|
+
first.replace('Changed first result')
|
|
124
|
+
hit = helper.documentation_t('memo_static')
|
|
125
|
+
expect(hit).to eq('English label')
|
|
126
|
+
expect(hit).not_to be_frozen
|
|
127
|
+
hit.replace('Changed hit')
|
|
128
|
+
expect(helper.documentation_t('memo_static')).to eq('English label')
|
|
129
|
+
end
|
|
130
|
+
expect(::I18n).to have_received(:t).with('haveapi.documentation.memo_static', locale: :en, default: nil).once
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'keeps explicit defaults, interpolation and non-String keys on the original lookup path' do
|
|
134
|
+
helper.with_documentation_locale(persist: false) do
|
|
135
|
+
2.times do
|
|
136
|
+
expect(helper.documentation_t('memo_default_only', default: 'Explicit default')).to eq('Explicit default')
|
|
137
|
+
expect(helper.documentation_t(:memo_static)).to eq('English label')
|
|
138
|
+
end
|
|
139
|
+
%w[first second].each do |name|
|
|
140
|
+
expect(helper.documentation_t('memo_interpolated', name:)).to eq("Hello #{name}")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
expect(::I18n).to have_received(:t).with('haveapi.documentation.memo_default_only', locale: :en, default: 'Explicit default').twice
|
|
145
|
+
expect(::I18n).to have_received(:t).with('haveapi.documentation.memo_static', locale: :en, default: nil).twice
|
|
146
|
+
expect(::I18n).to have_received(:t).with('haveapi.documentation.memo_interpolated', locale: :en, default: nil, name: 'first').once
|
|
147
|
+
expect(::I18n).to have_received(:t).with('haveapi.documentation.memo_interpolated', locale: :en, default: nil, name: 'second').once
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'does not retain missing, hash or other non-String results' do
|
|
151
|
+
helper.with_documentation_locale(persist: false) do
|
|
152
|
+
2.times do
|
|
153
|
+
expect(helper.documentation_t('memo_late')).to be_nil
|
|
154
|
+
expect(helper.documentation_t('memo_hash')).to eq(leaf: 'Leaf')
|
|
155
|
+
expect(helper.documentation_t('memo_number')).to eq(7)
|
|
156
|
+
end
|
|
157
|
+
::I18n.backend.store_translations(:en, haveapi: { documentation: { memo_late: 'Now available' } })
|
|
158
|
+
expect(helper.documentation_t('memo_late')).to eq('Now available')
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
expect(::I18n).to have_received(:t).with('haveapi.documentation.memo_late', locale: :en, default: nil).exactly(3).times
|
|
162
|
+
expect(::I18n).to have_received(:t).with('haveapi.documentation.memo_hash', locale: :en, default: nil).twice
|
|
163
|
+
expect(::I18n).to have_received(:t).with('haveapi.documentation.memo_number', locale: :en, default: nil).twice
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'starts fresh on each render and leaves no memo outside that scope' do
|
|
167
|
+
helper.with_documentation_locale(persist: false) { expect(helper.documentation_t('memo_static')).to eq('English label') }
|
|
168
|
+
::I18n.backend.store_translations(:en, haveapi: { documentation: { memo_static: 'Next render' } })
|
|
169
|
+
helper.with_documentation_locale(persist: false) { expect(helper.documentation_t('memo_static')).to eq('Next render') }
|
|
170
|
+
::I18n.backend.store_translations(:en, haveapi: { documentation: { memo_static: 'Outside render' } })
|
|
171
|
+
::I18n.with_locale(:en) { expect(helper.documentation_t('memo_static')).to eq('Outside render') }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'clears nested scopes after errors and restores the active locale' do
|
|
175
|
+
previous = ::I18n.locale
|
|
176
|
+
helper.scope_locale = :cs
|
|
177
|
+
helper.with_documentation_locale(persist: false) do
|
|
178
|
+
expect(helper.documentation_t('memo_static')).to eq('Český štítek')
|
|
179
|
+
helper.scope_locale = :en
|
|
180
|
+
expect do
|
|
181
|
+
helper.with_documentation_locale(persist: false) do
|
|
182
|
+
expect(helper.documentation_t('memo_static')).to eq('English label')
|
|
183
|
+
raise 'nested rendering failed'
|
|
184
|
+
end
|
|
185
|
+
end.to raise_error(RuntimeError, 'nested rendering failed')
|
|
186
|
+
expect(::I18n.locale).to eq(:cs)
|
|
187
|
+
::I18n.backend.store_translations(:cs, haveapi: { documentation: { memo_static: 'After inner scope' } })
|
|
188
|
+
expect(helper.documentation_t('memo_static')).to eq('After inner scope')
|
|
189
|
+
end
|
|
190
|
+
expect(::I18n.locale).to eq(previous)
|
|
191
|
+
|
|
192
|
+
helper.scope_locale = :en
|
|
193
|
+
expect do
|
|
194
|
+
helper.with_documentation_locale(persist: false) do
|
|
195
|
+
expect(helper.documentation_t('memo_static')).to eq('English label')
|
|
196
|
+
raise 'outer rendering failed'
|
|
197
|
+
end
|
|
198
|
+
end.to raise_error(RuntimeError, 'outer rendering failed')
|
|
199
|
+
::I18n.backend.store_translations(:en, haveapi: { documentation: { memo_static: 'After aborted render' } })
|
|
200
|
+
::I18n.with_locale(:en) { expect(helper.documentation_t('memo_static')).to eq('After aborted render') }
|
|
201
|
+
expect(::I18n.locale).to eq(previous)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it 'does not memoize a failed lookup' do
|
|
205
|
+
calls = 0
|
|
206
|
+
allow(::I18n).to receive(:t).with('haveapi.documentation.memo_failure', locale: :en, default: nil) do
|
|
207
|
+
calls += 1
|
|
208
|
+
raise 'lookup failed' if calls == 1
|
|
209
|
+
|
|
210
|
+
'Recovered label'
|
|
211
|
+
end
|
|
212
|
+
helper.with_documentation_locale(persist: false) do
|
|
213
|
+
expect { helper.documentation_t('memo_failure') }.to raise_error(RuntimeError, 'lookup failed')
|
|
214
|
+
2.times { expect(helper.documentation_t('memo_failure')).to eq('Recovered label') }
|
|
215
|
+
end
|
|
216
|
+
expect(calls).to eq(2)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def document_language
|
|
222
|
+
last_response.body[/<html lang="([^"]+)"/, 1]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def index_url
|
|
226
|
+
CGI.unescapeHTML(last_response.body[/data-search-index-url="([^"]+)"/, 1])
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it 'negotiates browser preferences independently of API configuration without saving detection' do
|
|
230
|
+
{ 'CS-cz, en;q=0.8' => 'cs', 'de, en-GB;q=0.8,cs;q=0.5' => 'en',
|
|
231
|
+
'en;q=0,cs-CZ;q=0.7' => 'cs', 'en-US,cs;q=0.9' => 'en',
|
|
232
|
+
'de,*;q=0.5' => 'en', '*' => 'en', '' => 'en' }.each do |browser, expected|
|
|
233
|
+
get '/api/v1/', {}, 'HTTP_ACCEPT_LANGUAGE' => browser, 'HTTP_X_API_LANGUAGE' => 'cs'
|
|
234
|
+
expect(last_response).to be_ok
|
|
235
|
+
expect(document_language).to eq(expected), browser
|
|
236
|
+
expect(last_response.headers['Set-Cookie'].to_s).not_to include('haveapi.docs.locale')
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it 'saves only explicit choices and gives a valid query priority over the saved language' do
|
|
241
|
+
get '/api/v1/?resource=thing&doc_lang=cs', {}, 'HTTP_ACCEPT_LANGUAGE' => 'en'
|
|
242
|
+
expect(document_language).to eq('cs')
|
|
243
|
+
cookie = last_response.headers['Set-Cookie'].to_s
|
|
244
|
+
expect(cookie).to include('haveapi.docs.locale=cs', 'path=/api/', 'samesite=lax')
|
|
245
|
+
expect(cookie.downcase).not_to include('domain=', 'secure')
|
|
246
|
+
get '/api/v1/', {}, 'HTTP_ACCEPT_LANGUAGE' => 'en'
|
|
247
|
+
expect(document_language).to eq('cs')
|
|
248
|
+
get '/api/v1/?doc_lang=en', {}, 'HTTP_ACCEPT_LANGUAGE' => 'cs'
|
|
249
|
+
expect(document_language).to eq('en')
|
|
250
|
+
expect(last_response.headers['Set-Cookie'].to_s).to include('haveapi.docs.locale=en')
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it 'uses a secure host-only cookie over HTTPS and works without returning that cookie' do
|
|
254
|
+
get 'https://example.org/api/v1/?doc_lang=cs'
|
|
255
|
+
expect(document_language).to eq('cs')
|
|
256
|
+
expect(last_response.headers['Set-Cookie'].to_s).to include('secure')
|
|
257
|
+
clear_cookies
|
|
258
|
+
get '/api/v1/?doc_lang=cs'
|
|
259
|
+
expect(document_language).to eq('cs')
|
|
260
|
+
clear_cookies
|
|
261
|
+
get '/api/v1/'
|
|
262
|
+
expect(document_language).to eq('en')
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it 'ignores unsupported, nested and duplicate preferences' do
|
|
266
|
+
set_cookie 'haveapi.docs.locale=unknown; path=/api/'
|
|
267
|
+
%w[doc_lang=unknown doc_lang=auto doc_lang[]=cs doc_lang=cs&doc_lang=en].each do |query|
|
|
268
|
+
get "/api/v1/?#{query}", {}, 'HTTP_ACCEPT_LANGUAGE' => 'en'
|
|
269
|
+
expect(document_language).to eq('en')
|
|
270
|
+
expect(last_response.headers['Set-Cookie'].to_s).not_to include('haveapi.docs.locale')
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
it 'localizes metadata and controls, retaining the exact language choices above Theme' do
|
|
275
|
+
get '/api/v1/?resource=thing&doc_lang=cs&q=dotaz'
|
|
276
|
+
body = last_response.body
|
|
277
|
+
expect(body).to include('Český prostředek', 'Český dotaz', 'data-documentation-language', '>English</option>', '>Česky</option>')
|
|
278
|
+
expect(body.index('data-documentation-language')).to be < body.index('docs-theme-label')
|
|
279
|
+
expect(body).to include('name="resource" value="thing"', 'name="q" value="dotaz"')
|
|
280
|
+
expect(body).not_to include('value="auto"')
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
it 'pins search to the displayed locale without persisting a read-only selector' do
|
|
284
|
+
get '/api/v1/?resource=thing&doc_lang=cs'
|
|
285
|
+
endpoint = index_url
|
|
286
|
+
expect(endpoint).to include('doc_lang=cs')
|
|
287
|
+
clear_cookies
|
|
288
|
+
get endpoint, {}, 'HTTP_ACCEPT_LANGUAGE' => 'en'
|
|
289
|
+
expect(last_response).to be_ok
|
|
290
|
+
expect(last_response.body).to include('"locale":"cs"', 'Český dotaz')
|
|
291
|
+
expect(last_response.headers['Set-Cookie'].to_s).not_to include('haveapi.docs.locale')
|
|
292
|
+
expect(last_response.headers['Cache-Control']).to eq('private, max-age=86400, immutable')
|
|
293
|
+
expect(last_response.headers['Vary']).to include('Cookie', 'Accept-Language', 'Authorization')
|
|
294
|
+
get endpoint.sub('doc_lang=cs', 'doc_lang=en')
|
|
295
|
+
expect(last_response.status).to eq(409)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
it 'preserves native Ruby and JavaScript tokens while translating authored comments' do
|
|
299
|
+
scanner = <<~JS
|
|
300
|
+
const ts = require(process.argv[1]);
|
|
301
|
+
let source = "";
|
|
302
|
+
process.stdin.on("data", chunk => source += chunk);
|
|
303
|
+
process.stdin.on("end", () => {
|
|
304
|
+
const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, ts.LanguageVariant.Standard, source);
|
|
305
|
+
const tokens = [];
|
|
306
|
+
let kind;
|
|
307
|
+
while ((kind = scanner.scan()) !== ts.SyntaxKind.EndOfFileToken) {
|
|
308
|
+
if (kind !== ts.SyntaxKind.SingleLineCommentTrivia && kind !== ts.SyntaxKind.MultiLineCommentTrivia) {
|
|
309
|
+
tokens.push([kind, scanner.getTokenText()]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
process.stdout.write(JSON.stringify(tokens));
|
|
313
|
+
});
|
|
314
|
+
JS
|
|
315
|
+
compiler = File.expand_path('../../../../clients/js/node_modules/typescript', __dir__)
|
|
316
|
+
rendered = %w[en cs].map do |locale|
|
|
317
|
+
get "/api/v1/?doc_lang=#{locale}"
|
|
318
|
+
last_response.body.scan(%r{<pre><code class="(ruby|javascript)"[^>]*>(.*?)</code></pre>}m).map do |language, html|
|
|
319
|
+
source = CGI.unescapeHTML(html)
|
|
320
|
+
if language == 'ruby'
|
|
321
|
+
Ripper.lex(source).reject { |(_, type, _, _)| type == :on_comment }.map { |(_, type, token, _)| [type, token] }
|
|
322
|
+
else
|
|
323
|
+
output, error, status = Open3.capture3('node', '-e', scanner, compiler, stdin_data: source)
|
|
324
|
+
expect(status).to be_success, error
|
|
325
|
+
JSON.parse(output)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
expect(rendered.first).not_to be_empty
|
|
330
|
+
expect(rendered.last).to eq(rendered.first)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
it 'leaves ordinary actions and OPTIONS in the configured API locale' do
|
|
334
|
+
set_cookie 'haveapi.docs.locale=cs; path=/api/'
|
|
335
|
+
get '/api/v1/things?doc_lang=cs', {}, 'HTTP_X_API_LANGUAGE' => 'en'
|
|
336
|
+
expect(JSON.parse(last_response.body).dig('response', 'thing', 'language')).to eq('en')
|
|
337
|
+
options '/api/v1/?doc_lang=cs', {}, 'HTTP_X_API_LANGUAGE' => 'en'
|
|
338
|
+
expect(last_response.body).to include('English resource', 'English query')
|
|
339
|
+
expect(last_response.body).not_to include('Český prostředek')
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
it 'restores the thread locale after successful and failed documentation rendering' do
|
|
343
|
+
previous = ::I18n.locale
|
|
344
|
+
get '/api/v1/?doc_lang=cs'
|
|
345
|
+
expect(::I18n.locale).to eq(previous)
|
|
346
|
+
allow(server).to receive(:describe_version) do
|
|
347
|
+
expect(::I18n.locale).to eq(:cs)
|
|
348
|
+
raise 'description failure'
|
|
349
|
+
end
|
|
350
|
+
app.set :raise_errors, true
|
|
351
|
+
expect { get '/api/v1/?doc_lang=cs' }.to raise_error(RuntimeError, 'description failure')
|
|
352
|
+
expect(::I18n.locale).to eq(previous)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
it 'uses localized articles and stable mounted links with explicit English fallback' do
|
|
356
|
+
get '/api/doc/protocol?doc_lang=cs'
|
|
357
|
+
expect(document_language).to eq('cs')
|
|
358
|
+
expect(last_response.body).to include('id="resources"', 'src="/api/doc/protocol.png"', 'href="/api/doc/typed-input-validation')
|
|
359
|
+
get '/api/doc/documentation-ui'
|
|
360
|
+
expect(document_language).to eq('cs')
|
|
361
|
+
expect(last_response.body).to include('lang="en"', 'Browsing API documentation')
|
|
362
|
+
get '/api/doc/protocol.md'
|
|
363
|
+
expect(last_response).to be_ok
|
|
364
|
+
get '/api/doc/../../Gemfile'
|
|
365
|
+
expect(last_response.status).to eq(404)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
|
+
require 'open3'
|
|
2
3
|
|
|
3
4
|
module DocumentationNavigationSpec
|
|
4
5
|
User = Struct.new(:login)
|
|
@@ -232,7 +233,7 @@ describe DocumentationNavigationSpec do
|
|
|
232
233
|
|
|
233
234
|
it 'links nested resource breadcrumbs to the directory and each real parent' do
|
|
234
235
|
get '/v1/?resource=widget.child'
|
|
235
|
-
breadcrumb = last_response.body.match(%r{<nav
|
|
236
|
+
breadcrumb = last_response.body.match(%r{<nav class="docs-breadcrumb"[^>]*data-resource-breadcrumbs>(.*?)</nav>}m)[1]
|
|
236
237
|
expect(breadcrumb).to include('href="/v1/?page=resources"', 'href="/v1/?resource=widget"',
|
|
237
238
|
'<li aria-current="page">Child</li>')
|
|
238
239
|
expect(last_response.body.index('data-action-nav')).to be < last_response.body.index('docs-preferences')
|
|
@@ -273,12 +274,69 @@ describe DocumentationNavigationSpec do
|
|
|
273
274
|
response = Rack::MockRequest.new(app).get('/v1/?resource=widget',
|
|
274
275
|
'HTTP_HOST' => 'example.test', 'SCRIPT_NAME' => '/rack')
|
|
275
276
|
expect(response.status).to eq(200)
|
|
276
|
-
|
|
277
|
+
digests = HaveAPI::DocumentationHelpers::ASSET_DIGESTS
|
|
278
|
+
expect(response.body).to include("src=\"/rack/js/documentation.js?v=#{digests.fetch('js/documentation.js')}\"",
|
|
277
279
|
'data-highlighter-url="/rack/js/highlighter.js"',
|
|
278
|
-
|
|
280
|
+
"href=\"/rack/css/documentation.css?v=#{digests.fetch('css/documentation.css')}\"",
|
|
279
281
|
'href="/rack/v1/?page=resources"')
|
|
280
282
|
end
|
|
281
283
|
|
|
284
|
+
it 'versions the entry assets by installed content without reading files during requests' do
|
|
285
|
+
public_root = File.expand_path('../../lib/haveapi/public', __dir__)
|
|
286
|
+
digests = HaveAPI::DocumentationHelpers::ASSET_DIGESTS
|
|
287
|
+
expect(digests).to eq(%w[js/documentation.js css/documentation.css].to_h do |asset|
|
|
288
|
+
[asset, Digest::SHA256.file(File.join(public_root, asset)).hexdigest]
|
|
289
|
+
end)
|
|
290
|
+
expect(digests).to be_frozen
|
|
291
|
+
expect(digests.values).to all(be_frozen)
|
|
292
|
+
allow(Digest::SHA256).to receive(:file).and_call_original
|
|
293
|
+
%w[en cs].each do |locale|
|
|
294
|
+
get "/v1/?doc_lang=#{locale}"
|
|
295
|
+
digests.each do |asset, digest|
|
|
296
|
+
expect(last_response.body).to include("/#{asset}?v=#{digest}\"")
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
expect(Digest::SHA256).not_to have_received(:file)
|
|
300
|
+
digests.each do |asset, digest|
|
|
301
|
+
get "/#{asset}?v=#{digest}"
|
|
302
|
+
expect(last_response).to be_ok
|
|
303
|
+
expect(last_response.body).to eq(File.binread(File.join(public_root, asset)))
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it 'retains content versions across loads and changes only the modified asset version' do
|
|
308
|
+
helper_path = File.expand_path('../../lib/haveapi/documentation_helpers.rb', __dir__)
|
|
309
|
+
script = <<~RUBY
|
|
310
|
+
require 'tmpdir'
|
|
311
|
+
require 'fileutils'
|
|
312
|
+
Dir.mktmpdir('haveapi-assets') do |root|
|
|
313
|
+
path = File.join(root, 'documentation_helpers.rb')
|
|
314
|
+
FileUtils.cp(ARGV.fetch(0), path)
|
|
315
|
+
%w[js/documentation.js css/documentation.css].each do |asset|
|
|
316
|
+
target = File.join(root, 'public', asset)
|
|
317
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
318
|
+
File.write(target, asset)
|
|
319
|
+
end
|
|
320
|
+
load path
|
|
321
|
+
first = HaveAPI::DocumentationHelpers::ASSET_DIGESTS
|
|
322
|
+
HaveAPI::DocumentationHelpers.send(:remove_const, :ASSET_DIGESTS)
|
|
323
|
+
load path
|
|
324
|
+
unchanged = HaveAPI::DocumentationHelpers::ASSET_DIGESTS
|
|
325
|
+
File.write(File.join(root, 'public/js/documentation.js'), 'changed JavaScript')
|
|
326
|
+
HaveAPI::DocumentationHelpers.send(:remove_const, :ASSET_DIGESTS)
|
|
327
|
+
load path
|
|
328
|
+
puts JSON.generate([first, unchanged, HaveAPI::DocumentationHelpers::ASSET_DIGESTS])
|
|
329
|
+
end
|
|
330
|
+
RUBY
|
|
331
|
+
output, status = Open3.capture2e(RbConfig.ruby, '-e', script, helper_path)
|
|
332
|
+
expect(status).to be_success, output
|
|
333
|
+
first, unchanged, changed = JSON.parse(output)
|
|
334
|
+
expect(unchanged).to eq(first)
|
|
335
|
+
expect(changed.fetch('css/documentation.css')).to eq(first.fetch('css/documentation.css'))
|
|
336
|
+
expect(changed.fetch('js/documentation.js')).to eq(Digest::SHA256.hexdigest('changed JavaScript'))
|
|
337
|
+
expect(changed.fetch('js/documentation.js')).not_to eq(first.fetch('js/documentation.js'))
|
|
338
|
+
end
|
|
339
|
+
|
|
282
340
|
it 'uses mounted canonical navigation and keeps shared documentation on its layout' do
|
|
283
341
|
server = HaveAPI::Server.new(get_opt(:api_module))
|
|
284
342
|
server.use_version(1)
|
|
@@ -137,6 +137,25 @@ describe HaveAPI::DocumentationOAuth do
|
|
|
137
137
|
authorization
|
|
138
138
|
end
|
|
139
139
|
|
|
140
|
+
it 'localizes failed browser sign-in without saving a detected language or leaking the thread locale' do
|
|
141
|
+
previous = ::I18n.locale
|
|
142
|
+
get 'https://docs.example/api/_documentation/oauth/callback', {}, 'HTTP_ACCEPT_LANGUAGE' => 'cs-CZ'
|
|
143
|
+
expect(last_response.status).to eq(400)
|
|
144
|
+
expect(last_response.body).to include(::I18n.t('haveapi.documentation.oauth.sign_in_failed', locale: :cs))
|
|
145
|
+
expect(cookie_headers).not_to include('haveapi.docs.locale')
|
|
146
|
+
expect(::I18n.locale).to eq(previous)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'localizes expired-session notices after authentication resolves the API locale' do
|
|
150
|
+
set_cookie 'haveapi.docs.locale=cs; path=/api/', URI('https://docs.example/api/')
|
|
151
|
+
sign_in
|
|
152
|
+
DocumentationOAuthSpec::Config.users = {}
|
|
153
|
+
get 'https://docs.example/api/v1/'
|
|
154
|
+
expect(last_response.status).to eq(200)
|
|
155
|
+
expect(last_response.body).to include(::I18n.t('haveapi.documentation.oauth.session_ended', locale: :cs))
|
|
156
|
+
expect(last_response.body).not_to include('PRIVATE_ACCESS_TOKEN')
|
|
157
|
+
end
|
|
158
|
+
|
|
140
159
|
def cookie_headers
|
|
141
160
|
Array(last_response.headers['set-cookie']).join("\n")
|
|
142
161
|
end
|
|
@@ -263,9 +282,32 @@ describe HaveAPI::DocumentationOAuth do
|
|
|
263
282
|
end
|
|
264
283
|
end
|
|
265
284
|
|
|
285
|
+
it 'keeps only the protected sign-out form when switching is unavailable' do
|
|
286
|
+
sign_in
|
|
287
|
+
get 'https://docs.example/api/v1/'
|
|
288
|
+
forms = last_response.body.scan(%r{<form method="post" action="https://docs.example/api/_documentation/oauth/logout">(.*?)</form>}m).flatten
|
|
289
|
+
expect(forms.length).to eq(1)
|
|
290
|
+
expect(forms.first).to include('>Sign out</button>')
|
|
291
|
+
expect(forms.first).to match(/name="csrf" value="[\w-]{43}"/)
|
|
292
|
+
expect(forms.first).not_to include('name="switch_user"', '>Switch user</button>')
|
|
293
|
+
end
|
|
294
|
+
|
|
266
295
|
context 'with valid session CSRF and a switch-capable client' do
|
|
267
296
|
let(:oauth_options) { super().merge(switch_user: true, switch_user_revoke_params: { 'end_shared_session' => '1' }) }
|
|
268
297
|
|
|
298
|
+
it 'places the native switch-user form before sign-out without changing their protected fields' do
|
|
299
|
+
sign_in
|
|
300
|
+
get 'https://docs.example/api/v1/'
|
|
301
|
+
forms = last_response.body.scan(%r{<form method="post" action="https://docs.example/api/_documentation/oauth/logout">(.*?)</form>}m).flatten
|
|
302
|
+
expect(forms.length).to eq(2)
|
|
303
|
+
expect(forms.first).to include('name="switch_user" value="1"', '>Switch user</button>')
|
|
304
|
+
expect(forms.last).to include('>Sign out</button>')
|
|
305
|
+
expect(forms.last).not_to include('name="switch_user"')
|
|
306
|
+
csrf_fields = forms.map { |form| form[/name="csrf" value="([^"]+)"/, 1] }
|
|
307
|
+
expect(csrf_fields.first).to match(/\A[\w-]{43}\z/)
|
|
308
|
+
expect(csrf_fields.last).to eq(csrf_fields.first)
|
|
309
|
+
end
|
|
310
|
+
|
|
269
311
|
[nil, 'null', 'https://foreign.example', 'http://docs.example', 'https://docs.example:444'].each do |origin|
|
|
270
312
|
[false, true].each do |switch_user|
|
|
271
313
|
it "rejects #{switch_user ? 'switching' : 'sign-out'} from #{origin || 'an absent Origin'} without sign-out effects" do
|
|
@@ -86,7 +86,7 @@ RSpec.describe HaveAPI::DocumentationSearch::Helpers do
|
|
|
86
86
|
expect(response.content_type).to match(%r{\Atext/html;\s*charset=utf-8\z})
|
|
87
87
|
expect(response['cache-control']).to eq('private, max-age=86400, immutable')
|
|
88
88
|
expect(response['etag']).to eq(%("#{key}"))
|
|
89
|
-
expect(response['vary'].split(/,\s*/)).to contain_exactly('X-Language', 'Cookie', 'Authorization')
|
|
89
|
+
expect(response['vary'].split(/,\s*/)).to contain_exactly('X-Language', 'Accept-Language', 'Cookie', 'Authorization')
|
|
90
90
|
expect(envelope(response)).to include('key' => key)
|
|
91
91
|
expect(response.body).to include('\\u003c/script\\u003e', '\\u0026')
|
|
92
92
|
expect(response.body).not_to include('<!DOCTYPE', 'docs-sidebar', '<script src=', 'fixture')
|
|
@@ -139,10 +139,14 @@ RSpec.describe HaveAPI::DocumentationSearch::Helpers do
|
|
|
139
139
|
expect(envelope(current)['index']).to include('version' => '1', 'base_path' => '/api/v1/')
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
-
it 'binds the projection to
|
|
142
|
+
it 'binds the projection to the documentation browser locale independently of the API header and Rack/API mounts' do
|
|
143
143
|
server.available_locales = %i[en cs]
|
|
144
|
-
english = envelope(request('?page=resources&search_index=current', '
|
|
145
|
-
czech = envelope(request('?page=resources&search_index=current', '
|
|
144
|
+
english = envelope(request('?page=resources&search_index=current', 'HTTP_ACCEPT_LANGUAGE' => 'en', 'HTTP_X_LANGUAGE' => 'cs'))
|
|
145
|
+
czech = envelope(request('?page=resources&search_index=current', 'HTTP_ACCEPT_LANGUAGE' => 'cs', 'HTTP_X_LANGUAGE' => 'en'))
|
|
146
|
+
custom_header = envelope(request('?page=resources&search_index=current', 'HTTP_X_LANGUAGE' => 'cs'))
|
|
147
|
+
expect(english['index']['locale']).to eq('en')
|
|
148
|
+
expect(custom_header['index']['locale']).to eq('en')
|
|
149
|
+
expect(custom_header['key']).to eq(english['key'])
|
|
146
150
|
expect(english['key']).not_to eq(czech['key'])
|
|
147
151
|
expect(czech['index']['locale']).to eq('cs')
|
|
148
152
|
mounted = envelope(request('?page=resources&search_index=current', 'SCRIPT_NAME' => '/rack'))
|
|
@@ -12,7 +12,7 @@ RSpec.describe HaveAPI::DocumentationSearch do
|
|
|
12
12
|
{ resources: { widget: { actions: { renew: action }, resources: {} } },
|
|
13
13
|
authentication: { token: { resources: { token: { actions: { request: action }, resources: {} } } } } }
|
|
14
14
|
end
|
|
15
|
-
let(:presentation) { Object.new.extend(described_class::Helpers) }
|
|
15
|
+
let(:presentation) { Object.new.extend(HaveAPI::DocumentationLocale).extend(described_class::Helpers) }
|
|
16
16
|
|
|
17
17
|
def search(query, source = description)
|
|
18
18
|
index = described_class::Index.new(description: source, base_path: '/v1/', version: '1', locale: 'en', visibility: 'anonymous')
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
2
|
require 'rack/mock'
|
|
3
|
+
require 'cgi'
|
|
4
|
+
require 'ripper'
|
|
3
5
|
|
|
4
6
|
RSpec.describe HaveAPI::SharedDocumentationHelpers do
|
|
5
7
|
api do
|
|
@@ -90,7 +92,7 @@ RSpec.describe HaveAPI::SharedDocumentationHelpers do
|
|
|
90
92
|
expect(last_response.body).to include(heading, 'aria-label="Breadcrumb"',
|
|
91
93
|
'href="/"', 'href="/doc"', 'href="/v1/?page=resources"')
|
|
92
94
|
expect_neutral_shell(last_response.body, 'guide')
|
|
93
|
-
breadcrumb = last_response.body.match(%r{<nav
|
|
95
|
+
breadcrumb = last_response.body.match(%r{<nav class="docs-breadcrumb"[^>]*>(.*?)</nav>}m)[1]
|
|
94
96
|
expect(breadcrumb).to include('API versions', 'HaveAPI internals')
|
|
95
97
|
expect(breadcrumb.scan(/<li\b/).length).to eq(path.match?(%r{\A/doc(?:/(?:index(?:\.md)?|readme))?\z}) ? 2 : 3)
|
|
96
98
|
expect(server).not_to have_received(:describe)
|
|
@@ -121,6 +123,65 @@ RSpec.describe HaveAPI::SharedDocumentationHelpers do
|
|
|
121
123
|
expect(pages.uniq.length).to eq(1)
|
|
122
124
|
end
|
|
123
125
|
|
|
126
|
+
def article_samples(body)
|
|
127
|
+
body.scan(%r{<pre><code(?: class="([^"]*)")?>(.*?)</code></pre>}m).map do |language, source|
|
|
128
|
+
code = CGI.unescapeHTML(source)
|
|
129
|
+
if language == 'ruby'
|
|
130
|
+
Ripper.lex(code).reject { |(_, type, _, _)| %i[on_sp on_nl on_ignored_nl on_comment].include?(type) }
|
|
131
|
+
.map { |(_, type, token, _)| [type, token] }
|
|
132
|
+
else
|
|
133
|
+
code.lines.reject { |line| line.lstrip.start_with?('#', '//') }.join.strip
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
{
|
|
139
|
+
'readme' => 'Serverová implementace',
|
|
140
|
+
'protocol' => 'Definice protokolu',
|
|
141
|
+
'create-client' => 'Definice klienta',
|
|
142
|
+
'typed-input-validation' => 'Validace typovaných vstupů',
|
|
143
|
+
'client-examples' => 'Klientské příklady'
|
|
144
|
+
}.each do |article, heading|
|
|
145
|
+
it "translates #{article} while preserving its English anchors and noncomment samples" do
|
|
146
|
+
get "/doc/#{article}", {}, 'HTTP_ACCEPT_LANGUAGE' => 'en'
|
|
147
|
+
english = last_response.body
|
|
148
|
+
expect(last_response).to be_ok
|
|
149
|
+
get "/doc/#{article}", {}, 'HTTP_ACCEPT_LANGUAGE' => 'cs-CZ'
|
|
150
|
+
czech = last_response.body
|
|
151
|
+
expect(last_response).to be_ok
|
|
152
|
+
expect(czech).to include('lang="cs"', heading)
|
|
153
|
+
expect(czech.scan(/<h[1-6]\b[^>]*\bid="([^"]+)"/).flatten)
|
|
154
|
+
.to eq(english.scan(/<h[1-6]\b[^>]*\bid="([^"]+)"/).flatten)
|
|
155
|
+
expect(article_samples(czech)).not_to be_empty
|
|
156
|
+
expect(article_samples(czech)).to eq(article_samples(english))
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'uses identical schema code for the English and Czech presentations' do
|
|
161
|
+
get '/doc/json-schema', {}, 'HTTP_ACCEPT_LANGUAGE' => 'en'
|
|
162
|
+
english = last_response.body
|
|
163
|
+
get '/doc/json-schema', {}, 'HTTP_ACCEPT_LANGUAGE' => 'cs'
|
|
164
|
+
expect(last_response).to be_ok
|
|
165
|
+
expect(last_response.body).to include('<h1 id="envelope">Obálka</h1>')
|
|
166
|
+
expect(article_samples(last_response.body)).to eq(article_samples(english))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'keeps translated article and shared asset links mounted under Rack and API prefixes' do
|
|
170
|
+
server = HaveAPI::Server.new(get_opt(:api_module))
|
|
171
|
+
server.use_version(1)
|
|
172
|
+
server.mount('/api/')
|
|
173
|
+
request = Rack::MockRequest.new(server.app)
|
|
174
|
+
response = request.get('/api/doc/protocol', 'HTTP_HOST' => 'example.test',
|
|
175
|
+
'SCRIPT_NAME' => '/rack', 'HTTP_ACCEPT_LANGUAGE' => 'cs')
|
|
176
|
+
expect(response.status).to eq(200)
|
|
177
|
+
expect(response.body).to include('href="/rack/api/doc/typed-input-validation"',
|
|
178
|
+
'src="/rack/api/doc/protocol.png"')
|
|
179
|
+
response = request.get('/api/doc/client-examples', 'HTTP_HOST' => 'example.test',
|
|
180
|
+
'SCRIPT_NAME' => '/rack', 'HTTP_ACCEPT_LANGUAGE' => 'cs')
|
|
181
|
+
expect(response.body).to include('href="/rack/api/doc/documentation-ui"',
|
|
182
|
+
'href="/rack/api/doc/documentation-oauth"')
|
|
183
|
+
end
|
|
184
|
+
|
|
124
185
|
it 'uses configured escaped identity or the actual host and port in neutral titles and brand' do
|
|
125
186
|
server = HaveAPI::Server.new(get_opt(:api_module))
|
|
126
187
|
server.use_version(1)
|
|
@@ -191,7 +252,7 @@ RSpec.describe HaveAPI::SharedDocumentationHelpers do
|
|
|
191
252
|
expect(response['Cache-Control']).to eq('private, no-store')
|
|
192
253
|
expect(response.body).to include('href="/rack/api/"', 'href="/rack/api/doc"',
|
|
193
254
|
'href="/rack/api/v1/"', 'href="/rack/api/v1/?page=resources"',
|
|
194
|
-
|
|
255
|
+
"src=\"/rack/js/documentation.js?v=#{HaveAPI::DocumentationHelpers::ASSET_DIGESTS.fetch('js/documentation.js')}\"")
|
|
195
256
|
expect(response.body).not_to include('href="/doc"', 'href="/v1/"')
|
|
196
257
|
end
|
|
197
258
|
end
|