437 lines
12 KiB
Ruby
437 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'spec_helper'
|
|
|
|
RSpec.describe Gitlab::ErrorTracking::ErrorRepository::OpenApiStrategy do
|
|
include AfterNextHelpers
|
|
|
|
let(:project) { build_stubbed(:project) }
|
|
let(:api_exception) { ErrorTrackingOpenAPI::ApiError.new(code: 500, response_body: 'b' * 101) }
|
|
|
|
subject(:repository) { Gitlab::ErrorTracking::ErrorRepository.build(project) }
|
|
|
|
before do
|
|
# Disabled in spec_helper by default thus we need to enable it here.
|
|
stub_feature_flags(use_click_house_database_for_error_tracking: true)
|
|
end
|
|
|
|
shared_examples 'exception logging' do
|
|
it 'logs error' do
|
|
expect(Gitlab::AppLogger).to receive(:error).with({
|
|
'open_api.http_code' => api_exception.code,
|
|
'open_api.response_body' => api_exception.response_body.truncate(100)
|
|
})
|
|
|
|
subject
|
|
end
|
|
end
|
|
|
|
shared_examples 'no logging' do
|
|
it 'does not log anything' do
|
|
expect(Gitlab::AppLogger).not_to receive(:debug)
|
|
expect(Gitlab::AppLogger).not_to receive(:info)
|
|
expect(Gitlab::AppLogger).not_to receive(:error)
|
|
end
|
|
end
|
|
|
|
describe '#report_error' do
|
|
let(:params) do
|
|
{
|
|
name: 'anything',
|
|
description: 'anything',
|
|
actor: 'anything',
|
|
platform: 'anything',
|
|
environment: 'anything',
|
|
level: 'anything',
|
|
occurred_at: Time.zone.now,
|
|
payload: {}
|
|
}
|
|
end
|
|
|
|
subject { repository.report_error(**params) }
|
|
|
|
it 'is not implemented' do
|
|
expect { subject }.to raise_error(NotImplementedError, 'Use ingestion endpoint')
|
|
end
|
|
end
|
|
|
|
describe '#find_error' do
|
|
let(:error) { build(:error_tracking_open_api_error, project_id: project.id) }
|
|
|
|
subject { repository.find_error(error.fingerprint) }
|
|
|
|
before do
|
|
allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api|
|
|
allow(open_api).to receive(:get_error).with(project.id, error.fingerprint)
|
|
.and_return(error)
|
|
|
|
allow(open_api).to receive(:list_events)
|
|
.with(project.id, error.fingerprint, { sort: 'occurred_at_asc', limit: 1 })
|
|
.and_return(list_events_asc)
|
|
|
|
allow(open_api).to receive(:list_events)
|
|
.with(project.id, error.fingerprint, { sort: 'occurred_at_desc', limit: 1 })
|
|
.and_return(list_events_desc)
|
|
end
|
|
end
|
|
|
|
context 'when request succeeds' do
|
|
context 'without events returned' do
|
|
let(:list_events_asc) { [] }
|
|
let(:list_events_desc) { [] }
|
|
|
|
include_examples 'no logging'
|
|
|
|
it 'returns detailed error' do
|
|
is_expected.to have_attributes(
|
|
id: error.fingerprint.to_s,
|
|
title: error.name,
|
|
message: error.description,
|
|
culprit: error.actor,
|
|
first_seen: error.first_seen_at.to_s,
|
|
last_seen: error.last_seen_at.to_s,
|
|
count: error.event_count,
|
|
user_count: error.approximated_user_count,
|
|
project_id: error.project_id,
|
|
status: error.status,
|
|
tags: { level: nil, logger: nil },
|
|
external_url: "http://localhost/#{project.full_path}/-/error_tracking/#{error.fingerprint}/details",
|
|
external_base_url: "http://localhost/#{project.full_path}",
|
|
integrated: true
|
|
)
|
|
end
|
|
|
|
it 'returns no first and last release version' do
|
|
is_expected.to have_attributes(
|
|
first_release_version: nil,
|
|
last_release_version: nil
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'with events returned' do
|
|
let(:first_event) { build(:error_tracking_open_api_error_event, project_id: project.id) }
|
|
let(:first_release) { parse_json(first_event.payload).fetch('release') }
|
|
let(:last_event) { build(:error_tracking_open_api_error_event, :golang, project_id: project.id) }
|
|
let(:last_release) { parse_json(last_event.payload).fetch('release') }
|
|
|
|
let(:list_events_asc) { [first_event] }
|
|
let(:list_events_desc) { [last_event] }
|
|
|
|
include_examples 'no logging'
|
|
|
|
it 'returns first and last release version' do
|
|
expect(first_release).to be_present
|
|
expect(last_release).to be_present
|
|
|
|
is_expected.to have_attributes(
|
|
first_release_version: first_release,
|
|
last_release_version: last_release
|
|
)
|
|
end
|
|
|
|
def parse_json(content)
|
|
Gitlab::Json.parse(content)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when request fails' do
|
|
before do
|
|
allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:get_error)
|
|
.with(project.id, error.fingerprint)
|
|
.and_raise(api_exception)
|
|
end
|
|
|
|
include_examples 'exception logging'
|
|
|
|
it { is_expected.to be_nil }
|
|
end
|
|
end
|
|
|
|
describe '#list_errors' do
|
|
let(:errors) { [] }
|
|
let(:response_with_info) { [errors, 200, headers] }
|
|
let(:result_errors) { result.first }
|
|
let(:result_pagination) { result.last }
|
|
|
|
let(:headers) do
|
|
{
|
|
'link' => [
|
|
'<url?cursor=next_cursor¶m>; rel="next"',
|
|
'<url?cursor=prev_cursor¶m>; rel="prev"'
|
|
].join(', ')
|
|
}
|
|
end
|
|
|
|
subject(:result) { repository.list_errors(**params) }
|
|
|
|
before do
|
|
allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info)
|
|
.with(project.id, kind_of(Hash))
|
|
.and_return(response_with_info)
|
|
end
|
|
|
|
context 'with errors' do
|
|
let(:limit) { 3 }
|
|
let(:params) { { limit: limit } }
|
|
let(:errors_size) { limit }
|
|
let(:errors) { build_list(:error_tracking_open_api_error, errors_size, project_id: project.id) }
|
|
|
|
include_examples 'no logging'
|
|
|
|
it 'maps errors to models' do
|
|
# All errors are identical
|
|
error = errors.first
|
|
|
|
expect(result_errors).to all(
|
|
have_attributes(
|
|
id: error.fingerprint.to_s,
|
|
title: error.name,
|
|
message: error.description,
|
|
culprit: error.actor,
|
|
first_seen: error.first_seen_at,
|
|
last_seen: error.last_seen_at,
|
|
status: error.status,
|
|
count: error.event_count,
|
|
user_count: error.approximated_user_count
|
|
))
|
|
end
|
|
|
|
context 'when n errors are returned' do
|
|
let(:errors_size) { limit }
|
|
|
|
include_examples 'no logging'
|
|
|
|
it 'returns the amount of errors' do
|
|
expect(result_errors.size).to eq(3)
|
|
end
|
|
|
|
it 'cursor links are preserved' do
|
|
expect(result_pagination).to have_attributes(
|
|
prev: 'prev_cursor',
|
|
next: 'next_cursor'
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'when less errors than requested are returned' do
|
|
let(:errors_size) { limit - 1 }
|
|
|
|
include_examples 'no logging'
|
|
|
|
it 'returns the amount of errors' do
|
|
expect(result_errors.size).to eq(2)
|
|
end
|
|
|
|
it 'cursor link for next is removed' do
|
|
expect(result_pagination).to have_attributes(
|
|
prev: 'prev_cursor',
|
|
next: nil
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'with params' do
|
|
let(:params) do
|
|
{
|
|
filters: { status: 'resolved', something: 'different' },
|
|
query: 'search term',
|
|
sort: 'first_seen',
|
|
limit: 2,
|
|
cursor: 'abc'
|
|
}
|
|
end
|
|
|
|
include_examples 'no logging'
|
|
|
|
it 'passes provided params to client' do
|
|
passed_params = {
|
|
sort: 'first_seen_desc',
|
|
status: 'resolved',
|
|
query: 'search term',
|
|
cursor: 'abc',
|
|
limit: 2
|
|
}
|
|
|
|
expect_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info)
|
|
.with(project.id, passed_params)
|
|
.and_return(response_with_info)
|
|
|
|
subject
|
|
end
|
|
end
|
|
|
|
context 'without explicit params' do
|
|
let(:params) { {} }
|
|
|
|
include_examples 'no logging'
|
|
|
|
it 'passes default params to client' do
|
|
passed_params = {
|
|
sort: 'last_seen_desc',
|
|
limit: 20,
|
|
cursor: {}
|
|
}
|
|
|
|
expect_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info)
|
|
.with(project.id, passed_params)
|
|
.and_return(response_with_info)
|
|
|
|
subject
|
|
end
|
|
end
|
|
|
|
context 'when request fails' do
|
|
let(:params) { {} }
|
|
|
|
before do
|
|
allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info)
|
|
.with(project.id, kind_of(Hash))
|
|
.and_raise(api_exception)
|
|
end
|
|
|
|
include_examples 'exception logging'
|
|
|
|
specify do
|
|
expect(result_errors).to eq([])
|
|
expect(result_pagination).to have_attributes(
|
|
next: nil,
|
|
prev: nil
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#last_event_for' do
|
|
let(:params) { { sort: 'occurred_at_desc', limit: 1 } }
|
|
let(:event) { build(:error_tracking_open_api_error_event, project_id: project.id) }
|
|
let(:error) { build(:error_tracking_open_api_error, project_id: project.id, fingerprint: event.fingerprint) }
|
|
|
|
subject { repository.last_event_for(error.fingerprint) }
|
|
|
|
context 'when both event and error is returned' do
|
|
before do
|
|
allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api|
|
|
allow(open_api).to receive(:list_events).with(project.id, error.fingerprint, params)
|
|
.and_return([event])
|
|
|
|
allow(open_api).to receive(:get_error).with(project.id, error.fingerprint)
|
|
.and_return(error)
|
|
end
|
|
end
|
|
|
|
include_examples 'no logging'
|
|
|
|
it 'returns mapped error event' do
|
|
is_expected.to have_attributes(
|
|
issue_id: event.fingerprint.to_s,
|
|
date_received: error.last_seen_at,
|
|
stack_trace_entries: kind_of(Array)
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'when event is not returned' do
|
|
before do
|
|
allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_events)
|
|
.with(project.id, event.fingerprint, params)
|
|
.and_return([])
|
|
end
|
|
|
|
include_examples 'no logging'
|
|
|
|
it { is_expected.to be_nil }
|
|
end
|
|
|
|
context 'when list_events request fails' do
|
|
before do
|
|
allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_events)
|
|
.with(project.id, event.fingerprint, params)
|
|
.and_raise(api_exception)
|
|
end
|
|
|
|
include_examples 'exception logging'
|
|
|
|
it { is_expected.to be_nil }
|
|
end
|
|
|
|
context 'when error is not returned' do
|
|
before do
|
|
allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api|
|
|
allow(open_api).to receive(:list_events).with(project.id, error.fingerprint, params)
|
|
.and_return([event])
|
|
|
|
allow(open_api).to receive(:get_error).with(project.id, error.fingerprint)
|
|
.and_return(nil)
|
|
end
|
|
end
|
|
|
|
include_examples 'no logging'
|
|
|
|
it { is_expected.to be_nil }
|
|
end
|
|
|
|
context 'when get_error request fails' do
|
|
before do
|
|
allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api|
|
|
allow(open_api).to receive(:list_events).with(project.id, error.fingerprint, params)
|
|
.and_return([event])
|
|
|
|
allow(open_api).to receive(:get_error).with(project.id, error.fingerprint)
|
|
.and_raise(api_exception)
|
|
end
|
|
end
|
|
|
|
include_examples 'exception logging'
|
|
|
|
it { is_expected.to be_nil }
|
|
end
|
|
end
|
|
|
|
describe '#update_error' do
|
|
let(:error) { build(:error_tracking_open_api_error, project_id: project.id) }
|
|
let(:update_params) { { status: 'resolved' } }
|
|
let(:passed_body) { ErrorTrackingOpenAPI::ErrorUpdatePayload.new(update_params) }
|
|
|
|
subject { repository.update_error(error.fingerprint, **update_params) }
|
|
|
|
before do
|
|
allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:update_error)
|
|
.with(project.id, error.fingerprint, passed_body)
|
|
.and_return(:anything)
|
|
end
|
|
|
|
context 'when update succeeds' do
|
|
include_examples 'no logging'
|
|
|
|
it { is_expected.to eq(true) }
|
|
end
|
|
|
|
context 'when update fails' do
|
|
before do
|
|
allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:update_error)
|
|
.with(project.id, error.fingerprint, passed_body)
|
|
.and_raise(api_exception)
|
|
end
|
|
|
|
include_examples 'exception logging'
|
|
|
|
it { is_expected.to eq(false) }
|
|
end
|
|
end
|
|
|
|
describe '#dsn_url' do
|
|
let(:public_key) { 'abc' }
|
|
let(:config) { ErrorTrackingOpenAPI::Configuration.default }
|
|
|
|
subject { repository.dsn_url(public_key) }
|
|
|
|
it do
|
|
is_expected
|
|
.to eq("#{config.scheme}://#{public_key}@#{config.host}/errortracking/api/v1/projects/#{project.id}")
|
|
end
|
|
end
|
|
end
|