diff --git a/app/assets/images/ORCID-iD_icon_unauth_vector.svg b/app/assets/images/ORCID-iD_icon_unauth_vector.svg new file mode 100644 index 000000000..02b90a8eb --- /dev/null +++ b/app/assets/images/ORCID-iD_icon_unauth_vector.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/ORCID-iD_icon_vector.svg b/app/assets/images/ORCID-iD_icon_vector.svg new file mode 100644 index 000000000..375b457a4 --- /dev/null +++ b/app/assets/images/ORCID-iD_icon_vector.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index dd748642c..a2c25b90d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -48,7 +48,7 @@ def handle_error(status_code = 500, message = nil) status_code = status_code.to_i @message = message respond_to do |format| - format.html { render 'static/error', status: status_code } + format.html { render 'static/error', status: status_code } format.json { render json: { error: { message: message, code: status_code } }, status: status_code } format.json_api { render json: { error: { message: message, code: status_code } }, status: status_code } format.any { head status_code } diff --git a/app/controllers/orcid_controller.rb b/app/controllers/orcid_controller.rb new file mode 100644 index 000000000..970c1a572 --- /dev/null +++ b/app/controllers/orcid_controller.rb @@ -0,0 +1,46 @@ +class OrcidController < ApplicationController + before_action :authenticate_user! + before_action :set_oauth_client, only: [:authenticate, :callback] + + # Faraday::ParsingError occurs in rack-oauth2 if the response does not contain JSON + rescue_from(Rack::OAuth2::Client::Error, Faraday::ParsingError) do + handle_error(:unprocessable_entity, t('orcid.error')) + end + + def authenticate + redirect_to @oauth2_client.authorization_uri(scope: '/authenticate'), allow_other_host: true + end + + def callback + @oauth2_client.authorization_code = params[:code] + token = Rack::OAuth2::AccessToken::Bearer.new(access_token: @oauth2_client.access_token!) + orcid = token.access_token&.raw_attributes['orcid'] + respond_to do |format| + profile = current_user.profile + if orcid.present? + if profile.authenticate_orcid(orcid) + flash[:notice] = t('orcid.authentication_success') + else + flash[:error] = profile.errors.full_messages.join(', ') + end + else + flash[:error] = t('orcid.authentication_failure') + end + format.html { redirect_to current_user } + end + end + + private + + def set_oauth_client + config = Rails.application.config.secrets.orcid + @oauth2_client ||= Rack::OAuth2::Client.new( + identifier: config[:client_id], + secret: config[:secret], + redirect_uri: config[:redirect_uri].presence || orcid_callback_url, + authorization_endpoint: '/oauth/authorize', + token_endpoint: '/oauth/token', + host: config[:host].presence || (Rails.env.production? ? 'orcid.org' : 'sandbox.orcid.org') + ) + end +end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 17e2ccbbc..7a0b752d4 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -5,4 +5,19 @@ def self.user_profile_resource_limit 30 end + def orcid_link(profile, **opts) + content_tag(:span) do + if profile.orcid_authenticated? + concat image_tag('ORCID-iD_icon_vector.svg', size: 16) + concat ' ' + concat external_link(profile.orcid, profile.orcid_url, **opts) + elsif profile.orcid.present? + concat image_tag('ORCID-iD_icon_unauth_vector.svg', size: 16) + concat ' ' + concat external_link(profile.orcid, profile.orcid_url, **opts) + concat ' (Unauthenticated)' + end + end + end + end diff --git a/app/models/profile.rb b/app/models/profile.rb index 70da09ec4..9935bcca9 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -25,6 +25,11 @@ def full_name "#{firstname} #{surname}".strip end + def orcid_url + return nil if orcid.blank? + "#{OrcidValidator::ORCID_PREFIX}#{orcid}" + end + def merge(*others) Profile.transaction do attrs = attributes @@ -43,16 +48,27 @@ def merge(*others) end end + def authenticate_orcid(orcid) + existing = Profile.where(orcid: orcid, orcid_authenticated: true) + self.orcid = orcid + self.orcid_authenticated = true + out = self.save + + if out + existing.each do |profile| + next if profile == self + profile.update_column(:orcid_authenticated, false) + end + end + + out + end + private def normalize_orcid return if orcid.blank? - self.orcid = orcid.strip - if orcid =~ OrcidValidator::ORCID_ID_REGEX - self.orcid = "#{OrcidValidator::ORCID_PREFIX}#{orcid}" - elsif orcid.start_with?(OrcidValidator::ORCID_DOMAIN_REGEX) - self.orcid = orcid.sub(OrcidValidator::ORCID_DOMAIN_REGEX, OrcidValidator::ORCID_PREFIX) - end + self.orcid = orcid.strip.sub(OrcidValidator::ORCID_DOMAIN_REGEX, '') end def check_public diff --git a/app/views/trainers/partials/_trainer_schemaorg.html.erb b/app/views/trainers/partials/_trainer_schemaorg.html.erb index 356315fe8..39a0e7ee9 100644 --- a/app/views/trainers/partials/_trainer_schemaorg.html.erb +++ b/app/views/trainers/partials/_trainer_schemaorg.html.erb @@ -13,7 +13,7 @@ <%= content_tag 'span', langs, { itemprop: 'knowsLanguage', content: langs, class: 'schemaorg-element' } %> <% end %> <%= content_tag :span, trainer.website, { itemprop: 'url', content: trainer.website, class: 'schemaorg-element' } %> - <%= content_tag :span, trainer.orcid, { itemprop: 'identifier', content: trainer.orcid, class: 'schemaorg-element' } %> + <%= content_tag :span, trainer.orcid_url, { itemprop: 'identifier', content: trainer.orcid_url, class: 'schemaorg-element' } %> <% if trainer.image_url and not trainer.image_url.empty? %> <%= content_tag :span, trainer.image_url, { itemprop: 'image', content: trainer.image_url, class: 'shemaorg-element' } %> <% end %> diff --git a/app/views/trainers/show.html.erb b/app/views/trainers/show.html.erb index 900bb94da..c9d5ad2b8 100644 --- a/app/views/trainers/show.html.erb +++ b/app/views/trainers/show.html.erb @@ -21,7 +21,7 @@ <% if @trainer.orcid.present? %>
- <%= external_link @trainer.orcid, @trainer.orcid, class: 'h5', target: '_blank', rel: 'noopener', track: true %> + <%= orcid_link(@trainer, class: 'h5', track: true) %>
<% end %> diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 91be9917a..c3424da77 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -50,7 +50,10 @@ <% if @user.profile.orcid.blank? %> None specified <% else %> - <%= link_to @user.profile.orcid, @user.profile.orcid, rel: 'nofollow', target: '_blank' %> + <%= orcid_link(@user.profile) %> + <% if current_user == @user && !@user.profile.orcid_authenticated? %> + <%= button_to t('orcid.authenticate'), authenticate_orcid_path, class: 'btn btn-default' %> + <% end %> <% end %>

diff --git a/config/locales/en.yml b/config/locales/en.yml index c72613a1a..610ec5f2d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -839,6 +839,8 @@ en: 422: "The request you sent was well-formed but the change you wanted was rejected (422 Unprocessable Entity)." 404: "The requested page could not be found - you may have mistyped the address or the page may have moved (404 Not Found)." 406: "The requested format is not available (406 Not Acceptable)." + messages: + orcid_taken: 'has already been linked to another profile.' warnings: link_broken: > %{site_name} has been unable to access this %{resource_type}'s URL since %{fail_date} - the page may have been moved. @@ -1114,3 +1116,8 @@ en: title: What are spaces? description: | Spaces are customizable, community-managed sub-portals within %{site_name}, each with their own catalogue of training content. + orcid: + error: 'An error occurred whilst trying to authenticate your ORCID.' + authenticate: 'Authenticate your ORCID' + authentication_success: 'You have successfully authenticated your ORCID.' + authentication_failure: 'Failed to authenticate your ORCID.' \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index ea28af4e4..60ca008c6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -179,6 +179,9 @@ match 'oai-pmh', to: "oai#index", via: [:get, :post] + post 'orcid/authenticate' => 'orcid#authenticate', as: :authenticate_orcid + get 'orcid/callback' => 'orcid#callback', as: :orcid_callback + # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes". diff --git a/config/secrets.example.yml b/config/secrets.example.yml index 018b0439e..51f0fa8f1 100644 --- a/config/secrets.example.yml +++ b/config/secrets.example.yml @@ -38,6 +38,10 @@ external_api_keys: &external_api_keys password: gpt_api_key: willma_api_key: + orcid: + client_id: + secret: + host: #Internal config development: diff --git a/db/migrate/20251013115523_add_orcid_authenticated_to_profiles.rb b/db/migrate/20251013115523_add_orcid_authenticated_to_profiles.rb new file mode 100644 index 000000000..cddda1127 --- /dev/null +++ b/db/migrate/20251013115523_add_orcid_authenticated_to_profiles.rb @@ -0,0 +1,5 @@ +class AddOrcidAuthenticatedToProfiles < ActiveRecord::Migration[7.2] + def change + add_column :profiles, :orcid_authenticated, :boolean, default: false + end +end diff --git a/db/migrate/20251128160923_update_orcids_in_profiles.rb b/db/migrate/20251128160923_update_orcids_in_profiles.rb new file mode 100644 index 000000000..5f2bf7e4b --- /dev/null +++ b/db/migrate/20251128160923_update_orcids_in_profiles.rb @@ -0,0 +1,14 @@ +class UpdateOrcidsInProfiles < ActiveRecord::Migration[7.2] + ORCID_PREFIX = 'https://orcid.org/'.freeze + def up + Profile.where('orcid IS NOT NULL').find_each do |profile| + profile.update_column(:orcid, profile.orcid.gsub(ORCID_PREFIX, '')) if profile.orcid.start_with?(ORCID_PREFIX) + end + end + + def down + Profile.where('orcid IS NOT NULL').find_each do |profile| + profile.update_column(:orcid, "#{ORCID_PREFIX}#{profile.orcid}") unless profile.orcid.start_with?(ORCID_PREFIX) + end + end +end diff --git a/db/migrate/20251201143501_add_index_on_orcid_to_profiles.rb b/db/migrate/20251201143501_add_index_on_orcid_to_profiles.rb new file mode 100644 index 000000000..d61e959d8 --- /dev/null +++ b/db/migrate/20251201143501_add_index_on_orcid_to_profiles.rb @@ -0,0 +1,5 @@ +class AddIndexOnOrcidToProfiles < ActiveRecord::Migration[7.2] + def change + add_index :profiles, :orcid + end +end diff --git a/db/schema.rb b/db/schema.rb index a6b3aa4f3..7354d68e5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_03_25_151745) do +ActiveRecord::Schema[7.2].define(version: 2025_12_01_143501) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -452,6 +452,8 @@ t.string "social_media", default: [], array: true t.string "type", default: "Profile" t.string "fields", default: [], array: true + t.boolean "orcid_authenticated", default: false + t.index ["orcid"], name: "index_profiles_on_orcid" t.index ["slug"], name: "index_profiles_on_slug", unique: true end diff --git a/test/config/test_secrets.yml b/test/config/test_secrets.yml index d80a913e1..54beb7019 100644 --- a/test/config/test_secrets.yml +++ b/test/config/test_secrets.yml @@ -19,3 +19,6 @@ eventbrite_api_v3: fairsharing: username: tess-test-bot password: abc123 +orcid: + client_id: test + secret: test diff --git a/test/controllers/orcid_controller_test.rb b/test/controllers/orcid_controller_test.rb new file mode 100644 index 000000000..18c12aa8c --- /dev/null +++ b/test/controllers/orcid_controller_test.rb @@ -0,0 +1,124 @@ +require 'test_helper' + +class OrcidControllerTest < ActionController::TestCase + include Devise::Test::ControllerHelpers + + test 'authenticate orcid logged-in user' do + sign_in users(:regular_user) + + post :authenticate + + assert_redirected_to /https:\/\/sandbox\.orcid\.org\/oauth\/authorize\?.+/ + end + + test 'do not authenticate orcid if user not logged-in' do + post :authenticate + + assert_redirected_to new_user_session_path + end + + test 'handle callback and assign orcid if free' do + mock_images + user = users(:regular_user) + sign_in user + + VCR.use_cassette('orcid/get_token_free_orcid') do + get :callback, params: { code: '123xyz' } + end + + profile = user.profile.reload + assert_equal '0009-0006-0987-5702', profile.orcid + assert profile.orcid_authenticated? + assert_redirected_to user + end + + test 'handle callback and assign orcid if unauthenticated' do + mock_images + user = users(:regular_user) + sign_in user + + VCR.use_cassette('orcid/get_token_unauth_orcid') do + get :callback, params: { code: '123xyz' } + end + + profile = user.profile.reload + assert_equal '0000-0002-0048-3300', profile.orcid + assert profile.orcid_authenticated? + assert_redirected_to user + end + + test 'handle callback and assign orcid even if already used' do + mock_images + user = users(:regular_user) + existing_orcid_user = users(:trainer_user) + assert existing_orcid_user.profile.orcid_authenticated? + sign_in user + + VCR.use_cassette('orcid/get_token_existing_orcid') do + get :callback, params: { code: '123xyz' } + end + + profile = user.profile.reload + assert_equal '0000-0002-1825-0097', profile.orcid + assert profile.orcid_authenticated? + refute existing_orcid_user.reload.profile.orcid_authenticated? + assert_redirected_to user + assert flash[:error].blank? + end + + test 'do not handle callback if not logged-in' do + mock_images + + get :callback, params: { code: '123xyz' } + + assert_redirected_to new_user_session_path + end + + test 'handle unauth error during callback' do + mock_images + user = users(:regular_user) + sign_in user + + VCR.use_cassette('orcid/error_unauth') do + get :callback, params: { code: '123xyz' } + end + + assert_response :unprocessable_entity + assert_select '#error-message', text: /error occurred.+ORCID/ + profile = user.profile.reload + assert profile.orcid.blank? + refute profile.orcid_authenticated? + end + + test 'handle unexpected error during callback' do + mock_images + user = users(:regular_user) + sign_in user + + VCR.use_cassette('orcid/error_500') do + get :callback, params: { code: '123xyz' } + end + + assert_response :unprocessable_entity + assert_select '#error-message', text: /error occurred.+ORCID/ + profile = user.profile.reload + assert profile.orcid.blank? + refute profile.orcid_authenticated? + end + + test 'handle missing orcid during callback' do + mock_images + user = users(:regular_user) + sign_in user + + VCR.use_cassette('orcid/get_token_orcid_missing') do + get :callback, params: { code: '123xyz' } + end + + assert_redirected_to user + assert_includes flash[:error], 'Failed to authenticat' + profile = user.profile.reload + assert profile.orcid.blank? + refute profile.orcid_authenticated? + end +end diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb index 5b41abc13..64e80f0bb 100644 --- a/test/controllers/users_controller_test.rb +++ b/test/controllers/users_controller_test.rb @@ -490,4 +490,39 @@ class UsersControllerTest < ActionController::TestCase assert_select '.masonry-brick-heading h4', text: 'Learn about plants' end end + + test 'should show authenticate orcid button if own profile' do + user = users(:private_user) + assert user.profile.orcid.present? + refute user.profile.orcid_authenticated? + + sign_in user + + get :show, params: { id: user } + + assert_response :success + assert_select '#sidebar button', text: 'Authenticate your ORCID' + end + + test 'should not show authenticate orcid button if not own profile' do + user = users(:private_user) + assert user.profile.orcid.present? + refute user.profile.orcid_authenticated? + + get :show, params: { id: user } + + assert_response :success + assert_select '#sidebar button', text: 'Authenticate your ORCID', count: 0 + end + + test 'should not show authenticate orcid button if already authenticated' do + user = users(:trainer_user) + assert user.profile.orcid.present? + assert user.profile.orcid_authenticated? + + get :show, params: { id: user } + + assert_response :success + assert_select '#sidebar button', text: 'Authenticate your ORCID', count: 0 + end end diff --git a/test/fixtures/profiles.yml b/test/fixtures/profiles.yml index 483bd2f39..ac7d208e5 100644 --- a/test/fixtures/profiles.yml +++ b/test/fixtures/profiles.yml @@ -58,7 +58,7 @@ trainer_one_profile: image_url: https://library.brown.edu/info/wp-content/uploads/2019/12/Carberry2-150x137.jpg email: jcarberry@research.org website: https://library.brown.edu/info/hay/carberry/ # demo page - orcid: https://orcid.org/000-0002-1825-0097 # demo orcid account + orcid: 0000-0002-1825-0097 # demo orcid account description: > Josiah Carberry is a fictitious person. This account is used as a demonstration account by ORCID, CrossRef and others who wish to demonstrate the @@ -68,6 +68,7 @@ trainer_one_profile: expertise_technical: ['python', 'R'] public: true type: Trainer + orcid_authenticated: true trainer_two_profile: user: private_user @@ -76,13 +77,14 @@ trainer_two_profile: image_url: email: samael@research.org website: '' - orcid: '' + orcid: 0000-0002-0048-3300 description: none location: 'Sydney, Australia' experience: much expertise_technical: ['java', 'python', 'ruby'] public: false type: Profile + orcid_authenticated: false admin_trainer_profile: user: admin_trainer diff --git a/test/helpers/users_helper_test.rb b/test/helpers/users_helper_test.rb new file mode 100644 index 000000000..11c176710 --- /dev/null +++ b/test/helpers/users_helper_test.rb @@ -0,0 +1,27 @@ +require 'test_helper' + +class UsersHelperTest < ActionView::TestCase + include ApplicationHelper + + test 'displays authenticated orcid link' do + profile = profiles(:trainer_one_profile) + assert profile.orcid.present? + assert profile.orcid_authenticated? + + l = orcid_link(profile) + + assert_includes l, 'ORCID-iD_icon_vector.svg' + assert_not_includes l, 'Unauthenticated' + end + + test 'displays unauthenticated orcid link' do + profile = profiles(:trainer_two_profile) + assert profile.orcid.present? + refute profile.orcid_authenticated? + + l = orcid_link(profile) + + assert_includes l, 'ORCID-iD_icon_unauth_vector.svg' + assert_includes l, 'Unauthenticated' + end +end diff --git a/test/integration/tracking_test.rb b/test/integration/tracking_test.rb index add1b7fbf..0be17857d 100644 --- a/test/integration/tracking_test.rb +++ b/test/integration/tracking_test.rb @@ -30,7 +30,7 @@ class TrackingTest < ActionDispatch::IntegrationTest get trainer_path(trainer) - assert_select 'a[href=?]', trainer.orcid do + assert_select 'a[href=?]', trainer.orcid_url do assert_select '[data-trackable]' assert_select '[data-trackable-id]', count: 0 end diff --git a/test/models/profile_test.rb b/test/models/profile_test.rb index 0a2c62f54..93355dafa 100644 --- a/test/models/profile_test.rb +++ b/test/models/profile_test.rb @@ -21,7 +21,7 @@ class ProfileTest < ActiveSupport::TestCase assert_equal 'Space', profile.firstname assert_equal 'Spaceson', profile.surname assert_equal 'http://website.com', profile.website - assert_equal 'https://orcid.org/0000-0002-1825-0097', profile.orcid + assert_equal '0000-0002-1825-0097', profile.orcid end test 'validates orcid' do @@ -46,11 +46,8 @@ class ProfileTest < ActiveSupport::TestCase # check validation of valid orcid - fixes non-secure scheme assert profile.update(orcid: 'http://orcid.org/0000-0002-1825-0097') - assert_equal 'https://orcid.org/0000-0002-1825-0097', profile.orcid - - # check validation of invalid orcid - scheme and host only - refute profile.update(orcid: 'https://orcid.org/') - assert_equal "ORCID isn't a valid ORCID identifier", profile.errors.full_messages_for(:orcid).first + assert_equal '0000-0002-1825-0097', profile.orcid + assert_equal 'https://orcid.org/0000-0002-1825-0097', profile.orcid_url # check validation of invalid orcid, preserves original value refute profile.update(orcid: 'some junk') @@ -91,4 +88,58 @@ class ProfileTest < ActiveSupport::TestCase assert profile.update(website: 'http://slowhost.com') refute profile.errors.added?(:website, 'is not accessible') end + + test 'assigns authenticated orcid' do + profile = users(:regular_user).profile + refute profile.orcid.present? + refute profile.orcid_authenticated + + assert profile.authenticate_orcid('0009-0006-0987-5702') + + assert_empty profile.errors + assert_equal '0009-0006-0987-5702', profile.orcid + assert profile.orcid_authenticated + end + + test 'assigns authenticated orcid and de-authenticates other profiles that use it' do + existing_profile = profiles(:trainer_one_profile) + profile = users(:regular_user).profile + assert existing_profile.orcid.present? + assert existing_profile.orcid_authenticated + refute profile.orcid.present? + refute profile.orcid_authenticated + existing_orcid = existing_profile.orcid + + profile.authenticate_orcid(existing_orcid) + + assert_empty profile.errors + assert_equal existing_orcid, profile.orcid + assert profile.orcid_authenticated + existing_profile.reload + assert_equal existing_orcid, existing_profile.orcid + refute existing_profile.orcid_authenticated + end + + test 'assigns authenticated orcid if existing orcid is not authenticated' do + existing_profile = profiles(:trainer_two_profile) + profile = users(:regular_user).profile + assert existing_profile.orcid.present? + refute existing_profile.orcid_authenticated + refute profile.orcid.present? + refute profile.orcid_authenticated + + profile.authenticate_orcid(existing_profile.orcid) + + assert_empty profile.errors + assert existing_profile.orcid.present? + refute existing_profile.orcid_authenticated + assert profile.orcid.present? + assert profile.orcid_authenticated + end + + test 'orcid_url' do + assert_nil Profile.new.orcid_url + assert_nil Profile.new(orcid: '').orcid_url + assert_equal 'https://orcid.org/0009-0006-0987-5702', Profile.new(orcid: '0009-0006-0987-5702').orcid_url + end end diff --git a/test/models/user_test.rb b/test/models/user_test.rb index e53976edb..4e0a42a26 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -390,7 +390,7 @@ class UserTest < ActiveSupport::TestCase profile = user1.profile assert_equal 'John', profile.firstname assert_equal 'Userton', profile.surname - assert_equal 'https://orcid.org/0000-0002-1825-0097', profile.orcid + assert_equal 'https://orcid.org/0000-0002-1825-0097', profile.orcid_url assert_equal 'Cool guy', profile.description assert_equal ['Python', 'Ruby', 'R', 'Java'], profile.expertise_technical diff --git a/test/vcr_cassettes/orcid/error_500.yml b/test/vcr_cassettes/orcid/error_500.yml new file mode 100644 index 000000000..e01a8728a --- /dev/null +++ b/test/vcr_cassettes/orcid/error_500.yml @@ -0,0 +1,55 @@ +--- +http_interactions: +- request: + method: post + uri: https://sandbox.orcid.org/oauth/token + body: + encoding: UTF-8 + string: code=123xyz&grant_type=authorization_code&redirect_uri=https%3A%2F%2Flocalhost%3A3001%2Forcid%2Fcallback + headers: + User-Agent: + - Rack::OAuth2 (2.2.0) + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 500 + message: Internal Server Error + headers: + Date: + - Fri, 28 Nov 2025 15:23:49 GMT + Content-Type: + - application/json;charset=UTF-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - 9a5ade5f5be79527-LHR + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Pragma: + - no-cache + Expires: + - '0' + X-Xss-Protection: + - 1; mode=block + X-Frame-Options: + - DENY + X-Content-Type-Options: + - nosniff + Vary: + - accept-encoding + Cf-Cache-Status: + - DYNAMIC + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: 'An error occurred' + recorded_at: Fri, 28 Nov 2025 15:23:49 GMT +recorded_with: VCR 6.2.0 diff --git a/test/vcr_cassettes/orcid/error_unauth.yml b/test/vcr_cassettes/orcid/error_unauth.yml new file mode 100644 index 000000000..e3245d1a9 --- /dev/null +++ b/test/vcr_cassettes/orcid/error_unauth.yml @@ -0,0 +1,56 @@ +--- +http_interactions: +- request: + method: post + uri: https://sandbox.orcid.org/oauth/token + body: + encoding: UTF-8 + string: code=123xyz&grant_type=authorization_code&redirect_uri=http%3A%2F%2Ftest.host%2Forcid%2Fcallback + headers: + User-Agent: + - Rack::OAuth2 (2.2.0) + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 401 + message: Unauthorized + headers: + Date: + - Mon, 01 Dec 2025 14:02:44 GMT + Content-Type: + - application/json;charset=UTF-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - 9a731fbcfdc7776d-LHR + Cache-Control: + - no-store + Pragma: + - no-cache + Www-Authenticate: + - 'Bearer realm="orcid", error="invalid_client", error_description="Client not + found: test"' + X-Xss-Protection: + - 1; mode=block + X-Frame-Options: + - DENY + X-Content-Type-Options: + - nosniff + Vary: + - accept-encoding + Cf-Cache-Status: + - DYNAMIC + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: '{"error":"invalid_client","error_description":"Client not found: test"}' + recorded_at: Mon, 01 Dec 2025 14:02:44 GMT +recorded_with: VCR 6.2.0 diff --git a/test/vcr_cassettes/orcid/get_token_existing_orcid.yml b/test/vcr_cassettes/orcid/get_token_existing_orcid.yml new file mode 100644 index 000000000..fd884599d --- /dev/null +++ b/test/vcr_cassettes/orcid/get_token_existing_orcid.yml @@ -0,0 +1,56 @@ +--- +http_interactions: +- request: + method: post + uri: https://sandbox.orcid.org/oauth/token + body: + encoding: UTF-8 + string: code=123xyz&grant_type=authorization_code&redirect_uri=https%3A%2F%2Flocalhost%3A3001%2Forcid%2Fcallback + headers: + User-Agent: + - Rack::OAuth2 (2.2.0) + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 28 Nov 2025 15:23:49 GMT + Content-Type: + - application/json;charset=UTF-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - 9a5ade5f5be79527-LHR + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Pragma: + - no-cache + Expires: + - '0' + X-Xss-Protection: + - 1; mode=block + X-Frame-Options: + - DENY + X-Content-Type-Options: + - nosniff + Vary: + - accept-encoding + Cf-Cache-Status: + - DYNAMIC + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: '{"access_token":"12345678-1234-1234-1234-123412341234","token_type":"bearer","refresh_token":"12345678-1234-1234-1234-123412341234","expires_in":631138518,"scope":"/authenticate", + "name":"Josiah Carberry","orcid":"0000-0002-1825-0097"}' + recorded_at: Fri, 28 Nov 2025 15:23:49 GMT +recorded_with: VCR 6.2.0 diff --git a/test/vcr_cassettes/orcid/get_token_free_orcid.yml b/test/vcr_cassettes/orcid/get_token_free_orcid.yml new file mode 100644 index 000000000..7289abe81 --- /dev/null +++ b/test/vcr_cassettes/orcid/get_token_free_orcid.yml @@ -0,0 +1,56 @@ +--- +http_interactions: +- request: + method: post + uri: https://sandbox.orcid.org/oauth/token + body: + encoding: UTF-8 + string: code=123xyz&grant_type=authorization_code&redirect_uri=https%3A%2F%2Flocalhost%3A3001%2Forcid%2Fcallback + headers: + User-Agent: + - Rack::OAuth2 (2.2.0) + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 28 Nov 2025 15:23:49 GMT + Content-Type: + - application/json;charset=UTF-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - 9a5ade5f5be79527-LHR + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Pragma: + - no-cache + Expires: + - '0' + X-Xss-Protection: + - 1; mode=block + X-Frame-Options: + - DENY + X-Content-Type-Options: + - nosniff + Vary: + - accept-encoding + Cf-Cache-Status: + - DYNAMIC + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: '{"access_token":"12345678-1234-1234-1234-123412341234","token_type":"bearer","refresh_token":"12345678-1234-1234-1234-123412341234","expires_in":631138518,"scope":"/authenticate", + "name":"Joe Bloggs","orcid":"0009-0006-0987-5702"}' + recorded_at: Fri, 28 Nov 2025 15:23:49 GMT +recorded_with: VCR 6.2.0 diff --git a/test/vcr_cassettes/orcid/get_token_orcid_missing.yml b/test/vcr_cassettes/orcid/get_token_orcid_missing.yml new file mode 100644 index 000000000..18305052a --- /dev/null +++ b/test/vcr_cassettes/orcid/get_token_orcid_missing.yml @@ -0,0 +1,55 @@ +--- +http_interactions: +- request: + method: post + uri: https://sandbox.orcid.org/oauth/token + body: + encoding: UTF-8 + string: code=123xyz&grant_type=authorization_code&redirect_uri=https%3A%2F%2Flocalhost%3A3001%2Forcid%2Fcallback + headers: + User-Agent: + - Rack::OAuth2 (2.2.0) + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 28 Nov 2025 15:23:49 GMT + Content-Type: + - application/json;charset=UTF-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - 9a5ade5f5be79527-LHR + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Pragma: + - no-cache + Expires: + - '0' + X-Xss-Protection: + - 1; mode=block + X-Frame-Options: + - DENY + X-Content-Type-Options: + - nosniff + Vary: + - accept-encoding + Cf-Cache-Status: + - DYNAMIC + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: '{"access_token":"12345678-1234-1234-1234-123412341234","token_type":"bearer","refresh_token":"12345678-1234-1234-1234-123412341234","expires_in":631138518,"scope":"/authenticate"}' + recorded_at: Fri, 28 Nov 2025 15:23:49 GMT +recorded_with: VCR 6.2.0 diff --git a/test/vcr_cassettes/orcid/get_token_unauth_orcid.yml b/test/vcr_cassettes/orcid/get_token_unauth_orcid.yml new file mode 100644 index 000000000..3457fc498 --- /dev/null +++ b/test/vcr_cassettes/orcid/get_token_unauth_orcid.yml @@ -0,0 +1,56 @@ +--- +http_interactions: +- request: + method: post + uri: https://sandbox.orcid.org/oauth/token + body: + encoding: UTF-8 + string: code=123xyz&grant_type=authorization_code&redirect_uri=https%3A%2F%2Flocalhost%3A3001%2Forcid%2Fcallback + headers: + User-Agent: + - Rack::OAuth2 (2.2.0) + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 28 Nov 2025 15:23:49 GMT + Content-Type: + - application/json;charset=UTF-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - 9a5ade5f5be79527-LHR + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Pragma: + - no-cache + Expires: + - '0' + X-Xss-Protection: + - 1; mode=block + X-Frame-Options: + - DENY + X-Content-Type-Options: + - nosniff + Vary: + - accept-encoding + Cf-Cache-Status: + - DYNAMIC + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: '{"access_token":"12345678-1234-1234-1234-123412341234","token_type":"bearer","refresh_token":"12345678-1234-1234-1234-123412341234","expires_in":631138518,"scope":"/authenticate", + "name":"Lucifer MorningStar","orcid":"0000-0002-0048-3300"}' + recorded_at: Fri, 28 Nov 2025 15:23:49 GMT +recorded_with: VCR 6.2.0