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.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/benchmarks/documentation.rb +29 -6
  4. data/benchmarks/endpoints.rb +153 -24
  5. data/benchmarks/spec/client_examples_spec.rb +109 -37
  6. data/doc/client-examples.md +218 -45
  7. data/doc/documentation-oauth.md +157 -0
  8. data/doc/documentation-ui.md +192 -0
  9. data/doc/index.md +3 -6
  10. data/doc/performance.md +39 -12
  11. data/haveapi.gemspec +1 -1
  12. data/lib/haveapi/authentication/chain.rb +19 -0
  13. data/lib/haveapi/authentication/oauth2/provider.rb +7 -1
  14. data/lib/haveapi/client_example.rb +143 -15
  15. data/lib/haveapi/client_examples/curl.rb +30 -20
  16. data/lib/haveapi/client_examples/dynamic_helpers.rb +1 -1
  17. data/lib/haveapi/client_examples/go_client.rb +9 -2
  18. data/lib/haveapi/client_examples/http.rb +9 -4
  19. data/lib/haveapi/client_examples/js_async_client.rb +7 -0
  20. data/lib/haveapi/client_examples/js_client.rb +47 -36
  21. data/lib/haveapi/client_examples/php_client.rb +161 -75
  22. data/lib/haveapi/client_examples/python_async_client.rb +3 -0
  23. data/lib/haveapi/client_examples/python_client.rb +61 -47
  24. data/lib/haveapi/client_examples/ruby_cli.rb +27 -18
  25. data/lib/haveapi/client_examples/ruby_client.rb +80 -31
  26. data/lib/haveapi/client_examples/rust_client.rb +20 -9
  27. data/lib/haveapi/client_examples/ts_async_client.rb +7 -0
  28. data/lib/haveapi/client_examples/ts_client.rb +3 -0
  29. data/lib/haveapi/client_examples/ts_generated_client.rb +10 -2
  30. data/lib/haveapi/documentation_context.rb +7 -2
  31. data/lib/haveapi/documentation_helpers.rb +223 -0
  32. data/lib/haveapi/documentation_oauth.rb +399 -0
  33. data/lib/haveapi/documentation_search.rb +225 -0
  34. data/lib/haveapi/documentation_search_index.rb +79 -0
  35. data/lib/haveapi/documentation_validators.rb +179 -0
  36. data/lib/haveapi/public/css/documentation.css +343 -0
  37. data/lib/haveapi/public/js/documentation.js +992 -0
  38. data/lib/haveapi/server.rb +144 -86
  39. data/lib/haveapi/shared_documentation_helpers.rb +54 -0
  40. data/lib/haveapi/version.rb +1 -1
  41. data/lib/haveapi/views/doc_layout.erb +16 -0
  42. data/lib/haveapi/views/doc_sidebars/create-client.erb +3 -3
  43. data/lib/haveapi/views/doc_sidebars/json-schema.erb +1 -1
  44. data/lib/haveapi/views/doc_sidebars/protocol.erb +5 -5
  45. data/lib/haveapi/views/documentation/search_form.erb +10 -0
  46. data/lib/haveapi/views/documentation/search_index.erb +1 -0
  47. data/lib/haveapi/views/documentation/search_results.erb +20 -0
  48. data/lib/haveapi/views/index.erb +2 -4
  49. data/lib/haveapi/views/main_layout.erb +12 -6
  50. data/lib/haveapi/views/shared_documentation.erb +12 -0
  51. data/lib/haveapi/views/shared_sidebar.erb +25 -0
  52. data/lib/haveapi/views/version_page/auth_body.erb +12 -33
  53. data/lib/haveapi/views/version_page/client_overview.erb +40 -0
  54. data/lib/haveapi/views/version_page/example_panels.erb +18 -0
  55. data/lib/haveapi/views/version_page/resource_body.erb +91 -132
  56. data/lib/haveapi/views/version_page/resource_directory.erb +14 -0
  57. data/lib/haveapi/views/version_page/usage.erb +7 -0
  58. data/lib/haveapi/views/version_page.erb +36 -117
  59. data/lib/haveapi/views/version_sidebar/auth_nav.erb +12 -16
  60. data/lib/haveapi/views/version_sidebar/auth_resource_nav.erb +19 -0
  61. data/lib/haveapi/views/version_sidebar/authentication_control.erb +26 -0
  62. data/lib/haveapi/views/version_sidebar/authentication_status.erb +3 -0
  63. data/lib/haveapi/views/version_sidebar/brand.erb +31 -0
  64. data/lib/haveapi/views/version_sidebar/resource_nav.erb +11 -20
  65. data/lib/haveapi/views/version_sidebar.erb +59 -48
  66. data/spec/authentication/basic_spec.rb +10 -10
  67. data/spec/authentication/oauth2_chain_spec.rb +72 -0
  68. data/spec/authentication/oauth2_token_validation_spec.rb +38 -0
  69. data/spec/client_examples/assets/documentation_test.cjs +817 -0
  70. data/spec/client_examples/context_performance_spec.rb +5 -4
  71. data/spec/client_examples/context_spec.rb +23 -2
  72. data/spec/client_examples/detail_spec.rb +175 -0
  73. data/spec/client_examples/dynamic_acceptance.md +59 -26
  74. data/spec/client_examples/dynamic_acceptance.rb +164 -44
  75. data/spec/client_examples/dynamic_acceptance_spec.rb +140 -23
  76. data/spec/client_examples/dynamic_fixture.rb +4 -3
  77. data/spec/client_examples/dynamic_js_spec.rb +20 -11
  78. data/spec/client_examples/dynamic_metadata_spec.rb +69 -0
  79. data/spec/client_examples/dynamic_mounted_paths_spec.rb +9 -7
  80. data/spec/client_examples/dynamic_python_spec.rb +62 -11
  81. data/spec/client_examples/go_spec.rb +14 -1
  82. data/spec/client_examples/presentation_spec.rb +247 -0
  83. data/spec/client_examples/profile_mount_spec.rb +40 -13
  84. data/spec/client_examples/rust_client_spec.rb +35 -1
  85. data/spec/client_examples/shell_spec.rb +268 -0
  86. data/spec/client_examples/syntax_spec.rb +192 -0
  87. data/spec/client_examples/ts_generated_client_spec.rb +15 -2
  88. data/spec/client_examples/typescript_profiles_spec.rb +1 -1
  89. data/spec/documentation/auth_filtering_spec.rb +9 -4
  90. data/spec/documentation/canonical_routes_spec.rb +89 -0
  91. data/spec/documentation/client_examples_spec.rb +39 -5
  92. data/spec/documentation/client_overview_spec.rb +145 -0
  93. data/spec/documentation/current_user_html_escaping_spec.rb +7 -2
  94. data/spec/documentation/example_variants_spec.rb +93 -0
  95. data/spec/documentation/fixtures/search_index.json +476 -0
  96. data/spec/documentation/identity_and_guides_spec.rb +130 -0
  97. data/spec/documentation/identity_context_spec.rb +180 -0
  98. data/spec/documentation/navigation_spec.rb +296 -0
  99. data/spec/documentation/oauth_fixture_spec.rb +39 -0
  100. data/spec/documentation/oauth_spec.rb +912 -0
  101. data/spec/documentation/oauth_transport_spec.rb +45 -0
  102. data/spec/documentation/performance_spec.rb +47 -6
  103. data/spec/documentation/reference_headings_spec.rb +190 -0
  104. data/spec/documentation/search_index_routes_spec.rb +202 -0
  105. data/spec/documentation/search_index_spec.rb +85 -0
  106. data/spec/documentation/search_routes_spec.rb +199 -0
  107. data/spec/documentation/search_spec.rb +134 -0
  108. data/spec/documentation/shared_pages_spec.rb +240 -0
  109. data/spec/documentation/validators_spec.rb +123 -0
  110. data/spec/documentation_spec.rb +3 -3
  111. data/spec/packaging_spec.rb +21 -2
  112. data/spec/performance/documentation_identity_fixture_spec.rb +47 -0
  113. data/spec/performance/documentation_shapes_spec.rb +97 -0
  114. data/spec/performance/json_activation_spec.rb +78 -1
  115. data/spec/performance/release_lineage_spec.rb +4 -3
  116. data/spec/performance/runner_spec.rb +1 -0
  117. data/test_support/all_client_performance_fixture.rb +6 -10
  118. data/test_support/documentation_oauth_fixture.rb +54 -0
  119. data/test_support/documentation_performance_fixture.rb +19 -1
  120. data/test_support/dynamic_documentation_api.rb +35 -10
  121. data/test_support/dynamic_python_login_spec.rb +3 -11
  122. 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
- <li>
2
- <a href="#<%= "#{prefix}-#{resource}" %>">.<%= resource.humanize %></a>
3
-
4
- <ul>
5
- <% sort_hash(info[:actions]).each do |action, info| %>
6
- <li>
7
- <a href="#<%= "#{prefix}-#{resource}-#{action}" %>">#<%= action.capitalize %></a>
8
- </li>
9
- <% end %>
10
-
11
- <% sort_hash(info[:resources]).each do |r, i| %>
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
- <h1>Authentication</h1>
2
- <p class="authentication">
3
- <% if current_user %>
4
- Logged as <%= escape_html(current_user.login) %> [<a class="logout" href="<%= escape_html(logout_url) %>">logout</a>]
5
- <% elsif @help[:authentication].any? %>
6
- <a class="login btn btn-default" href="<%= escape_html(url("#{root}_login")) %>">Login</a>
7
- <% else %>
8
- Authentication disabled.
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
- </p>
11
- <p>
12
- <% if current_user %>
13
- Listing only accessible resources, actions and parameters.
14
- <% else %>
15
- Listing all resources, actions and parameters.
16
- <% end %>
17
- </p>
18
-
19
- <h1>Contents</h1>
20
- <ul class="top-level">
21
- <li><a href="#api">API v<%= @v %></a></li>
22
- <li>
23
- <a href="#auth">Authentication</a>
24
- <%= erb :"version_sidebar/auth_nav", locals: {methods: @help[:authentication]} %>
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 'returns an authentication error envelope from _login' do
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(401)
124
- expect(last_response.headers['www-authenticate']).to include('Basic realm=')
125
- expect(api_response).to be_failed
126
- expect(api_response.message).to eq('This action requires authentication')
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 'returns an authentication error envelope from _logout' do
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(401)
133
- expect(last_response.headers['www-authenticate']).to include('Basic realm=')
134
- expect(api_response).to be_failed
135
- expect(api_response.message).to eq('This action requires authentication')
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