diff options
14 files changed, 643 insertions, 72 deletions
diff --git a/app/controllers/projects/error_tracking_controller.rb b/app/controllers/projects/error_tracking_controller.rb index 9e403e1d25b..2cd3dad12a2 100644 --- a/app/controllers/projects/error_tracking_controller.rb +++ b/app/controllers/projects/error_tracking_controller.rb @@ -15,6 +15,14 @@ class Projects::ErrorTrackingController < Projects::ApplicationController end end + def list_projects + respond_to do |format| + format.json do + render_project_list_json + end + end + end + private def render_index_json @@ -32,6 +40,32 @@ class Projects::ErrorTrackingController < Projects::ApplicationController } end + def render_project_list_json + service = ErrorTracking::ListProjectsService.new( + project, + current_user, + list_projects_params + ) + result = service.execute + + unless result[:status] == :success + return render( + status: result[:http_status] || :bad_request, + json: { + message: result[:message] + } + ) + end + + render json: { + projects: serialize_projects(result[:projects]) + } + end + + def list_projects_params + params.require(:error_tracking_setting).permit([:api_host, :token]) + end + def set_polling_interval Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL) end @@ -41,4 +75,10 @@ class Projects::ErrorTrackingController < Projects::ApplicationController .new(project: project, user: current_user) .represent(errors) end + + def serialize_projects(projects) + ErrorTracking::ProjectSerializer + .new(project: project, user: current_user) + .represent(projects) + end end diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb index 521ec2acebb..1ba0ff8be7b 100644 --- a/app/controllers/projects/settings/operations_controller.rb +++ b/app/controllers/projects/settings/operations_controller.rb @@ -14,16 +14,37 @@ module Projects def update result = ::Projects::Operations::UpdateService.new(project, current_user, update_params).execute + render_update_response(result) + end + + private + + # overridden in EE + def render_update_response(result) + respond_to do |format| + format.json do + render_update_json_response(result) + end + end + end + + def render_update_json_response(result) if result[:status] == :success - flash[:notice] = _('Your changes have been saved') - redirect_to project_settings_operations_path(@project) + render json: { + status: result[:status], + message: _('Your changes have been saved') + } else - render 'show' + render( + status: result[:http_status] || :bad_request, + json: { + status: result[:status], + message: result[:message] + } + ) end end - private - def error_tracking_setting @error_tracking_setting ||= project.error_tracking_setting || project.build_error_tracking_setting @@ -35,7 +56,14 @@ module Projects # overridden in EE def permitted_project_params - { error_tracking_setting_attributes: [:enabled, :api_url, :token] } + { + error_tracking_setting_attributes: [ + :enabled, + :api_host, + :token, + project: [:slug, :name, :organization_slug, :organization_name] + ] + } end def check_license diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb index 31084c54bdc..892eef8eb99 100644 --- a/app/models/error_tracking/project_error_tracking_setting.rb +++ b/app/models/error_tracking/project_error_tracking_setting.rb @@ -40,6 +40,8 @@ module ErrorTracking end def self.build_api_url_from(api_host:, project_slug:, organization_slug:) + return if api_host.blank? + uri = Addressable::URI.parse("#{api_host}/api/0/projects/#{organization_slug}/#{project_slug}/") uri.path = uri.path.squeeze('/') @@ -63,13 +65,29 @@ module ErrorTracking end def list_sentry_projects - { projects: sentry_client.list_projects } + with_reactive_cache('list_projects', {}) do |result| + result + end end def calculate_reactive_cache(request, opts) case request when 'list_issues' sentry_client.list_issues(**opts.symbolize_keys) + when 'list_projects' + begin + { projects: sentry_client.list_projects } + rescue Sentry::Client::SentryError => e + { + error: e.message, + http_status: :unprocessable_entity + } + rescue Sentry::Client::Error => e + { + error: e.message, + http_status: :bad_request + } + end end end diff --git a/app/serializers/error_tracking/project_serializer.rb b/app/serializers/error_tracking/project_serializer.rb index b2406f4d631..68724088fff 100644 --- a/app/serializers/error_tracking/project_serializer.rb +++ b/app/serializers/error_tracking/project_serializer.rb @@ -2,6 +2,6 @@ module ErrorTracking class ProjectSerializer < BaseSerializer - entity ProjectEntity + entity ErrorTracking::ProjectEntity end end diff --git a/app/services/error_tracking/list_projects_service.rb b/app/services/error_tracking/list_projects_service.rb index c6e8be0f2be..f2d0904a41f 100644 --- a/app/services/error_tracking/list_projects_service.rb +++ b/app/services/error_tracking/list_projects_service.rb @@ -11,12 +11,15 @@ module ErrorTracking return error(setting.errors.full_messages.join(', '), :bad_request) end - begin - result = setting.list_sentry_projects - rescue Sentry::Client::Error => e - return error(e.message, :bad_request) - rescue Sentry::Client::SentryError => e - return error(e.message, :unprocessable_entity) + result = setting.list_sentry_projects + + # our results are not yet ready + if result.nil? + return error('not ready', :no_content) + end + + if result[:error].present? + return error(result[:error], result[:http_status] || :bad_request) end success(projects: result[:projects]) diff --git a/app/services/error_tracking/list_sentry_projects_service.rb b/app/services/error_tracking/list_sentry_projects_service.rb new file mode 100644 index 00000000000..c07ab0e84a2 --- /dev/null +++ b/app/services/error_tracking/list_sentry_projects_service.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module ErrorTracking + class ListSentryProjectsService < ::BaseService + def execute + return error('access denied') unless can_read? + + e = project_error_tracking_setting + + unless e.valid? + return error(e.errors.full_messages.join(', '), :bad_request) + end + + begin + result = e.list_sentry_projects + rescue Sentry::Client::Error => e + return error(e.message, :bad_request) + rescue Sentry::Client::SentryError => e + return error(e.message, :unprocessable_entity) + end + + success(projects: result[:projects]) + end + + private + + def project_error_tracking_setting + e = project.error_tracking_setting || project.build_error_tracking_setting + + e.api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from( + api_host: params[:api_host], + organization_slug: nil, + project_slug: nil + ) + e.token = params[:token] + e.enabled = true + + e + end + + def can_read? + can?(current_user, :read_sentry_issue, project) + end + end +end diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb index abd6d8de750..aedf79c86d7 100644 --- a/app/services/projects/operations/update_service.rb +++ b/app/services/projects/operations/update_service.rb @@ -12,7 +12,28 @@ module Projects private def project_update_params - params.slice(:error_tracking_setting_attributes) + error_tracking_params + end + + def error_tracking_params + settings = params[:error_tracking_setting_attributes] + return {} if settings.blank? + + api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from( + api_host: settings[:api_host], + project_slug: settings.dig(:project, :slug), + organization_slug: settings.dig(:project, :organization_slug) + ) + + { + error_tracking_setting_attributes: { + api_url: api_url, + token: settings[:token], + enabled: settings[:enabled], + project_name: settings.dig(:project, :name), + organization_name: settings.dig(:project, :organization_name) + } + } end end end diff --git a/config/routes/project.rb b/config/routes/project.rb index 21793e7756a..59b4a119a08 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -443,7 +443,11 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end - resources :error_tracking, only: [:index], controller: :error_tracking + resources :error_tracking, only: [:index], controller: :error_tracking do + collection do + post :list_projects + end + end # Since both wiki and repository routing contains wildcard characters # its preferable to keep it below all other project routes diff --git a/spec/controllers/projects/error_tracking_controller_spec.rb b/spec/controllers/projects/error_tracking_controller_spec.rb index 6464398cea1..f65b00aabfd 100644 --- a/spec/controllers/projects/error_tracking_controller_spec.rb +++ b/spec/controllers/projects/error_tracking_controller_spec.rb @@ -107,8 +107,11 @@ describe Projects::ErrorTrackingController do let(:http_status) { :no_content } before do - expect(list_issues_service).to receive(:execute) - .and_return(status: :error, message: error_message, http_status: http_status) + expect(list_issues_service).to receive(:execute).and_return( + status: :error, + message: error_message, + http_status: http_status + ) end it 'returns http_status with message' do @@ -122,9 +125,130 @@ describe Projects::ErrorTrackingController do end end + describe 'POST #list_projects' do + context 'with insufficient permissions' do + before do + project.add_guest(user) + end + + it 'returns 404' do + post :list_projects, params: list_projects_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with an anonymous user' do + before do + sign_out(user) + end + + it 'redirects to sign-in page' do + post :list_projects, params: list_projects_params + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'with authorized user' do + let(:list_projects_service) { spy(:list_projects_service) } + let(:sentry_project) { build(:error_tracking_project) } + + before do + expect(ErrorTracking::ListProjectsService) + .to receive(:new).with( + project, + user, + ActionController::Parameters.new( + list_projects_params[:error_tracking_setting] + ).permit! + ) + .and_return(list_projects_service) + end + + context 'service result is successful' do + before do + expect(list_projects_service).to receive(:execute) + .and_return(status: :success, projects: [sentry_project]) + end + + it 'returns a list of projects' do + post :list_projects, params: list_projects_params + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('error_tracking/list_projects') + expect(json_response['projects']).to eq([sentry_project].as_json) + end + end + + context 'service result is erroneous' do + let(:error_message) { 'error message' } + + context 'without http_status' do + before do + expect(list_projects_service).to receive(:execute) + .and_return(status: :error, message: error_message) + end + + it 'returns 400 with message' do + get :list_projects, params: list_projects_params + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq(error_message) + end + end + + context 'with explicit http_status' do + let(:http_status) { :no_content } + + before do + expect(list_projects_service).to receive(:execute).and_return( + status: :error, + message: error_message, + http_status: http_status + ) + end + + it 'returns http_status with message' do + get :list_projects, params: list_projects_params + + expect(response).to have_gitlab_http_status(http_status) + expect(json_response['message']).to eq(error_message) + end + end + end + end + + private + + def list_projects_params(opts = {}) + opts.reverse_merge( + namespace_id: project.namespace, + project_id: project, + format: :json, + error_tracking_setting: { + api_host: 'gitlab.com', + token: 'token' + } + ) + end + end + private def project_params(opts = {}) opts.reverse_merge(namespace_id: project.namespace, project_id: project) end + + def list_projects_params(opts = {}) + opts.reverse_merge( + namespace_id: project.namespace, + project_id: project, + format: :json, + error_tracking_setting: { + api_host: 'gitlab.com', + token: 'token' + } + ) + end end diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb index d989ec22481..825aa2c7db2 100644 --- a/spec/controllers/projects/settings/operations_controller_spec.rb +++ b/spec/controllers/projects/settings/operations_controller_spec.rb @@ -74,38 +74,56 @@ describe Projects::Settings::OperationsController do { error_tracking_setting_attributes: { enabled: '1', - api_url: 'http://url', - token: 'token' + api_host: 'http://url', + token: 'token', + project: { + slug: 'sentry-project', + name: 'Sentry Project', + organization_slug: 'sentry-org', + organization_name: 'Sentry Org' + } } } end + let(:error_tracking_permitted) do ActionController::Parameters.new(error_tracking_params).permit! end - context 'when update succeeds' do - before do - stub_operations_update_service_returning(status: :success) - end - - it 'shows a notice' do - patch :update, params: project_params(project, error_tracking_params) - - expect(response).to redirect_to(operations_url) - expect(flash[:notice]).to eq _('Your changes have been saved') - end - end - - context 'when update fails' do - before do - stub_operations_update_service_returning(status: :error) + context 'format json' do + context 'when update succeeds' do + before do + stub_operations_update_service_returning(status: :success) + end + + it 'returns success status' do + patch :update, + params: project_params(project, error_tracking_params), + format: :json + + expect(json_response).to eq( + 'status' => 'success', + 'message' => _('Your changes have been saved') + ) + end end - it 'renders show page' do - patch :update, params: project_params(project, error_tracking_params) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to render_template(:show) + context 'when update fails' do + before do + stub_operations_update_service_returning( + status: :error, + message: 'error message' + ) + end + + it 'returns error' do + patch :update, + params: project_params(project, error_tracking_params), + format: :json + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).not_to be_nil + end end end @@ -147,11 +165,11 @@ describe Projects::Settings::OperationsController do private - def project_params(project, params = {}) + def project_params(project, project_params = {}, other_params = {}) { namespace_id: project.namespace, project_id: project, - project: params - } + project: project_params + }.merge(other_params) end end diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb index d30228b863c..eed2ccc8ea0 100644 --- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb +++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb @@ -149,15 +149,63 @@ describe ErrorTracking::ProjectErrorTrackingSetting do describe '#list_sentry_projects' do let(:projects) { [:list, :of, :projects] } - let(:sentry_client) { spy(:sentry_client) } - it 'calls sentry client' do - expect(subject).to receive(:sentry_client).and_return(sentry_client) - expect(sentry_client).to receive(:list_projects).and_return(projects) + let(:result) do + subject.list_sentry_projects + end + + context 'when cached' do + let(:sentry_client) { spy(:sentry_client) } + + before do + stub_reactive_cache(subject, projects, {}) + synchronous_reactive_cache(subject) + + expect(subject).to receive(:sentry_client).and_return(sentry_client) + expect(sentry_client).to receive(:list_projects) + .and_return(projects) + end + + it 'returns cached projects' do + expect(result).to eq(projects: projects) + end + end + + context 'when not cached' do + it 'returns nil' do + expect(subject).not_to receive(:sentry_client) + expect(result).to be_nil + end + end + + context 'when sentry client raises exception' do + let(:sentry_client) { spy(:sentry_client) } + + before do + synchronous_reactive_cache(subject) + + expect(subject).to receive(:sentry_client).and_return(sentry_client) + end + + it 'returns error for Sentry::Client::Error exception' do + expect(sentry_client).to receive(:list_projects) + .and_raise(Sentry::Client::Error, 'error message') - result = subject.list_sentry_projects + expect(result).to eq( + error: 'error message', + http_status: :bad_request + ) + end + + it 'returns error for Sentry::Client::SentryError exception' do + expect(sentry_client).to receive(:list_projects) + .and_raise(Sentry::Client::SentryError, 'error message') - expect(result).to eq(projects: projects) + expect(result).to eq( + error: 'error message', + http_status: :unprocessable_entity + ) + end end end @@ -257,6 +305,16 @@ describe ErrorTracking::ProjectErrorTrackingSetting do expect(api_url).to eq(':::') end + + it 'returns nil when api_host is blank' do + api_url = described_class.build_api_url_from( + api_host: '', + organization_slug: 'org-slug', + project_slug: 'proj-slug' + ) + + expect(api_url).to be_nil + end end describe '#api_host' do diff --git a/spec/services/error_tracking/list_projects_service_spec.rb b/spec/services/error_tracking/list_projects_service_spec.rb index ee9c59e3f65..6204ca87576 100644 --- a/spec/services/error_tracking/list_projects_service_spec.rb +++ b/spec/services/error_tracking/list_projects_service_spec.rb @@ -50,18 +50,6 @@ describe ErrorTracking::ListProjectsService do end end - context 'sentry client raises exception' do - before do - expect(error_tracking_setting).to receive(:list_sentry_projects) - .and_raise(Sentry::Client::Error, 'Sentry response error: 500') - end - - it 'returns error response' do - expect(result[:message]).to eq('Sentry response error: 500') - expect(result[:http_status]).to eq(:bad_request) - end - end - context 'with invalid url' do let(:params) do ActionController::Parameters.new( @@ -92,6 +80,39 @@ describe ErrorTracking::ListProjectsService do expect(result).to eq(status: :success, projects: projects) end end + + context 'when list_sentry_projects returns nil' do + before do + expect(error_tracking_setting) + .to receive(:list_sentry_projects).and_return(nil) + end + + it 'result is not ready' do + result = subject.execute + + expect(result).to eq( + status: :error, + http_status: :no_content, + message: 'not ready' + ) + end + end + + context 'when list_sentry_projects returns empty array' do + before do + expect(error_tracking_setting) + .to receive(:list_sentry_projects).and_return({ projects: [] }) + end + + it 'returns the empty array' do + result = subject.execute + + expect(result).to eq( + status: :success, + projects: [] + ) + end + end end context 'with unauthorized user' do @@ -112,10 +133,14 @@ describe ErrorTracking::ListProjectsService do .to receive(:list_sentry_projects).and_return(projects: []) error_tracking_setting.enabled = false + error_tracking_setting.save! end it 'ignores enabled flag' do expect(result).to include(status: :success, projects: []) + + error_tracking_setting.reload + expect(error_tracking_setting.enabled).to be false end end diff --git a/spec/services/error_tracking/list_sentry_projects_service_spec.rb b/spec/services/error_tracking/list_sentry_projects_service_spec.rb new file mode 100644 index 00000000000..ad9ea56f279 --- /dev/null +++ b/spec/services/error_tracking/list_sentry_projects_service_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ErrorTracking::ListSentryProjectsService do + set(:user) { create(:user) } + set(:project) { create(:project) } + + let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' } + let(:token) { 'test-token' } + let(:new_api_host) { 'https://gitlab.com/' } + let(:new_token) { 'new-token' } + let(:params) { ActionController::Parameters.new(api_host: new_api_host, token: new_token) } + + let(:error_tracking_setting) do + create(:project_error_tracking_setting, api_url: sentry_url, token: token, project: project) + end + + subject { described_class.new(project, user, params) } + + before do + project.add_reporter(user) + end + + describe '#execute' do + context 'with authorized user' do + before do + expect(project).to receive(:error_tracking_setting).at_least(:once) + .and_return(error_tracking_setting) + end + + it 'uses new api_url and token' do + sentry_client = spy(:sentry_client) + + expect(Sentry::Client).to receive(:new) + .with(new_api_host + 'api/0/projects/', new_token).and_return(sentry_client) + expect(sentry_client).to receive(:list_projects).and_return([]) + + subject.execute + end + + context 'sentry client raises exception' do + it 'returns error response' do + expect(error_tracking_setting).to receive(:list_sentry_projects) + .and_raise(Sentry::Client::Error, 'Sentry response error: 500') + + subject = described_class.new(project, user, params) + result = subject.execute + + expect(result[:message]).to eq('Sentry response error: 500') + expect(result[:http_status]).to eq(:bad_request) + end + end + + context 'with invalid url' do + let(:params) do + ActionController::Parameters.new( + api_host: 'https://localhost', + token: new_token + ) + end + + subject { described_class.new(project, user, params) } + + before do + error_tracking_setting.enabled = false + end + + it 'returns error' do + result = subject.execute + + expect(error_tracking_setting).not_to be_valid + expect(result[:message]).to start_with('Api url is blocked') + end + end + + context 'when list_sentry_projects returns projects' do + let(:projects) { [:list, :of, :projects] } + + before do + expect(error_tracking_setting) + .to receive(:list_sentry_projects).and_return(projects: projects) + end + + it 'returns the projects' do + result = subject.execute + + expect(result).to eq(status: :success, projects: projects) + end + end + end + + context 'with unauthorized user' do + let(:unauthorized_user) { create(:user) } + + subject { described_class.new(project, unauthorized_user) } + + it 'returns error' do + result = subject.execute + + expect(result).to include(status: :error, message: 'access denied') + end + end + + context 'with error tracking disabled' do + before do + expect(project).to receive(:error_tracking_setting).at_least(:once) + .and_return(error_tracking_setting) + expect(error_tracking_setting) + .to receive(:list_sentry_projects).and_return(projects: []) + + error_tracking_setting.enabled = false + end + + it 'ignores enabled flag' do + result = subject.execute + + expect(result).to include(status: :success, projects: []) + end + end + + context 'error_tracking_setting is nil' do + let(:new_api_host) { 'https://gitlab.com/' } + let(:new_token) { 'new-token' } + let(:params) { ActionController::Parameters.new(api_host: new_api_host, token: new_token) } + + before do + expect(project).to receive(:error_tracking_setting).at_least(:once) + .and_return(nil) + end + + it 'builds a new error_tracking_setting' do + sentry_client = spy(:sentry_client) + + expect(Sentry::Client).to receive(:new) + .with(new_api_host + 'api/0/projects/', new_token) + .and_return(sentry_client) + + expect(sentry_client).to receive(:list_projects) + .and_return([:project1, :project2]) + + result = subject.execute + + expect(result[:projects]).to eq([:project1, :project2]) + expect(project.error_tracking_setting).to be_nil + end + end + end +end diff --git a/spec/services/projects/operations/update_service_spec.rb b/spec/services/projects/operations/update_service_spec.rb index 6afae3da80c..cf90230732c 100644 --- a/spec/services/projects/operations/update_service_spec.rb +++ b/spec/services/projects/operations/update_service_spec.rb @@ -14,13 +14,19 @@ describe Projects::Operations::UpdateService do context 'error tracking' do context 'with existing error tracking setting' do let(:params) do - { + ActionController::Parameters.new({ error_tracking_setting_attributes: { enabled: false, - api_url: 'http://gitlab.com/api/0/projects/org/project', - token: 'token' + api_host: 'http://gitlab.com/', + token: 'token', + project: { + slug: 'project', + name: 'Project', + organization_slug: 'org', + organization_name: 'Org' + } } - } + }).permit! end before do @@ -32,28 +38,60 @@ describe Projects::Operations::UpdateService do project.reload expect(project.error_tracking_setting).not_to be_enabled - expect(project.error_tracking_setting.api_url).to eq('http://gitlab.com/api/0/projects/org/project') + expect(project.error_tracking_setting.api_url).to eq( + 'http://gitlab.com/api/0/projects/org/project/' + ) expect(project.error_tracking_setting.token).to eq('token') + expect(project.error_tracking_setting[:project_name]).to eq('Project') + expect(project.error_tracking_setting[:organization_name]).to eq('Org') + end + + context 'disable error tracking' do + before do + params[:error_tracking_setting_attributes][:api_host] = '' + params[:error_tracking_setting_attributes][:enabled] = false + end + + it 'can set api_url to nil' do + expect(result[:status]).to eq(:success) + + project.reload + expect(project.error_tracking_setting).not_to be_enabled + expect(project.error_tracking_setting.api_url).to be_nil + expect(project.error_tracking_setting.token).to eq('token') + expect(project.error_tracking_setting[:project_name]).to eq('Project') + expect(project.error_tracking_setting[:organization_name]).to eq('Org') + end end end context 'without an existing error tracking setting' do let(:params) do - { + ActionController::Parameters.new({ error_tracking_setting_attributes: { enabled: true, - api_url: 'http://gitlab.com/api/0/projects/org/project', - token: 'token' + api_host: 'http://gitlab.com/', + token: 'token', + project: { + slug: 'project', + name: 'Project', + organization_slug: 'org', + organization_name: 'Org' + } } - } + }).permit! end it 'creates a setting' do expect(result[:status]).to eq(:success) expect(project.error_tracking_setting).to be_enabled - expect(project.error_tracking_setting.api_url).to eq('http://gitlab.com/api/0/projects/org/project') + expect(project.error_tracking_setting.api_url).to eq( + 'http://gitlab.com/api/0/projects/org/project/' + ) expect(project.error_tracking_setting.token).to eq('token') + expect(project.error_tracking_setting[:project_name]).to eq('Project') + expect(project.error_tracking_setting[:organization_name]).to eq('Org') end end |