summaryrefslogtreecommitdiff
path: root/spec/lib/error_tracking/sentry_client
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-03-16 18:18:33 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-16 18:18:33 +0000
commitf64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch)
treea2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /spec/lib/error_tracking/sentry_client
parentbfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff)
downloadgitlab-ce-f64a639bcfa1fc2bc89ca7db268f594306edfd7c.tar.gz
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'spec/lib/error_tracking/sentry_client')
-rw-r--r--spec/lib/error_tracking/sentry_client/api_urls_spec.rb85
-rw-r--r--spec/lib/error_tracking/sentry_client/event_spec.rb75
-rw-r--r--spec/lib/error_tracking/sentry_client/issue_link_spec.rb65
-rw-r--r--spec/lib/error_tracking/sentry_client/issue_spec.rb330
-rw-r--r--spec/lib/error_tracking/sentry_client/pagination_parser_spec.rb60
-rw-r--r--spec/lib/error_tracking/sentry_client/projects_spec.rb100
-rw-r--r--spec/lib/error_tracking/sentry_client/repo_spec.rb39
7 files changed, 754 insertions, 0 deletions
diff --git a/spec/lib/error_tracking/sentry_client/api_urls_spec.rb b/spec/lib/error_tracking/sentry_client/api_urls_spec.rb
new file mode 100644
index 00000000000..bd701748dc2
--- /dev/null
+++ b/spec/lib/error_tracking/sentry_client/api_urls_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ErrorTracking::SentryClient::ApiUrls do
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/' }
+ let(:token) { 'test-token' }
+ let(:issue_id) { '123456' }
+ let(:issue_id_with_reserved_chars) { '123$%' }
+ let(:escaped_issue_id) { '123%24%25' }
+ let(:api_urls) { described_class.new(sentry_url) }
+
+ # Sentry API returns 404 if there are extra slashes in the URL!
+ shared_examples 'correct url with extra slashes' do
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects//sentry-org/sentry-project/' }
+
+ it_behaves_like 'correct url'
+ end
+
+ shared_examples 'correctly escapes issue ID' do
+ context 'with param a string with reserved chars' do
+ let(:issue_id) { issue_id_with_reserved_chars }
+
+ it { expect(subject.to_s).to include(escaped_issue_id) }
+ end
+
+ context 'with param a symbol with reserved chars' do
+ let(:issue_id) { issue_id_with_reserved_chars.to_sym }
+
+ it { expect(subject.to_s).to include(escaped_issue_id) }
+ end
+
+ context 'with param an integer' do
+ let(:issue_id) { 12345678 }
+
+ it { expect(subject.to_s).to include(issue_id.to_s) }
+ end
+ end
+
+ describe '#issues_url' do
+ subject { api_urls.issues_url }
+
+ shared_examples 'correct url' do
+ it { is_expected.to eq_uri('https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/issues/') }
+ end
+
+ it_behaves_like 'correct url'
+ it_behaves_like 'correct url with extra slashes'
+ end
+
+ describe '#issue_url' do
+ subject { api_urls.issue_url(issue_id) }
+
+ shared_examples 'correct url' do
+ it { is_expected.to eq_uri("https://sentrytest.gitlab.com/api/0/issues/#{issue_id}/") }
+ end
+
+ it_behaves_like 'correct url'
+ it_behaves_like 'correct url with extra slashes'
+ it_behaves_like 'correctly escapes issue ID'
+ end
+
+ describe '#projects_url' do
+ subject { api_urls.projects_url }
+
+ shared_examples 'correct url' do
+ it { is_expected.to eq_uri('https://sentrytest.gitlab.com/api/0/projects/') }
+ end
+
+ it_behaves_like 'correct url'
+ it_behaves_like 'correct url with extra slashes'
+ end
+
+ describe '#issue_latest_event_url' do
+ subject { api_urls.issue_latest_event_url(issue_id) }
+
+ shared_examples 'correct url' do
+ it { is_expected.to eq_uri("https://sentrytest.gitlab.com/api/0/issues/#{issue_id}/events/latest/") }
+ end
+
+ it_behaves_like 'correct url'
+ it_behaves_like 'correct url with extra slashes'
+ it_behaves_like 'correctly escapes issue ID'
+ end
+end
diff --git a/spec/lib/error_tracking/sentry_client/event_spec.rb b/spec/lib/error_tracking/sentry_client/event_spec.rb
new file mode 100644
index 00000000000..64e674f1e9b
--- /dev/null
+++ b/spec/lib/error_tracking/sentry_client/event_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ErrorTracking::SentryClient do
+ include SentryClientHelpers
+
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
+ let(:token) { 'test-token' }
+ let(:default_httparty_options) do
+ {
+ follow_redirects: false,
+ headers: { "Authorization" => "Bearer test-token" }
+ }
+ end
+
+ let(:client) { described_class.new(sentry_url, token) }
+
+ describe '#issue_latest_event' do
+ let(:sample_response) do
+ Gitlab::Utils.deep_indifferent_access(
+ Gitlab::Json.parse(fixture_file('sentry/issue_latest_event_sample_response.json'))
+ )
+ end
+
+ let(:issue_id) { '1234' }
+ let(:sentry_api_response) { sample_response }
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0' }
+ let(:sentry_request_url) { "#{sentry_url}/issues/#{issue_id}/events/latest/" }
+ let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response) }
+
+ subject { client.issue_latest_event(issue_id: issue_id) }
+
+ it_behaves_like 'calls sentry api'
+
+ it 'has correct return type' do
+ expect(subject).to be_a(Gitlab::ErrorTracking::ErrorEvent)
+ end
+
+ shared_examples 'assigns error tracking event correctly' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:event_object, :sentry_response) do
+ :issue_id | :groupID
+ :date_received | :dateReceived
+ end
+
+ with_them do
+ it { expect(subject.public_send(event_object)).to eq(sentry_api_response.dig(*sentry_response)) }
+ end
+ end
+
+ context 'error object created from sentry response' do
+ it_behaves_like 'assigns error tracking event correctly'
+
+ it 'parses the stack trace' do
+ expect(subject.stack_trace_entries).to be_a Array
+ expect(subject.stack_trace_entries).not_to be_empty
+ end
+
+ context 'error without stack trace' do
+ before do
+ sample_response['entries'] = []
+ stub_sentry_request(sentry_request_url, body: sample_response)
+ end
+
+ it_behaves_like 'assigns error tracking event correctly'
+
+ it 'returns an empty array for stack_trace_entries' do
+ expect(subject.stack_trace_entries).to eq []
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/error_tracking/sentry_client/issue_link_spec.rb b/spec/lib/error_tracking/sentry_client/issue_link_spec.rb
new file mode 100644
index 00000000000..f86d328ef89
--- /dev/null
+++ b/spec/lib/error_tracking/sentry_client/issue_link_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ErrorTracking::SentryClient::IssueLink do
+ include SentryClientHelpers
+
+ let_it_be(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
+ let_it_be(:error_tracking_setting) { create(:project_error_tracking_setting, api_url: sentry_url) }
+ let_it_be(:issue) { create(:issue, project: error_tracking_setting.project) }
+
+ let(:client) { error_tracking_setting.sentry_client }
+ let(:sentry_issue_id) { 11111111 }
+
+ describe '#create_issue_link' do
+ let(:sentry_issue_link_url) { "https://sentrytest.gitlab.com/api/0/groups/#{sentry_issue_id}/integrations/#{integration_id}/" }
+ let(:integration_id) { 44444 }
+
+ let(:issue_link_sample_response) { Gitlab::Json.parse(fixture_file('sentry/global_integration_link_sample_response.json')) }
+ let(:sentry_api_response) { issue_link_sample_response }
+ let!(:sentry_api_request) { stub_sentry_request(sentry_issue_link_url, :put, body: sentry_api_response, status: 201) }
+
+ subject { client.create_issue_link(integration_id, sentry_issue_id, issue) }
+
+ it_behaves_like 'calls sentry api'
+
+ it { is_expected.to be_present }
+
+ context 'redirects' do
+ let(:sentry_api_url) { sentry_issue_link_url }
+
+ it_behaves_like 'no Sentry redirects', :put
+ end
+
+ context 'when exception is raised' do
+ let(:sentry_request_url) { sentry_issue_link_url }
+
+ it_behaves_like 'maps Sentry exceptions', :put
+ end
+
+ context 'when integration_id is not provided' do
+ let(:sentry_issue_link_url) { "https://sentrytest.gitlab.com/api/0/issues/#{sentry_issue_id}/plugins/gitlab/link/" }
+ let(:integration_id) { nil }
+
+ let(:issue_link_sample_response) { Gitlab::Json.parse(fixture_file('sentry/plugin_link_sample_response.json')) }
+ let!(:sentry_api_request) { stub_sentry_request(sentry_issue_link_url, :post, body: sentry_api_response) }
+
+ it_behaves_like 'calls sentry api'
+
+ it { is_expected.to be_present }
+
+ context 'redirects' do
+ let(:sentry_api_url) { sentry_issue_link_url }
+
+ it_behaves_like 'no Sentry redirects', :post
+ end
+
+ context 'when exception is raised' do
+ let(:sentry_request_url) { sentry_issue_link_url }
+
+ it_behaves_like 'maps Sentry exceptions', :post
+ end
+ end
+ end
+end
diff --git a/spec/lib/error_tracking/sentry_client/issue_spec.rb b/spec/lib/error_tracking/sentry_client/issue_spec.rb
new file mode 100644
index 00000000000..e54296c58e0
--- /dev/null
+++ b/spec/lib/error_tracking/sentry_client/issue_spec.rb
@@ -0,0 +1,330 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ErrorTracking::SentryClient::Issue do
+ include SentryClientHelpers
+
+ let(:token) { 'test-token' }
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0' }
+ let(:client) { ErrorTracking::SentryClient.new(sentry_url, token) }
+ let(:issue_id) { 11 }
+
+ describe '#list_issues' do
+ shared_examples 'issues have correct return type' do |klass|
+ it "returns objects of type #{klass}" do
+ expect(subject[:issues]).to all( be_a(klass) )
+ end
+ end
+
+ shared_examples 'issues have correct length' do |length|
+ it { expect(subject[:issues].length).to eq(length) }
+ end
+
+ let(:issues_sample_response) do
+ Gitlab::Utils.deep_indifferent_access(
+ Gitlab::Json.parse(fixture_file('sentry/issues_sample_response.json'))
+ )
+ end
+
+ let(:default_httparty_options) do
+ {
+ follow_redirects: false,
+ headers: { 'Content-Type' => 'application/json', 'Authorization' => "Bearer test-token" }
+ }
+ end
+
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
+ let(:issue_status) { 'unresolved' }
+ let(:limit) { 20 }
+ let(:search_term) { '' }
+ let(:cursor) { nil }
+ let(:sort) { 'last_seen' }
+ let(:sentry_api_response) { issues_sample_response }
+ let(:sentry_request_url) { sentry_url + '/issues/?limit=20&query=is:unresolved' }
+ let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response) }
+
+ subject { client.list_issues(issue_status: issue_status, limit: limit, search_term: search_term, sort: sort, cursor: cursor) }
+
+ it_behaves_like 'calls sentry api'
+
+ it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error
+ it_behaves_like 'issues have correct length', 3
+
+ shared_examples 'has correct external_url' do
+ context 'external_url' do
+ it 'is constructed correctly' do
+ expect(subject[:issues][0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11')
+ end
+ end
+ end
+
+ context 'when response has a pagination info' do
+ let(:headers) do
+ {
+ link: '<https://sentrytest.gitlab.com>; rel="previous"; results="true"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="true"; cursor="1572959139000:0:0"'
+ }
+ end
+
+ let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response, headers: headers) }
+
+ it 'parses the pagination' do
+ expect(subject[:pagination]).to eq(
+ 'previous' => { 'cursor' => '1573556671000:0:1' },
+ 'next' => { 'cursor' => '1572959139000:0:0' }
+ )
+ end
+ 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[:issues][0].public_send(error_object)).to eq(sentry_api_response[0].dig(*sentry_response)) }
+ end
+
+ it_behaves_like 'has correct external_url'
+ end
+
+ context 'redirects' do
+ let(:sentry_api_url) { sentry_url + '/issues/?limit=20&query=is:unresolved' }
+
+ it_behaves_like 'no Sentry redirects'
+ end
+
+ context 'requests with sort parameter in sentry api' do
+ let(:sentry_request_url) do
+ 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/' \
+ 'issues/?limit=20&query=is:unresolved&sort=freq'
+ end
+
+ let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response) }
+
+ subject { client.list_issues(issue_status: issue_status, limit: limit, sort: 'frequency') }
+
+ it 'calls the sentry api with sort params' do
+ expect(Gitlab::HTTP).to receive(:get).with(
+ URI("#{sentry_url}/issues/"),
+ default_httparty_options.merge(query: { limit: 20, query: "is:unresolved", sort: "freq" })
+ ).and_call_original
+
+ subject
+
+ expect(sentry_api_request).to have_been_requested
+ end
+ end
+
+ context 'with invalid sort params' do
+ subject { client.list_issues(issue_status: issue_status, limit: limit, sort: 'fish') }
+
+ it 'throws an error' do
+ expect { subject }.to raise_error(ErrorTracking::SentryClient::BadRequestError, 'Invalid value for sort param')
+ end
+ end
+
+ context 'Older sentry versions where keys are not present' do
+ let(:sentry_api_response) do
+ issues_sample_response[0...1].map do |issue|
+ issue[:project].delete(:id)
+ issue
+ end
+ end
+
+ it_behaves_like 'calls sentry api'
+
+ it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error
+ it_behaves_like 'issues have correct length', 1
+
+ it_behaves_like 'has correct external_url'
+ end
+
+ context 'essential keys missing in API response' do
+ let(:sentry_api_response) do
+ issues_sample_response[0...1].map do |issue|
+ issue.except(:id)
+ end
+ end
+
+ it 'raises exception' do
+ expect { subject }.to raise_error(ErrorTracking::SentryClient::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"')
+ end
+ end
+
+ context 'sentry api response too large' do
+ it 'raises exception' do
+ deep_size = double('Gitlab::Utils::DeepSize', valid?: false)
+ allow(Gitlab::Utils::DeepSize).to receive(:new).with(sentry_api_response).and_return(deep_size)
+
+ expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError, 'Sentry API response is too big. Limit is 1 MB.')
+ end
+ end
+
+ it_behaves_like 'maps Sentry exceptions'
+
+ context 'when search term is present' do
+ let(:search_term) { 'NoMethodError' }
+ let(:sentry_request_url) { "#{sentry_url}/issues/?limit=20&query=is:unresolved NoMethodError" }
+
+ it_behaves_like 'calls sentry api'
+
+ it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error
+ it_behaves_like 'issues have correct length', 3
+ end
+
+ context 'when cursor is present' do
+ let(:cursor) { '1572959139000:0:0' }
+ let(:sentry_request_url) { "#{sentry_url}/issues/?limit=20&cursor=#{cursor}&query=is:unresolved" }
+
+ it_behaves_like 'calls sentry api'
+
+ it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error
+ it_behaves_like 'issues have correct length', 3
+ end
+ end
+
+ describe '#issue_details' do
+ let(:issue_sample_response) do
+ Gitlab::Utils.deep_indifferent_access(
+ Gitlab::Json.parse(fixture_file('sentry/issue_sample_response.json'))
+ )
+ end
+
+ let(:sentry_request_url) { "#{sentry_url}/issues/#{issue_id}/" }
+ let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: issue_sample_response) }
+
+ subject { client.issue_details(issue_id: issue_id) }
+
+ 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]
+ :first_release_last_commit | [:firstRelease, :lastCommit]
+ :last_release_last_commit | [:lastRelease, :lastCommit]
+ :first_release_short_version | [:firstRelease, :shortVersion]
+ :last_release_short_version | [:lastRelease, :shortVersion]
+ :first_release_version | [:firstRelease, :version]
+ :last_release_version | [:lastRelease, :version]
+ end
+
+ with_them do
+ it do
+ expect(subject.public_send(error_object)).to eq(issue_sample_response.dig(*sentry_response))
+ end
+ end
+
+ it 'has a correct external URL' do
+ expect(subject.external_url).to eq('https://sentrytest.gitlab.com/api/0/issues/11')
+ end
+
+ it 'issue has a correct external base url' do
+ expect(subject.external_base_url).to eq('https://sentrytest.gitlab.com/api/0')
+ end
+
+ it 'has a correct GitLab issue url' do
+ expect(subject.gitlab_issue).to eq('https://gitlab.com/gitlab-org/gitlab/issues/1')
+ end
+
+ context 'when issue annotations exist' do
+ before do
+ issue_sample_response['annotations'] = [
+ nil,
+ '',
+ "<a href=\"http://github.com/issues/6\">github-issue-6</a>",
+ "<div>annotation</a>",
+ "<a href=\"http://localhost/gitlab-org/gitlab/issues/2\">gitlab-org/gitlab#2</a>"
+ ]
+ stub_sentry_request(sentry_request_url, body: issue_sample_response)
+ end
+
+ it 'has a correct GitLab issue url' do
+ expect(subject.gitlab_issue).to eq('http://localhost/gitlab-org/gitlab/issues/2')
+ end
+ end
+
+ context 'when no GitLab issue is linked' do
+ before do
+ issue_sample_response['pluginIssues'] = []
+ stub_sentry_request(sentry_request_url, body: issue_sample_response)
+ end
+
+ it 'does not find a GitLab issue' do
+ expect(subject.gitlab_issue).to be_nil
+ end
+ end
+
+ it 'has the correct tags' do
+ expect(subject.tags).to eq({ level: issue_sample_response['level'], logger: issue_sample_response['logger'] })
+ end
+ end
+ end
+
+ describe '#update_issue' do
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0' }
+ let(:sentry_request_url) { "#{sentry_url}/issues/#{issue_id}/" }
+
+ before do
+ stub_sentry_request(sentry_request_url, :put)
+ end
+
+ let(:params) do
+ {
+ status: 'resolved'
+ }
+ end
+
+ subject { client.update_issue(issue_id: issue_id, params: params) }
+
+ it_behaves_like 'calls sentry api' do
+ let(:sentry_api_request) { stub_sentry_request(sentry_request_url, :put) }
+ end
+
+ it 'returns a truthy result' do
+ expect(subject).to be_truthy
+ end
+
+ context 'error encountered' do
+ let(:error) { StandardError.new('error') }
+
+ before do
+ allow(client).to receive(:update_issue).and_raise(error)
+ end
+
+ it 'raises the error' do
+ expect { subject }.to raise_error(error)
+ end
+ end
+ end
+end
diff --git a/spec/lib/error_tracking/sentry_client/pagination_parser_spec.rb b/spec/lib/error_tracking/sentry_client/pagination_parser_spec.rb
new file mode 100644
index 00000000000..c4b771d5b93
--- /dev/null
+++ b/spec/lib/error_tracking/sentry_client/pagination_parser_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe ErrorTracking::SentryClient::PaginationParser do
+ describe '.parse' do
+ subject { described_class.parse(headers) }
+
+ context 'when headers do not have "link" param' do
+ let(:headers) { {} }
+
+ it 'returns empty hash' do
+ is_expected.to eq({})
+ end
+ end
+
+ context 'when headers.link has previous and next pages' do
+ let(:headers) do
+ {
+ 'link' => '<https://sentrytest.gitlab.com>; rel="previous"; results="true"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="true"; cursor="1572959139000:0:0"'
+ }
+ end
+
+ it 'returns info about both pages' do
+ is_expected.to eq(
+ 'previous' => { 'cursor' => '1573556671000:0:1' },
+ 'next' => { 'cursor' => '1572959139000:0:0' }
+ )
+ end
+ end
+
+ context 'when headers.link has only next page' do
+ let(:headers) do
+ {
+ 'link' => '<https://sentrytest.gitlab.com>; rel="previous"; results="false"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="true"; cursor="1572959139000:0:0"'
+ }
+ end
+
+ it 'returns only info about the next page' do
+ is_expected.to eq(
+ 'next' => { 'cursor' => '1572959139000:0:0' }
+ )
+ end
+ end
+
+ context 'when headers.link has only previous page' do
+ let(:headers) do
+ {
+ 'link' => '<https://sentrytest.gitlab.com>; rel="previous"; results="true"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="false"; cursor="1572959139000:0:0"'
+ }
+ end
+
+ it 'returns only info about the previous page' do
+ is_expected.to eq(
+ 'previous' => { 'cursor' => '1573556671000:0:1' }
+ )
+ end
+ end
+ end
+end
diff --git a/spec/lib/error_tracking/sentry_client/projects_spec.rb b/spec/lib/error_tracking/sentry_client/projects_spec.rb
new file mode 100644
index 00000000000..247f9c1c085
--- /dev/null
+++ b/spec/lib/error_tracking/sentry_client/projects_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ErrorTracking::SentryClient::Projects do
+ include SentryClientHelpers
+
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
+ let(:token) { 'test-token' }
+ let(:client) { ErrorTracking::SentryClient.new(sentry_url, token) }
+ let(:projects_sample_response) do
+ Gitlab::Utils.deep_indifferent_access(
+ Gitlab::Json.parse(fixture_file('sentry/list_projects_sample_response.json'))
+ )
+ end
+
+ shared_examples 'has correct return type' do |klass|
+ it "returns objects of type #{klass}" do
+ expect(subject).to all( be_a(klass) )
+ end
+ end
+
+ shared_examples 'has correct length' do |length|
+ it { expect(subject.length).to eq(length) }
+ end
+
+ describe '#projects' do
+ let(:sentry_list_projects_url) { 'https://sentrytest.gitlab.com/api/0/projects/' }
+ let(:sentry_api_response) { projects_sample_response }
+ let!(:sentry_api_request) { stub_sentry_request(sentry_list_projects_url, body: sentry_api_response) }
+
+ subject { client.projects }
+
+ it_behaves_like 'calls sentry api'
+
+ it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project
+ it_behaves_like 'has correct length', 2
+
+ context 'essential keys missing in API response' do
+ let(:sentry_api_response) do
+ projects_sample_response[0...1].map do |project|
+ project.except(:slug)
+ end
+ end
+
+ it 'raises exception' do
+ expect { subject }.to raise_error(ErrorTracking::SentryClient::MissingKeysError, 'Sentry API response is missing keys. key not found: "slug"')
+ end
+ end
+
+ context 'optional keys missing in sentry response' do
+ let(:sentry_api_response) do
+ projects_sample_response[0...1].map do |project|
+ project[:organization].delete(:id)
+ project.delete(:id)
+ project.except(:status)
+ end
+ end
+
+ it_behaves_like 'calls sentry api'
+
+ it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project
+ it_behaves_like 'has correct length', 1
+ end
+
+ context 'error object created from sentry response' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:sentry_project_object, :sentry_response) do
+ :id | :id
+ :name | :name
+ :status | :status
+ :slug | :slug
+ :organization_name | [:organization, :name]
+ :organization_id | [:organization, :id]
+ :organization_slug | [:organization, :slug]
+ end
+
+ with_them do
+ it do
+ expect(subject[0].public_send(sentry_project_object)).to(
+ eq(sentry_api_response[0].dig(*sentry_response))
+ )
+ end
+ end
+ end
+
+ context 'redirects' do
+ let(:sentry_api_url) { sentry_list_projects_url }
+
+ it_behaves_like 'no Sentry redirects'
+ end
+
+ context 'when exception is raised' do
+ let(:sentry_request_url) { sentry_list_projects_url }
+
+ it_behaves_like 'maps Sentry exceptions'
+ end
+ end
+end
diff --git a/spec/lib/error_tracking/sentry_client/repo_spec.rb b/spec/lib/error_tracking/sentry_client/repo_spec.rb
new file mode 100644
index 00000000000..9a1c7a69c3d
--- /dev/null
+++ b/spec/lib/error_tracking/sentry_client/repo_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ErrorTracking::SentryClient::Repo do
+ include SentryClientHelpers
+
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
+ let(:token) { 'test-token' }
+ let(:client) { ErrorTracking::SentryClient.new(sentry_url, token) }
+ let(:repos_sample_response) { Gitlab::Json.parse(fixture_file('sentry/repos_sample_response.json')) }
+
+ describe '#repos' do
+ let(:organization_slug) { 'gitlab' }
+ let(:sentry_repos_url) { "https://sentrytest.gitlab.com/api/0/organizations/#{organization_slug}/repos/" }
+ let(:sentry_api_response) { repos_sample_response }
+ let!(:sentry_api_request) { stub_sentry_request(sentry_repos_url, body: sentry_api_response) }
+
+ subject { client.repos(organization_slug) }
+
+ it_behaves_like 'calls sentry api'
+
+ it { is_expected.to all( be_a(Gitlab::ErrorTracking::Repo)) }
+
+ it { expect(subject.length).to eq(1) }
+
+ context 'redirects' do
+ let(:sentry_api_url) { sentry_repos_url }
+
+ it_behaves_like 'no Sentry redirects'
+ end
+
+ context 'when exception is raised' do
+ let(:sentry_request_url) { sentry_repos_url }
+
+ it_behaves_like 'maps Sentry exceptions'
+ end
+ end
+end