diff --git a/app/controllers/projects/error_tracking_controller.rb b/app/controllers/projects/error_tracking_controller.rb new file mode 100644 index 00000000000..4596b6c91f2 --- /dev/null +++ b/app/controllers/projects/error_tracking_controller.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class Projects::ErrorTrackingController < Projects::ApplicationController + before_action :check_feature_flag! + before_action :authorize_read_sentry_issue! + before_action :push_feature_flag_to_frontend + + POLLING_INTERVAL = 10_000 + + def index + respond_to do |format| + format.html + format.json do + set_polling_interval + render_index_json + end + end + end + + private + + def render_index_json + service = ErrorTracking::ListIssuesService.new(project, current_user) + result = service.execute + + unless result[:status] == :success + return render json: { message: result[:message] }, + status: result[:http_status] || :bad_request + end + + render json: { + errors: serialize_errors(result[:issues]), + external_url: service.external_url + } + end + + def set_polling_interval + Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL) + end + + def serialize_errors(errors) + ErrorTracking::ErrorSerializer + .new(project: project, user: current_user) + .represent(errors) + end + + def check_feature_flag! + render_404 unless Feature.enabled?(:error_tracking, project) + end + + def push_feature_flag_to_frontend + push_frontend_feature_flag(:error_tracking, current_user) + end +end diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb index 632c64c2f1c..7f4947ba27a 100644 --- a/app/models/error_tracking/project_error_tracking_setting.rb +++ b/app/models/error_tracking/project_error_tracking_setting.rb @@ -2,13 +2,58 @@ module ErrorTracking class ProjectErrorTrackingSetting < ActiveRecord::Base + include ReactiveCaching + + self.reactive_cache_key = ->(setting) { [setting.class.model_name.singular, setting.project_id] } + belongs_to :project validates :api_url, length: { maximum: 255 }, public_url: true, url: { enforce_sanitization: true } + validate :validate_api_url_path + attr_encrypted :token, mode: :per_attribute_iv, key: Settings.attr_encrypted_db_key_base_truncated, algorithm: 'aes-256-gcm' + + after_save :clear_reactive_cache! + + def sentry_client + Sentry::Client.new(api_url, token) + end + + def sentry_external_url + self.class.extract_sentry_external_url(api_url) + end + + def list_sentry_issues(opts = {}) + with_reactive_cache('list_issues', opts.stringify_keys) do |result| + { issues: result } + end + end + + def calculate_reactive_cache(request, opts) + case request + when 'list_issues' + sentry_client.list_issues(**opts.symbolize_keys) + end + end + + # http://HOST/api/0/projects/ORG/PROJECT + # -> + # http://HOST/ORG/PROJECT + def self.extract_sentry_external_url(url) + url.sub('api/0/projects/', '') + end + + private + + def validate_api_url_path + unless URI(api_url).path.starts_with?('/api/0/projects') + errors.add(:api_url, 'path needs to start with /api/0/projects') + end + rescue URI::InvalidURIError + end end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index d70417e710e..12f9f29dcc1 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -200,6 +200,7 @@ class ProjectPolicy < BasePolicy enable :read_environment enable :read_deployment enable :read_merge_request + enable :read_sentry_issue end # We define `:public_user_access` separately because there are cases in gitlab-ee diff --git a/app/serializers/error_tracking/error_entity.rb b/app/serializers/error_tracking/error_entity.rb new file mode 100644 index 00000000000..91388e7c3ad --- /dev/null +++ b/app/serializers/error_tracking/error_entity.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module ErrorTracking + class ErrorEntity < Grape::Entity + expose :id, :title, :type, :user_count, :count, + :first_seen, :last_seen, :message, :culprit, + :external_url, :project_id, :project_name, :project_slug, + :short_id, :status, :frequency + end +end diff --git a/app/serializers/error_tracking/error_serializer.rb b/app/serializers/error_tracking/error_serializer.rb new file mode 100644 index 00000000000..ff9a645eb16 --- /dev/null +++ b/app/serializers/error_tracking/error_serializer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ErrorTracking + class ErrorSerializer < BaseSerializer + entity ErrorEntity + end +end diff --git a/app/services/error_tracking/list_issues_service.rb b/app/services/error_tracking/list_issues_service.rb new file mode 100644 index 00000000000..4cc35cfa4a8 --- /dev/null +++ b/app/services/error_tracking/list_issues_service.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module ErrorTracking + class ListIssuesService < ::BaseService + DEFAULT_ISSUE_STATUS = 'unresolved' + DEFAULT_LIMIT = 20 + + def execute + return error('not enabled') unless enabled? + return error('access denied') unless can_read? + + result = project_error_tracking_setting + .list_sentry_issues(issue_status: issue_status, limit: limit) + + # our results are not yet ready + unless result + return error('not ready', :no_content) + end + + success(issues: result[:issues]) + end + + def external_url + project_error_tracking_setting&.sentry_external_url + end + + private + + def project_error_tracking_setting + project.error_tracking_setting + end + + def issue_status + params[:issue_status] || DEFAULT_ISSUE_STATUS + end + + def limit + params[:limit] || DEFAULT_LIMIT + end + + def enabled? + project_error_tracking_setting&.enabled? + end + + def can_read? + can?(current_user, :read_sentry_issue, project) + end + end +end diff --git a/app/views/projects/error_tracking/index.html.haml b/app/views/projects/error_tracking/index.html.haml new file mode 100644 index 00000000000..a3e0dc75f6f --- /dev/null +++ b/app/views/projects/error_tracking/index.html.haml @@ -0,0 +1 @@ +- page_title _('Errors') diff --git a/config/routes/project.rb b/config/routes/project.rb index cf5a57300cf..e6ecb4bc9d8 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -442,6 +442,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end + resources :error_tracking, only: [:index], controller: :error_tracking + # Since both wiki and repository routing contains wildcard characters # its preferable to keep it below all other project routes draw :wiki diff --git a/lib/gitlab/error_tracking/error.rb b/lib/gitlab/error_tracking/error.rb new file mode 100644 index 00000000000..4af5192aa6a --- /dev/null +++ b/lib/gitlab/error_tracking/error.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Gitlab + module ErrorTracking + class Error + include ActiveModel::Model + + attr_accessor :id, :title, :type, :user_count, :count, + :first_seen, :last_seen, :message, :culprit, + :external_url, :project_id, :project_name, :project_slug, + :short_id, :status, :frequency + end + end +end diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb new file mode 100644 index 00000000000..343f2c49a7f --- /dev/null +++ b/lib/sentry/client.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +module Sentry + class Client + Error = Class.new(StandardError) + + attr_accessor :url, :token + + def initialize(api_url, token) + @url = api_url + @token = token + end + + def list_issues(issue_status:, limit:) + issues = get_issues(issue_status: issue_status, limit: limit) + map_to_errors(issues) + end + + private + + def request_params + { + headers: { + 'Authorization' => "Bearer #{@token}" + }, + follow_redirects: false + } + end + + def get_issues(issue_status:, limit:) + resp = Gitlab::HTTP.get( + issues_api_url, + **request_params.merge(query: { + query: "is:#{issue_status}", + limit: limit + }) + ) + + handle_response(resp) + end + + def handle_response(response) + unless response.code == 200 + raise Client::Error, "Sentry response error: #{response.code}" + end + + response.as_json + end + + def issues_api_url + issues_url = URI(@url + '/issues/') + issues_url.path.squeeze!('/') + + issues_url + end + + def map_to_errors(issues) + issues.map do |issue| + map_to_error(issue) + end + end + + def issue_url(id) + issues_url = @url + "/issues/#{id}" + issues_url = ErrorTracking::ProjectErrorTrackingSetting.extract_sentry_external_url(issues_url) + + uri = URI(issues_url) + uri.path.squeeze!('/') + + uri.to_s + end + + def map_to_error(issue) + id = issue.fetch('id') + project = issue.fetch('project') + + count = issue.fetch('count', nil) + + frequency = issue.dig('stats', '24h') + message = issue.dig('metadata', 'value') + + external_url = issue_url(id) + + Gitlab::ErrorTracking::Error.new( + id: id, + first_seen: issue.fetch('firstSeen', nil), + last_seen: issue.fetch('lastSeen', nil), + title: issue.fetch('title', nil), + type: issue.fetch('type', nil), + user_count: issue.fetch('userCount', nil), + count: count, + message: message, + culprit: issue.fetch('culprit', nil), + external_url: external_url, + short_id: issue.fetch('shortId', nil), + status: issue.fetch('status', nil), + frequency: frequency, + project_id: project.fetch('id'), + project_name: project.fetch('name', nil), + project_slug: project.fetch('slug', nil) + ) + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 501acb6947a..bb7c58ae244 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2959,6 +2959,9 @@ msgstr "" msgid "Error while loading the merge request. Please try again." msgstr "" +msgid "Errors" +msgstr "" + msgid "Estimated" msgstr "" diff --git a/spec/controllers/projects/error_tracking_controller_spec.rb b/spec/controllers/projects/error_tracking_controller_spec.rb new file mode 100644 index 00000000000..729e71b87a6 --- /dev/null +++ b/spec/controllers/projects/error_tracking_controller_spec.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Projects::ErrorTrackingController do + set(:project) { create(:project) } + set(:user) { create(:user) } + + before do + sign_in(user) + project.add_maintainer(user) + end + + describe 'GET #index' do + describe 'html' do + it 'renders index with 200 status code' do + get :index, params: project_params + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:index) + end + + context 'with feature flag disabled' do + before do + stub_feature_flags(error_tracking: false) + end + + it 'returns 404' do + get :index, params: project_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with insufficient permissions' do + before do + project.add_guest(user) + end + + it 'returns 404' do + get :index, params: project_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 + get :index, params: project_params + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + describe 'format json' do + shared_examples 'no data' do + it 'returns no data' do + get :index, params: project_params(format: :json) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('error_tracking/index') + expect(json_response['external_url']).to be_nil + expect(json_response['errors']).to eq([]) + end + end + + let(:list_issues_service) { spy(:list_issues_service) } + let(:external_url) { 'http://example.com' } + + before do + expect(ErrorTracking::ListIssuesService) + .to receive(:new).with(project, user) + .and_return(list_issues_service) + end + + context 'service result is successful' do + before do + expect(list_issues_service).to receive(:execute) + .and_return(status: :success, issues: [error]) + expect(list_issues_service).to receive(:external_url) + .and_return(external_url) + end + + let(:error) { build(:error_tracking_error) } + + it 'returns a list of errors' do + get :index, params: project_params(format: :json) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('error_tracking/index') + expect(json_response['external_url']).to eq(external_url) + expect(json_response['errors']).to eq([error].as_json) + end + end + + context 'service result is erroneous' do + let(:error_message) { 'error message' } + + context 'without http_status' do + before do + expect(list_issues_service).to receive(:execute) + .and_return(status: :error, message: error_message) + end + + it 'returns 400 with message' do + get :index, params: project_params(format: :json) + + 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_issues_service).to receive(:execute) + .and_return(status: :error, message: error_message, http_status: http_status) + end + + it 'returns http_status with message' do + get :index, params: project_params(format: :json) + + expect(response).to have_gitlab_http_status(http_status) + expect(json_response['message']).to eq(error_message) + end + end + end + end + end + + private + + def project_params(opts = {}) + opts.reverse_merge(namespace_id: project.namespace, project_id: project) + end +end diff --git a/spec/factories/error_tracking/error.rb b/spec/factories/error_tracking/error.rb new file mode 100644 index 00000000000..ff883a3d22c --- /dev/null +++ b/spec/factories/error_tracking/error.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :error_tracking_error, class: Gitlab::ErrorTracking::Error do + id 'id' + title 'title' + type 'error' + user_count 1 + count 2 + first_seen { Time.now } + last_seen { Time.now } + message 'message' + culprit 'culprit' + external_url 'http://example.com/id' + project_id 'project1' + project_name 'project name' + project_slug 'project_name' + short_id 'ID' + status 'unresolved' + frequency [] + + skip_create + end +end diff --git a/spec/factories/project_error_tracking_settings.rb b/spec/factories/project_error_tracking_settings.rb index f044cbe8755..fbd8dfd395c 100644 --- a/spec/factories/project_error_tracking_settings.rb +++ b/spec/factories/project_error_tracking_settings.rb @@ -3,7 +3,7 @@ FactoryBot.define do factory :project_error_tracking_setting, class: ErrorTracking::ProjectErrorTrackingSetting do project - api_url 'https://gitlab.com' + api_url 'https://gitlab.com/api/0/projects/sentry-org/sentry-project' enabled true token 'access_token_123' end diff --git a/spec/fixtures/api/schemas/error_tracking/error.json b/spec/fixtures/api/schemas/error_tracking/error.json new file mode 100644 index 00000000000..df2c02d7d5d --- /dev/null +++ b/spec/fixtures/api/schemas/error_tracking/error.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "required" : [ + "external_url", + "last_seen", + "message", + "type" + ], + "properties" : { + "id": { "type": "string"}, + "first_seen": { "type": "string", "format": "date-time" }, + "last_seen": { "type": "string", "format": "date-time" }, + "type": { "type": "string" }, + "message": { "type": "string" }, + "culprit": { "type": "string" }, + "count": { "type": "integer"}, + "external_url": { "type": "string" }, + "user_count": { "type": "integer"} + }, + "additionalProperties": true +} diff --git a/spec/fixtures/api/schemas/error_tracking/index.json b/spec/fixtures/api/schemas/error_tracking/index.json new file mode 100644 index 00000000000..d3abc29ffa7 --- /dev/null +++ b/spec/fixtures/api/schemas/error_tracking/index.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "required": [ + "external_url", + "errors" + ], + "properties": { + "external_url": { "type": ["string", "null"] }, + "errors": { + "type": "array", + "items": { "$ref": "error.json" } + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/sentry/issues_sample_response.json b/spec/fixtures/sentry/issues_sample_response.json new file mode 100644 index 00000000000..ed22499cfa1 --- /dev/null +++ b/spec/fixtures/sentry/issues_sample_response.json @@ -0,0 +1,42 @@ +[{ + "lastSeen": "2018-12-31T12:00:11Z", + "numComments": 0, + "userCount": 0, + "stats": { + "24h": [ + [ + 1546437600, + 0 + ] + ] + }, + "culprit": "sentry.tasks.reports.deliver_organization_user_report", + "title": "gaierror: [Errno -2] Name or service not known", + "id": "11", + "assignedTo": null, + "logger": null, + "type": "error", + "annotations": [], + "metadata": { + "type": "gaierror", + "value": "[Errno -2] Name or service not known" + }, + "status": "unresolved", + "subscriptionDetails": null, + "isPublic": false, + "hasSeen": false, + "shortId": "INTERNAL-4", + "shareId": null, + "firstSeen": "2018-12-17T12:00:14Z", + "count": "21", + "permalink": "35.228.54.90/sentry/internal/issues/11/", + "level": "error", + "isSubscribed": true, + "isBookmarked": false, + "project": { + "slug": "internal", + "id": "1", + "name": "Internal" + }, + "statusDetails": {} + }] diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb new file mode 100644 index 00000000000..b36be0fd9c1 --- /dev/null +++ b/spec/lib/sentry/client_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Sentry::Client do + let(:issue_status) { 'unresolved' } + let(:limit) { 20 } + let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' } + let(:token) { 'test-token' } + + let(:sample_response) do + Gitlab::Utils.deep_indifferent_access( + JSON.parse(File.read(Rails.root.join('spec/fixtures/sentry/issues_sample_response.json'))) + ) + end + + subject(:client) { described_class.new(sentry_url, token) } + + describe '#list_issues' do + subject { client.list_issues(issue_status: issue_status, limit: limit) } + + before do + stub_sentry_request(sentry_url + '/issues/?limit=20&query=is:unresolved', body: sample_response) + end + + it 'returns objects of type ErrorTracking::Error' do + expect(subject.length).to eq(1) + expect(subject[0]).to be_a(Gitlab::ErrorTracking::Error) + end + + context 'error object created from sentry response' do + using RSpec::Parameterized::TableSyntax + + where(:error_object, :sentry_response) do + :id | :id + :first_seen | :firstSeen + :last_seen | :lastSeen + :title | :title + :type | :type + :user_count | :userCount + :count | :count + :message | [:metadata, :value] + :culprit | :culprit + :short_id | :shortId + :status | :status + :frequency | [:stats, '24h'] + :project_id | [:project, :id] + :project_name | [:project, :name] + :project_slug | [:project, :slug] + end + + with_them do + it { expect(subject[0].public_send(error_object)).to eq(sample_response[0].dig(*sentry_response)) } + end + + context 'external_url' do + it 'is constructed correctly' do + expect(subject[0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11') + end + end + end + + context 'redirects' do + let(:redirect_to) { 'https://redirected.example.com' } + let(:other_url) { 'https://other.example.org' } + + let!(:redirected_req_stub) { stub_sentry_request(other_url) } + + let!(:redirect_req_stub) do + stub_sentry_request( + sentry_url + '/issues/?limit=20&query=is:unresolved', + status: 302, + headers: { location: redirect_to } + ) + end + + it 'does not follow redirects' do + expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response error: 302') + expect(redirect_req_stub).to have_been_requested + expect(redirected_req_stub).not_to have_been_requested + end + end + + # Sentry API returns 404 if there are extra slashes in the URL! + context 'extra slashes in URL' do + let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects//sentry-org/sentry-project/' } + let(:client) { described_class.new(sentry_url, token) } + + let!(:valid_req_stub) do + stub_sentry_request( + 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/' \ + 'issues/?limit=20&query=is:unresolved' + ) + end + + it 'removes extra slashes in api url' do + expect(Gitlab::HTTP).to receive(:get).with( + URI('https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/issues/'), + anything + ).and_call_original + + client.list_issues(issue_status: issue_status, limit: limit) + + expect(valid_req_stub).to have_been_requested + end + end + end + + private + + def stub_sentry_request(url, body: {}, status: 200, headers: {}) + WebMock.stub_request(:get, url) + .to_return( + status: status, + headers: { 'Content-Type' => 'application/json' }.merge(headers), + body: body.to_json + ) + 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 83f29718eda..2f8ab21d4b2 100644 --- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb +++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb @@ -3,33 +3,106 @@ require 'spec_helper' describe ErrorTracking::ProjectErrorTrackingSetting do + include ReactiveCachingHelpers + set(:project) { create(:project) } + subject { create(:project_error_tracking_setting, project: project) } + describe 'Associations' do it { is_expected.to belong_to(:project) } end describe 'Validations' do - subject { create(:project_error_tracking_setting, project: project) } - context 'when api_url is over 255 chars' do - before do - subject.api_url = 'https://' + 'a' * 250 - end - it 'fails validation' do + subject.api_url = 'https://' + 'a' * 250 + expect(subject).not_to be_valid expect(subject.errors.messages[:api_url]).to include('is too long (maximum is 255 characters)') end end context 'With unsafe url' do - let(:project_error_tracking_setting) { create(:project_error_tracking_setting, project: project) } - it 'fails validation' do - project_error_tracking_setting.api_url = "https://replaceme.com/'>" + subject.api_url = "https://replaceme.com/'>" - expect(project_error_tracking_setting).not_to be_valid + expect(subject).not_to be_valid + end + end + + context 'URL path' do + it 'fails validation with wrong path' do + subject.api_url = 'http://gitlab.com/project1/something' + + expect(subject).not_to be_valid + expect(subject.errors.messages[:api_url]).to include('path needs to start with /api/0/projects') + end + + it 'passes validation with correct path' do + subject.api_url = 'http://gitlab.com/api/0/projects/project1/something' + + expect(subject).to be_valid + end + end + end + + describe '#sentry_external_url' do + let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' } + + before do + subject.api_url = sentry_url + end + + it 'returns the correct url' do + expect(subject.class).to receive(:extract_sentry_external_url).with(sentry_url).and_call_original + + result = subject.sentry_external_url + + expect(result).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project') + end + end + + describe '#sentry_client' do + it 'returns sentry client' do + expect(subject.sentry_client).to be_a(Sentry::Client) + end + end + + describe '#list_sentry_issues' do + let(:issues) { [:list, :of, :issues] } + + let(:opts) do + { issue_status: 'unresolved', limit: 10 } + end + + let(:result) do + subject.list_sentry_issues(**opts) + end + + context 'when cached' do + let(:sentry_client) { spy(:sentry_client) } + + before do + stub_reactive_cache(subject, issues, opts) + synchronous_reactive_cache(subject) + + expect(subject).to receive(:sentry_client).and_return(sentry_client) + end + + it 'returns cached issues' do + expect(sentry_client).to receive(:list_issues).with(opts) + .and_return(issues) + + expect(result).to eq(issues: issues) + 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 end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 9cb20854f6e..2a4030de998 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -24,7 +24,7 @@ describe ProjectPolicy do download_code fork_project create_project_snippet update_issue admin_issue admin_label admin_list read_commit_status read_build read_container_image read_pipeline read_environment read_deployment - read_merge_request download_wiki_code + read_merge_request download_wiki_code read_sentry_issue ] end diff --git a/spec/services/error_tracking/list_issues_service_spec.rb b/spec/services/error_tracking/list_issues_service_spec.rb new file mode 100644 index 00000000000..d9dab1d705c --- /dev/null +++ b/spec/services/error_tracking/list_issues_service_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ErrorTracking::ListIssuesService 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(:result) { subject.execute } + + 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) } + + before do + expect(project).to receive(:error_tracking_setting).at_least(:once).and_return(error_tracking_setting) + + project.add_reporter(user) + end + + describe '#execute' do + context 'with authorized user' do + context 'when list_sentry_issues returns issues' do + let(:issues) { [:list, :of, :issues] } + + before do + expect(error_tracking_setting) + .to receive(:list_sentry_issues).and_return(issues: issues) + end + + it 'returns the issues' do + expect(result).to eq(status: :success, issues: issues) + end + end + + context 'when list_sentry_issues returns nil' do + before do + expect(error_tracking_setting) + .to receive(:list_sentry_issues).and_return(nil) + end + + it 'result is not ready' do + expect(result).to eq( + status: :error, http_status: :no_content, message: 'not ready') + 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 + error_tracking_setting.enabled = false + end + + it 'raises error' do + result = subject.execute + + expect(result).to include(status: :error, message: 'not enabled') + end + end + end + + describe '#sentry_external_url' do + let(:external_url) { 'https://sentrytest.gitlab.com/sentry-org/sentry-project' } + + it 'calls ErrorTracking::ProjectErrorTrackingSetting' do + expect(error_tracking_setting).to receive(:sentry_external_url).and_call_original + + subject.external_url + end + end +end diff --git a/spec/services/projects/operations/update_service_spec.rb b/spec/services/projects/operations/update_service_spec.rb index 731be907453..6afae3da80c 100644 --- a/spec/services/projects/operations/update_service_spec.rb +++ b/spec/services/projects/operations/update_service_spec.rb @@ -17,7 +17,7 @@ describe Projects::Operations::UpdateService do { error_tracking_setting_attributes: { enabled: false, - api_url: 'http://url', + api_url: 'http://gitlab.com/api/0/projects/org/project', token: 'token' } } @@ -32,7 +32,7 @@ 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://url') + 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') end end @@ -42,7 +42,7 @@ describe Projects::Operations::UpdateService do { error_tracking_setting_attributes: { enabled: true, - api_url: 'http://url', + api_url: 'http://gitlab.com/api/0/projects/org/project', token: 'token' } } @@ -52,7 +52,7 @@ describe Projects::Operations::UpdateService 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://url') + 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') end end