diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-11 18:08:44 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-11 18:08:44 +0300 |
commit | bcc77054ee9aefd1e332e04a4189390fd5a3112e (patch) | |
tree | e6e1908c310e4733038794e932196cae0d66ba9a /spec | |
parent | 05b5c609cb8c260b10c2eb1b92b711dc82d32c3f (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
9 files changed, 586 insertions, 62 deletions
diff --git a/spec/factories/incident_management/project_incident_management_settings.rb b/spec/factories/incident_management/project_incident_management_settings.rb new file mode 100644 index 00000000000..5b6a71d87d5 --- /dev/null +++ b/spec/factories/incident_management/project_incident_management_settings.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :project_incident_management_setting, class: 'IncidentManagement::ProjectIncidentManagementSetting' do + project + create_issue { false } + issue_template_key { nil } + send_email { false } + end +end diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb index a1b53718577..0d24b02a64c 100644 --- a/spec/features/issues/issue_detail_spec.rb +++ b/spec/features/issues/issue_detail_spec.rb @@ -23,16 +23,18 @@ describe 'Issue Detail', :js do context 'when issue description has xss snippet' do before do issue.update!(description: '![xss" onload=alert(1);//](a)') + sign_in(user) visit project_issue_path(project, issue) - wait_for_requests end it 'encodes the description to prevent xss issues' do page.within('.issuable-details .detail-page-description') do + image = find('img.js-lazy-loaded') + expect(page).to have_selector('img', count: 1) - expect(find('img')['onerror']).to be_nil - expect(find('img')['src']).to end_with('/a') + expect(image['onerror']).to be_nil + expect(image['src']).to end_with('/a') end end end diff --git a/spec/features/projects/files/template_type_dropdown_spec.rb b/spec/features/projects/files/template_type_dropdown_spec.rb index ba52a7e7deb..03b4b9b4517 100644 --- a/spec/features/projects/files/template_type_dropdown_spec.rb +++ b/spec/features/projects/files/template_type_dropdown_spec.rb @@ -75,6 +75,11 @@ describe 'Projects > Files > Template type dropdown selector', :js do check_type_selector_toggle_text('.gitignore') end + it 'sets the toggle text when selecting the template type' do + select_template_type('.gitignore') + check_type_selector_toggle_text('.gitignore') + end + it 'selects every template type correctly' do try_selecting_all_types end diff --git a/spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb index f617e8b3ce7..c193ab2b50f 100644 --- a/spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb +++ b/spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb @@ -103,7 +103,75 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do end end - context 'when multiple orders are defined' do + shared_examples 'nodes are in ascending order' do + context 'when no cursor is passed' do + let(:arguments) { {} } + + it 'returns projects in ascending order' do + expect(subject.sliced_nodes).to eq(ascending_nodes) + end + end + + context 'when before cursor value is not NULL' do + let(:arguments) { { before: encoded_cursor(ascending_nodes[2]) } } + + it 'returns all projects before the cursor' do + expect(subject.sliced_nodes).to eq(ascending_nodes.first(2)) + end + end + + context 'when after cursor value is not NULL' do + let(:arguments) { { after: encoded_cursor(ascending_nodes[1]) } } + + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(ascending_nodes.last(3)) + end + end + + context 'when before and after cursor' do + let(:arguments) { { before: encoded_cursor(ascending_nodes.last), after: encoded_cursor(ascending_nodes.first) } } + + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(ascending_nodes[1..3]) + end + end + end + + shared_examples 'nodes are in descending order' do + context 'when no cursor is passed' do + let(:arguments) { {} } + + it 'only returns projects in descending order' do + expect(subject.sliced_nodes).to eq(descending_nodes) + end + end + + context 'when before cursor value is not NULL' do + let(:arguments) { { before: encoded_cursor(descending_nodes[2]) } } + + it 'returns all projects before the cursor' do + expect(subject.sliced_nodes).to eq(descending_nodes.first(2)) + end + end + + context 'when after cursor value is not NULL' do + let(:arguments) { { after: encoded_cursor(descending_nodes[1]) } } + + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(descending_nodes.last(3)) + end + end + + context 'when before and after cursor' do + let(:arguments) { { before: encoded_cursor(descending_nodes.last), after: encoded_cursor(descending_nodes.first) } } + + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(descending_nodes[1..3]) + end + end + end + + context 'when multiple orders with nil values are defined' do let!(:project1) { create(:project, last_repository_check_at: 10.days.ago) } # Asc: project5 Desc: project3 let!(:project2) { create(:project, last_repository_check_at: nil) } # Asc: project1 Desc: project1 let!(:project3) { create(:project, last_repository_check_at: 5.days.ago) } # Asc: project3 Desc: project5 @@ -114,14 +182,9 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do let(:nodes) do Project.order(Arel.sql('projects.last_repository_check_at IS NULL')).order(last_repository_check_at: :asc).order(id: :asc) end + let(:ascending_nodes) { [project5, project1, project3, project2, project4] } - context 'when no cursor is passed' do - let(:arguments) { {} } - - it 'returns projects in ascending order' do - expect(subject.sliced_nodes).to eq([project5, project1, project3, project2, project4]) - end - end + it_behaves_like 'nodes are in ascending order' context 'when before cursor value is NULL' do let(:arguments) { { before: encoded_cursor(project4) } } @@ -131,14 +194,6 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do end end - context 'when before cursor value is not NULL' do - let(:arguments) { { before: encoded_cursor(project3) } } - - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq([project5, project1]) - end - end - context 'when after cursor value is NULL' do let(:arguments) { { after: encoded_cursor(project2) } } @@ -146,36 +201,15 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do expect(subject.sliced_nodes).to eq([project4]) end end - - context 'when after cursor value is not NULL' do - let(:arguments) { { after: encoded_cursor(project1) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq([project3, project2, project4]) - end - end - - context 'when before and after cursor' do - let(:arguments) { { before: encoded_cursor(project4), after: encoded_cursor(project5) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq([project1, project3, project2]) - end - end end context 'when descending' do let(:nodes) do Project.order(Arel.sql('projects.last_repository_check_at IS NULL')).order(last_repository_check_at: :desc).order(id: :asc) end + let(:descending_nodes) { [project3, project1, project5, project2, project4] } - context 'when no cursor is passed' do - let(:arguments) { {} } - - it 'only returns projects in descending order' do - expect(subject.sliced_nodes).to eq([project3, project1, project5, project2, project4]) - end - end + it_behaves_like 'nodes are in descending order' context 'when before cursor value is NULL' do let(:arguments) { { before: encoded_cursor(project4) } } @@ -185,14 +219,6 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do end end - context 'when before cursor value is not NULL' do - let(:arguments) { { before: encoded_cursor(project5) } } - - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq([project3, project1]) - end - end - context 'when after cursor value is NULL' do let(:arguments) { { after: encoded_cursor(project2) } } @@ -200,22 +226,32 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do expect(subject.sliced_nodes).to eq([project4]) end end + end + end - context 'when after cursor value is not NULL' do - let(:arguments) { { after: encoded_cursor(project1) } } + context 'when ordering uses LOWER' do + let!(:project1) { create(:project, name: 'A') } # Asc: project1 Desc: project4 + let!(:project2) { create(:project, name: 'c') } # Asc: project5 Desc: project2 + let!(:project3) { create(:project, name: 'b') } # Asc: project3 Desc: project3 + let!(:project4) { create(:project, name: 'd') } # Asc: project2 Desc: project5 + let!(:project5) { create(:project, name: 'a') } # Asc: project4 Desc: project1 - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq([project5, project2, project4]) - end + context 'when ascending' do + let(:nodes) do + Project.order(Arel::Table.new(:projects)['name'].lower.asc).order(id: :asc) end + let(:ascending_nodes) { [project1, project5, project3, project2, project4] } - context 'when before and after cursor' do - let(:arguments) { { before: encoded_cursor(project4), after: encoded_cursor(project3) } } + it_behaves_like 'nodes are in ascending order' + end - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq([project1, project5, project2]) - end + context 'when descending' do + let(:nodes) do + Project.order(Arel::Table.new(:projects)['name'].lower.desc).order(id: :desc) end + let(:descending_nodes) { [project4, project2, project3, project5, project1] } + + it_behaves_like 'nodes are in descending order' end end diff --git a/spec/models/container_expiration_policy_spec.rb b/spec/models/container_expiration_policy_spec.rb index 1bce4c3b20a..c22362ed5d4 100644 --- a/spec/models/container_expiration_policy_spec.rb +++ b/spec/models/container_expiration_policy_spec.rb @@ -49,9 +49,9 @@ RSpec.describe ContainerExpirationPolicy, type: :model do it 'preloads the associations' do subject - query = ActiveRecord::QueryRecorder.new { subject.each(&:project) } + query = ActiveRecord::QueryRecorder.new { subject.map(&:project).map(&:full_path) } - expect(query.count).to eq(2) + expect(query.count).to eq(3) end end diff --git a/spec/models/incident_management/project_incident_management_setting_spec.rb b/spec/models/incident_management/project_incident_management_setting_spec.rb new file mode 100644 index 00000000000..ac3f97e2d89 --- /dev/null +++ b/spec/models/incident_management/project_incident_management_setting_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe IncidentManagement::ProjectIncidentManagementSetting do + let_it_be(:project) { create(:project, :repository, create_templates: :issue) } + + describe 'Associations' do + it { is_expected.to belong_to(:project) } + end + + describe 'Validations' do + describe 'validate issue_template_exists' do + subject { build(:project_incident_management_setting, project: project) } + + context 'with create_issue enabled' do + before do + subject.create_issue = true + end + + context 'with valid issue_template_key' do + before do + subject.issue_template_key = 'bug' + end + + it { is_expected.to be_valid } + end + + context 'with empty issue_template_key' do + before do + subject.issue_template_key = '' + end + + it { is_expected.to be_valid } + end + + context 'with nil issue_template_key' do + before do + subject.issue_template_key = nil + end + + it { is_expected.to be_valid } + end + + context 'with invalid issue_template_key' do + before do + subject.issue_template_key = 'unknown' + end + + it { is_expected.to be_invalid } + + it 'returns error' do + subject.valid? + + expect(subject.errors[:issue_template_key]).to eq(['not found']) + end + end + end + + context 'with create_issue disabled' do + before do + subject.create_issue = false + end + + context 'with unknown issue_template_key' do + before do + subject.issue_template_key = 'unknown' + end + + it { is_expected.to be_valid } + end + end + end + end + + describe '#issue_template_content' do + subject { build(:project_incident_management_setting, project: project) } + + shared_examples 'no content' do + it 'returns no content' do + expect(subject.issue_template_content).to be_nil + end + end + + context 'with valid issue_template_key' do + before do + subject.issue_template_key = 'bug' + end + + it 'returns issue content' do + expect(subject.issue_template_content).to eq('something valid') + end + end + + context 'with unknown issue_template_key' do + before do + subject.issue_template_key = 'unknown' + end + + it_behaves_like 'no content' + end + + context 'without issue_template_key' do + before do + subject.issue_template_key = nil + end + + it_behaves_like 'no content' + end + end +end diff --git a/spec/services/incident_management/create_issue_service_spec.rb b/spec/services/incident_management/create_issue_service_spec.rb new file mode 100644 index 00000000000..e720aafb897 --- /dev/null +++ b/spec/services/incident_management/create_issue_service_spec.rb @@ -0,0 +1,311 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe IncidentManagement::CreateIssueService do + let(:project) { create(:project, :repository, :private) } + let(:user) { User.alert_bot } + let(:service) { described_class.new(project, alert_payload) } + let(:alert_starts_at) { Time.now } + let(:alert_title) { 'TITLE' } + let(:alert_annotations) { { title: alert_title } } + + let(:alert_payload) do + build_alert_payload( + annotations: alert_annotations, + starts_at: alert_starts_at + ) + end + + let(:alert_presenter) do + Gitlab::Alerting::Alert.new(project: project, payload: alert_payload).present + end + + let!(:setting) do + create(:project_incident_management_setting, project: project) + end + + subject { service.execute } + + context 'when create_issue enabled' do + let(:issue) { subject[:issue] } + let(:summary_separator) { "\n---\n\n" } + + before do + setting.update!(create_issue: true) + end + + context 'without issue_template_content' do + it 'creates an issue with alert summary only' do + expect(subject).to include(status: :success) + + expect(issue.author).to eq(user) + expect(issue.title).to eq(alert_title) + expect(issue.description).to include(alert_presenter.issue_summary_markdown.strip) + expect(separator_count(issue.description)).to eq 0 + end + end + + context 'with erroneous issue service' do + let(:invalid_issue) do + build(:issue, project: project, title: nil).tap(&:valid?) + end + + let(:issue_error) { invalid_issue.errors.full_messages.to_sentence } + + it 'returns and logs the issue error' do + expect_next_instance_of(Issues::CreateService) do |issue_service| + expect(issue_service).to receive(:execute).and_return(invalid_issue) + end + + expect(service) + .to receive(:log_error) + .with(error_message(issue_error)) + + expect(subject).to include(status: :error, message: issue_error) + end + end + + shared_examples 'GFM template' do + context 'plain content' do + let(:template_content) { 'some content' } + + it 'creates an issue appending issue template' do + expect(subject).to include(status: :success) + + expect(issue.description).to include(alert_presenter.issue_summary_markdown) + expect(separator_count(issue.description)).to eq 1 + expect(issue.description).to include(template_content) + end + end + + context 'quick actions' do + let(:user) { create(:user) } + let(:plain_text) { 'some content' } + + let(:template_content) do + <<~CONTENT + #{plain_text} + /due tomorrow + /assign @#{user.username} + CONTENT + end + + before do + project.add_maintainer(user) + end + + it 'creates an issue interpreting quick actions' do + expect(subject).to include(status: :success) + + expect(issue.description).to include(plain_text) + expect(issue.due_date).to be_present + expect(issue.assignees).to eq([user]) + end + end + end + + context 'with gitlab_incident_markdown' do + let(:alert_annotations) do + { title: alert_title, gitlab_incident_markdown: template_content } + end + + it_behaves_like 'GFM template' + end + + context 'with issue_template_content' do + before do + create_issue_template('bug', template_content) + setting.update!(issue_template_key: 'bug') + end + + it_behaves_like 'GFM template' + + context 'and gitlab_incident_markdown' do + let(:template_content) { 'plain text'} + let(:alt_template) { 'alternate text' } + let(:alert_annotations) do + { title: alert_title, gitlab_incident_markdown: alt_template } + end + + it 'includes both templates' do + expect(subject).to include(status: :success) + + expect(issue.description).to include(alert_presenter.issue_summary_markdown) + expect(issue.description).to include(template_content) + expect(issue.description).to include(alt_template) + expect(separator_count(issue.description)).to eq 2 + end + end + + private + + def create_issue_template(name, content) + project.repository.create_file( + project.creator, + ".gitlab/issue_templates/#{name}.md", + content, + message: 'message', + branch_name: 'master' + ) + end + end + + context 'with gitlab alert' do + let(:gitlab_alert) { create(:prometheus_alert, project: project) } + + before do + alert_payload['labels'] = { + 'gitlab_alert_id' => gitlab_alert.prometheus_metric_id.to_s + } + end + + it 'creates an issue' do + query_title = "#{gitlab_alert.title} #{gitlab_alert.computed_operator} #{gitlab_alert.threshold}" + + expect(subject).to include(status: :success) + + expect(issue.author).to eq(user) + expect(issue.title).to eq(alert_presenter.full_title) + expect(issue.title).to include(gitlab_alert.environment.name) + expect(issue.title).to include(query_title) + expect(issue.title).to include('for 5 minutes') + expect(issue.description).to include(alert_presenter.issue_summary_markdown.strip) + expect(separator_count(issue.description)).to eq 0 + end + end + + describe 'with invalid alert payload' do + shared_examples 'invalid alert' do + it 'does not create an issue' do + expect(service) + .to receive(:log_error) + .with(error_message('invalid alert')) + + expect(subject).to eq(status: :error, message: 'invalid alert') + end + end + + context 'without title' do + let(:alert_annotations) { {} } + + it_behaves_like 'invalid alert' + end + + context 'without startsAt' do + let(:alert_starts_at) { nil } + + it_behaves_like 'invalid alert' + end + end + + describe "label `incident`" do + let(:title) { 'incident' } + let(:color) { '#CC0033' } + let(:description) do + <<~DESCRIPTION.chomp + Denotes a disruption to IT services and \ + the associated issues require immediate attention + DESCRIPTION + end + + shared_examples 'existing label' do + it 'adds the existing label' do + expect { subject }.not_to change(Label, :count) + + expect(issue.labels).to eq([label]) + end + end + + shared_examples 'new label' do + it 'adds newly created label' do + expect { subject }.to change(Label, :count).by(1) + + label = project.reload.labels.last + expect(issue.labels).to eq([label]) + expect(label.title).to eq(title) + expect(label.color).to eq(color) + expect(label.description).to eq(description) + end + end + + context 'with predefined project label' do + it_behaves_like 'existing label' do + let!(:label) { create(:label, project: project, title: title) } + end + end + + context 'with predefined group label' do + let(:project) { create(:project, group: group) } + let(:group) { create(:group) } + + it_behaves_like 'existing label' do + let!(:label) { create(:group_label, group: group, title: title) } + end + end + + context 'without label' do + it_behaves_like 'new label' + end + + context 'with duplicate labels', issue: 'https://gitlab.com/gitlab-org/gitlab-foss/issues/65042' do + before do + # Replicate race condition to create duplicates + build(:label, project: project, title: title).save!(validate: false) + build(:label, project: project, title: title).save!(validate: false) + end + + it 'create an issue without labels' do + # Verify we have duplicates + expect(project.labels.size).to eq(2) + expect(project.labels.map(&:title)).to all(eq(title)) + + message = <<~MESSAGE.chomp + Cannot create incident issue with labels ["#{title}"] for \ + "#{project.full_name}": Labels is invalid. + Retrying without labels. + MESSAGE + + expect(service) + .to receive(:log_info) + .with(message) + + expect(subject).to include(status: :success) + expect(issue.labels).to be_empty + end + end + end + end + + context 'when create_issue disabled' do + before do + setting.update!(create_issue: false) + end + + it 'returns an error' do + expect(service) + .to receive(:log_error) + .with(error_message('setting disabled')) + + expect(subject).to eq(status: :error, message: 'setting disabled') + end + end + + private + + def build_alert_payload(annotations: {}, starts_at: Time.now) + { + 'annotations' => annotations.stringify_keys + }.tap do |payload| + payload['startsAt'] = starts_at.rfc3339 if starts_at + end + end + + def error_message(message) + %{Cannot create incident issue for "#{project.full_name}": #{message}} + end + + def separator_count(text) + text.scan(summary_separator).size + end +end diff --git a/spec/workers/concerns/worker_context_spec.rb b/spec/workers/concerns/worker_context_spec.rb index 97a88eecd73..4e8c81c57dc 100644 --- a/spec/workers/concerns/worker_context_spec.rb +++ b/spec/workers/concerns/worker_context_spec.rb @@ -106,5 +106,15 @@ describe WorkerContext do expect(Labkit::Context.current.to_h).to include('meta.user' => 'jane-doe') end end + + it 'yields the arguments to the block' do + a_user = build_stubbed(:user) + a_project = build_stubbed(:project) + + worker.new.with_context(user: a_user, project: a_project) do |user:, project:| + expect(user).to eq(a_user) + expect(project).to eq(a_project) + end + end end end diff --git a/spec/workers/incident_management/process_alert_worker_spec.rb b/spec/workers/incident_management/process_alert_worker_spec.rb new file mode 100644 index 00000000000..9f40833dfd7 --- /dev/null +++ b/spec/workers/incident_management/process_alert_worker_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe IncidentManagement::ProcessAlertWorker do + let_it_be(:project) { create(:project) } + + describe '#perform' do + let(:alert) { :alert } + let(:create_issue_service) { spy(:create_issue_service) } + + subject { described_class.new.perform(project.id, alert) } + + it 'calls create issue service' do + expect(Project).to receive(:find_by_id).and_call_original + + expect(IncidentManagement::CreateIssueService) + .to receive(:new).with(project, :alert) + .and_return(create_issue_service) + + expect(create_issue_service).to receive(:execute) + + subject + end + + context 'with invalid project' do + let(:invalid_project_id) { 0 } + + subject { described_class.new.perform(invalid_project_id, alert) } + + it 'does not create issues' do + expect(Project).to receive(:find_by_id).and_call_original + expect(IncidentManagement::CreateIssueService).not_to receive(:new) + + subject + end + end + end +end |