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,19 @@
|
|
|
1
|
+
<ul>
|
|
2
|
+
<% sort_hash(resources).each do |resource, info| %>
|
|
3
|
+
<% resource_path = path + [resource.to_s] %>
|
|
4
|
+
<li><a data-doc-subsection href="#<%= escape_html(documentation_resource_target(resource_path: resource_path, authentication: authentication_provider)) %>"><%= escape_html(resource_path.join('.')) %></a>
|
|
5
|
+
<ul>
|
|
6
|
+
<% sort_hash(info[:actions]).each do |action, action_info| %>
|
|
7
|
+
<li><a data-doc-subsection href="#<%= escape_html(documentation_resource_target(resource_path: resource_path, action_name: action, authentication: authentication_provider)) %>"><%= escape_html(action.to_s.humanize) %></a>
|
|
8
|
+
<ul>
|
|
9
|
+
<% documentation_action_sections(action_info, resource_path: resource_path, action_name: action, authentication: authentication_provider).each do |section| %>
|
|
10
|
+
<li><a data-doc-subsection href="#<%= escape_html(section[:id]) %>"><%= escape_html(section[:label]) %></a></li>
|
|
11
|
+
<% end %>
|
|
12
|
+
</ul>
|
|
13
|
+
</li>
|
|
14
|
+
<% end %>
|
|
15
|
+
</ul>
|
|
16
|
+
<% unless info[:resources].empty? %><%= erb :'version_sidebar/auth_resource_nav', locals: {resources: info[:resources], path: resource_path, authentication_provider: authentication_provider} %><% end %>
|
|
17
|
+
</li>
|
|
18
|
+
<% end %>
|
|
19
|
+
</ul>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<% controls = account[:controls] %>
|
|
2
|
+
<% if controls %>
|
|
3
|
+
<div class="authentication">
|
|
4
|
+
<% if account[:user] && controls[:signed_in] %>
|
|
5
|
+
<div class="docs-account-content">
|
|
6
|
+
<span class="sr-only">Account </span><span class="docs-identity"><%= escape_html(account[:user].login) %></span>
|
|
7
|
+
<form method="post" action="<%= escape_html(controls[:logout_url]) %>">
|
|
8
|
+
<input type="hidden" name="csrf" value="<%= escape_html(controls[:csrf_token]) %>">
|
|
9
|
+
<button type="submit">Sign out</button>
|
|
10
|
+
</form>
|
|
11
|
+
<% if controls[:switch_user] %>
|
|
12
|
+
<form method="post" action="<%= escape_html(controls[:logout_url]) %>">
|
|
13
|
+
<input type="hidden" name="csrf" value="<%= escape_html(controls[:csrf_token]) %>">
|
|
14
|
+
<input type="hidden" name="switch_user" value="1">
|
|
15
|
+
<button type="submit">Switch user</button>
|
|
16
|
+
</form>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
19
|
+
<% else %>
|
|
20
|
+
<a class="docs-sign-in" href="<%= escape_html(controls[:login_url]) %>" data-documentation-login aria-label="Sign in" title="Sign in">
|
|
21
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"></circle><path d="M4 21v-2a8 8 0 0 1 16 0v2"></path></svg>
|
|
22
|
+
<span>Sign in</span>
|
|
23
|
+
</a>
|
|
24
|
+
<% end %>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<% controls = account[:controls] %>
|
|
2
|
+
<p class="docs-visibility-note"><%= account[:user] ? 'Showing resources, actions and parameters available to this account.' : 'Showing all resources, actions and parameters.' %></p>
|
|
3
|
+
<% if controls && controls[:message] %><p role="status"><%= escape_html(controls[:message]) %></p><% end %>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<div class="docs-brand">
|
|
2
|
+
<a href="<%= escape_html(url(root, false)) %>"><%= escape_html(documentation_identity_name) %></a>
|
|
3
|
+
<% if @v %><small class="docs-api-version">API v<%= escape_html(@v) %></small><% end %>
|
|
4
|
+
<details data-documentation-menu>
|
|
5
|
+
<summary aria-label="Documentation settings" title="Documentation settings">
|
|
6
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2"><path d="m9.5 3-.5 2.1-1.7 1-2.1-.6-2.5 4.3 1.6 1.5v2l-1.6 1.5 2.5 4.3 2.1-.6 1.7 1 .5 2.1h5l.5-2.1 1.7-1 2.1.6 2.5-4.3-1.6-1.5v-2l1.6-1.5-2.5-4.3-2.1.6-1.7-1L14.5 3Z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
|
7
|
+
</summary>
|
|
8
|
+
<div data-documentation-menu-content>
|
|
9
|
+
<span id="docs-theme-label" class="docs-theme-label">Theme</span>
|
|
10
|
+
<div class="docs-theme-choices" role="group" aria-labelledby="docs-theme-label">
|
|
11
|
+
<% {auto: 'Automatic theme', light: 'Light theme', dark: 'Dark theme'}.each do |choice, label| %>
|
|
12
|
+
<button type="button" data-theme-choice="<%= choice %>" aria-label="<%= label %>" title="<%= label %>" aria-pressed="<%= choice == :auto %>">
|
|
13
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2">
|
|
14
|
+
<% if choice == :auto %><circle cx="12" cy="12" r="8"></circle><path d="M12 4a8 8 0 0 0 0 16Z" fill="currentColor"></path>
|
|
15
|
+
<% elsif choice == :light %><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l2 2m10 10 2 2M5 19l2-2M17 7l2-2"></path>
|
|
16
|
+
<% else %><path d="M20 15.5A8.5 8.5 0 0 1 8.5 4 8.5 8.5 0 1 0 20 15.5Z"></path><% end %>
|
|
17
|
+
</svg>
|
|
18
|
+
</button>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
<% if account %>
|
|
22
|
+
<%= erb :'version_sidebar/authentication_control', locals: {account: account} %>
|
|
23
|
+
<% elsif (account_url = documentation_neutral_account_url) %>
|
|
24
|
+
<a class="docs-account-link" href="<%= escape_html(account_url) %>" aria-label="Account" title="Account"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"></circle><path d="M4 21v-2a8 8 0 0 1 16 0v2"></path></svg><span>Account</span></a>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
27
|
+
</details>
|
|
28
|
+
<% if account %>
|
|
29
|
+
<%= erb :'version_sidebar/authentication_status', locals: {account: account} %>
|
|
30
|
+
<% end %>
|
|
31
|
+
</div>
|
|
@@ -1,21 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<%=
|
|
13
|
-
erb :"version_sidebar/resource_nav", locals: {
|
|
14
|
-
resource: r,
|
|
15
|
-
info: i,
|
|
16
|
-
prefix: "#{prefix}-#{resource}",
|
|
17
|
-
}
|
|
18
|
-
%>
|
|
19
|
-
<% end %>
|
|
20
|
-
</ul>
|
|
1
|
+
<% resource_path = path + [resource.to_s] %>
|
|
2
|
+
<% key = resource_path.join('.') %>
|
|
3
|
+
<li data-resource-item>
|
|
4
|
+
<a href="<%= escape_html(documentation_url(resource: key)) %>" data-resource-link="<%= escape_html(key) %>"<% if @documentation_resource && @documentation_resource[:key] == key %> aria-current="page"<% end %>><%= escape_html(key) %></a>
|
|
5
|
+
<% unless info[:resources].empty? %>
|
|
6
|
+
<ul>
|
|
7
|
+
<% sort_hash(info[:resources]).each do |child, child_info| %>
|
|
8
|
+
<%= erb :'version_sidebar/resource_nav', locals: {resource: child, info: child_info, path: resource_path} %>
|
|
9
|
+
<% end %>
|
|
10
|
+
</ul>
|
|
11
|
+
<% end %>
|
|
21
12
|
</li>
|
|
@@ -1,49 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
<%= erb :'version_sidebar/brand', locals: {account: documentation_account_context} %>
|
|
2
|
+
<% if (search_options = documentation_search_form_options(id: 'docs-search-sidebar')) %>
|
|
3
|
+
<%= erb :'documentation/search_form', locals: search_options %>
|
|
4
|
+
<% end %>
|
|
5
|
+
<nav aria-label="Documentation pages">
|
|
6
|
+
<ul>
|
|
7
|
+
<li><a href="<%= escape_html(documentation_url) %>"<% if @documentation_page == 'overview' %> aria-current="page"<% end %>>Overview</a></li>
|
|
8
|
+
<li>
|
|
9
|
+
<a href="<%= escape_html(documentation_url(page: 'authentication')) %>"<% if @documentation_page == 'authentication' %> aria-current="page"<% end %>>Authentication</a>
|
|
10
|
+
<% if @documentation_page == 'authentication' %>
|
|
11
|
+
<nav aria-label="Authentication methods" data-action-nav><%= erb :'version_sidebar/auth_nav', locals: {methods: @help[:authentication]} %></nav>
|
|
12
|
+
<% end %>
|
|
13
|
+
</li>
|
|
14
|
+
<li><a href="<%= escape_html(documentation_url(page: 'resources')) %>"<% if @documentation_page == 'resources' %> aria-current="page"<% end %> data-resource-directory-link>Resources</a></li>
|
|
15
|
+
</ul>
|
|
16
|
+
</nav>
|
|
17
|
+
<% if @documentation_resource %>
|
|
18
|
+
<p data-current-resource><a href="<%= escape_html(documentation_url(resource: @documentation_resource[:key])) %>"><strong><%= escape_html(@documentation_resource[:key]) %></strong></a></p>
|
|
19
|
+
<nav aria-label="Actions on this resource" data-action-nav>
|
|
20
|
+
<h2>Actions</h2>
|
|
21
|
+
<ul class="docs-action-list">
|
|
22
|
+
<% sort_hash(@documentation_resource[:info][:actions]).each do |action, info| %>
|
|
23
|
+
<% target = documentation_resource_target(resource_path: @documentation_resource[:path], action_name: action) %>
|
|
24
|
+
<li data-doc-nav-owner="<%= escape_html(target) %>">
|
|
25
|
+
<a data-doc-primary href="#<%= escape_html(target) %>"><%= escape_html(action.to_s.humanize) %></a>
|
|
26
|
+
<ul data-doc-subnav="<%= escape_html(target) %>">
|
|
27
|
+
<% documentation_action_sections(info, resource_path: @documentation_resource[:path], action_name: action).each do |section| %>
|
|
28
|
+
<li><a data-doc-subsection href="#<%= escape_html(section[:id]) %>"><%= escape_html(section[:label]) %></a></li>
|
|
29
|
+
<% end %>
|
|
30
|
+
</ul>
|
|
31
|
+
</li>
|
|
32
|
+
<% end %>
|
|
33
|
+
</ul>
|
|
34
|
+
</nav>
|
|
35
|
+
<% unless @documentation_resource[:info][:resources].empty? %>
|
|
36
|
+
<nav aria-label="Subresources">
|
|
37
|
+
<h2>Subresources</h2>
|
|
38
|
+
<ul>
|
|
39
|
+
<% sort_hash(@documentation_resource[:info][:resources]).each do |child, _child_info| %>
|
|
40
|
+
<% child_path = @documentation_resource[:path] + [child.to_s] %>
|
|
41
|
+
<li><a href="<%= escape_html(documentation_url(resource: child_path)) %>"><%= escape_html(child_path.join('.')) %></a></li>
|
|
42
|
+
<% end %>
|
|
43
|
+
</ul>
|
|
44
|
+
</nav>
|
|
9
45
|
<% end %>
|
|
10
|
-
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
</li>
|
|
26
|
-
<li><a href="#resources">Resources</a></li>
|
|
27
|
-
<% sort_hash(@help[:resources]).each do |resource, info| %>
|
|
28
|
-
<%=
|
|
29
|
-
erb :"version_sidebar/resource_nav", locals: {
|
|
30
|
-
resource: resource,
|
|
31
|
-
info: info,
|
|
32
|
-
prefix: 'root',
|
|
33
|
-
}
|
|
34
|
-
%>
|
|
35
|
-
<% end %>
|
|
36
|
-
</ul>
|
|
37
|
-
|
|
38
|
-
<h1>Browser</h1>
|
|
39
|
-
<p>
|
|
40
|
-
Browse this API with <a href="https://github.com/vpsfreecz/haveapi-webui">haveapi-webui</a>:
|
|
41
|
-
</p>
|
|
42
|
-
<p>
|
|
43
|
-
<a
|
|
44
|
-
href="https://webui.haveapi.org/v<%= version %>/#<%= urlescape(base_url) %>"
|
|
45
|
-
class="btn btn-default"
|
|
46
|
-
target="_blank">
|
|
47
|
-
Connect
|
|
48
|
-
</a>
|
|
49
|
-
</p>
|
|
46
|
+
<% end %>
|
|
47
|
+
<div class="docs-preferences">
|
|
48
|
+
<label>Language <select data-language-select>
|
|
49
|
+
<% documentation_client_metadata.uniq { |client| client[:language] }.each do |client| %>
|
|
50
|
+
<option value="<%= escape_html(client[:language]) %>"><%= escape_html(client[:language_label]) %></option>
|
|
51
|
+
<% end %>
|
|
52
|
+
</select></label>
|
|
53
|
+
<label data-variant-control>Client <select data-variant-select>
|
|
54
|
+
<% documentation_clients.each do |client| %>
|
|
55
|
+
<option value="<%= escape_html(client.id) %>"><%= escape_html(client.variant || client.language_label) %></option>
|
|
56
|
+
<% end %>
|
|
57
|
+
</select></label>
|
|
58
|
+
<label data-detail-control>Examples <select data-detail-select><option value="simple">Simple</option><option value="thorough">With error handling</option></select></label>
|
|
59
|
+
<p data-preference-status role="status"></p>
|
|
60
|
+
</div>
|
|
@@ -117,21 +117,21 @@ describe HaveAPI::Authentication::Basic::Provider do
|
|
|
117
117
|
expect(seen_users.last).to be_nil
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
-
it '
|
|
120
|
+
it 'redirects the legacy documentation login without an authentication challenge' do
|
|
121
121
|
get '/_login'
|
|
122
122
|
|
|
123
|
-
expect(last_response.status).to eq(
|
|
124
|
-
expect(last_response.headers['
|
|
125
|
-
expect(
|
|
126
|
-
expect(
|
|
123
|
+
expect(last_response.status).to eq(303)
|
|
124
|
+
expect(last_response.headers['location']).to eq('/v1/')
|
|
125
|
+
expect(last_response.headers['www-authenticate']).to be_nil
|
|
126
|
+
expect(seen_users).to be_empty
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
-
it '
|
|
129
|
+
it 'redirects the legacy documentation logout without an authentication challenge' do
|
|
130
130
|
get '/_logout'
|
|
131
131
|
|
|
132
|
-
expect(last_response.status).to eq(
|
|
133
|
-
expect(last_response.headers['
|
|
134
|
-
expect(
|
|
135
|
-
expect(
|
|
132
|
+
expect(last_response.status).to eq(303)
|
|
133
|
+
expect(last_response.headers['location']).to eq('/v1/')
|
|
134
|
+
expect(last_response.headers['www-authenticate']).to be_nil
|
|
135
|
+
expect(seen_users).to be_empty
|
|
136
136
|
end
|
|
137
137
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe HaveAPI::Authentication::Chain do
|
|
4
|
+
let(:server) { instance_double(HaveAPI::Server, add_auth_routes: nil) }
|
|
5
|
+
let(:chain) { described_class.new(server) }
|
|
6
|
+
let(:request) { Rack::Request.new(Rack::MockRequest.env_for('https://docs.example/v1/?access_token=api-token')) }
|
|
7
|
+
let(:calls) { [] }
|
|
8
|
+
|
|
9
|
+
def provider(name, result: nil, token_validation: true, deny_token: false)
|
|
10
|
+
recorded = calls
|
|
11
|
+
Class.new(HaveAPI::Authentication::Base) do
|
|
12
|
+
auth_method name
|
|
13
|
+
define_method(:authenticate) do |req|
|
|
14
|
+
recorded << [:ordinary, @version, req]
|
|
15
|
+
result
|
|
16
|
+
end
|
|
17
|
+
if token_validation
|
|
18
|
+
define_method(:authenticate_access_token) do |req, token|
|
|
19
|
+
recorded << [:cookie, @version, req, token]
|
|
20
|
+
deny if deny_token
|
|
21
|
+
result
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'validates only the requested version OAuth providers in registration order' do
|
|
28
|
+
chain[1] << provider(:basic, result: :basic_user)
|
|
29
|
+
chain[1] << provider(:oauth2)
|
|
30
|
+
chain[1] << provider(:token, result: :token_user)
|
|
31
|
+
chain[1] << provider(:oauth2, result: :cookie_user)
|
|
32
|
+
chain[1] << provider(:oauth2, result: :unused_user)
|
|
33
|
+
chain[2] << provider(:oauth2, result: :other_version)
|
|
34
|
+
chain.setup([1, 2])
|
|
35
|
+
|
|
36
|
+
expect(chain.authenticate_oauth2_token(1, request, 'cookie-token')).to eq(:cookie_user)
|
|
37
|
+
expect(calls).to eq([[:cookie, 1, request, 'cookie-token'], [:cookie, 1, request, 'cookie-token']])
|
|
38
|
+
calls.clear
|
|
39
|
+
expect(chain.authenticate_oauth2_token(2, request, 'cookie-token')).to eq(:other_version)
|
|
40
|
+
expect(calls).to eq([[:cookie, 2, request, 'cookie-token']])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'fails closed for a custom oauth2 provider without explicit token validation' do
|
|
44
|
+
chain[1] << provider(:oauth2, result: :ordinary_user, token_validation: false)
|
|
45
|
+
chain[1] << provider(:basic, result: :basic_user)
|
|
46
|
+
chain.setup([1])
|
|
47
|
+
|
|
48
|
+
expect(chain.authenticate_oauth2_token(1, request, 'cookie-token')).to be_nil
|
|
49
|
+
expect(calls).to be_empty
|
|
50
|
+
expect(chain.authenticate(1, request)).to eq(:ordinary_user)
|
|
51
|
+
expect(calls).to eq([[:ordinary, 1, request]])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'preserves provider deny semantics instead of trying a later OAuth provider' do
|
|
55
|
+
chain[1] << provider(:oauth2, deny_token: true)
|
|
56
|
+
chain[1] << provider(:oauth2, result: :later_user)
|
|
57
|
+
chain.setup([1])
|
|
58
|
+
|
|
59
|
+
expect(chain.authenticate_oauth2_token(1, request, 'cookie-token')).to be_nil
|
|
60
|
+
expect(calls).to eq([[:cookie, 1, request, 'cookie-token']])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'does not authenticate another version or ordinary provider when OAuth is unavailable' do
|
|
64
|
+
chain[1] << provider(:basic, result: :basic_user)
|
|
65
|
+
chain[2] << provider(:oauth2, result: :other_user)
|
|
66
|
+
chain.setup([1, 2])
|
|
67
|
+
|
|
68
|
+
expect(chain.authenticate_oauth2_token(1, request, 'cookie-token')).to be_nil
|
|
69
|
+
expect(chain.authenticate_oauth2_token(3, request, 'cookie-token')).to be_nil
|
|
70
|
+
expect(calls).to be_empty
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe HaveAPI::Authentication::OAuth2::Provider do
|
|
4
|
+
let(:server) { instance_double(HaveAPI::Server, allow_header: nil) }
|
|
5
|
+
let(:provider) { described_class.new(server, 1, HaveAPI::Authentication::OAuth2::Config) }
|
|
6
|
+
let(:request) do
|
|
7
|
+
env = Rack::MockRequest.env_for(
|
|
8
|
+
'https://docs.example/v1/?access_token=query-token',
|
|
9
|
+
'HTTP_AUTHORIZATION' => 'Bearer api-token', 'HTTP_X_HAVEAPI_OAUTH2_TOKEN' => 'custom-token'
|
|
10
|
+
)
|
|
11
|
+
Rack::Request.new(env)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'validates the supplied token with the original request without extracting or mutating API credentials' do
|
|
15
|
+
request.params
|
|
16
|
+
original = request.env.dup
|
|
17
|
+
user = Object.new
|
|
18
|
+
allow(provider.config).to receive(:find_user_by_access_token).with(request, 'cookie-token').and_return(user)
|
|
19
|
+
|
|
20
|
+
expect(provider.authenticate_access_token(request, 'cookie-token')).to equal(user)
|
|
21
|
+
expect(provider.config).to have_received(:find_user_by_access_token).once
|
|
22
|
+
expect(request.env).to eq(original)
|
|
23
|
+
expect(request.params).to eq('access_token' => 'query-token')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'retains ordinary multiple-token conflict handling before backend lookup' do
|
|
27
|
+
allow(provider.config).to receive(:find_user_by_access_token)
|
|
28
|
+
expect { provider.authenticate(request) }.to raise_error(HaveAPI::Authentication::TokenConflict)
|
|
29
|
+
expect(provider.config).not_to have_received(:find_user_by_access_token)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'converts only the existing authentication error into a failed token lookup' do
|
|
33
|
+
allow(provider.config).to receive(:find_user_by_access_token).and_raise(HaveAPI::AuthenticationError)
|
|
34
|
+
expect(provider.authenticate_access_token(request, 'cookie-token')).to be_nil
|
|
35
|
+
allow(provider.config).to receive(:find_user_by_access_token).and_raise(TypeError, 'backend contract error')
|
|
36
|
+
expect { provider.authenticate_access_token(request, 'cookie-token') }.to raise_error(TypeError, 'backend contract error')
|
|
37
|
+
end
|
|
38
|
+
end
|