From c55032e233042a04e5347a9b1439963b36671987 Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Thu, 21 Sep 2023 15:57:58 +0200 Subject: [PATCH 1/2] [#49113] ensure to display file links for one drive storages - https://community.openproject.org/wp/49113 - refactored errors returned by connection manager to be storage errors - added source indicator to storage error data - removed nextcloud specific method names from sync service - check all three states of the file link list - not logged in - with auth error - happy case --- .../oauth_clients/connection_manager.rb | 46 +++++++++++-------- .../one_drive/files_info_query.rb | 11 ++++- .../one_drive/internal/drive_item_query.rb | 9 ++-- .../storage_interaction/one_drive/util.rb | 1 + .../app/models/storages/storage_error.rb | 2 + .../app/models/storages/storage_error_data.rb | 40 ++++++++++++++++ .../storages/file_link_sync_service.rb | 4 +- .../work_packages_file_links_api.rb | 11 +---- 8 files changed, 87 insertions(+), 37 deletions(-) create mode 100644 modules/storages/app/models/storages/storage_error_data.rb diff --git a/app/services/oauth_clients/connection_manager.rb b/app/services/oauth_clients/connection_manager.rb index eefce9cd2f33..c309ab1d62fb 100644 --- a/app/services/oauth_clients/connection_manager.rb +++ b/app/services/oauth_clients/connection_manager.rb @@ -60,6 +60,8 @@ def get_access_token(state: nil) ServiceResult.failure(result: @redirect_url) end + # rubocop:disable Metrics/AbcSize + # The bearer/access token has expired or is due for renew for other reasons. # Talk to OAuth2 Authorization Server to exchange the renew_token for a new bearer token. def refresh_token @@ -79,11 +81,17 @@ def refresh_token ServiceResult.success(result: oauth_client_token) end else - service_result_with_error(I18n.t('oauth_client.errors.refresh_token_called_without_existing_token')) + storage_error = ::Storages::StorageError.new( + code: :error, + log_message: I18n.t('oauth_client.errors.refresh_token_called_without_existing_token') + ) + ServiceResult.failure(result: :error, errors: storage_error) end end end + # rubocop:enable Metrics/AbcSize + # Returns the URI of the "authorize" endpoint of the OAuth2 Authorization Server. # @param state (OAuth2 RFC) is a nonce referencing a cookie containing the calling page (URL + params) to which to # return to at the end of the whole flow. @@ -93,6 +101,8 @@ def get_authorization_uri(state: nil) client.authorization_uri(scope: @config.scope, state:) end + # rubocop:disable Metrics/AbcSize + # Called by callback_page with a cryptographic "code" that indicates # that the user has successfully authorized the OAuth2 Authorization Server. # We now are going to exchange this code to a token (bearer+refresh) @@ -128,6 +138,8 @@ def code_to_token(code) ServiceResult.success(result: oauth_client_token) end + # rubocop:enable Metrics/AbcSize + # Called by StorageRepresenter to inquire about the status of the OAuth2 # authentication server. # Returns :connected/:authorization_failed or :error for a general error. @@ -167,11 +179,7 @@ def request_with_token_refresh(oauth_client_token) if yield_service_result.failure? && yield_service_result.result == :unauthorized refresh_service_result = refresh_token - if refresh_service_result.failure? - failed_service_result = ServiceResult.failure(result: :error) - failed_service_result.merge!(refresh_service_result) - return failed_service_result - end + return refresh_service_result if refresh_service_result.failure? oauth_client_token.reload yield_service_result = yield(oauth_client_token) # Should contain result= in case of success @@ -189,27 +197,27 @@ def get_existing_token OAuthClientToken.find_by(user_id: @user, oauth_client_id: @oauth_client.id) end - # Calls client.access_token! - # Convert the various exceptions into user-friendly error strings. + # rubocop:disable Metrics/AbcSize def request_new_token(options = {}) rack_access_token = rack_oauth_client(options).access_token!(:body) ServiceResult.success(result: rack_access_token) rescue Rack::OAuth2::Client::Error => e - service_result_with_error(i18n_rack_oauth2_error_message(e), e.message) + service_result_with_error(e.response, i18n_rack_oauth2_error_message(e)) rescue Faraday::TimeoutError, Faraday::ConnectionFailed, Faraday::ParsingError, Faraday::SSLError => e service_result_with_error( - "#{I18n.t('oauth_client.errors.oauth_returned_http_error')}: #{e.class}: #{e.message.to_html}", - e.message + e.response, + "#{I18n.t('oauth_client.errors.oauth_returned_http_error')}: #{e.class}: #{e.message.to_html}" ) rescue StandardError => e service_result_with_error( - "#{I18n.t('oauth_client.errors.oauth_returned_standard_error')}: #{e.class}: #{e.message.to_html}", - e.message + e.response, + "#{I18n.t('oauth_client.errors.oauth_returned_standard_error')}: #{e.class}: #{e.message.to_html}" ) end - # Localize the error message + # rubocop:enable Metrics/AbcSize + def i18n_rack_oauth2_error_message(rack_oauth2_client_exception) i18n_key = "oauth_client.errors.rack_oauth2.#{rack_oauth2_client_exception.message}" if I18n.exists? i18n_key @@ -250,12 +258,10 @@ def update_oauth_client_token(oauth_client_token, rack_oauth2_access_token) end end - # Shortcut method to convert an error message into an unsuccessful - # ServiceResult with that error message - def service_result_with_error(message, result = nil) - ServiceResult.failure(result:).tap do |r| - r.errors.add(:base, message) - end + def service_result_with_error(data, log_message = nil) + error_data = ::Storages::StorageErrorData.new(source: self, payload: data) + ServiceResult.failure(result: :bad_request, + errors: ::Storages::StorageError.new(code: :bad_request, data: error_data, log_message:)) end end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_info_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_info_query.rb index 9426560f9c23..1375b6ff9f33 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_info_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_info_query.rb @@ -55,7 +55,14 @@ def call(user:, file_ids:) end result = file_ids.map do |file_id| - wrap_storage_file_error(file_id, FileInfoQuery.call(storage: @storage, user:, file_id:)) + file_info_result = FileInfoQuery.call(storage: @storage, user:, file_id:) + if file_info_result.failure? && + file_info_result.errors.data.source.is_a?(::OAuthClients::ConnectionManager) + # errors in the connection manager must short circuit the query and return the error + return file_info_result + end + + wrap_storage_file_error(file_id, file_info_result) end ServiceResult.success(result:) @@ -70,7 +77,7 @@ def wrap_storage_file_error(file_id, query_result) storage_error = query_result.errors StorageFileInfo.new( id: file_id, - status: storage_error.data.dig(:error, :code), + status: storage_error.data.payload.dig(:error, :code), status_code: Rack::Utils::SYMBOL_TO_STATUS_CODE[storage_error.code] ) end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/internal/drive_item_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/internal/drive_item_query.rb index ab1ee734cafa..d0125ed5ba02 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/internal/drive_item_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/internal/drive_item_query.rb @@ -41,22 +41,23 @@ def make_file_request(drive_item_id, token, select_url_query) def handle_responses(response) json = MultiJson.load(response.body, symbolize_keys: true) + error_data = ::Storages::StorageErrorData.new(source: self, payload: json) case response when Net::HTTPSuccess ServiceResult.success(result: json) when Net::HTTPNotFound ServiceResult.failure(result: :not_found, - errors: ::Storages::StorageError.new(code: :not_found, data: json)) + errors: ::Storages::StorageError.new(code: :not_found, data: error_data)) when Net::HTTPForbidden ServiceResult.failure(result: :forbidden, - errors: ::Storages::StorageError.new(code: :forbidden, data: json)) + errors: ::Storages::StorageError.new(code: :forbidden, data: error_data)) when Net::HTTPUnauthorized ServiceResult.failure(result: :unauthorized, - errors: ::Storages::StorageError.new(code: :unauthorized, data: json)) + errors: ::Storages::StorageError.new(code: :unauthorized, data: error_data)) else ServiceResult.failure(result: :error, - errors: ::Storages::StorageError.new(code: :error, data: json)) + errors: ::Storages::StorageError.new(code: :error, data: error_data)) end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/util.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/util.rb index 8f413f9a56e0..293b76519f32 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/util.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/util.rb @@ -51,6 +51,7 @@ def using_user_token(storage, user, &) result: :unauthorized, errors: ::Storages::StorageError.new( code: :unauthorized, + data: ::Storages::StorageErrorData.new(source: connection_manager), log_message: 'Query could not be created! No access token found!' ) ) diff --git a/modules/storages/app/models/storages/storage_error.rb b/modules/storages/app/models/storages/storage_error.rb index fe80cb9cf10f..ce2f314d481a 100644 --- a/modules/storages/app/models/storages/storage_error.rb +++ b/modules/storages/app/models/storages/storage_error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) 2012-2023 the OpenProject GmbH diff --git a/modules/storages/app/models/storages/storage_error_data.rb b/modules/storages/app/models/storages/storage_error_data.rb new file mode 100644 index 000000000000..fce9b1785ee7 --- /dev/null +++ b/modules/storages/app/models/storages/storage_error_data.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Storages + class StorageErrorData + attr_reader :source, :payload + + def initialize(source:, payload: nil) + @source = source + @payload = payload + end + end +end diff --git a/modules/storages/app/services/storages/file_link_sync_service.rb b/modules/storages/app/services/storages/file_link_sync_service.rb index 7758ba25cf3c..93d2d572babc 100644 --- a/modules/storages/app/services/storages/file_link_sync_service.rb +++ b/modules/storages/app/services/storages/file_link_sync_service.rb @@ -38,7 +38,7 @@ def initialize(user:) def call(file_links) resulting_file_links = file_links .group_by(&:storage_id) - .map { |storage_id, storage_file_links| sync_nextcloud(storage_id, storage_file_links) } + .map { |storage_id, storage_file_links| sync_storage_data(storage_id, storage_file_links) } .reduce([]) do |state, sync_result| sync_result.match( on_success: ->(sr) { state + sr }, @@ -51,7 +51,7 @@ def call(file_links) private - def sync_nextcloud(storage_id, file_links) + def sync_storage_data(storage_id, file_links) storage = ::Storages::Storage.find(storage_id) ::Storages::Peripherals::Registry .resolve("queries.#{storage.short_provider_type}.files_info") diff --git a/modules/storages/lib/api/v3/file_links/work_packages_file_links_api.rb b/modules/storages/lib/api/v3/file_links/work_packages_file_links_api.rb index 91c6827378c7..112f76326615 100644 --- a/modules/storages/lib/api/v3/file_links/work_packages_file_links_api.rb +++ b/modules/storages/lib/api/v3/file_links/work_packages_file_links_api.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) 2012-2023 the OpenProject GmbH @@ -26,16 +28,10 @@ # See COPYRIGHT and LICENSE files for more details. #++ -# This class provides definitions for API routes and endpoints for the file_links namespace. It inherits the -# functionality from the Grape REST API framework. It is mounted in lib/api/v3/work_packages/work_packages_api.rb, -# which puts the file_links namespace behind the provided namespace of the work packages api -# -> /api/v3/work_packages/:id/file_links/... class API::V3::FileLinks::WorkPackagesFileLinksAPI < API::OpenProjectAPI # The `:resources` keyword defines the API namespace -> /api/v3/work_packages/:id/file_links/... resources :file_links do - # Get the list of FileLinks related to a work package, with updated information from Nextcloud. get do - # API supports query filters on storages, for example { storage: { operator: '=', values: [storage_id] } query = ParamsToQueryService .new(::Storages::Storage, current_user, @@ -48,12 +44,9 @@ class API::V3::FileLinks::WorkPackagesFileLinksAPI < API::OpenProjectAPI end result = if current_user.allowed_to?(:view_file_links, @work_package.project) - # Get a (potentially huge...) list of all FileLinks for the work package. file_links = query.results.where(container_id: @work_package.id, container_type: 'WorkPackage', storage: @work_package.project.storages) - # Synchronize with Nextcloud. StorageAPI has handled OAuth2 for us before. - # We ignore the result, because partial errors (storage network issues) are written to each FileLink ::Storages::FileLinkSyncService .new(user: current_user) .call(file_links) From 00caa2e602f3f84a5647a7da740050e69cd843ab Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Fri, 22 Sep 2023 16:06:57 +0200 Subject: [PATCH 2/2] [#49113] fixed 20000 redundant test cases for connection manager --- .../oauth_clients/connection_manager.rb | 16 +- .../peripherals/service_result_refinements.rb | 17 +++ .../one_drive/files_info_query.rb | 7 +- .../one_drive/file_info_query_spec.rb | 4 +- .../one_drive/open_link_query_spec.rb | 4 +- .../oauth_clients/connection_manager_spec.rb | 139 +++++++++--------- .../one_drive_connection_manager_spec.rb | 4 +- 7 files changed, 106 insertions(+), 85 deletions(-) diff --git a/app/services/oauth_clients/connection_manager.rb b/app/services/oauth_clients/connection_manager.rb index c309ab1d62fb..45c05a1dd1fc 100644 --- a/app/services/oauth_clients/connection_manager.rb +++ b/app/services/oauth_clients/connection_manager.rb @@ -158,7 +158,7 @@ def authorization_state service_result = refresh_token if service_result.success? :connected - elsif service_result.result == 'invalid_request' + elsif service_result.errors.data.payload[:error] == 'invalid_request' :failed_authorization else :error @@ -203,15 +203,17 @@ def request_new_token(options = {}) ServiceResult.success(result: rack_access_token) rescue Rack::OAuth2::Client::Error => e - service_result_with_error(e.response, i18n_rack_oauth2_error_message(e)) + service_result_with_error(:bad_request, e.response, i18n_rack_oauth2_error_message(e)) rescue Faraday::TimeoutError, Faraday::ConnectionFailed, Faraday::ParsingError, Faraday::SSLError => e service_result_with_error( - e.response, + :internal_server_error, + e, "#{I18n.t('oauth_client.errors.oauth_returned_http_error')}: #{e.class}: #{e.message.to_html}" ) rescue StandardError => e service_result_with_error( - e.response, + :error, + e, "#{I18n.t('oauth_client.errors.oauth_returned_standard_error')}: #{e.class}: #{e.message.to_html}" ) end @@ -258,10 +260,10 @@ def update_oauth_client_token(oauth_client_token, rack_oauth2_access_token) end end - def service_result_with_error(data, log_message = nil) + def service_result_with_error(code, data, log_message = nil) error_data = ::Storages::StorageErrorData.new(source: self, payload: data) - ServiceResult.failure(result: :bad_request, - errors: ::Storages::StorageError.new(code: :bad_request, data: error_data, log_message:)) + ServiceResult.failure(result: code, + errors: ::Storages::StorageError.new(code:, data: error_data, log_message:)) end end end diff --git a/modules/storages/app/common/storages/peripherals/service_result_refinements.rb b/modules/storages/app/common/storages/peripherals/service_result_refinements.rb index 3c9b813a5838..7385c7ceed8c 100644 --- a/modules/storages/app/common/storages/peripherals/service_result_refinements.rb +++ b/modules/storages/app/common/storages/peripherals/service_result_refinements.rb @@ -27,6 +27,11 @@ #++ module Storages::Peripherals + # rubocop:disable Lint/EmptyClass + class UnknownSource; end + + # rubocop:enable Lint/EmptyClass + module ServiceResultRefinements refine ServiceResult do def match(on_success:, on_failure:) @@ -50,6 +55,18 @@ def >>(other) bind(&other) end + + def error_source + if errors.is_a?(::Storages::StorageError) && errors.data&.source.present? + errors.data.source + else + UnknownSource + end + end + + def error_payload + errors.data&.payload + end end end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_info_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_info_query.rb index 1375b6ff9f33..4726eb267a75 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_info_query.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_info_query.rb @@ -57,7 +57,7 @@ def call(user:, file_ids:) result = file_ids.map do |file_id| file_info_result = FileInfoQuery.call(storage: @storage, user:, file_id:) if file_info_result.failure? && - file_info_result.errors.data.source.is_a?(::OAuthClients::ConnectionManager) + file_info_result.error_source.is_a?(::OAuthClients::ConnectionManager) # errors in the connection manager must short circuit the query and return the error return file_info_result end @@ -74,11 +74,10 @@ def wrap_storage_file_error(file_id, query_result) if query_result.success? query_result.result else - storage_error = query_result.errors StorageFileInfo.new( id: file_id, - status: storage_error.data.payload.dig(:error, :code), - status_code: Rack::Utils::SYMBOL_TO_STATUS_CODE[storage_error.code] + status: query_result.error_payload.dig(:error, :code), + status_code: Rack::Utils::SYMBOL_TO_STATUS_CODE[query_result.errors.code] ) end end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/file_info_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/file_info_query_spec.rb index 32371a640b30..6bf0a7d6bead 100644 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/file_info_query_spec.rb +++ b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/file_info_query_spec.rb @@ -90,7 +90,7 @@ expect(storage_files).to be_failure expect(storage_files.result).to eq(:not_found) - expect(storage_files.errors.data.to_json).to eq(not_found_json) + expect(storage_files.errors.data.payload.to_json).to eq(not_found_json) end it 'returns a forbidden error if the API call returns a 403' do @@ -102,7 +102,7 @@ expect(storage_files).to be_failure expect(storage_files.result).to eq(:forbidden) - expect(storage_files.errors.data.to_json).to eq(forbidden_json) + expect(storage_files.errors.data.payload.to_json).to eq(forbidden_json) end end end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/open_link_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/open_link_query_spec.rb index d8a7d1dbfadf..0c0786e52d5c 100644 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/open_link_query_spec.rb +++ b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/open_link_query_spec.rb @@ -84,7 +84,7 @@ expect(open_link_result).to be_failure expect(open_link_result.result).to eq(:not_found) - expect(open_link_result.errors.data.to_json).to eq(not_found_json) + expect(open_link_result.errors.data.payload.to_json).to eq(not_found_json) end it 'returns a forbidden error if the API call returns a 403' do @@ -96,7 +96,7 @@ expect(open_link_result).to be_failure expect(open_link_result.result).to eq(:forbidden) - expect(open_link_result.errors.data.to_json).to eq(forbidden_json) + expect(open_link_result.errors.data.payload.to_json).to eq(forbidden_json) end end end diff --git a/spec/services/oauth_clients/connection_manager_spec.rb b/spec/services/oauth_clients/connection_manager_spec.rb index e6a4179cd923..0d6bb5998dbe 100644 --- a/spec/services/oauth_clients/connection_manager_spec.rb +++ b/spec/services/oauth_clients/connection_manager_spec.rb @@ -31,6 +31,8 @@ require 'spec_helper' RSpec.describe OAuthClients::ConnectionManager, :webmock, type: :model do + using Storages::Peripherals::ServiceResultRefinements + let(:user) { create(:user) } let(:host) { "https://example.org" } @@ -105,7 +107,7 @@ context 'with no OAuthClientToken present' do it 'returns a redirection URL' do - expect(subject.success).to be_falsey + expect(subject.success).to be_falsy expect(subject.result).to be_a String # Details of string are tested above in section #get_authorization_uri end @@ -117,7 +119,7 @@ it 'returns the redirect URL' do allow(configuration).to receive(:scope).and_return(%w[email]) - expect(subject.success).to be_falsey + expect(subject.success).to be_falsy expect(subject.result).to be_a String expect(subject.result).to include oauth_client.integration.host expect(subject.result).to include "&state=some_state" @@ -183,10 +185,9 @@ shared_examples 'OAuth2 error response' do it 'returns a specific error message' do - expect(subject.success).to be_falsey - expect(subject.result).to eq error_message - expect(subject.errors[:base].count).to be(1) - expect(subject.errors[:base].first).to include I18n.t("oauth_client.errors.rack_oauth2.#{error_message}") + expect(subject.success).to be_falsy + expect(subject.result).to eq(:bad_request) + expect(subject.error_payload[:error]).to eq(error_message) end end @@ -203,22 +204,6 @@ end end - context 'with known reply invalid_grant' do - before do - stub_request(:post, File.join(host, '/index.php/apps/oauth2/api/v1/token')) - .to_return(status: 400, - body: { error: "invalid_grant" }.to_json, - headers: { "content-type" => "application/json; charset=utf-8" }) - end - - it 'returns a specific error message' do - expect(subject.success).to be_falsey - expect(subject.result).to eq 'invalid_grant' - expect(subject.errors[:base].count).to be(1) - expect(subject.errors[:base].first).to include I18n.t('oauth_client.errors.rack_oauth2.invalid_grant') - end - end - context 'with unknown reply' do before do stub_request(:post, File.join(host, '/index.php/apps/oauth2/api/v1/token')) @@ -227,11 +212,12 @@ headers: { "content-type" => "application/json; charset=utf-8" }) end - it 'returns an unspecific error message' do - expect(subject.success).to be_falsey - expect(subject.result).to eq 'invalid_requesttt' - expect(subject.errors[:base].count).to be(1) - expect(subject.errors[:base].first).to include I18n.t('oauth_client.errors.oauth_returned_error') + it 'returns an error wrapping the unknown response' do + expect(subject.success).to be_falsy + expect(subject.result).to eq(:bad_request) + expect(subject.error_payload[:error]).to eq('invalid_requesttt') + expect(subject.error_source).to be_a(described_class) + expect(subject.errors.log_message).to include I18n.t('oauth_client.errors.oauth_returned_error') end end @@ -245,11 +231,12 @@ ) end - it 'returns an unspecific error message' do - expect(subject.success).to be_falsey - expect(subject.result).to eq "unexpected token at 'some: very, invalid>