diff options
Diffstat (limited to 'spec/support/shared_examples')
192 files changed, 7386 insertions, 1907 deletions
diff --git a/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb new file mode 100644 index 00000000000..9c096c5a158 --- /dev/null +++ b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb @@ -0,0 +1,629 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'validation on Time arguments' do + context 'when `to` parameter is higher than `from`' do + let(:variables) do + { + path: full_path, + from: 1.day.ago.iso8601, + to: 2.days.ago.iso8601 + } + end + + it 'returns error' do + expect(result).to be_nil + expect(graphql_errors.first['message']).to include('`from` argument must be before `to` argument') + end + end + + context 'when from and to parameter range is higher than 180 days' do + let(:variables) do + { + path: full_path, + from: Time.now, + to: 181.days.from_now + } + end + + it 'returns error' do + expect(result).to be_nil + expect(graphql_errors.first['message']).to include('Max of 180 days timespan is allowed') + end + end +end + +RSpec.shared_examples 'value stream analytics flow metrics issueCount examples' do + let_it_be(:milestone) { create(:milestone, group: group) } + let_it_be(:label) { create(:group_label, group: group) } + + let_it_be(:author) { create(:user) } + let_it_be(:assignee) { create(:user) } + + let_it_be(:issue1) { create(:issue, project: project1, author: author, created_at: 12.days.ago) } + let_it_be(:issue2) { create(:issue, project: project2, author: author, created_at: 13.days.ago) } + + let_it_be(:issue3) do + create(:labeled_issue, + project: project1, + labels: [label], + author: author, + milestone: milestone, + assignees: [assignee], + created_at: 14.days.ago) + end + + let_it_be(:issue4) do + create(:labeled_issue, + project: project2, + labels: [label], + assignees: [assignee], + created_at: 15.days.ago) + end + + let_it_be(:issue_outside_of_the_range) { create(:issue, project: project2, author: author, created_at: 50.days.ago) } + + let(:query) do + <<~QUERY + query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) { + #{context}(fullPath: $path) { + flowMetrics { + issueCount(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) { + value + unit + identifier + title + } + } + } + } + QUERY + end + + let(:variables) do + { + path: full_path, + from: 20.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + subject(:result) do + post_graphql(query, current_user: current_user, variables: variables) + + graphql_data.dig(context.to_s, 'flowMetrics', 'issueCount') + end + + it 'returns the correct count' do + expect(result).to eq({ + 'identifier' => 'issues', + 'unit' => nil, + 'value' => 4, + 'title' => n_('New Issue', 'New Issues', 4) + }) + end + + context 'with partial filters' do + let(:variables) do + { + path: full_path, + assigneeUsernames: [assignee.username], + labelNames: [label.title], + from: 20.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + it 'returns filtered count' do + expect(result).to eq({ + 'identifier' => 'issues', + 'unit' => nil, + 'value' => 2, + 'title' => n_('New Issue', 'New Issues', 2) + }) + end + end + + context 'with all filters' do + let(:variables) do + { + path: full_path, + assigneeUsernames: [assignee.username], + labelNames: [label.title], + authorUsername: author.username, + milestoneTitle: milestone.title, + from: 20.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + it 'returns filtered count' do + expect(result).to eq({ + 'identifier' => 'issues', + 'unit' => nil, + 'value' => 1, + 'title' => n_('New Issue', 'New Issues', 1) + }) + end + end + + context 'when the user is not authorized' do + let(:current_user) { create(:user) } + + it 'returns nil' do + expect(result).to eq(nil) + end + end + + it_behaves_like 'validation on Time arguments' +end + +RSpec.shared_examples 'value stream analytics flow metrics deploymentCount examples' do + let_it_be(:deployment1) do + create(:deployment, :success, environment: production_environment1, finished_at: 5.days.ago) + end + + let_it_be(:deployment2) do + create(:deployment, :success, environment: production_environment2, finished_at: 10.days.ago) + end + + let_it_be(:deployment3) do + create(:deployment, :success, environment: production_environment2, finished_at: 15.days.ago) + end + + let(:variables) do + { + path: full_path, + from: 12.days.ago.iso8601, + to: 3.days.ago.iso8601 + } + end + + let(:query) do + <<~QUERY + query($path: ID!, $from: Time!, $to: Time!) { + #{context}(fullPath: $path) { + flowMetrics { + deploymentCount(from: $from, to: $to) { + value + unit + identifier + title + } + } + } + } + QUERY + end + + subject(:result) do + post_graphql(query, current_user: current_user, variables: variables) + + graphql_data.dig(context.to_s, 'flowMetrics', 'deploymentCount') + end + + it 'returns the correct count' do + expect(result).to eq({ + 'identifier' => 'deploys', + 'unit' => nil, + 'value' => 2, + 'title' => n_('Deploy', 'Deploys', 2) + }) + end + + context 'when the user is not authorized' do + let(:current_user) { create(:user) } + + it 'returns nil' do + expect(result).to eq(nil) + end + end + + context 'when outside of the date range' do + let(:variables) do + { + path: full_path, + from: 20.days.ago.iso8601, + to: 18.days.ago.iso8601 + } + end + + it 'returns 0 count' do + expect(result).to eq({ + 'identifier' => 'deploys', + 'unit' => nil, + 'value' => 0, + 'title' => n_('Deploy', 'Deploys', 0) + }) + end + end + + it_behaves_like 'validation on Time arguments' +end + +RSpec.shared_examples 'value stream analytics flow metrics leadTime examples' do + let_it_be(:milestone) { create(:milestone, group: group) } + let_it_be(:label) { create(:group_label, group: group) } + + let_it_be(:author) { create(:user) } + let_it_be(:assignee) { create(:user) } + + let_it_be(:issue1) do + create(:issue, project: project1, author: author, created_at: 17.days.ago, closed_at: 12.days.ago) + end + + let_it_be(:issue2) do + create(:issue, project: project2, author: author, created_at: 16.days.ago, closed_at: 13.days.ago) + end + + let_it_be(:issue3) do + create(:labeled_issue, + project: project1, + labels: [label], + author: author, + milestone: milestone, + assignees: [assignee], + created_at: 14.days.ago, + closed_at: 11.days.ago) + end + + let_it_be(:issue4) do + create(:labeled_issue, + project: project2, + labels: [label], + assignees: [assignee], + created_at: 20.days.ago, + closed_at: 15.days.ago) + end + + before do + Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute + end + + let(:query) do + <<~QUERY + query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) { + #{context}(fullPath: $path) { + flowMetrics { + leadTime(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) { + value + unit + identifier + title + links { + label + url + } + } + } + } + } + QUERY + end + + let(:variables) do + { + path: full_path, + from: 21.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + subject(:result) do + post_graphql(query, current_user: current_user, variables: variables) + + graphql_data.dig(context.to_s, 'flowMetrics', 'leadTime') + end + + it 'returns the correct value' do + expect(result).to match(a_hash_including({ + 'identifier' => 'lead_time', + 'unit' => n_('day', 'days', 4), + 'value' => 4, + 'title' => _('Lead Time'), + 'links' => [ + { 'label' => s_('ValueStreamAnalytics|Dashboard'), 'url' => match(/issues_analytics/) }, + { 'label' => s_('ValueStreamAnalytics|Go to docs'), 'url' => match(/definitions/) } + ] + })) + end + + context 'when the user is not authorized' do + let(:current_user) { create(:user) } + + it 'returns nil' do + expect(result).to eq(nil) + end + end + + context 'when outside of the date range' do + let(:variables) do + { + path: full_path, + from: 30.days.ago.iso8601, + to: 25.days.ago.iso8601 + } + end + + it 'returns 0 count' do + expect(result).to match(a_hash_including({ 'value' => nil })) + end + end + + context 'with all filters' do + let(:variables) do + { + path: full_path, + assigneeUsernames: [assignee.username], + labelNames: [label.title], + authorUsername: author.username, + milestoneTitle: milestone.title, + from: 20.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + it 'returns filtered count' do + expect(result).to match(a_hash_including({ 'value' => 3 })) + end + end +end + +RSpec.shared_examples 'value stream analytics flow metrics cycleTime examples' do + let_it_be(:milestone) { create(:milestone, group: group) } + let_it_be(:label) { create(:group_label, group: group) } + + let_it_be(:author) { create(:user) } + let_it_be(:assignee) { create(:user) } + + let_it_be(:issue1) do + create(:issue, project: project1, author: author, closed_at: 12.days.ago).tap do |issue| + issue.metrics.update!(first_mentioned_in_commit_at: 17.days.ago) + end + end + + let_it_be(:issue2) do + create(:issue, project: project2, author: author, closed_at: 13.days.ago).tap do |issue| + issue.metrics.update!(first_mentioned_in_commit_at: 16.days.ago) + end + end + + let_it_be(:issue3) do + create(:labeled_issue, + project: project1, + labels: [label], + author: author, + milestone: milestone, + assignees: [assignee], + closed_at: 11.days.ago).tap do |issue| + issue.metrics.update!(first_mentioned_in_commit_at: 14.days.ago) + end + end + + let_it_be(:issue4) do + create(:labeled_issue, + project: project2, + labels: [label], + assignees: [assignee], + closed_at: 15.days.ago).tap do |issue| + issue.metrics.update!(first_mentioned_in_commit_at: 20.days.ago) + end + end + + before do + Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute + end + + let(:query) do + <<~QUERY + query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) { + #{context}(fullPath: $path) { + flowMetrics { + cycleTime(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) { + value + unit + identifier + title + links { + label + url + } + } + } + } + } + QUERY + end + + let(:variables) do + { + path: full_path, + from: 21.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + subject(:result) do + post_graphql(query, current_user: current_user, variables: variables) + + graphql_data.dig(context.to_s, 'flowMetrics', 'cycleTime') + end + + it 'returns the correct value' do + expect(result).to eq({ + 'identifier' => 'cycle_time', + 'unit' => n_('day', 'days', 4), + 'value' => 4, + 'title' => _('Cycle Time'), + 'links' => [] + }) + end + + context 'when the user is not authorized' do + let(:current_user) { create(:user) } + + it 'returns nil' do + expect(result).to eq(nil) + end + end + + context 'when outside of the date range' do + let(:variables) do + { + path: full_path, + from: 30.days.ago.iso8601, + to: 25.days.ago.iso8601 + } + end + + it 'returns 0 count' do + expect(result).to match(a_hash_including({ 'value' => nil })) + end + end + + context 'with all filters' do + let(:variables) do + { + path: full_path, + assigneeUsernames: [assignee.username], + labelNames: [label.title], + authorUsername: author.username, + milestoneTitle: milestone.title, + from: 20.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + it 'returns filtered count' do + expect(result).to match(a_hash_including({ 'value' => 3 })) + end + end +end + +RSpec.shared_examples 'value stream analytics flow metrics issuesCompleted examples' do + let_it_be(:milestone) { create(:milestone, group: group) } + let_it_be(:label) { create(:group_label, group: group) } + + let_it_be(:author) { create(:user) } + let_it_be(:assignee) { create(:user) } + + # we don't care about opened date, only closed date. + let_it_be(:issue1) do + create(:issue, project: project1, author: author, created_at: 17.days.ago, closed_at: 12.days.ago) + end + + let_it_be(:issue2) do + create(:issue, project: project2, author: author, created_at: 16.days.ago, closed_at: 13.days.ago) + end + + let_it_be(:issue3) do + create(:labeled_issue, + project: project1, + labels: [label], + author: author, + milestone: milestone, + assignees: [assignee], + created_at: 14.days.ago, + closed_at: 11.days.ago) + end + + let_it_be(:issue4) do + create(:labeled_issue, + project: project2, + labels: [label], + assignees: [assignee], + created_at: 20.days.ago, + closed_at: 15.days.ago) + end + + before do + Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute + end + + let(:query) do + <<~QUERY + query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) { + #{context}(fullPath: $path) { + flowMetrics { + issuesCompletedCount(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) { + value + unit + identifier + title + links { + label + url + } + } + } + } + } + QUERY + end + + let(:variables) do + { + path: full_path, + from: 21.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + subject(:result) do + post_graphql(query, current_user: current_user, variables: variables) + + graphql_data.dig(context.to_s, 'flowMetrics', 'issuesCompletedCount') + end + + it 'returns the correct value' do + expect(result).to match(a_hash_including({ + 'identifier' => 'issues_completed', + 'unit' => n_('issue', 'issues', 4), + 'value' => 4, + 'title' => _('Issues Completed'), + 'links' => [ + { 'label' => s_('ValueStreamAnalytics|Dashboard'), 'url' => match(/issues_analytics/) }, + { 'label' => s_('ValueStreamAnalytics|Go to docs'), 'url' => match(/definitions/) } + ] + })) + end + + context 'when the user is not authorized' do + let(:current_user) { create(:user) } + + it 'returns nil' do + expect(result).to eq(nil) + end + end + + context 'when outside of the date range' do + let(:variables) do + { + path: full_path, + from: 30.days.ago.iso8601, + to: 25.days.ago.iso8601 + } + end + + it 'returns 0 count' do + expect(result).to match(a_hash_including({ 'value' => 0.0 })) + end + end + + context 'with all filters' do + let(:variables) do + { + path: full_path, + assigneeUsernames: [assignee.username], + labelNames: [label.title], + authorUsername: author.username, + milestoneTitle: milestone.title, + from: 20.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + it 'returns filtered count' do + expect(result).to match(a_hash_including({ 'value' => 1.0 })) + end + end +end diff --git a/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb new file mode 100644 index 00000000000..ef9830fbce8 --- /dev/null +++ b/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'unlicensed cycle analytics request params' do + let(:params) do + { + created_after: '2019-01-01', + created_before: '2019-03-01', + project_ids: [2, 3], + namespace: namespace, + current_user: user + } + end + + subject { described_class.new(params) } + + before do + root_group.add_owner(user) + end + + describe 'validations' do + it 'is valid' do + expect(subject).to be_valid + end + + context 'when `created_before` is missing' do + before do + params[:created_before] = nil + end + + it 'is valid', time_travel_to: '2019-03-01' do + expect(subject).to be_valid + end + end + + context 'when `created_before` is earlier than `created_after`' do + before do + params[:created_before] = '2015-01-01' + end + + it 'is invalid' do + expect(subject).not_to be_valid + expect(subject.errors.messages[:created_before]).not_to be_empty + end + end + + context 'when the date range exceeds 180 days' do + before do + params[:created_before] = '2019-07-15' + end + + it 'is invalid' do + expect(subject).not_to be_valid + message = s_('CycleAnalytics|The given date range is larger than 180 days') + expect(subject.errors.messages[:created_after]).to include(message) + end + end + end + + it 'casts `created_after` to `Time`' do + expect(subject.created_after).to be_a_kind_of(Time) + end + + it 'casts `created_before` to `Time`' do + expect(subject.created_before).to be_a_kind_of(Time) + end + + describe 'optional `value_stream`' do + context 'when `value_stream` is not empty' do + let(:value_stream) { instance_double('Analytics::CycleAnalytics::ValueStream') } + + before do + params[:value_stream] = value_stream + end + + it { expect(subject.value_stream).to eq(value_stream) } + end + + context 'when `value_stream` is nil' do + before do + params[:value_stream] = nil + end + + it { expect(subject.value_stream).to eq(nil) } + end + end + + describe 'sorting params' do + before do + params.merge!(sort: 'duration', direction: 'asc') + end + + it 'converts sorting params to symbol when passing it to data collector' do + data_collector_params = subject.to_data_collector_params + + expect(data_collector_params[:sort]).to eq(:duration) + expect(data_collector_params[:direction]).to eq(:asc) + end + + it 'adds sorting params to data attributes' do + data_attributes = subject.to_data_attributes + + expect(data_attributes[:sort]).to eq('duration') + expect(data_attributes[:direction]).to eq('asc') + end + end + + describe 'aggregation params' do + context 'when not licensed' do + it 'returns nil' do + data_collector_params = subject.to_data_attributes + expect(data_collector_params[:aggregation]).to eq(nil) + end + end + end + + describe 'use_aggregated_data_collector param' do + subject(:value) { described_class.new(params).to_data_collector_params[:use_aggregated_data_collector] } + + it { is_expected.to eq(false) } + end + + describe 'feature availablity data attributes' do + subject(:value) { described_class.new(params).to_data_attributes } + + it 'disables all paid features' do + is_expected.to match(a_hash_including(enable_tasks_by_type_chart: 'false', + enable_customizable_stages: 'false', + enable_projects_filter: 'false')) + end + end +end diff --git a/spec/support/shared_examples/banzai/filters/filter_timeout_shared_examples.rb b/spec/support/shared_examples/banzai/filters/filter_timeout_shared_examples.rb new file mode 100644 index 00000000000..618be53cb3b --- /dev/null +++ b/spec/support/shared_examples/banzai/filters/filter_timeout_shared_examples.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +# These shared_examples require the following variables: +# - text: The text to be run through the filter +# +# Usage: +# +# it_behaves_like 'html filter timeout' do +# let(:text) { 'some text' } +# end +RSpec.shared_examples 'html filter timeout' do + context 'when rendering takes too long' do + let_it_be(:project) { create(:project) } + let_it_be(:context) { { project: project } } + + it 'times out' do + stub_const("Banzai::Filter::TimeoutHtmlPipelineFilter::RENDER_TIMEOUT", 0.1) + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:call_with_timeout) do + sleep(0.2) + text + end + end + + expect(Gitlab::RenderTimeout).to receive(:timeout).and_call_original + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + instance_of(Timeout::Error), + project_id: context[:project].id, + class_name: described_class.name.demodulize + ) + + result = filter(text) + + expect(result.to_html).to eq text + end + end +end + +# Usage: +# +# it_behaves_like 'text html filter timeout' do +# let(:text) { 'some text' } +# end +RSpec.shared_examples 'text filter timeout' do + context 'when rendering takes too long' do + let_it_be(:project) { create(:project) } + let_it_be(:context) { { project: project } } + + it 'times out' do + stub_const("Banzai::Filter::TimeoutTextPipelineFilter::RENDER_TIMEOUT", 0.1) + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:call_with_timeout) do + sleep(0.2) + text + end + end + + expect(Gitlab::RenderTimeout).to receive(:timeout).and_call_original + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + instance_of(Timeout::Error), + project_id: context[:project].id, + class_name: described_class.name.demodulize + ) + + result = filter(text) + + expect(result).to eq text + end + end +end diff --git a/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb b/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb index 599161abbfe..8f2f3f89914 100644 --- a/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb +++ b/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb @@ -7,6 +7,10 @@ RSpec.shared_examples 'a metrics embed filter' do let(:input) { %(<a href="#{url}">example</a>) } let(:doc) { filter(input) } + before do + stub_feature_flags(remove_monitor_metrics: false) + end + context 'when the document has an external link' do let(:url) { 'https://foo.com' } @@ -38,6 +42,18 @@ RSpec.shared_examples 'a metrics embed filter' do expect(doc.at_css('.js-render-metrics')).to be_present end end + + context 'when metrics dashboard feature is unavailable' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it 'does not append a metrics chart placeholder' do + node = doc.at_css('.js-render-metrics') + + expect(node).not_to be_present + end + end end # Nokogiri escapes the URLs, but we don't care about that diff --git a/spec/support/shared_examples/banzai/filters/reference_filter_shared_examples.rb b/spec/support/shared_examples/banzai/filters/reference_filter_shared_examples.rb new file mode 100644 index 00000000000..6912bcaee34 --- /dev/null +++ b/spec/support/shared_examples/banzai/filters/reference_filter_shared_examples.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +# Specs for reference links containing HTML. +# +# Requires a reference: +# let(:reference) { '#42' } +RSpec.shared_examples 'a reference containing an element node' do + let(:inner_html) { 'element <code>node</code> inside' } + let(:reference_with_element) { %(<a href="#{reference}">#{inner_html}</a>) } + + it 'does not escape inner html' do + doc = reference_filter(reference_with_element) + expect(doc.children.first.inner_html).to eq(inner_html) + end +end + +# Requires a reference, subject and subject_name: +# subject { create(:user) } +# let(:reference) { subject.to_reference } +# let(:subject_name) { 'user' } +RSpec.shared_examples 'user reference or project reference' do + shared_examples 'it contains a data- attribute' do + it 'includes a data- attribute' do + doc = reference_filter("Hey #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute("data-#{subject_name}") + expect(link.attr("data-#{subject_name}")).to eq subject.id.to_s + end + end + + context 'when mentioning a resource' do + it_behaves_like 'a reference containing an element node' + it_behaves_like 'it contains a data- attribute' + + it "links to a resource" do + doc = reference_filter("Hey #{reference}") + expect(doc.css('a').first.attr('href')).to eq urls.send("#{subject_name}_url", subject) + end + + it 'links to a resource with a period' do + subject = create(subject_name.to_sym, name: 'alphA.Beta') + + doc = reference_filter("Hey #{get_reference(subject)}") + expect(doc.css('a').length).to eq 1 + end + + it 'links to a resource with an underscore' do + subject = create(subject_name.to_sym, name: 'ping_pong_king') + + doc = reference_filter("Hey #{get_reference(subject)}") + expect(doc.css('a').length).to eq 1 + end + + it 'links to a resource with different case-sensitivity' do + subject = create(subject_name.to_sym, name: 'RescueRanger') + reference = get_reference(subject) + + doc = reference_filter("Hey #{reference.upcase}") + expect(doc.css('a').length).to eq 1 + expect(doc.css('a').text).to eq(reference) + end + end + + it 'supports an :only_path context' do + doc = reference_filter("Hey #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r{https?://} + expect(link).to eq urls.send "#{subject_name}_path", subject + end + + describe 'referencing a resource in a link href' do + let(:reference) { %(<a href="#{get_reference(subject)}">Some text</a>) } + + it_behaves_like 'it contains a data- attribute' + + it 'links to the resource' do + doc = reference_filter("Hey #{reference}") + expect(doc.css('a').first.attr('href')).to eq urls.send "#{subject_name}_url", subject + end + + it 'links with adjacent text' do + doc = reference_filter("Mention me (#{reference}.)") + expect(doc.to_html).to match(%r{\(<a.+>Some text</a>\.\)}) + end + end +end diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb index 7df4b7635d3..ddd3bbd636a 100644 --- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb +++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb @@ -80,7 +80,7 @@ RSpec.shared_examples 'multiple issue boards' do click_button 'Select a label' - page.choose(planning.title) + find('label', text: planning.title).click click_button 'Add to board' diff --git a/spec/support/shared_examples/bulk_imports/visibility_level_examples.rb b/spec/support/shared_examples/bulk_imports/visibility_level_examples.rb index 40e9726f89c..02eae250e6a 100644 --- a/spec/support/shared_examples/bulk_imports/visibility_level_examples.rb +++ b/spec/support/shared_examples/bulk_imports/visibility_level_examples.rb @@ -27,14 +27,6 @@ RSpec.shared_examples 'visibility level settings' do expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE) end end - - context 'when destination is blank' do - let(:destination_namespace) { '' } - - it 'sets visibility level to public' do - expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PUBLIC) - end - end end context 'when internal' do @@ -63,27 +55,6 @@ RSpec.shared_examples 'visibility level settings' do expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE) end end - - context 'when destination is blank' do - let(:destination_namespace) { '' } - - it 'sets visibility level to internal' do - expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::INTERNAL) - end - - context 'when visibility level is restricted' do - it 'sets visibility level to private' do - stub_application_setting( - restricted_visibility_levels: [ - Gitlab::VisibilityLevel::INTERNAL, - Gitlab::VisibilityLevel::PUBLIC - ] - ) - - expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE) - end - end - end end context 'when private' do @@ -112,13 +83,5 @@ RSpec.shared_examples 'visibility level settings' do expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE) end end - - context 'when destination is blank' do - let(:destination_namespace) { '' } - - it 'sets visibility level to private' do - expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE) - end - end end end diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb index de38d1ff9f8..af1843bae28 100644 --- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb @@ -138,6 +138,19 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do .not_to exceed_all_query_limit(control_count) end + context 'when user is not allowed to import projects' do + let(:user) { create(:user) } + let!(:group) { create(:group).tap { |group| group.add_developer(user) } } + + it 'returns 404' do + expect(stub_client(repos: [], orgs: [])).to receive(:repos) + + get :status, params: { namespace_id: group.id }, format: :html + + expect(response).to have_gitlab_http_status(:not_found) + end + end + context 'when filtering' do let(:repo_2) { repo_fake.new(login: 'emacs', full_name: 'asd/emacs', name: 'emacs', owner: { login: 'owner' }) } let(:project) { create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') } diff --git a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb index 44baadaaade..e94f063399d 100644 --- a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb +++ b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb @@ -19,4 +19,26 @@ RSpec.shared_examples 'import controller status' do expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id) end + + context 'when format is html' do + context 'when namespace_id is present' do + let!(:developer_group) { create(:group).tap { |g| g.add_developer(user) } } + + context 'when user cannot import projects' do + it 'returns 404' do + get :status, params: { namespace_id: developer_group.id }, format: :html + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when user can import projects' do + it 'returns 200' do + get :status, params: { namespace_id: group.id }, format: :html + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + end end diff --git a/spec/support/shared_examples/controllers/project_import_rate_limiter_shared_examples.rb b/spec/support/shared_examples/controllers/project_import_rate_limiter_shared_examples.rb new file mode 100644 index 00000000000..66d753a4010 --- /dev/null +++ b/spec/support/shared_examples/controllers/project_import_rate_limiter_shared_examples.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'project import rate limiter' do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + context 'when limit exceeds' do + before do + allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true) + end + + it 'notifies and redirects user' do + post :create, params: {} + + expect(flash[:alert]).to eq('This endpoint has been requested too many times. Try again later.') + expect(response).to have_gitlab_http_status(:found) + end + end +end diff --git a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb index cc28a79b4ca..e75188f8249 100644 --- a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb @@ -60,19 +60,6 @@ RSpec.shared_examples Repositories::GitHttpController do expect(response).to have_gitlab_http_status(:ok) end - it 'updates the user activity' do - activity_project = container.is_a?(PersonalSnippet) ? nil : project - - activity_service = instance_double(Users::ActivityService) - - args = { author: user, project: activity_project, namespace: activity_project&.namespace } - expect(Users::ActivityService).to receive(:new).with(args).and_return(activity_service) - - expect(activity_service).to receive(:execute) - - get :info_refs, params: params - end - include_context 'parsed logs' do it 'adds user info to the logs' do get :info_refs, params: params @@ -87,14 +74,20 @@ RSpec.shared_examples Repositories::GitHttpController do end describe 'POST #git_upload_pack' do - before do + it 'returns 200' do allow(controller).to receive(:verify_workhorse_api!).and_return(true) - end - it 'returns 200' do post :git_upload_pack, params: params expect(response).to have_gitlab_http_status(:ok) end + + context 'when JWT token is not provided' do + it 'returns 403' do + post :git_upload_pack, params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + end end end diff --git a/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb b/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb index 112b9cbb204..f658cfac0f5 100644 --- a/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb +++ b/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb @@ -15,8 +15,9 @@ RSpec.shared_examples 'snippets sort order' do context 'when no sort param is provided' do it 'calls SnippetsFinder with updated_at sort option' do - expect(SnippetsFinder).to receive(:new).with(user, - hash_including(sort: 'updated_desc')).and_call_original + expect(SnippetsFinder).to receive(:new) + .with(user, hash_including(sort: 'updated_desc')) + .and_call_original subject end @@ -27,8 +28,9 @@ RSpec.shared_examples 'snippets sort order' do let(:sort_argument) { { sort: order } } it 'calls SnippetsFinder with the given sort param' do - expect(SnippetsFinder).to receive(:new).with(user, - hash_including(sort: order)).and_call_original + expect(SnippetsFinder).to receive(:new) + .with(user, hash_including(sort: order)) + .and_call_original subject end diff --git a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb index 38c3157e898..b5528afa0b5 100644 --- a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb +++ b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb @@ -7,6 +7,9 @@ RSpec.shared_examples 'tracking unique hll events' do it 'tracks unique event' do + # Allow any event tracking before we expect the specific event we want to check below + allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).and_call_original + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to( receive(:track_event) .with(target_event, values: expected_value) diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index 5d77ed5fdfc..32aa566c27e 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -15,20 +15,33 @@ RSpec.shared_examples 'wiki controller actions' do sign_in(user) end - shared_examples 'recovers from git timeout' do + shared_examples 'recovers from git errors' do let(:method_name) { :page } - context 'when we encounter git command errors' do + context 'when we encounter CommandTimedOut error' do it 'renders the appropriate template', :aggregate_failures do - expect(controller).to receive(method_name) do - raise ::Gitlab::Git::CommandTimedOut, 'Deadline Exceeded' - end + expect(controller) + .to receive(method_name) + .and_raise(::Gitlab::Git::CommandTimedOut, 'Deadline Exceeded') request expect(response).to render_template('shared/wikis/git_error') end end + + context 'when we encounter a NoRepository error' do + it 'renders the appropriate template', :aggregate_failures do + expect(controller) + .to receive(method_name) + .and_raise(Gitlab::Git::Repository::NoRepository) + + request + + expect(response).to render_template('shared/wikis/empty') + expect(assigns(:error)).to eq('Could not access the Wiki Repository at this time.') + end + end end describe 'GET #new' do @@ -65,7 +78,7 @@ RSpec.shared_examples 'wiki controller actions' do get :pages, params: routing_params.merge(id: wiki_title) end - it_behaves_like 'recovers from git timeout' do + it_behaves_like 'recovers from git errors' do subject(:request) { get :pages, params: routing_params.merge(id: wiki_title) } let(:method_name) { :wiki_pages } @@ -122,7 +135,7 @@ RSpec.shared_examples 'wiki controller actions' do end end - it_behaves_like 'recovers from git timeout' do + it_behaves_like 'recovers from git errors' do subject(:request) { get :history, params: routing_params.merge(id: wiki_title) } let(:allow_read_wiki) { true } @@ -170,7 +183,7 @@ RSpec.shared_examples 'wiki controller actions' do end end - it_behaves_like 'recovers from git timeout' do + it_behaves_like 'recovers from git errors' do subject(:request) { get :diff, params: routing_params.merge(id: wiki_title, version_id: wiki.repository.commit.id) } end end @@ -185,7 +198,7 @@ RSpec.shared_examples 'wiki controller actions' do context 'when page exists' do let(:id) { wiki_title } - it_behaves_like 'recovers from git timeout' + it_behaves_like 'recovers from git errors' it 'renders the page' do request @@ -366,7 +379,7 @@ RSpec.shared_examples 'wiki controller actions' do subject(:request) { get(:edit, params: routing_params.merge(id: id_param)) } it_behaves_like 'edit action' - it_behaves_like 'recovers from git timeout' + it_behaves_like 'recovers from git errors' context 'when page content encoding is valid' do render_views @@ -386,11 +399,10 @@ RSpec.shared_examples 'wiki controller actions' do let(:id_param) { wiki_title } subject(:request) do - patch(:update, - params: routing_params.merge( - id: id_param, - wiki: { title: new_title, content: new_content } - )) + patch(:update, params: routing_params.merge( + id: id_param, + wiki: { title: new_title, content: new_content } + )) end it_behaves_like 'edit action' @@ -426,10 +438,9 @@ RSpec.shared_examples 'wiki controller actions' do let(:new_content) { 'New content' } subject(:request) do - post(:create, - params: routing_params.merge( - wiki: { title: new_title, content: new_content } - )) + post(:create, params: routing_params.merge( + wiki: { title: new_title, content: new_content } + )) end context 'when page is valid' do @@ -463,10 +474,9 @@ RSpec.shared_examples 'wiki controller actions' do let(:delete_user) { user } subject(:request) do - delete(:destroy, - params: routing_params.merge( - id: id_param - )) + delete(:destroy, params: routing_params.merge( + id: id_param + )) end before do diff --git a/spec/support/shared_examples/db/seeds/data_seeder_shared_examples.rb b/spec/support/shared_examples/db/seeds/data_seeder_shared_examples.rb new file mode 100644 index 00000000000..4e8d65ac25e --- /dev/null +++ b/spec/support/shared_examples/db/seeds/data_seeder_shared_examples.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'raises an error when specifying an invalid factory' do + it 'raises an error' do + expect { parser.parse }.to raise_error(RuntimeError, /invalids.*to a valid registered Factory/) + end +end + +RSpec.shared_examples 'specifying invalid traits to a factory' do + it 'raises an error', :aggregate_failures do + expect { parser.parse }.to raise_error do |error| + expect(error).to be_a(RuntimeError) + expect(error.message).to include('Trait not registered: \\"invalid\\"') + expect(error.message).to include('for Factory \\"issue\\"') + end + end +end + +RSpec.shared_examples 'specifying invalid attributes to a factory' do + it 'raises an error' do + expect { parser.parse }.to raise_error(RuntimeError, /is not a valid attribute/) + end + + it 'contains possible alternatives' do + expect { parser.parse }.to raise_error(RuntimeError, /Did you mean/) + end +end + +RSpec.shared_examples 'an id already exists' do + it 'raises a validation error' do + expect { parser.parse }.to raise_error(/id `my_label` must be unique/) + end +end + +RSpec.shared_examples 'name is not specified' do + it 'raises an error when name is not specified' do + expect { parser.parse }.to raise_error(/Seed file must specify a name/) + end +end + +RSpec.shared_examples 'factory definitions' do + it 'has exactly two definitions' do + parser.parse + + expect(parser.definitions.size).to eq(2) + end + + it 'creates the group label' do + expect { parser.parse }.to change { GroupLabel.count }.by(1) + end + + it 'creates the project' do + expect { parser.parse }.to change { Project.count }.by(1) + end +end + +RSpec.shared_examples 'passes traits' do + it 'passes traits' do + expect_next_instance_of(Gitlab::DataSeeder::FactoryDefinitions::FactoryDefinition) do |instance| + # `described` trait will automaticaly generate a description + expect(instance.build(binding).description).to eq('Description of Test Label') + end + + parser.parse + end +end + +RSpec.shared_examples 'has a name' do + it 'has a name' do + parser.parse + + expect(parser.name).to eq('Test') + end +end + +RSpec.shared_examples 'definition has an id' do + it 'binds the object', :aggregate_failures do + parser.parse + + expect(group_labels).to be_a(OpenStruct) # rubocop:disable Style/OpenStructUse + expect(group_labels.my_label).to be_a(GroupLabel) + expect(group_labels.my_label.title).to eq('My Label') + end +end + +RSpec.shared_examples 'id has spaces' do + it 'binds to an underscored variable', :aggregate_failures do + parser.parse + + expect(group_labels).to respond_to(:id_with_spaces) + expect(group_labels.id_with_spaces.title).to eq('With Spaces') + end + + it 'renders a warning' do + expect { parser.parse }.to output(%(parsing id "id with spaces" as "id_with_spaces"\n)).to_stderr + end +end + +RSpec.shared_examples 'definition does not have an id' do + it 'does not bind the object' do + parser.parse + + expect(group_labels.to_h).to be_empty + end +end + +RSpec.shared_examples 'invalid id' do |message| + it 'raises an error' do + expect { parser.parse }.to raise_error(message) + end +end diff --git a/spec/support/shared_examples/features/2fa_shared_examples.rb b/spec/support/shared_examples/features/2fa_shared_examples.rb index 44f30c32472..6c4e98c9989 100644 --- a/spec/support/shared_examples/features/2fa_shared_examples.rb +++ b/spec/support/shared_examples/features/2fa_shared_examples.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true RSpec.shared_examples 'hardware device for 2fa' do |device_type| - include Spec::Support::Helpers::Features::TwoFactorHelpers + include Features::TwoFactorHelpers include Spec::Support::Helpers::ModalHelpers def register_device(device_type, **kwargs) case device_type.downcase - when "u2f" - register_u2f_device(**kwargs) when "webauthn" register_webauthn_device(**kwargs) else @@ -98,9 +96,7 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type| end it 'provides a button that shows the fallback otp code UI' do - expect(page).to have_link('Sign in via 2FA code') - - click_link('Sign in via 2FA code') + click_button(_('Sign in via 2FA code')) assert_fallback_ui(page) end diff --git a/spec/support/shared_examples/features/abuse_report_shared_examples.rb b/spec/support/shared_examples/features/abuse_report_shared_examples.rb new file mode 100644 index 00000000000..ea9b4e9f4b2 --- /dev/null +++ b/spec/support/shared_examples/features/abuse_report_shared_examples.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'reports the user with an abuse category' do + it 'creates abuse report' do + click_button 'Report abuse' + choose "They're posting spam." + click_button 'Next' + + page.attach_file('spec/fixtures/dk.png') do + click_button "Choose file" + end + + fill_in 'abuse_report_message', with: 'This user sends spam' + click_button 'Send report' + + expect(page).to have_content 'Thank you for your report' + end +end diff --git a/spec/support/shared_examples/features/access_tokens_shared_examples.rb b/spec/support/shared_examples/features/access_tokens_shared_examples.rb index 32a7b32ac72..3c78869ffaa 100644 --- a/spec/support/shared_examples/features/access_tokens_shared_examples.rb +++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb @@ -9,7 +9,7 @@ RSpec.shared_examples 'resource access tokens missing access rights' do end RSpec.shared_examples 'resource access tokens creation' do |resource_type| - include Spec::Support::Helpers::AccessTokenHelpers + include Features::AccessTokenHelpers it 'allows creation of an access token', :aggregate_failures do name = 'My access token' diff --git a/spec/support/shared_examples/features/confidential_notes_shared_examples.rb b/spec/support/shared_examples/features/confidential_notes_shared_examples.rb index 289da025af6..cd0e8f94934 100644 --- a/spec/support/shared_examples/features/confidential_notes_shared_examples.rb +++ b/spec/support/shared_examples/features/confidential_notes_shared_examples.rb @@ -3,7 +3,7 @@ require "spec_helper" RSpec.shared_examples 'confidential notes on issuables' do - include Spec::Support::Helpers::Features::NotesHelpers + include Features::NotesHelpers context 'when user does not have permissions' do it 'does not show confidential note checkbox' do diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb index 6cd9c4ce1c4..41114197ff5 100644 --- a/spec/support/shared_examples/features/content_editor_shared_examples.rb +++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb @@ -1,116 +1,310 @@ # frozen_string_literal: true +require 'spec_helper' + RSpec.shared_examples 'edits content using the content editor' do + include ContentEditorHelpers + let(:content_editor_testid) { '[data-testid="content-editor"] [contenteditable].ProseMirror' } - def switch_to_content_editor - click_button _('View rich text') - click_button _('Rich text') - end + let(:is_mac) { page.evaluate_script('navigator.platform').include?('Mac') } + let(:modifier_key) { is_mac ? :command : :control } - def type_in_content_editor(keys) - find(content_editor_testid).send_keys keys - end + it 'saves page content in local storage if the user navigates away' do + switch_to_content_editor - def open_insert_media_dropdown - page.find('svg[data-testid="media-icon"]').click - end + expect(page).to have_css(content_editor_testid) - def set_source_editor_content(content) - find('.js-gfm-input').set content - end + type_in_content_editor ' Typing text in the content editor' - def expect_formatting_menu_to_be_visible - expect(page).to have_css('[data-testid="formatting-bubble-menu"]') - end + wait_until_hidden_field_is_updated /Typing text in the content editor/ - def expect_formatting_menu_to_be_hidden - expect(page).not_to have_css('[data-testid="formatting-bubble-menu"]') - end + begin + refresh + rescue Selenium::WebDriver::Error::UnexpectedAlertOpenError + end - def expect_media_bubble_menu_to_be_visible - expect(page).to have_css('[data-testid="media-bubble-menu"]') + expect(page).to have_text('Typing text in the content editor') end - def upload_asset(fixture_name) - attach_file('content_editor_image', Rails.root.join('spec', 'fixtures', fixture_name), make_visible: true) - end + describe 'creating and editing links' do + before do + switch_to_content_editor + end - def wait_until_hidden_field_is_updated(value) - expect(page).to have_field('wiki[content]', with: value, type: 'hidden') - end + context 'when clicking the link icon in the toolbar' do + it 'shows the link bubble menu' do + page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click - it 'saves page content in local storage if the user navigates away' do - switch_to_content_editor + expect(page).to have_css('[data-testid="link-bubble-menu"]') + end - expect(page).to have_css(content_editor_testid) + context 'if no text is selected' do + before do + page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click + end + + it 'opens an empty inline modal to create a link' do + page.within '[data-testid="link-bubble-menu"]' do + expect(page).to have_field('link-text', with: '') + expect(page).to have_field('link-href', with: '') + end + end + + context 'when the user clicks the apply button' do + it 'applies the changes to the document' do + page.within '[data-testid="link-bubble-menu"]' do + fill_in 'link-text', with: 'Link to GitLab home page' + fill_in 'link-href', with: 'https://gitlab.com' + + click_button 'Apply' + end + + page.within content_editor_testid do + expect(page).to have_css('a[href="https://gitlab.com"]') + expect(page).to have_text('Link to GitLab home page') + end + end + end + + context 'when the user clicks the cancel button' do + it 'does not apply the changes to the document' do + page.within '[data-testid="link-bubble-menu"]' do + fill_in 'link-text', with: 'Link to GitLab home page' + fill_in 'link-href', with: 'https://gitlab.com' + + click_button 'Cancel' + end + + page.within content_editor_testid do + expect(page).not_to have_css('a') + end + end + end + end - type_in_content_editor ' Typing text in the content editor' + context 'if text is selected' do + before do + type_in_content_editor 'The quick brown fox jumps over the lazy dog' + type_in_content_editor [:shift, :left] + type_in_content_editor [:shift, :left] + type_in_content_editor [:shift, :left] + + page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click + end + + it 'prefills inline modal to create a link' do + page.within '[data-testid="link-bubble-menu"]' do + expect(page).to have_field('link-text', with: 'dog') + expect(page).to have_field('link-href', with: '') + end + end + + context 'when the user clicks the apply button' do + it 'applies the changes to the document' do + page.within '[data-testid="link-bubble-menu"]' do + fill_in 'link-text', with: 'new dog' + fill_in 'link-href', with: 'https://en.wikipedia.org/wiki/Shiba_Inu' + + click_button 'Apply' + end + + page.within content_editor_testid do + expect(page).to have_selector('a[href="https://en.wikipedia.org/wiki/Shiba_Inu"]', + text: 'new dog' + ) + end + end + end + end + end - wait_until_hidden_field_is_updated /Typing text in the content editor/ + context 'if cursor is placed on an existing link' do + before do + type_in_content_editor 'Link to [GitLab home **page**](https://gitlab.com)' + type_in_content_editor :left + end - refresh + it 'prefills inline modal to edit the link' do + page.within '[data-testid="link-bubble-menu"]' do + page.find('[data-testid="edit-link"]').click - expect(page).to have_text('Typing text in the content editor') + expect(page).to have_field('link-text', with: 'GitLab home page') + expect(page).to have_field('link-href', with: 'https://gitlab.com') + end + end - refresh # also retained after second refresh + it 'updates the link attributes if text is not updated' do + page.within '[data-testid="link-bubble-menu"]' do + page.find('[data-testid="edit-link"]').click - expect(page).to have_text('Typing text in the content editor') + fill_in 'link-href', with: 'https://about.gitlab.com' - click_link 'Cancel' # draft is deleted on cancel + click_button 'Apply' + end - page.go_back + page.within content_editor_testid do + expect(page).to have_selector('a[href="https://about.gitlab.com"]') + expect(page.find('a')).to have_text('GitLab home page') + expect(page).to have_selector('strong', text: 'page') + end + end - expect(page).not_to have_text('Typing text in the content editor') - end + it 'updates the link attributes and text if text is updated' do + page.within '[data-testid="link-bubble-menu"]' do + page.find('[data-testid="edit-link"]').click - describe 'formatting bubble menu' do - it 'shows a formatting bubble menu for a regular paragraph and headings' do - switch_to_content_editor + fill_in 'link-text', with: 'GitLab about page' + fill_in 'link-href', with: 'https://about.gitlab.com' - expect(page).to have_css(content_editor_testid) + click_button 'Apply' + end - type_in_content_editor 'Typing text in the content editor' - type_in_content_editor [:shift, :left] + page.within content_editor_testid do + expect(page).to have_selector('a[href="https://about.gitlab.com"]', + text: 'GitLab about page' + ) + expect(page).not_to have_selector('strong') + end + end - expect_formatting_menu_to_be_visible + it 'does nothing if Cancel is clicked' do + page.within '[data-testid="link-bubble-menu"]' do + page.find('[data-testid="edit-link"]').click - type_in_content_editor [:right, :right, :enter, '## Heading'] + click_button 'Cancel' + end - expect_formatting_menu_to_be_hidden + page.within content_editor_testid do + expect(page).to have_selector('a[href="https://gitlab.com"]', + text: 'GitLab home page' + ) + expect(page).to have_selector('strong') + end + end - type_in_content_editor [:shift, :left] + context 'when the user clicks the unlink button' do + it 'removes the link' do + page.within '[data-testid="link-bubble-menu"]' do + page.find('[data-testid="remove-link"]').click + end + + page.within content_editor_testid do + expect(page).not_to have_selector('a') + expect(page).to have_selector('strong', text: 'page') + end + end + end + end + + context 'when selection spans more than a link' do + before do + type_in_content_editor 'a [b **c**](https://gitlab.com)' + + type_in_content_editor [:shift, :left] + type_in_content_editor [:shift, :left] + type_in_content_editor [:shift, :left] + type_in_content_editor [:shift, :left] + type_in_content_editor [:shift, :left] + + page.find('[data-testid="formatting-toolbar"] [data-testid="link"]').click + end + + it 'prefills inline modal with the entire selection' do + page.within '[data-testid="link-bubble-menu"]' do + expect(page).to have_field('link-text', with: 'a b c') + expect(page).to have_field('link-href', with: '') + end + end - expect_formatting_menu_to_be_visible + it 'expands the link and updates the link attributes if text is not updated' do + page.within '[data-testid="link-bubble-menu"]' do + fill_in 'link-href', with: 'https://about.gitlab.com' + + click_button 'Apply' + end + + page.within content_editor_testid do + expect(page).to have_selector('a[href="https://about.gitlab.com"]') + expect(page.find('a')).to have_text('a b c') + expect(page).to have_selector('strong', text: 'c') + end + end + + it 'expands the link, updates the link attributes and text if text is updated' do + page.within '[data-testid="link-bubble-menu"]' do + fill_in 'link-text', with: 'new text' + fill_in 'link-href', with: 'https://about.gitlab.com' + + click_button 'Apply' + end + + page.within content_editor_testid do + expect(page).to have_selector('a[href="https://about.gitlab.com"]', + text: 'new text' + ) + expect(page).not_to have_selector('strong') + end + end end end - describe 'media elements bubble menu' do + describe 'selecting text' do before do switch_to_content_editor - open_insert_media_dropdown + # delete all text first + type_in_content_editor [modifier_key, 'a'] + type_in_content_editor :backspace + + type_in_content_editor 'The quick **brown** fox _jumps_ over the lazy dog!' + type_in_content_editor :enter + type_in_content_editor '[Link](https://gitlab.com)' + type_in_content_editor :enter + type_in_content_editor 'Jackdaws love my ~~big~~ sphinx of quartz!' + + # select all text + type_in_content_editor [modifier_key, 'a'] end - def test_displays_media_bubble_menu(media_element_selector, fixture_file) - upload_asset fixture_file + it 'renders selected text in a .content-editor-selection class' do + page.within content_editor_testid do + assert_selected 'The quick' + assert_selected 'brown' + assert_selected 'fox' + assert_selected 'jumps' + assert_selected 'over the lazy dog!' - wait_for_requests + assert_selected 'Link' - expect(page).to have_css(media_element_selector) + assert_selected 'Jackdaws love my' + assert_selected 'big' + assert_selected 'sphinx of quartz!' + end + end - page.find(media_element_selector).click + def assert_selected(text) + expect(page).to have_selector('.content-editor-selection', text: text) + end + end - expect_formatting_menu_to_be_hidden - expect_media_bubble_menu_to_be_visible + describe 'media elements bubble menu' do + before do + switch_to_content_editor + + click_attachment_button end it 'displays correct media bubble menu for images', :js do - test_displays_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'dk.png' + display_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'dk.png' + + expect_media_bubble_menu_to_be_visible end it 'displays correct media bubble menu for video', :js do - test_displays_media_bubble_menu '[data-testid="content_editor_editablebox"] video', 'video_sample.mp4' + display_media_bubble_menu '[data-testid="content_editor_editablebox"] video', 'video_sample.mp4' + + expect_media_bubble_menu_to_be_visible end end @@ -150,7 +344,6 @@ RSpec.shared_examples 'edits content using the content editor' do type_in_content_editor 'var a = 0' type_in_content_editor [:shift, :left] - expect_formatting_menu_to_be_hidden expect(page).to have_css('[data-testid="code-block-bubble-menu"]') end @@ -187,8 +380,8 @@ RSpec.shared_examples 'edits content using the content editor' do expect(iframe['src']).to include('/-/sandbox/mermaid') within_frame(iframe) do - expect(find('svg').text).to include('JohnDoe12') - expect(find('svg').text).to include('HelloWorld34') + expect(find('svg .nodes').text).to include('JohnDoe12') + expect(find('svg .nodes').text).to include('HelloWorld34') end expect(iframe['height'].to_i).to be > 100 @@ -198,12 +391,13 @@ RSpec.shared_examples 'edits content using the content editor' do within_frame(iframe) do page.has_content?('JaneDoe34') - expect(find('svg').text).to include('JaneDoe34') - expect(find('svg').text).to include('HelloWorld56') + expect(find('svg .nodes').text).to include('JaneDoe34') + expect(find('svg .nodes').text).to include('HelloWorld56') end end - it 'toggles the diagram when preview button is clicked' do + it 'toggles the diagram when preview button is clicked', + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/397682' do find('[data-testid="preview-diagram"]').click expect(find(content_editor_testid)).not_to have_selector('iframe') @@ -213,8 +407,61 @@ RSpec.shared_examples 'edits content using the content editor' do iframe = find(content_editor_testid).find('iframe') within_frame(iframe) do - expect(find('svg').text).to include('JohnDoe12') - expect(find('svg').text).to include('HelloWorld34') + expect(find('svg .nodes').text).to include('JohnDoe12') + expect(find('svg .nodes').text).to include('HelloWorld34') + end + end + end + + describe 'pasting text' do + before do + switch_to_content_editor + + type_in_content_editor "Some **rich** _text_ ~~content~~ [link](https://gitlab.com)" + + type_in_content_editor [modifier_key, 'a'] + type_in_content_editor [modifier_key, 'x'] + end + + it 'pastes text with formatting if ctrl + v is pressed' do + type_in_content_editor [modifier_key, 'v'] + + page.within content_editor_testid do + expect(page).to have_selector('strong', text: 'rich') + expect(page).to have_selector('em', text: 'text') + expect(page).to have_selector('s', text: 'content') + expect(page).to have_selector('a[href="https://gitlab.com"]', text: 'link') + end + end + + it 'pastes raw text without formatting if shift + ctrl + v is pressed' do + type_in_content_editor [modifier_key, :shift, 'v'] + + page.within content_editor_testid do + expect(page).to have_text('Some rich text content link') + + expect(page).not_to have_selector('strong') + expect(page).not_to have_selector('em') + expect(page).not_to have_selector('s') + expect(page).not_to have_selector('a') + end + end + + it 'pastes raw text without formatting, stripping whitespaces, if shift + ctrl + v is pressed' do + type_in_content_editor " Some **rich**" + type_in_content_editor :enter + type_in_content_editor " _text_" + type_in_content_editor :enter + type_in_content_editor " ~~content~~" + type_in_content_editor :enter + type_in_content_editor " [link](https://gitlab.com)" + + type_in_content_editor [modifier_key, 'a'] + type_in_content_editor [modifier_key, 'x'] + type_in_content_editor [modifier_key, :shift, 'v'] + + page.within content_editor_testid do + expect(page).to have_text('Some rich text content link') end end end @@ -225,7 +472,7 @@ RSpec.shared_examples 'edits content using the content editor' do before do if defined?(project) create(:issue, project: project, title: 'My Cool Linked Issue') - create(:merge_request, source_project: project, title: 'My Cool Merge Request') + create(:merge_request, source_project: project, source_branch: 'branch-1', title: 'My Cool Merge Request') create(:label, project: project, title: 'My Cool Label') create(:milestone, project: project, title: 'My Cool Milestone') @@ -234,7 +481,7 @@ RSpec.shared_examples 'edits content using the content editor' do project = create(:project, group: group) create(:issue, project: project, title: 'My Cool Linked Issue') - create(:merge_request, source_project: project, title: 'My Cool Merge Request') + create(:merge_request, source_project: project, source_branch: 'branch-1', title: 'My Cool Merge Request') create(:group_label, group: group, title: 'My Cool Label') create(:milestone, group: group, title: 'My Cool Milestone') @@ -251,7 +498,9 @@ RSpec.shared_examples 'edits content using the content editor' do expect(find(suggestions_dropdown)).to have_text('abc123') expect(find(suggestions_dropdown)).to have_text('all') - expect(find(suggestions_dropdown)).to have_text('Group Members (2)') + expect(find(suggestions_dropdown)).to have_text('Group Members') + + type_in_content_editor 'bc' send_keys [:arrow_down, :enter] @@ -332,3 +581,23 @@ RSpec.shared_examples 'edits content using the content editor' do end end end + +RSpec.shared_examples 'inserts diagrams.net diagram using the content editor' do + include ContentEditorHelpers + + before do + switch_to_content_editor + + click_attachment_button + end + + it 'displays correct media bubble menu with edit diagram button' do + display_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'diagram.drawio.svg' + + expect_media_bubble_menu_to_be_visible + + click_edit_diagram_button + + expect_drawio_editor_is_opened + end +end diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb index 96e57980c68..7e0e235698e 100644 --- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb @@ -5,20 +5,20 @@ RSpec.shared_examples 'a creatable merge request' do include ListboxHelpers it 'creates new merge request', :js do - find('.js-assignee-search').click + find('[data-testid="assignee-ids-dropdown-toggle"]').click page.within '.dropdown-menu-user' do click_link user2.name end expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s) - page.within '.js-assignee-search' do + page.within '[data-testid="assignee-ids-dropdown-toggle"]' do expect(page).to have_content user2.name end click_link 'Assign to me' expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user.id.to_s) - page.within '.js-assignee-search' do + page.within '[data-testid="assignee-ids-dropdown-toggle"]' do expect(page).to have_content user.name end diff --git a/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb b/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb index efbd735c451..9b5d9d66890 100644 --- a/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/dashboard/sidebar_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples "a dashboard page with sidebar" do |page_path, menu_label| +RSpec.shared_examples 'a "Your work" page with sidebar and breadcrumbs' do |page_path, menu_label| before do sign_in(user) visit send(page_path) @@ -18,4 +18,13 @@ RSpec.shared_examples "a dashboard page with sidebar" do |page_path, menu_label| expect(page).to have_css(active_menu_item_css) end end + + describe "breadcrumbs" do + it 'has "Your work" as its root breadcrumb' do + breadcrumbs = page.find('[data-testid="breadcrumb-links"]') + within breadcrumbs do + expect(page).to have_css("li:first-child a[href=\"#{root_path}\"]", text: "Your work") + end + end + end end diff --git a/spec/support/shared_examples/features/deploy_token_shared_examples.rb b/spec/support/shared_examples/features/deploy_token_shared_examples.rb index 9fe08e5c996..80f5f1d805c 100644 --- a/spec/support/shared_examples/features/deploy_token_shared_examples.rb +++ b/spec/support/shared_examples/features/deploy_token_shared_examples.rb @@ -17,9 +17,11 @@ RSpec.shared_examples 'a deploy token in settings' do it 'add a new deploy token', :js do visit page_path - fill_in _('Name'), with: 'new_deploy_key' - fill_in _('Expiration date (optional)'), with: (Date.today + 1.month).to_s - fill_in _('Username (optional)'), with: 'deployer' + within('#js-deploy-tokens') do + fill_in _('Name'), with: 'new_deploy_key' + fill_in _('Expiration date (optional)'), with: (Date.today + 1.month).to_s + fill_in _('Username (optional)'), with: 'deployer' + end check 'read_repository' check 'read_registry' click_button 'Create deploy token' diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb index ea6d1655694..14e53dc8655 100644 --- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb @@ -77,15 +77,21 @@ RSpec.shared_examples 'an editable merge request' do expect(page).to have_selector('.js-quick-submit') end - it 'warns about version conflict' do + it 'warns about version conflict', :js do merge_request.update!(title: "New title") fill_in 'merge_request_title', with: 'bug 345' fill_in 'merge_request_description', with: 'bug description' - click_button 'Save changes' + click_button _('Save changes') - expect(page).to have_content 'Someone edited the merge request the same time you did' + expect(page).to have_content( + format( + _("Someone edited this %{model_name} at the same time you did. Please check out the %{link_to_model} and make sure your changes will not unintentionally remove theirs."), # rubocop:disable Layout/LineLength + model_name: _('merge request'), + link_to_model: _('merge request') + ) + ) end it 'preserves description textarea height', :js do @@ -104,8 +110,8 @@ RSpec.shared_examples 'an editable merge request' do fill_in 'merge_request_description', with: long_description height = get_textarea_height - find('.js-md-preview-button').click - find('.js-md-write-button').click + click_button("Preview") + click_button("Continue editing") new_height = get_textarea_height expect(height).to eq(new_height) diff --git a/spec/support/shared_examples/features/explore/sidebar_shared_examples.rb b/spec/support/shared_examples/features/explore/sidebar_shared_examples.rb new file mode 100644 index 00000000000..1754c8bf53d --- /dev/null +++ b/spec/support/shared_examples/features/explore/sidebar_shared_examples.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'an "Explore" page with sidebar and breadcrumbs' do |page_path, menu_label| + before do + visit send(page_path) + end + + let(:sidebar_css) { 'aside.nav-sidebar[aria-label="Explore"]' } + let(:active_menu_item_css) { "li.active[data-track-label=\"#{menu_label}_menu\"]" } + + it 'shows the "Explore" sidebar' do + expect(page).to have_css(sidebar_css) + end + + it 'shows the correct sidebar menu item as active' do + within(sidebar_css) do + expect(page).to have_css(active_menu_item_css) + end + end + + describe 'breadcrumbs' do + it 'has "Explore" as its root breadcrumb' do + within '.breadcrumbs-list' do + expect(page).to have_css("li:first a[href=\"#{explore_root_path}\"]", text: 'Explore') + end + end + end +end diff --git a/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb b/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb index dab125caa60..b8e42843e6f 100644 --- a/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb +++ b/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'for each incident details route' do |example, tab_text:| +RSpec.shared_examples 'for each incident details route' do |example, tab_text:, tab:| before do sign_in(user) visit incident_path @@ -25,4 +25,16 @@ RSpec.shared_examples 'for each incident details route' do |example, tab_text:| it_behaves_like example end + + context "for /-/issues/incident/:id/#{tab} route" do + let(:incident_path) { incident_project_issues_path(project, incident, tab) } + + it_behaves_like example + end + + context "for /-/issues/:id/#{tab} route" do + let(:incident_path) { incident_issue_project_issue_path(project, incident, tab) } + + it_behaves_like example + end end diff --git a/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb b/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb index 4c312b42c0a..148ff2cfb54 100644 --- a/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb +++ b/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb @@ -8,7 +8,7 @@ RSpec.shared_examples 'user activates the Mattermost Slash Command integration' it 'shows a token placeholder' do token_placeholder = find_field('service_token')['placeholder'] - expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx') + expect(token_placeholder).to eq('') end it 'redirects to the integrations page after saving but not activating' do diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb index b6f7094e422..b8c6b85adb2 100644 --- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb +++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'issuable invite members' do - include Spec::Support::Helpers::Features::InviteMembersModalHelper + include Features::InviteMembersModalHelpers context 'when a privileged user can invite' do before do @@ -17,8 +17,6 @@ RSpec.shared_examples 'issuable invite members' do page.within '.dropdown-menu-user' do expect(page).to have_link('Invite Members') - expect(page).to have_selector('[data-track-action="click_invite_members"]') - expect(page).to have_selector('[data-track-label="edit_assignee"]') end click_link 'Invite Members' diff --git a/spec/support/shared_examples/features/manage_applications_shared_examples.rb b/spec/support/shared_examples/features/manage_applications_shared_examples.rb index b59f3f1e27b..b8fd58e7efa 100644 --- a/spec/support/shared_examples/features/manage_applications_shared_examples.rb +++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb @@ -5,87 +5,40 @@ RSpec.shared_examples 'manage applications' do let_it_be(:application_name_changed) { "#{application_name} changed" } let_it_be(:application_redirect_uri) { 'https://foo.bar' } - context 'when hash_oauth_secrets flag set' do - before do - stub_feature_flags(hash_oauth_secrets: true) - end - - it 'allows user to manage applications', :js do - visit new_application_path + it 'allows user to manage applications', :js do + visit new_application_path - expect(page).to have_content 'Add new application' + expect(page).to have_content 'Add new application' - fill_in :doorkeeper_application_name, with: application_name - fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri - check :doorkeeper_application_scopes_read_user - click_on 'Save application' + fill_in :doorkeeper_application_name, with: application_name + fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri + check :doorkeeper_application_scopes_read_user + click_on 'Save application' - validate_application(application_name, 'Yes') - expect(page).to have_content _('This is the only time the secret is accessible. Copy the secret and store it securely') - expect(page).to have_link('Continue', href: index_path) + validate_application(application_name, 'Yes') + expect(page).to have_content _('This is the only time the secret is accessible. Copy the secret and store it securely') + expect(page).to have_link('Continue', href: index_path) - expect(page).to have_css("button[title=\"Copy secret\"]", text: 'Copy') + expect(page).to have_button(_('Copy secret')) - click_on 'Edit' + click_on 'Edit' - application_name_changed = "#{application_name} changed" + application_name_changed = "#{application_name} changed" - fill_in :doorkeeper_application_name, with: application_name_changed - uncheck :doorkeeper_application_confidential - click_on 'Save application' - - validate_application(application_name_changed, 'No') - expect(page).not_to have_link('Continue') - expect(page).to have_content _('The secret is only available when you first create the application') - - visit_applications_path - - page.within '.oauth-applications' do - click_on 'Destroy' - end - expect(page.find('.oauth-applications')).not_to have_content 'test_changed' - end - end - - context 'when hash_oauth_secrets flag not set' do - before do - stub_feature_flags(hash_oauth_secrets: false) - end - - it 'allows user to manage applications', :js do - visit new_application_path - - expect(page).to have_content 'Add new application' - - fill_in :doorkeeper_application_name, with: application_name - fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri - check :doorkeeper_application_scopes_read_user - click_on 'Save application' - - validate_application(application_name, 'Yes') - expect(page).to have_link('Continue', href: index_path) - - application = Doorkeeper::Application.find_by(name: application_name) - expect(page).to have_css("button[title=\"Copy secret\"][data-clipboard-text=\"#{application.secret}\"]", text: 'Copy') - - click_on 'Edit' - - application_name_changed = "#{application_name} changed" - - fill_in :doorkeeper_application_name, with: application_name_changed - uncheck :doorkeeper_application_confidential - click_on 'Save application' + fill_in :doorkeeper_application_name, with: application_name_changed + uncheck :doorkeeper_application_confidential + click_on 'Save application' - validate_application(application_name_changed, 'No') - expect(page).not_to have_link('Continue') + validate_application(application_name_changed, 'No') + expect(page).not_to have_link('Continue') + expect(page).to have_content _('The secret is only available when you create the application or renew the secret.') - visit_applications_path + visit_applications_path - page.within '.oauth-applications' do - click_on 'Destroy' - end - expect(page.find('.oauth-applications')).not_to have_content 'test_changed' + page.within '.oauth-applications' do + click_on 'Destroy' end + expect(page.find('.oauth-applications')).not_to have_content 'test_changed' end context 'when scopes are blank' do diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb index c2dc87b0fb0..6487e6a94c1 100644 --- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb +++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'Maintainer manages access requests' do - include Spec::Support::Helpers::Features::MembersHelpers + include Features::MembersHelpers let(:user) { create(:user) } let(:maintainer) { create(:user) } diff --git a/spec/support/shared_examples/features/milestone_editing_shared_examples.rb b/spec/support/shared_examples/features/milestone_editing_shared_examples.rb new file mode 100644 index 00000000000..d21bf62ecfa --- /dev/null +++ b/spec/support/shared_examples/features/milestone_editing_shared_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'milestone handling version conflicts' do + it 'warns about version conflict when milestone has been updated in the background' do + # Update the milestone in the background in order to trigger a version conflict + milestone.update!(title: "New title") + + fill_in _('Title'), with: 'Title for version conflict' + fill_in _('Description'), with: 'Description for version conflict' + + click_button _('Save changes') + + expect(page).to have_content( + format( + _("Someone edited this %{model_name} at the same time you did. Please check out the %{link_to_model} and make sure your changes will not unintentionally remove theirs."), # rubocop:disable Layout/LineLength + model_name: _('milestone'), + link_to_model: _('milestone') + ) + ) + end +end diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb index f09cf0613a1..5126e849c2e 100644 --- a/spec/support/shared_examples/features/packages_shared_examples.rb +++ b/spec/support/shared_examples/features/packages_shared_examples.rb @@ -9,7 +9,7 @@ RSpec.shared_examples 'packages list' do |check_project_name: false| expect(package_row).to have_content(pkg.name) expect(package_row).to have_content(pkg.version) - expect(package_row).to have_content(pkg.project.name) if check_project_name + expect(package_row).to have_content(pkg.project.path) if check_project_name end end @@ -18,7 +18,35 @@ RSpec.shared_examples 'packages list' do |check_project_name: false| end end +RSpec.shared_examples 'pipelines on packages list' do + let_it_be(:pipelines) do + %w[c83d6e391c22777fca1ed3012fce84f633d7fed0 + d83d6e391c22777fca1ed3012fce84f633d7fed0].map do |sha| + create(:ci_pipeline, project: project, sha: sha) + end + end + + before do + pipelines.each do |pipeline| + create(:package_build_info, package: package, pipeline: pipeline) + end + end + + it 'shows the latest pipeline' do + # Test after reload + page.evaluate_script 'window.location.reload()' + + wait_for_requests + + expect(page).to have_content('d83d6e39') + end +end + RSpec.shared_examples 'package details link' do |property| + before do + stub_application_setting(npm_package_requests_forwarding: false) + end + it 'navigates to the correct url' do page.within(packages_table_selector) do click_link package.name @@ -30,6 +58,45 @@ RSpec.shared_examples 'package details link' do |property| expect(page).to have_content('Installation') expect(page).to have_content('Registry setup') + expect(page).to have_content('Other versions 0') + end + + context 'with other versions' do + let_it_be(:npm_package1) { create(:npm_package, project: project, name: 'zzz', version: '1.1.0') } + let_it_be(:npm_package2) { create(:npm_package, project: project, name: 'zzz', version: '1.2.0') } + + before do + page.within(packages_table_selector) do + first(:link, package.name).click + end + end + + it 'shows tab with count' do + expect(page).to have_content('Other versions 2') + end + + it 'visiting tab shows total on page' do + click_link 'Other versions' + + expect(page).to have_content('2 versions') + end + + it 'deleting version updates count' do + click_link 'Other versions' + + find('[data-testid="delete-dropdown"]', match: :first).click + find('[data-testid="action-delete"]', match: :first).click + click_button('Permanently delete') + + expect(page).to have_content 'Package deleted successfully' + + expect(page).to have_content('Other versions 1') + expect(page).to have_content('1 version') + + expect(page).not_to have_content('1.0.0') + expect(page).to have_content('1.1.0') + expect(page).to have_content('1.2.0') + end end end diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb index 81d548e000a..2d3f1949716 100644 --- a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb +++ b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb @@ -1,126 +1,67 @@ # frozen_string_literal: true RSpec.shared_examples "protected branches > access control > CE" do - ProtectedRefAccess::HUMAN_ACCESS_LEVELS.each do |(access_type_id, access_type_name)| + let(:no_one) { ProtectedRef::AccessLevel.humanize(::Gitlab::Access::NO_ACCESS) } + + ProtectedRef::AccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| it "allows creating protected branches that #{access_type_name} can push to" do visit project_protected_branches_path(project) set_protected_branch_name('master') - - find(".js-allowed-to-merge").click - within('[data-testid="allowed-to-merge-dropdown"]') do - expect(first("li")).to have_content("Roles") - find(:link, 'No one').click - end - - within('.js-new-protected-branch') do - allowed_to_push_button = find(".js-allowed-to-push") - - unless allowed_to_push_button.text == access_type_name - allowed_to_push_button.click - within(".dropdown.show .dropdown-menu") { click_on access_type_name } - end - end - + set_allowed_to('merge', no_one) + set_allowed_to('push', access_type_name) click_on_protect - wait_for_requests expect(ProtectedBranch.count).to eq(1) expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id]) end - it "allows updating protected branches so that #{access_type_name} can push to them" do + it "allows creating protected branches that #{access_type_name} can merge to" do visit project_protected_branches_path(project) set_protected_branch_name('master') - - find(".js-allowed-to-merge").click - within('[data-testid="allowed-to-merge-dropdown"]') do - expect(first("li")).to have_content("Roles") - find(:link, 'No one').click - end - - find(".js-allowed-to-push").click - within('[data-testid="allowed-to-push-dropdown"]') do - expect(first("li")).to have_content("Roles") - find(:link, 'No one').click - end - + set_allowed_to('merge', access_type_name) + set_allowed_to('push', no_one) click_on_protect expect(ProtectedBranch.count).to eq(1) - - within(".protected-branches-list") do - find(".js-allowed-to-push").click - - within('.js-allowed-to-push-container') do - expect(first("li")).to have_content("Roles") - find(:link, access_type_name).click - end - - find(".js-allowed-to-push").click - end - - wait_for_requests - - expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id) + expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id]) end - end - ProtectedRefAccess::HUMAN_ACCESS_LEVELS.each do |(access_type_id, access_type_name)| - it "allows creating protected branches that #{access_type_name} can merge to" do + it "allows updating protected branches so that #{access_type_name} can push to them" do visit project_protected_branches_path(project) set_protected_branch_name('master') + set_allowed_to('merge', no_one) + set_allowed_to('push', no_one) + click_on_protect - within('.js-new-protected-branch') do - allowed_to_merge_button = find(".js-allowed-to-merge") + expect(ProtectedBranch.count).to eq(1) - unless allowed_to_merge_button.text == access_type_name - allowed_to_merge_button.click - within(".dropdown.show .dropdown-menu") { click_on access_type_name } + within(".protected-branches-list") do + within_select(".js-allowed-to-push") do + click_on(access_type_name) end end - find(".js-allowed-to-push").click - within('[data-testid="allowed-to-push-dropdown"]') do - expect(first("li")).to have_content("Roles") - find(:link, 'No one').click - end - - click_on_protect + wait_for_requests - expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id]) + expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id) end it "allows updating protected branches so that #{access_type_name} can merge to them" do visit project_protected_branches_path(project) set_protected_branch_name('master') - - find(".js-allowed-to-merge").click - within('[data-testid="allowed-to-merge-dropdown"]') do - expect(first("li")).to have_content("Roles") - find(:link, 'No one').click - end - - find(".js-allowed-to-push").click - within('[data-testid="allowed-to-push-dropdown"]') do - expect(first("li")).to have_content("Roles") - find(:link, 'No one').click - end - + set_allowed_to('merge', no_one) + set_allowed_to('push', no_one) click_on_protect expect(ProtectedBranch.count).to eq(1) within(".protected-branches-list") do - find(".js-allowed-to-merge").click - - within('.js-allowed-to-merge-container') do - expect(first("li")).to have_content("Roles") - find(:link, access_type_name).click + within_select(".js-allowed-to-merge") do + click_on(access_type_name) end end diff --git a/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb new file mode 100644 index 00000000000..cc0984b6226 --- /dev/null +++ b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'Deploy keys with protected tags' do + let(:dropdown_sections_minus_deploy_keys) { all_dropdown_sections - ['Deploy Keys'] } + + context 'when deploy keys are enabled to this project' do + let!(:deploy_key_1) { create(:deploy_key, title: 'title 1', projects: [project]) } + let!(:deploy_key_2) { create(:deploy_key, title: 'title 2', projects: [project]) } + + context 'when only one deploy key can push' do + before do + deploy_key_1.deploy_keys_projects.first.update!(can_push: true) + end + + it "shows all dropdown sections in the 'Allowed to create' main dropdown, with only one deploy key" do + visit project_protected_tags_path(project) + + find(".js-allowed-to-create").click + wait_for_requests + + within('[data-testid="allowed-to-create-dropdown"]') do + dropdown_headers = page.all('.dropdown-header').map(&:text) + + expect(dropdown_headers).to contain_exactly(*all_dropdown_sections) + expect(page).to have_content('title 1') + expect(page).not_to have_content('title 2') + end + end + + it "shows all sections in the 'Allowed to create' update dropdown" do + create(:protected_tag, :no_one_can_create, project: project, name: 'v1.0.0') + + visit project_protected_tags_path(project) + + within(".js-protected-tag-edit-form") do + find(".js-allowed-to-create").click + wait_for_requests + + dropdown_headers = page.all('.dropdown-header').map(&:text) + + expect(dropdown_headers).to contain_exactly(*all_dropdown_sections) + end + end + end + + context 'when no deploy key can push' do + it "just shows all sections but not deploy keys in the 'Allowed to create' dropdown" do + visit project_protected_tags_path(project) + + find(".js-allowed-to-create").click + wait_for_requests + + within('[data-testid="allowed-to-create-dropdown"]') do + dropdown_headers = page.all('.dropdown-header').map(&:text) + + expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys) + end + end + end + end +end diff --git a/spec/support/shared_examples/features/reportable_note_shared_examples.rb b/spec/support/shared_examples/features/reportable_note_shared_examples.rb index bb3fab5b23e..133da230bed 100644 --- a/spec/support/shared_examples/features/reportable_note_shared_examples.rb +++ b/spec/support/shared_examples/features/reportable_note_shared_examples.rb @@ -20,7 +20,7 @@ RSpec.shared_examples 'reportable note' do |type| dropdown = comment.find(more_actions_selector) open_dropdown(dropdown) - expect(dropdown).to have_button('Report abuse to administrator') + expect(dropdown).to have_button('Report abuse') if type == 'issue' || type == 'merge_request' expect(dropdown).to have_button('Delete comment') @@ -33,7 +33,7 @@ RSpec.shared_examples 'reportable note' do |type| dropdown = comment.find(more_actions_selector) open_dropdown(dropdown) - dropdown.click_button('Report abuse to administrator') + dropdown.click_button('Report abuse') choose "They're posting spam." click_button "Next" @@ -48,6 +48,6 @@ RSpec.shared_examples 'reportable note' do |type| restore_window_size dropdown.find('.more-actions-toggle').click - dropdown.find('.dropdown-menu li', match: :first) + dropdown.find('.more-actions li', match: :first) end end diff --git a/spec/support/shared_examples/features/rss_shared_examples.rb b/spec/support/shared_examples/features/rss_shared_examples.rb index ad865b084e1..f6566214e32 100644 --- a/spec/support/shared_examples/features/rss_shared_examples.rb +++ b/spec/support/shared_examples/features/rss_shared_examples.rb @@ -13,6 +13,12 @@ RSpec.shared_examples "it has an RSS button with current_user's feed token" do end end +RSpec.shared_examples "it has an RSS link with current_user's feed token" do + it "shows the RSS link with current_user's feed token" do + expect(page).to have_link 'Subscribe to RSS feed', href: /feed_token=#{user.feed_token}/ + end +end + RSpec.shared_examples "an autodiscoverable RSS feed without a feed token" do it "has an RSS autodiscovery link tag without a feed token" do expect(page).to have_css("link[type*='atom+xml']:not([href*='feed_token'])", visible: false) @@ -26,10 +32,18 @@ RSpec.shared_examples "it has an RSS button without a feed token" do end end +RSpec.shared_examples "it has an RSS link without a feed token" do + it "shows the RSS link without a feed token" do + expect(page).to have_link 'Subscribe to RSS feed' + expect(page).not_to have_link 'Subscribe to RSS feed', href: /feed_token/ + end +end + RSpec.shared_examples "updates atom feed link" do |type| it "for #{type}" do sign_in(user) visit path + click_button 'Actions', match: :first link = find_link('Subscribe to RSS feed') params = CGI.parse(URI.parse(link[:href]).query) diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb index 63a0832117d..7edf306183e 100644 --- a/spec/support/shared_examples/features/runners_shared_examples.rb +++ b/spec/support/shared_examples/features/runners_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'shows and resets runner registration token' do - include Spec::Support::Helpers::Features::RunnersHelpers + include Features::RunnersHelpers include Spec::Support::Helpers::ModalHelpers before do @@ -63,16 +63,15 @@ RSpec.shared_examples 'shows and resets runner registration token' do end RSpec.shared_examples 'shows no runners registered' do - it 'shows total count with 0' do + it 'shows 0 count and the empty state' do expect(find('[data-testid="runner-type-tabs"]')).to have_text "#{s_('Runners|All')} 0" # No stats are shown expect(page).not_to have_text s_('Runners|Online') expect(page).not_to have_text s_('Runners|Offline') expect(page).not_to have_text s_('Runners|Stale') - end - it 'shows "no runners" message' do + # "no runners" message expect(page).to have_text s_('Runners|Get started with runners') end end @@ -84,16 +83,14 @@ RSpec.shared_examples 'shows no runners found' do end RSpec.shared_examples 'shows runner in list' do - it 'does not show empty state' do - expect(page).not_to have_content s_('Runners|Get started with runners') - end - - it 'shows runner row' do + it 'shows runner row and no empty state' do within_runner_row(runner.id) do expect(page).to have_text "##{runner.id}" expect(page).to have_text runner.short_sha expect(page).to have_text runner.description end + + expect(page).not_to have_content s_('Runners|Get started with runners') end end @@ -229,3 +226,33 @@ RSpec.shared_examples 'submits edit runner form' do end end end + +RSpec.shared_examples 'creates runner and shows register page' do + context 'when runner is saved' do + before do + fill_in s_('Runners|Runner description'), with: 'runner-foo' + fill_in s_('Runners|Tags'), with: 'tag1' + click_on _('Submit') + wait_for_requests + end + + it 'navigates to registration page and opens install instructions drawer' do + expect(page.find('[data-testid="alert-success"]')).to have_content(s_('Runners|Runner created.')) + expect(current_url).to match(register_path_pattern) + + click_on 'How do I install GitLab Runner?' + expect(page.find('[data-testid="runner-platforms-drawer"]')).to have_content('gitlab-runner install') + end + + it 'warns from leaving page without finishing registration' do + click_on s_('Runners|Go to runners page') + + alert = page.driver.browser.switch_to.alert + + expect(alert).not_to be_nil + alert.dismiss + + expect(current_url).to match(register_path_pattern) + end + end +end diff --git a/spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb b/spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb index 4d242d0e719..cbd0ffbab21 100644 --- a/spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb +++ b/spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb @@ -48,14 +48,18 @@ RSpec.shared_examples 'a redacted search results' do it 'redacts the inaccessible issue' do expect(search_service.send(:logger)) .to receive(:error) - .with(hash_including( - message: "redacted_search_results", - current_user_id: user.id, - query: search, - filtered: array_including( - [ - { class_name: 'Issue', id: unreadable.id, ability: :read_issue } - ]))) + .with( + hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'Issue', id: unreadable.id, ability: :read_issue } + ] + ) + ) + ) expect(result).to contain_exactly(readable) end @@ -95,16 +99,18 @@ RSpec.shared_examples 'a redacted search results' do end let(:unredacted_results) do - ar_relation(Note, - readable_note_on_commit, - readable_diff_note, - readable_note_on_mr, - readable_diff_note_on_mr, - readable_note_on_project_snippet, - unreadable_note_on_commit, - unreadable_diff_note, - unreadable_note_on_mr, - unreadable_note_on_project_snippet) + ar_relation( + Note, + readable_note_on_commit, + readable_diff_note, + readable_note_on_mr, + readable_diff_note_on_mr, + readable_note_on_project_snippet, + unreadable_note_on_commit, + unreadable_diff_note, + unreadable_note_on_mr, + unreadable_note_on_project_snippet + ) end let(:scope) { 'notes' } @@ -112,23 +118,29 @@ RSpec.shared_examples 'a redacted search results' do it 'redacts the inaccessible notes' do expect(search_service.send(:logger)) .to receive(:error) - .with(hash_including( - message: "redacted_search_results", - current_user_id: user.id, - query: search, - filtered: array_including( - [ - { class_name: 'Note', id: unreadable_note_on_commit.id, ability: :read_note }, - { class_name: 'DiffNote', id: unreadable_diff_note.id, ability: :read_note }, - { class_name: 'DiscussionNote', id: unreadable_note_on_mr.id, ability: :read_note }, - { class_name: 'Note', id: unreadable_note_on_project_snippet.id, ability: :read_note } - ]))) - - expect(result).to contain_exactly(readable_note_on_commit, - readable_diff_note, - readable_note_on_mr, - readable_diff_note_on_mr, - readable_note_on_project_snippet) + .with( + hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'Note', id: unreadable_note_on_commit.id, ability: :read_note }, + { class_name: 'DiffNote', id: unreadable_diff_note.id, ability: :read_note }, + { class_name: 'DiscussionNote', id: unreadable_note_on_mr.id, ability: :read_note }, + { class_name: 'Note', id: unreadable_note_on_project_snippet.id, ability: :read_note } + ] + ) + ) + ) + + expect(result).to contain_exactly( + readable_note_on_commit, + readable_diff_note, + readable_note_on_mr, + readable_diff_note_on_mr, + readable_note_on_project_snippet + ) end end @@ -141,14 +153,18 @@ RSpec.shared_examples 'a redacted search results' do it 'redacts the inaccessible merge request' do expect(search_service.send(:logger)) .to receive(:error) - .with(hash_including( - message: "redacted_search_results", - current_user_id: user.id, - query: search, - filtered: array_including( - [ - { class_name: 'MergeRequest', id: unreadable.id, ability: :read_merge_request } - ]))) + .with( + hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'MergeRequest', id: unreadable.id, ability: :read_merge_request } + ] + ) + ) + ) expect(result).to contain_exactly(readable) end @@ -169,14 +185,18 @@ RSpec.shared_examples 'a redacted search results' do it 'redacts the inaccessible blob' do expect(search_service.send(:logger)) .to receive(:error) - .with(hash_including( - message: "redacted_search_results", - current_user_id: user.id, - query: search, - filtered: array_including( - [ - { class_name: 'Gitlab::Search::FoundBlob', id: unreadable.id, ability: :read_blob } - ]))) + .with( + hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'Gitlab::Search::FoundBlob', id: unreadable.id, ability: :read_blob } + ] + ) + ) + ) expect(result).to contain_exactly(readable) end @@ -191,14 +211,18 @@ RSpec.shared_examples 'a redacted search results' do it 'redacts the inaccessible blob' do expect(search_service.send(:logger)) .to receive(:error) - .with(hash_including( - message: "redacted_search_results", - current_user_id: user.id, - query: search, - filtered: array_including( - [ - { class_name: 'Gitlab::Search::FoundWikiPage', id: unreadable.id, ability: :read_wiki_page } - ]))) + .with( + hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'Gitlab::Search::FoundWikiPage', id: unreadable.id, ability: :read_wiki_page } + ] + ) + ) + ) expect(result).to contain_exactly(readable) end @@ -213,14 +237,18 @@ RSpec.shared_examples 'a redacted search results' do it 'redacts the inaccessible snippet' do expect(search_service.send(:logger)) .to receive(:error) - .with(hash_including( - message: "redacted_search_results", - current_user_id: user.id, - query: search, - filtered: array_including( - [ - { class_name: 'ProjectSnippet', id: unreadable.id, ability: :read_snippet } - ]))) + .with( + hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'ProjectSnippet', id: unreadable.id, ability: :read_snippet } + ] + ) + ) + ) expect(result).to contain_exactly(readable) end @@ -239,14 +267,18 @@ RSpec.shared_examples 'a redacted search results' do it 'redacts the inaccessible snippet' do expect(search_service.send(:logger)) .to receive(:error) - .with(hash_including( - message: "redacted_search_results", - current_user_id: user.id, - query: search, - filtered: array_including( - [ - { class_name: 'PersonalSnippet', id: unreadable.id, ability: :read_snippet } - ]))) + .with( + hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'PersonalSnippet', id: unreadable.id, ability: :read_snippet } + ] + ) + ) + ) expect(result).to contain_exactly(readable) end @@ -265,14 +297,18 @@ RSpec.shared_examples 'a redacted search results' do it 'redacts the inaccessible commit' do expect(search_service.send(:logger)) .to receive(:error) - .with(hash_including( - message: "redacted_search_results", - current_user_id: user.id, - query: search, - filtered: array_including( - [ - { class_name: 'Commit', id: unreadable.id, ability: :read_commit } - ]))) + .with( + hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'Commit', id: unreadable.id, ability: :read_commit } + ] + ) + ) + ) expect(result).to contain_exactly(readable) end diff --git a/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb b/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb index 028e075c87a..231406289b4 100644 --- a/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb +++ b/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb @@ -10,7 +10,7 @@ RSpec.shared_examples 'Secure OAuth Authorizations' do end context 'when user is unconfirmed' do - let(:user) { create(:user, confirmed_at: nil) } + let(:user) { create(:user, :unconfirmed) } it 'displays an error' do expect(page).to have_text I18n.t('doorkeeper.errors.messages.unconfirmed_email') diff --git a/spec/support/shared_examples/features/trial_email_validation_shared_example.rb b/spec/support/shared_examples/features/trial_email_validation_shared_example.rb index 8304a91af86..81c9ac1164b 100644 --- a/spec/support/shared_examples/features/trial_email_validation_shared_example.rb +++ b/spec/support/shared_examples/features/trial_email_validation_shared_example.rb @@ -1,59 +1,38 @@ # frozen_string_literal: true RSpec.shared_examples 'user email validation' do - let(:email_hint_message) { 'We recommend a work email address.' } - let(:email_error_message) { 'Please provide a valid email address.' } + let(:email_hint_message) { _('We recommend a work email address.') } + let(:email_error_message) { _('Please provide a valid email address.') } let(:email_warning_message) do - 'This email address does not look right, are you sure you typed it correctly?' + _('This email address does not look right, are you sure you typed it correctly?') end - context 'with trial_email_validation flag enabled' do - it 'shows an error message until a correct email is entered' do - visit path - expect(page).to have_content(email_hint_message) - expect(page).not_to have_content(email_error_message) - expect(page).not_to have_content(email_warning_message) + it 'shows an error message until a correct email is entered' do + visit path + expect(page).to have_content(email_hint_message) + expect(page).not_to have_content(email_error_message) + expect(page).not_to have_content(email_warning_message) - fill_in 'new_user_email', with: 'foo@' - fill_in 'new_user_first_name', with: '' + fill_in 'new_user_email', with: 'foo@' + fill_in 'new_user_first_name', with: '' - expect(page).not_to have_content(email_hint_message) - expect(page).to have_content(email_error_message) - expect(page).not_to have_content(email_warning_message) + expect(page).not_to have_content(email_hint_message) + expect(page).to have_content(email_error_message) + expect(page).not_to have_content(email_warning_message) - fill_in 'new_user_email', with: 'foo@bar' - fill_in 'new_user_first_name', with: '' + fill_in 'new_user_email', with: 'foo@bar' + fill_in 'new_user_first_name', with: '' - expect(page).not_to have_content(email_hint_message) - expect(page).not_to have_content(email_error_message) - expect(page).to have_content(email_warning_message) + expect(page).not_to have_content(email_hint_message) + expect(page).not_to have_content(email_error_message) + expect(page).to have_content(email_warning_message) - fill_in 'new_user_email', with: 'foo@gitlab.com' - fill_in 'new_user_first_name', with: '' + fill_in 'new_user_email', with: 'foo@gitlab.com' + fill_in 'new_user_first_name', with: '' - expect(page).not_to have_content(email_hint_message) - expect(page).not_to have_content(email_error_message) - expect(page).not_to have_content(email_warning_message) - end - end - - context 'when trial_email_validation flag disabled' do - before do - stub_feature_flags trial_email_validation: false - end - - it 'does not show an error message' do - visit path - expect(page).to have_content(email_hint_message) - expect(page).not_to have_content(email_error_message) - expect(page).not_to have_content(email_warning_message) - - fill_in 'new_user_email', with: 'foo@' - - expect(page).to have_content(email_hint_message) - expect(page).not_to have_content(email_error_message) - expect(page).not_to have_content(email_warning_message) - end + expect(page).not_to have_content(email_hint_message) + expect(page).not_to have_content(email_error_message) + expect(page).not_to have_content(email_warning_message) end end diff --git a/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb b/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb new file mode 100644 index 00000000000..0b0c9edcb42 --- /dev/null +++ b/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'variable list pagination' do |variable_type| + first_page_count = 20 + + before do + first_page_count.times do |i| + case variable_type + when :ci_variable + create(variable_type, key: "test_key_#{i}", value: 'test_value', masked: true, project: project) + when :ci_group_variable + create(variable_type, key: "test_key_#{i}", value: 'test_value', masked: true, group: group) + else + create(variable_type, key: "test_key_#{i}", value: 'test_value', masked: true) + end + end + + visit page_path + wait_for_requests + end + + it 'can navigate between pages' do + page.within('[data-testid="ci-variable-table"]') do + expect(page.all('.js-ci-variable-row').length).to be(first_page_count) + end + + click_button 'Next' + wait_for_requests + + page.within('[data-testid="ci-variable-table"]') do + expect(page.all('.js-ci-variable-row').length).to be(1) + end + + click_button 'Previous' + wait_for_requests + + page.within('[data-testid="ci-variable-table"]') do + expect(page.all('.js-ci-variable-row').length).to be(first_page_count) + end + end + + it 'sorts variables alphabetically in ASC and DESC order' do + page.within('[data-testid="ci-variable-table"]') do + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key) + expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]').text).to eq('test_key_8') + end + + click_button 'Next' + wait_for_requests + + page.within('[data-testid="ci-variable-table"]') do + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('test_key_9') + end + + page.within('[data-testid="ci-variable-table"]') do + find('.b-table-sort-icon-left').click + end + + wait_for_requests + + page.within('[data-testid="ci-variable-table"]') do + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('test_key_9') + expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]').text).to eq('test_key_0') + end + end +end diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb index f0b72cfaee3..1211c9d19e6 100644 --- a/spec/support/shared_examples/features/variable_list_shared_examples.rb +++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'variable list' do |is_admin| +RSpec.shared_examples 'variable list' do it 'shows a list of variables' do page.within('[data-testid="ci-variable-table"]') do expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key) @@ -256,14 +256,6 @@ RSpec.shared_examples 'variable list' do |is_admin| expect(find('[data-testid="ci-variable-protected-checkbox"]')).to be_checked end end - - it 'shows a message regarding the changed default' do - if is_admin - expect(page).to have_content 'Environment variables on this GitLab instance are configured to be protected by default' - else - expect(page).to have_content 'Environment variables are configured by your administrator to be protected by default' - end - end end context 'application setting is false' do diff --git a/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb index 7a3b94ad81d..6451c531aec 100644 --- a/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb @@ -62,7 +62,7 @@ RSpec.shared_examples 'wiki file attachments' do attach_with_dropzone(true) wait_for_requests - find('.js-md-preview-button').click + click_button("Preview") file_path = page.find('input[name="files[]"]', visible: :hidden).value link = page.find('a.no-attachment-icon')['href'] img_link = page.find('a.no-attachment-icon img')['src'] diff --git a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb index 3e285bb8ad7..ca68df9a89b 100644 --- a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb @@ -78,7 +78,7 @@ RSpec.shared_examples 'User previews wiki changes' do it_behaves_like 'relative links' do before do - click_on 'Preview' + click_button("Preview") end let(:element) { preview } @@ -88,7 +88,7 @@ RSpec.shared_examples 'User previews wiki changes' do # using two `\n` ensures we're sublist to it's own line due # to list auto-continue fill_in :wiki_content, with: "1. one\n\n - sublist\n" - click_on "Preview" + click_button("Preview") # the above generates two separate lists (not embedded) in CommonMark expect(preview).to have_content("sublist") @@ -102,7 +102,7 @@ RSpec.shared_examples 'User previews wiki changes' do [[also_do_not_linkify]] ``` HEREDOC - click_on "Preview" + click_button("Preview") expect(preview).to have_content("do_not_linkify") expect(preview).to have_content('[[do_not_linkify]]') diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb index 0334187e4b1..c1e4185e058 100644 --- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb @@ -150,6 +150,7 @@ RSpec.shared_examples 'User updates wiki page' do end it_behaves_like 'edits content using the content editor' + it_behaves_like 'inserts diagrams.net diagram using the content editor' it_behaves_like 'autocompletes items' end diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb index a7c32932ba7..767caffd417 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb @@ -9,9 +9,11 @@ RSpec.shared_examples 'User views a wiki page' do let(:path) { 'image.png' } let(:wiki_page) do - create(:wiki_page, - wiki: wiki, - title: 'home', content: "Look at this [image](#{path})\n\n ") + create( + :wiki_page, + wiki: wiki, + title: 'home', content: "Look at this [image](#{path})\n\n " + ) end before do diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb index 639eb3f2b99..21c7e2b6c75 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb @@ -84,6 +84,44 @@ RSpec.shared_examples 'User views wiki sidebar' do expect(page).not_to have_link('View All Pages') end + it 'shows all collapse buttons in the sidebar' do + visit wiki_path(wiki) + + within('.right-sidebar') do + expect(page.all("[data-testid='chevron-down-icon']").size).to eq(3) + end + end + + it 'collapses/expands children when click collapse/expand button in the sidebar', :js do + visit wiki_path(wiki) + + within('.right-sidebar') do + first("[data-testid='chevron-down-icon']").click + (11..15).each { |i| expect(page).not_to have_content("my page #{i}") } + expect(page.all("[data-testid='chevron-down-icon']").size).to eq(1) + expect(page.all("[data-testid='chevron-right-icon']").size).to eq(1) + + first("[data-testid='chevron-right-icon']").click + (11..15).each { |i| expect(page).to have_content("my page #{i}") } + expect(page.all("[data-testid='chevron-down-icon']").size).to eq(3) + expect(page.all("[data-testid='chevron-right-icon']").size).to eq(0) + end + end + + it 'shows create child page button when hover to the page title in the sidebar', :js do + visit wiki_path(wiki) + + within('.right-sidebar') do + first_wiki_list = first("[data-testid='wiki-list']") + wiki_link = first("[data-testid='wiki-list'] a:last-of-type")['href'] + + first_wiki_list.hover + wiki_new_page_link = first("[data-testid='wiki-list'] a")['href'] + + expect(wiki_new_page_link).to eq "#{wiki_link}/%7Bnew_page_title%7D" + end + end + context 'when there are more than 15 existing pages' do before do create(:wiki_page, wiki: wiki, title: 'my page 16') diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb index 4f3d957ad71..526a56e7dab 100644 --- a/spec/support/shared_examples/features/work_items_shared_examples.rb +++ b/spec/support/shared_examples/features/work_items_shared_examples.rb @@ -1,5 +1,20 @@ # frozen_string_literal: true +RSpec.shared_examples 'work items title' do + let(:title_selector) { '[data-testid="work-item-title"]' } + + it 'successfully shows and changes the title of the work item' do + expect(work_item.reload.title).to eq work_item.title + + find(title_selector).set("Work item title") + find(title_selector).native.send_keys(:return) + + wait_for_requests + + expect(work_item.reload.title).to eq 'Work item title' + end +end + RSpec.shared_examples 'work items status' do let(:state_selector) { '[data-testid="work-item-state-select"]' } @@ -15,18 +30,110 @@ RSpec.shared_examples 'work items status' do end end -RSpec.shared_examples 'work items comments' do +RSpec.shared_examples 'work items comments' do |type| let(:form_selector) { '[data-testid="work-item-add-comment"]' } + let(:textarea_selector) { '[data-testid="work-item-add-comment"] #work-item-add-or-edit-comment' } + let(:is_mac) { page.evaluate_script('navigator.platform').include?('Mac') } + let(:modifier_key) { is_mac ? :command : :control } + let(:comment) { 'Test comment' } + + def set_comment + find(form_selector).fill_in(with: comment) + end it 'successfully creates and shows comments' do - click_button 'Add a comment' + set_comment - find(form_selector).fill_in(with: "Test comment") click_button "Comment" wait_for_requests - expect(page).to have_content "Test comment" + page.within(".main-notes-list") do + expect(page).to have_content comment + end + end + + context 'for work item note actions signed in user with developer role' do + it 'shows work item note actions' do + set_comment + + click_button "Comment" + + wait_for_requests + + page.within(".main-notes-list") do + expect(page).to have_selector('[data-testid="work-item-note-actions"]') + + find('[data-testid="work-item-note-actions"]', match: :first).click + + expect(page).to have_selector('[data-testid="copy-link-action"]') + expect(page).not_to have_selector('[data-testid="assign-note-action"]') + end + end + end + + it 'successfully posts comments using shortcut and checks if textarea is blank when reinitiated' do + set_comment + + send_keys([modifier_key, :enter]) + + wait_for_requests + + page.within(".main-notes-list") do + expect(page).to have_content comment + end + + expect(find(textarea_selector)).to have_content "" + end + + context 'when using quick actions' do + it 'autocompletes quick actions common to all work item types', :aggregate_failures do + click_reply_and_enter_slash + + page.within('#at-view-commands') do + expect(page).to have_text("/title") + expect(page).to have_text("/shrug") + expect(page).to have_text("/tableflip") + expect(page).to have_text("/close") + expect(page).to have_text("/cc") + end + end + + context 'when a widget is enabled' do + before do + WorkItems::Type.default_by_type(type).widget_definitions + .find_by_widget_type(:assignees).update!(disabled: false) + end + + it 'autocompletes quick action for the enabled widget' do + click_reply_and_enter_slash + + page.within('#at-view-commands') do + expect(page).to have_text("/assign") + end + end + end + + context 'when a widget is disabled' do + before do + WorkItems::Type.default_by_type(type).widget_definitions + .find_by_widget_type(:assignees).update!(disabled: true) + end + + it 'does not autocomplete quick action for the disabled widget' do + click_reply_and_enter_slash + + page.within('#at-view-commands') do + expect(page).not_to have_text("/assign") + end + end + end + + def click_reply_and_enter_slash + find(form_selector).fill_in(with: "/") + + wait_for_all_requests + end end end @@ -39,7 +146,6 @@ RSpec.shared_examples 'work items assignees' do # submit and simulate blur to save send_keys(:enter) find("body").click - wait_for_requests expect(work_item.assignees).to include(user) @@ -47,6 +153,8 @@ RSpec.shared_examples 'work items assignees' do end RSpec.shared_examples 'work items labels' do + let(:label_title_selector) { '[data-testid="labels-title"]' } + it 'successfully assigns a label' do label = create(:label, project: work_item.project, title: "testing-label") @@ -55,8 +163,7 @@ RSpec.shared_examples 'work items labels' do # submit and simulate blur to save send_keys(:enter) - find("body").click - + find(label_title_selector).click wait_for_requests expect(work_item.labels).to include(label) @@ -83,7 +190,7 @@ RSpec.shared_examples 'work items description' do wait_for_requests - page.within('.atwho-container') do + page.within('#at-view-commands') do expect(page).to have_text("title") expect(page).to have_text("shrug") expect(page).to have_text("tableflip") @@ -125,7 +232,7 @@ RSpec.shared_examples 'work items description' do end RSpec.shared_examples 'work items invite members' do - include Spec::Support::Helpers::Features::InviteMembersModalHelper + include Features::InviteMembersModalHelpers it 'successfully assigns the current user by searching' do # The button is only when the mouse is over the input @@ -139,3 +246,143 @@ RSpec.shared_examples 'work items invite members' do end end end + +RSpec.shared_examples 'work items milestone' do + def set_milestone(milestone_dropdown, milestone_text) + milestone_dropdown.click + + find('[data-testid="work-item-milestone-dropdown"] .gl-form-input', visible: true).send_keys "\"#{milestone_text}\"" + wait_for_requests + + click_button(milestone_text) + wait_for_requests + end + + let(:milestone_dropdown_selector) { '[data-testid="work-item-milestone-dropdown"]' } + + it 'searches and sets or removes milestone for the work item' do + set_milestone(find(milestone_dropdown_selector), milestone.title) + + expect(page.find(milestone_dropdown_selector)).to have_text(milestone.title) + + set_milestone(find(milestone_dropdown_selector), 'No milestone') + + expect(page.find(milestone_dropdown_selector)).to have_text('Add to milestone') + end +end + +RSpec.shared_examples 'work items comment actions for guest users' do + context 'for guest user' do + it 'hides other actions other than copy link' do + page.within(".main-notes-list") do + expect(page).to have_selector('[data-testid="work-item-note-actions"]') + + find('[data-testid="work-item-note-actions"]', match: :first).click + + expect(page).to have_selector('[data-testid="copy-link-action"]') + expect(page).not_to have_selector('[data-testid="assign-note-action"]') + end + end + end +end + +RSpec.shared_examples 'work items notifications' do + let(:actions_dropdown_selector) { '[data-testid="work-item-actions-dropdown"]' } + let(:notifications_toggle_selector) { '[data-testid="notifications-toggle-action"] > button' } + + it 'displays toast when notification is toggled' do + find(actions_dropdown_selector).click + + page.within('[data-testid="notifications-toggle-form"]') do + expect(page).not_to have_css(".is-checked") + + find(notifications_toggle_selector).click + wait_for_requests + + expect(page).to have_css(".is-checked") + end + + page.within('.gl-toast') do + expect(find('.toast-body')).to have_content(_('Notifications turned on.')) + end + end +end + +RSpec.shared_examples 'work items todos' do + let(:todos_action_selector) { '[data-testid="work-item-todos-action"]' } + let(:todos_icon_selector) { '[data-testid="work-item-todos-icon"]' } + let(:header_section_selector) { '[data-testid="work-item-body"]' } + + def toggle_todo_action + find(todos_action_selector).click + wait_for_requests + end + + it 'adds item to the list' do + page.within(header_section_selector) do + expect(find(todos_action_selector)['aria-label']).to eq('Add a to do') + + toggle_todo_action + + expect(find(todos_action_selector)['aria-label']).to eq('Mark as done') + end + + page.within ".header-content span[aria-label='#{_('Todos count')}']" do + expect(page).to have_content '1' + end + end + + it 'marks a todo as done' do + page.within(header_section_selector) do + toggle_todo_action + toggle_todo_action + end + + expect(find(todos_action_selector)['aria-label']).to eq('Add a to do') + expect(page).to have_selector(".header-content span[aria-label='#{_('Todos count')}']", visible: :hidden) + end +end + +RSpec.shared_examples 'work items award emoji' do + let(:award_section_selector) { '[data-testid="work-item-award-list"]' } + let(:award_action_selector) { '[data-testid="award-button"]' } + let(:selected_award_action_selector) { '[data-testid="award-button"].selected' } + let(:emoji_picker_action_selector) { '[data-testid="emoji-picker"]' } + let(:basketball_emoji_selector) { 'gl-emoji[data-name="basketball"]' } + + def select_emoji + first(award_action_selector).click + + wait_for_requests + end + + it 'adds award to the work item' do + within(award_section_selector) do + select_emoji + + expect(page).to have_selector(selected_award_action_selector) + expect(first(award_action_selector)).to have_content '1' + end + end + + it 'removes award from work item' do + within(award_section_selector) do + select_emoji + + expect(first(award_action_selector)).to have_content '1' + + select_emoji + + expect(first(award_action_selector)).to have_content '0' + end + end + + it 'add custom award to the work item' do + within(award_section_selector) do + find(emoji_picker_action_selector).click + find(basketball_emoji_selector).click + + expect(page).to have_selector(basketball_emoji_selector) + end + end +end diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb index 93f9e42241b..67fed00b5ca 100644 --- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb +++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb @@ -161,10 +161,12 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context let_it_be(:another_release) { create(:release, project: project1, tag: 'v2.0.0') } let_it_be(:another_milestone) { create(:milestone, project: project1, releases: [another_release]) } let_it_be(:another_item) do - create(factory, - project: project1, - milestone: another_milestone, - title: 'another item') + create( + factory, + project: project1, + milestone: another_milestone, + title: 'another item' + ) end let(:params) { { not: { release_tag: release.tag, project_id: project1.id } } } @@ -421,8 +423,11 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context let!(:created_items) do milestones.map do |milestone| - create(factory, project: milestone.project || project_in_group, - milestone: milestone, author: user, assignees: [user]) + create( + factory, + project: milestone.project || project_in_group, + milestone: milestone, author: user, assignees: [user] + ) end end @@ -593,7 +598,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context end context 'filtering by no label' do - let(:params) { { label_name: described_class::Params::FILTER_NONE } } + let(:params) { { label_name: IssuableFinder::Params::FILTER_NONE } } it 'returns items with no labels' do expect(items).to contain_exactly(item1, item4, item5) @@ -601,7 +606,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context end context 'filtering by any label' do - let(:params) { { label_name: described_class::Params::FILTER_ANY } } + let(:params) { { label_name: IssuableFinder::Params::FILTER_ANY } } it 'returns items that have one or more label' do create_list(:label_link, 2, label: create(:label, project: project2), target: item3) @@ -909,9 +914,9 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context end context 'filtering by item type' do - let_it_be(:incident_item) { create(factory, issue_type: :incident, project: project1) } - let_it_be(:objective) { create(factory, issue_type: :objective, project: project1) } - let_it_be(:key_result) { create(factory, issue_type: :key_result, project: project1) } + let_it_be(:incident_item) { create(factory, :incident, project: project1) } + let_it_be(:objective) { create(factory, :objective, project: project1) } + let_it_be(:key_result) { create(factory, :key_result, project: project1) } context 'no type given' do let(:params) { { issue_types: [] } } @@ -983,9 +988,9 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context let_it_be(:root_group) { create(:group) } let_it_be(:group) { create(:group, parent: root_group) } let_it_be(:project_crm) { create(:project, :public, group: group) } - let_it_be(:organization) { create(:organization, group: root_group) } - let_it_be(:contact1) { create(:contact, group: root_group, organization: organization) } - let_it_be(:contact2) { create(:contact, group: root_group, organization: organization) } + let_it_be(:crm_organization) { create(:crm_organization, group: root_group) } + let_it_be(:contact1) { create(:contact, group: root_group, organization: crm_organization) } + let_it_be(:contact2) { create(:contact, group: root_group, organization: crm_organization) } let_it_be(:contact1_item1) { create(factory, project: project_crm) } let_it_be(:contact1_item2) { create(factory, project: project_crm) } @@ -1023,10 +1028,10 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context end context 'filtering by crm organization' do - let(:params) { { project_id: project_crm.id, crm_organization_id: organization.id } } + let(:params) { { project_id: project_crm.id, crm_organization_id: crm_organization.id } } context 'when the user can read crm organization' do - it 'returns for that organization' do + it 'returns for that crm organization' do root_group.add_reporter(user) expect(items).to contain_exactly(contact1_item1, contact1_item2, contact2_item1) @@ -1034,7 +1039,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context end context 'when the user can not read crm organization' do - it 'does not filter by organization' do + it 'does not filter by crm organization' do expect(items).to match_array(all_project_issues) end end diff --git a/spec/support/shared_examples/graphql/members_shared_examples.rb b/spec/support/shared_examples/graphql/members_shared_examples.rb index 5cba8baa829..5ab17f5a49d 100644 --- a/spec/support/shared_examples/graphql/members_shared_examples.rb +++ b/spec/support/shared_examples/graphql/members_shared_examples.rb @@ -39,8 +39,10 @@ RSpec.shared_examples 'querying members with a group' do let(:base_args) { { relations: described_class.arguments['relations'].default_value } } subject do - resolve(described_class, obj: resource, args: base_args.merge(args), - ctx: { current_user: user_4 }, arg_style: :internal) + resolve( + described_class, obj: resource, args: base_args.merge(args), + ctx: { current_user: user_4 }, arg_style: :internal + ) end describe '#resolve' do @@ -83,8 +85,10 @@ RSpec.shared_examples 'querying members with a group' do let_it_be(:other_user) { create(:user) } subject do - resolve(described_class, obj: resource, args: base_args.merge(args), - ctx: { current_user: other_user }, arg_style: :internal) + resolve( + described_class, obj: resource, args: base_args.merge(args), + ctx: { current_user: other_user }, arg_style: :internal + ) end it 'generates an error' do diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb index dc590e23ace..808fb097f29 100644 --- a/spec/support/shared_examples/graphql/mutation_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb @@ -15,7 +15,7 @@ RSpec.shared_examples 'a mutation that returns top-level errors' do |errors: []| expect(graphql_errors).to be_present - error_messages = graphql_errors.map { |e| e['message'] } + error_messages = graphql_errors.pluck('message') expect(error_messages).to match_errors end @@ -25,7 +25,7 @@ end # the mutation. RSpec.shared_examples 'a mutation that returns a top-level access error' do include_examples 'a mutation that returns top-level errors', - errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] end RSpec.shared_examples 'an invalid argument to the mutation' do |argument_name:| diff --git a/spec/support/shared_examples/graphql/mutations/members/bulk_update_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/members/bulk_update_shared_examples.rb new file mode 100644 index 00000000000..e885b5d283e --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/members/bulk_update_shared_examples.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'members bulk update mutation' do + let_it_be(:current_user) { create(:user) } + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:member1) { create(member_type, source: source, user: user1) } + let_it_be(:member2) { create(member_type, source: source, user: user2) } + + let(:extra_params) { { expires_at: 10.days.from_now } } + let(:input_params) { input.merge(extra_params) } + let(:mutation) { graphql_mutation(mutation_name, input_params) } + let(:mutation_response) { graphql_mutation_response(mutation_name) } + + let(:input) do + { + source_id_key => source.to_global_id.to_s, + 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s], + 'access_level' => 'GUEST' + } + end + + context 'when user is not logged-in' do + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user is not an owner' do + before do + source.add_developer(current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user is an owner' do + before do + source.add_owner(current_user) + end + + shared_examples 'updates the user access role' do + specify do + post_graphql_mutation(mutation, current_user: current_user) + + new_access_levels = mutation_response[response_member_field].map do |member| + member['accessLevel']['integerValue'] + end + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to be_empty + expect(new_access_levels).to all(be Gitlab::Access::GUEST) + end + end + + it_behaves_like 'updates the user access role' + + context 'when inherited members are passed' do + let(:input) do + { + source_id_key => source.to_global_id.to_s, + 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s, parent_group_member.user.to_global_id.to_s], + 'access_level' => 'GUEST' + } + end + + it 'does not update the members' do + post_graphql_mutation(mutation, current_user: current_user) + + error = Mutations::Members::BulkUpdateBase::INVALID_MEMBERS_ERROR + expect(json_response['errors'].first['message']).to include(error) + end + end + + context 'when members count is more than the allowed limit' do + let(:max_members_update_limit) { 1 } + + before do + stub_const('Mutations::Members::BulkUpdateBase::MAX_MEMBERS_UPDATE_LIMIT', max_members_update_limit) + end + + it 'does not update the members' do + post_graphql_mutation(mutation, current_user: current_user) + + error = Mutations::Members::BulkUpdateBase::MAX_MEMBERS_UPDATE_ERROR + expect(json_response['errors'].first['message']).to include(error) + end + end + + context 'when the update service raises access denied error' do + before do + allow_next_instance_of(Members::UpdateService) do |instance| + allow(instance).to receive(:execute).and_raise(Gitlab::Access::AccessDeniedError) + end + end + + it 'does not update the members' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response[response_member_field]).to be_nil + expect(mutation_response['errors']) + .to contain_exactly("Unable to update members, please check user permissions.") + end + end + + context 'when the update service returns an error message' do + before do + allow_next_instance_of(Members::UpdateService) do |instance| + error_result = { + message: 'Expires at cannot be a date in the past', + status: :error, + members: [member1] + } + allow(instance).to receive(:execute).and_return(error_result) + end + end + + it 'will pass through the error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response[response_member_field].first['id']).to eq(member1.to_global_id.to_s) + expect(mutation_response['errors']).to contain_exactly('Expires at cannot be a date in the past') + end + end + end +end diff --git a/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb index 022e2308517..3b9dadf2e80 100644 --- a/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb @@ -16,10 +16,12 @@ RSpec.shared_examples 'an assignable resource' do let(:mode) { described_class.arguments['operationMode'].default_value } subject do - mutation.resolve(project_path: resource.project.full_path, - iid: resource.iid, - operation_mode: mode, - assignee_usernames: assignee_usernames) + mutation.resolve( + project_path: resource.project.full_path, + iid: resource.iid, + operation_mode: mode, + assignee_usernames: assignee_usernames + ) end it 'raises an error if the resource is not accessible to the user' do diff --git a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb index 0d2e9f6ec8c..99d122e8254 100644 --- a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb +++ b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb @@ -4,9 +4,11 @@ RSpec.shared_context 'exposing regular notes on a noteable in GraphQL' do include GraphqlHelpers let(:note) do - create(:note, - noteable: noteable, - project: (noteable.project if noteable.respond_to?(:project))) + create( + :note, + noteable: noteable, + project: (noteable.project if noteable.respond_to?(:project)) + ) end let(:user) { note.author } @@ -46,7 +48,7 @@ RSpec.shared_context 'exposing regular notes on a noteable in GraphQL' do discussions { edges { node { - #{all_graphql_fields_for('Discussion', max_depth: 4)} + #{all_graphql_fields_for('Discussion', max_depth: 4, excluded: ['productAnalyticsState'])} } } } diff --git a/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb b/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb new file mode 100644 index 00000000000..52908c5b6df --- /dev/null +++ b/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb @@ -0,0 +1,195 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'work item supports assignee widget updates via quick actions' do + let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } + + context 'when assigning a user' do + let(:body) { "/assign @#{developer.username}" } + + it 'updates the work item assignee' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.to change { noteable.assignee_ids }.from([]).to([developer.id]) + + expect(response).to have_gitlab_http_status(:success) + end + end + + context 'when unassigning a user' do + let(:body) { "/unassign @#{developer.username}" } + + before do + noteable.update!(assignee_ids: [developer.id]) + end + + it 'updates the work item assignee' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.to change { noteable.assignee_ids }.from([developer.id]).to([]) + + expect(response).to have_gitlab_http_status(:success) + end + end +end + +RSpec.shared_examples 'work item does not support assignee widget updates via quick actions' do + let(:developer) { create(:user).tap { |user| project.add_developer(user) } } + let(:body) { "Updating assignee.\n/assign @#{developer.username}" } + + before do + WorkItems::Type.default_by_type(:task).widget_definitions + .find_by_widget_type(:assignees).update!(disabled: true) + end + + it 'ignores the quick action' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.not_to change { noteable.assignee_ids } + end +end + +RSpec.shared_examples 'work item supports labels widget updates via quick actions' do + shared_examples 'work item labels are updated' do + it do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.to change { noteable.labels.count }.to(expected_labels.count) + + expect(noteable.labels).to match_array(expected_labels) + end + end + + let_it_be(:existing_label) { create(:label, project: project) } + let_it_be(:label1) { create(:label, project: project) } + let_it_be(:label2) { create(:label, project: project) } + + let(:add_label_ids) { [] } + let(:remove_label_ids) { [] } + + before_all do + noteable.update!(labels: [existing_label]) + end + + context 'when only removing labels' do + let(:remove_label_ids) { [existing_label.to_gid.to_s] } + let(:expected_labels) { [] } + let(:body) { "/remove_label ~\"#{existing_label.name}\"" } + + it_behaves_like 'work item labels are updated' + end + + context 'when only adding labels' do + let(:add_label_ids) { [label1.to_gid.to_s, label2.to_gid.to_s] } + let(:expected_labels) { [label1, label2, existing_label] } + let(:body) { "/labels ~\"#{label1.name}\" ~\"#{label2.name}\"" } + + it_behaves_like 'work item labels are updated' + end + + context 'when adding and removing labels' do + let(:remove_label_ids) { [existing_label.to_gid.to_s] } + let(:add_label_ids) { [label1.to_gid.to_s, label2.to_gid.to_s] } + let(:expected_labels) { [label1, label2] } + let(:body) { "/label ~\"#{label1.name}\" ~\"#{label2.name}\"\n/remove_label ~\"#{existing_label.name}\"" } + + it_behaves_like 'work item labels are updated' + end +end + +RSpec.shared_examples 'work item does not support labels widget updates via quick actions' do + let(:label1) { create(:label, project: project) } + let(:body) { "Updating labels.\n/labels ~\"#{label1.name}\"" } + + before do + WorkItems::Type.default_by_type(:task).widget_definitions + .find_by_widget_type(:labels).update!(disabled: true) + end + + it 'ignores the quick action' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.not_to change { noteable.labels.count } + + expect(noteable.labels).to be_empty + end +end + +RSpec.shared_examples 'work item supports start and due date widget updates via quick actions' do + let(:due_date) { Date.today } + let(:body) { "/remove_due_date" } + + before do + noteable.update!(due_date: due_date) + end + + it 'updates start and due date' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.to not_change(noteable, :start_date).and( + change { noteable.due_date }.from(due_date).to(nil) + ) + end +end + +RSpec.shared_examples 'work item does not support start and due date widget updates via quick actions' do + let(:body) { "Updating due date.\n/due today" } + + before do + WorkItems::Type.default_by_type(:task).widget_definitions + .find_by_widget_type(:start_and_due_date).update!(disabled: true) + end + + it 'ignores the quick action' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.not_to change { noteable.due_date } + end +end + +RSpec.shared_examples 'work item supports type change via quick actions' do + let_it_be(:assignee) { create(:user) } + let_it_be(:task_type) { WorkItems::Type.default_by_type(:task) } + + let(:body) { "Updating type.\n/type Issue" } + + before do + noteable.update!(work_item_type: task_type, issue_type: task_type.base_type) + end + + it 'updates type' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.to change { noteable.work_item_type.base_type }.from('task').to('issue') + + expect(response).to have_gitlab_http_status(:success) + end + + context 'when quick command for unsupported widget is present' do + let(:body) { "\n/type Issue\n/assign @#{assignee.username}" } + + before do + WorkItems::Type.default_by_type(:issue).widget_definitions + .find_by_widget_type(:assignees).update!(disabled: true) + end + + it 'updates only type' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.to change { noteable.work_item_type.base_type }.from('task').to('issue') + .and change { noteable.assignees }.to([]) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']) + .to include("Commands only Type changed successfully. Assigned @#{assignee.username}.") + end + end +end diff --git a/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb new file mode 100644 index 00000000000..8551bd052ce --- /dev/null +++ b/spec/support/shared_examples/graphql/resolvers/data_transfer_resolver_shared_examples.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'Data transfer resolver' do + it 'returns mock data' do |_query_object| + mocked_data = ['mocked_data'] + + allow_next_instance_of(DataTransfer::MockedTransferFinder) do |instance| + allow(instance).to receive(:execute).and_return(mocked_data) + end + + expect(resolve_egress[:egress_nodes]).to eq(mocked_data) + end + + context 'when data_transfer_monitoring is disabled' do + before do + stub_feature_flags(data_transfer_monitoring: false) + end + + it 'returns empty result' do + expect(resolve_egress).to eq(egress_nodes: []) + end + end +end diff --git a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb index 4dc2ce61c4d..b346f35bdc9 100644 --- a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb +++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb @@ -65,7 +65,7 @@ RSpec.shared_examples 'Gitlab-style deprecations' do deprecable = subject(deprecated: { milestone: '1.10', reason: :alpha }) expect(deprecable.deprecation_reason).to eq( - 'This feature is in Alpha. It can be changed or removed at any time. Introduced in 1.10.' + 'This feature is an Experiment. It can be changed or removed at any time. Introduced in 1.10.' ) end @@ -73,7 +73,7 @@ RSpec.shared_examples 'Gitlab-style deprecations' do deprecable = subject(alpha: { milestone: '1.10' }) expect(deprecable.deprecation_reason).to eq( - 'This feature is in Alpha. It can be changed or removed at any time. Introduced in 1.10.' + 'This feature is an Experiment. It can be changed or removed at any time. Introduced in 1.10.' ) end @@ -82,7 +82,7 @@ RSpec.shared_examples 'Gitlab-style deprecations' do subject(alpha: { milestone: '1.10' }, deprecated: { milestone: '1.10', reason: 'my reason' } ) end.to raise_error( ArgumentError, - eq("`alpha` and `deprecated` arguments cannot be passed at the same time") + eq("`experiment` and `deprecated` arguments cannot be passed at the same time") ) end diff --git a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb index bb33a7559dc..3dffc2066ae 100644 --- a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb +++ b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb @@ -42,8 +42,13 @@ RSpec.shared_examples "a user type with merge request interaction type" do profileEnableGitpodPath savedReplies savedReply + user_achievements ] + # TODO: 'workspaces' needs to be included, but only when this spec is run in EE context, to account for the + # ee-only extension in ee/app/graphql/ee/types/user_interface.rb. Not sure how else to handle this. + expected_fields << 'workspaces' if Gitlab.ee? + expect(described_class).to have_graphql_fields(*expected_fields) end diff --git a/spec/support/shared_examples/helpers/callouts_for_web_hooks.rb b/spec/support/shared_examples/helpers/callouts_for_web_hooks.rb new file mode 100644 index 00000000000..b3d3000aa06 --- /dev/null +++ b/spec/support/shared_examples/helpers/callouts_for_web_hooks.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'CalloutsHelper#web_hook_disabled_dismissed shared examples' do + context 'when the web-hook failure callout has never been dismissed' do + it 'is false' do + expect(helper).not_to be_web_hook_disabled_dismissed(container) + end + end + + context 'when the web-hook failure callout has been dismissed', :freeze_time, :clean_gitlab_redis_shared_state do + before do + create(factory, + feature_name: Users::CalloutsHelper::WEB_HOOK_DISABLED, + user: user, + dismissed_at: 1.week.ago, + container_key => container) + end + + it 'is true' do + expect(helper).to be_web_hook_disabled_dismissed(container) + end + + it 'is true when passed as a presenter' do + skip "Does not apply to #{container.class}" unless container.is_a?(Presentable) + + expect(helper).to be_web_hook_disabled_dismissed(container.present) + end + + context 'when there was an older failure' do + before do + Gitlab::Redis::SharedState.with { |r| r.set(key, 1.month.ago.iso8601) } + end + + it 'is true' do + expect(helper).to be_web_hook_disabled_dismissed(container) + end + end + + context 'when there has been a more recent failure' do + before do + Gitlab::Redis::SharedState.with { |r| r.set(key, 1.day.ago.iso8601) } + end + + it 'is false' do + expect(helper).not_to be_web_hook_disabled_dismissed(container) + end + end + end +end diff --git a/spec/support/shared_examples/integrations/integration_settings_form.rb b/spec/support/shared_examples/integrations/integration_settings_form.rb index aeb4e0feb12..c43bdfa53ff 100644 --- a/spec/support/shared_examples/integrations/integration_settings_form.rb +++ b/spec/support/shared_examples/integrations/integration_settings_form.rb @@ -2,12 +2,16 @@ RSpec.shared_examples 'integration settings form' do include IntegrationsHelper + + before do + stub_feature_flags(remove_monitor_metrics: false) + end + # Note: these specs don't validate channel fields # which are present on a few integrations it 'displays all the integrations', feature_category: :integrations do aggregate_failures do integrations.each do |integration| - stub_feature_flags(integration_slack_app_notifications: false) navigate_to_integration(integration) page.within('form.integration-settings-form') do diff --git a/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb b/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb new file mode 100644 index 00000000000..7ace223723c --- /dev/null +++ b/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'delegates AI request to Workhorse' do |provider_flag| + context "when #{provider_flag} is disabled" do + before do + stub_feature_flags(provider_flag => false) + end + + it 'responds as not found' do + post api(url, current_user), params: input_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when ai_experimentation_api is disabled' do + before do + stub_feature_flags(ai_experimentation_api: false) + end + + it 'responds as not found' do + post api(url, current_user), params: input_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + it 'responds with Workhorse send-url headers' do + post api(url, current_user), params: input_params + + expect(response.body).to eq('""') + expect(response).to have_gitlab_http_status(:ok) + + send_url_prefix, encoded_data = response.headers['Gitlab-Workhorse-Send-Data'].split(':') + data = Gitlab::Json.parse(Base64.urlsafe_decode64(encoded_data)) + + expect(send_url_prefix).to eq('send-url') + expect(data).to eq({ + 'AllowRedirects' => false, + 'Method' => 'POST' + }.merge(expected_params)) + end +end diff --git a/spec/support/shared_examples/lib/api/terraform_state_enabled_shared_examples.rb b/spec/support/shared_examples/lib/api/terraform_state_enabled_shared_examples.rb new file mode 100644 index 00000000000..b88eade7db2 --- /dev/null +++ b/spec/support/shared_examples/lib/api/terraform_state_enabled_shared_examples.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'it depends on value of the `terraform_state.enabled` config' do |params = {}| + let(:expected_success_status) { params[:success_status] || :ok } + + context 'when terraform_state.enabled=false' do + before do + stub_config(terraform_state: { enabled: false }) + end + + it 'returns `forbidden` response' do + request + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when terraform_state.enabled=true' do + before do + stub_config(terraform_state: { enabled: true }) + end + + it 'returns a successful response' do + request + + expect(response).to have_gitlab_http_status(expected_success_status) + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb index d471a758f3e..c8d62205c1e 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb @@ -1,14 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'deployment metrics examples' do - def create_deployment(args) - project = args[:project] - environment = project.environments.production.first || create(:environment, :production, project: project) - create(:deployment, :success, args.merge(environment: environment)) - - # this is needed for the DORA API so we have aggregated data - ::Dora::DailyMetrics::RefreshWorker.new.perform(environment.id, Time.current.to_date.to_s) if Gitlab.ee? - end + include CycleAnalyticsHelpers describe "#deploys" do subject { stage_summary.third } diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb index bce889b454d..5740adb3f0e 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb @@ -68,3 +68,64 @@ RSpec.shared_examples_for 'LEFT JOIN-able value stream analytics event' do end end end + +RSpec.shared_examples_for 'value stream analytics first assignment event methods' do + let_it_be(:model1) { create(model_factory) } # rubocop: disable Rails/SaveBang + let_it_be(:model2) { create(model_factory) } # rubocop: disable Rails/SaveBang + + let_it_be(:assignment_event1) do + create(event_factory, action: :add, created_at: 3.years.ago, model_factory => model1) + end + + let_it_be(:assignment_event2) do + create(event_factory, action: :add, created_at: 2.years.ago, model_factory => model1) + end + + let_it_be(:unassignment_event1) do + create(event_factory, action: :remove, created_at: 1.year.ago, model_factory => model1) + end + + let(:query) { model1.class.where(id: [model1.id, model2.id]) } + let(:event) { described_class.new({}) } + + describe '#apply_query_customization' do + subject(:records) { event.apply_query_customization(query).pluck(:id, *event.column_list).to_a } + + it 'looks up the first assignment event timestamp' do + expect(records).to match_array([[model1.id, be_within(1.second).of(assignment_event1.created_at)]]) + end + end + + describe '#apply_negated_query_customization' do + subject(:records) { event.apply_negated_query_customization(query).pluck(:id).to_a } + + it 'returns records where the event has not happened yet' do + expect(records).to eq([model2.id]) + end + end + + describe '#include_in' do + subject(:records) { event.include_in(query).pluck(:id, *event.column_list).to_a } + + it 'returns both records' do + expect(records).to match_array([ + [model1.id, be_within(1.second).of(assignment_event1.created_at)], + [model2.id, nil] + ]) + end + + context 'when invoked multiple times' do + subject(:records) do + scope = event.include_in(query) + event.include_in(scope).pluck(:id, *event.column_list).to_a + end + + it 'returns both records' do + expect(records).to match_array([ + [model1.id, be_within(1.second).of(assignment_event1.created_at)], + [model2.id, nil] + ]) + end + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/database/async_constraints_validation_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/async_constraints_validation_shared_examples.rb new file mode 100644 index 00000000000..b9d71183851 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/async_constraints_validation_shared_examples.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'async constraints validation' do + include ExclusiveLeaseHelpers + + let!(:lease) { stub_exclusive_lease(lease_key, :uuid, timeout: lease_timeout) } + let(:lease_key) { "gitlab/database/asyncddl/actions/#{Gitlab::Database::PRIMARY_DATABASE_NAME}" } + let(:lease_timeout) { described_class::TIMEOUT_PER_ACTION } + + let(:constraints_model) { Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation } + let(:table_name) { '_test_async_constraints' } + let(:constraint_name) { 'constraint_parent_id' } + + let(:validation) do + create(:postgres_async_constraint_validation, + table_name: table_name, + name: constraint_name, + constraint_type: constraint_type) + end + + let(:connection) { validation.connection } + + subject { described_class.new(validation) } + + it 'validates the constraint while controlling statement timeout' do + allow(connection).to receive(:execute).and_call_original + expect(connection).to receive(:execute) + .with("SET statement_timeout TO '43200s'").ordered.and_call_original + expect(connection).to receive(:execute) + .with(/ALTER TABLE "#{table_name}" VALIDATE CONSTRAINT "#{constraint_name}";/).ordered.and_call_original + expect(connection).to receive(:execute) + .with("RESET statement_timeout").ordered.and_call_original + + subject.perform + end + + it 'removes the constraint validation record from table' do + expect(validation).to receive(:destroy!).and_call_original + + expect { subject.perform }.to change { constraints_model.count }.by(-1) + end + + it 'skips logic if not able to acquire exclusive lease' do + expect(lease).to receive(:try_obtain).ordered.and_return(false) + expect(connection).not_to receive(:execute).with(/ALTER TABLE/) + expect(validation).not_to receive(:destroy!) + + expect { subject.perform }.not_to change { constraints_model.count } + end + + it 'logs messages around execution' do + allow(Gitlab::AppLogger).to receive(:info).and_call_original + + subject.perform + + expect(Gitlab::AppLogger) + .to have_received(:info) + .with(a_hash_including(message: 'Starting to validate constraint')) + + expect(Gitlab::AppLogger) + .to have_received(:info) + .with(a_hash_including(message: 'Finished validating constraint')) + end + + context 'when the constraint does not exist' do + before do + connection.create_table(table_name, force: true) + end + + it 'skips validation and removes the record' do + expect(connection).not_to receive(:execute).with(/ALTER TABLE/) + + expect { subject.perform }.to change { constraints_model.count }.by(-1) + end + + it 'logs an appropriate message' do + expected_message = /Skipping #{constraint_name} validation since it does not exist/ + + allow(Gitlab::AppLogger).to receive(:info).and_call_original + + subject.perform + + expect(Gitlab::AppLogger) + .to have_received(:info) + .with(a_hash_including(message: expected_message)) + end + end + + context 'with error handling' do + before do + allow(connection).to receive(:execute).and_call_original + + allow(connection).to receive(:execute) + .with(/ALTER TABLE "#{table_name}" VALIDATE CONSTRAINT "#{constraint_name}";/) + .and_raise(ActiveRecord::StatementInvalid) + end + + context 'on production' do + before do + allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false) + end + + it 'increases execution attempts' do + expect { subject.perform }.to change { validation.attempts }.by(1) + + expect(validation.last_error).to be_present + expect(validation).not_to be_destroyed + end + + it 'logs an error message including the constraint_name' do + expect(Gitlab::AppLogger) + .to receive(:error) + .with(a_hash_including(:message, :constraint_name)) + .and_call_original + + subject.perform + end + end + + context 'on development' do + it 'also raises errors' do + expect { subject.perform } + .to raise_error(ActiveRecord::StatementInvalid) + .and change { validation.attempts }.by(1) + + expect(validation.last_error).to be_present + expect(validation).not_to be_destroyed + end + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb new file mode 100644 index 00000000000..6f0cede7130 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples "index validators" do |validator, expected_result| + let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') } + let(:database_indexes) do + [ + ['wrong_index', 'CREATE UNIQUE INDEX wrong_index ON public.table_name (column_name)'], + ['extra_index', 'CREATE INDEX extra_index ON public.table_name (column_name)'], + ['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))'] + ] + end + + let(:inconsistency_type) { validator.name.demodulize.underscore } + + let(:database_name) { 'main' } + + let(:database_model) { Gitlab::Database.database_base_models[database_name] } + + let(:connection) { database_model.connection } + + let(:schema) { connection.current_schema } + + let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) } + let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, schema) } + + subject(:result) { validator.new(structure_file, database).execute } + + before do + allow(connection).to receive(:select_rows).and_return(database_indexes) + end + + it 'returns index inconsistencies' do + expect(result.map(&:object_name)).to match_array(expected_result) + expect(result.map(&:type)).to all(eql inconsistency_type) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb new file mode 100644 index 00000000000..ec7a881f7ce --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples "schema objects assertions for" do |stmt_name| + let(:stmt) { PgQuery.parse(statement).tree.stmts.first.stmt } + let(:schema_object) { described_class.new(stmt.public_send(stmt_name)) } + + describe '#name' do + it 'returns schema object name' do + expect(schema_object.name).to eq(name) + end + end + + describe '#statement' do + it 'returns schema object statement' do + expect(schema_object.statement).to eq(statement) + end + end + + describe '#table_name' do + it 'returns schema object table_name' do + expect(schema_object.table_name).to eq(table_name) + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/database/table_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/table_validators_shared_examples.rb new file mode 100644 index 00000000000..96e58294675 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/table_validators_shared_examples.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples "table validators" do |validator, expected_result| + subject(:result) { validator.new(structure_file, database).execute } + + let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') } + let(:inconsistency_type) { validator.name.demodulize.underscore } + let(:database_model) { Gitlab::Database.database_base_models['main'] } + let(:connection) { database_model.connection } + let(:schema) { connection.current_schema } + let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) } + let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, schema) } + let(:database_tables) do + [ + { + 'table_name' => 'wrong_table', + 'column_name' => 'id', + 'not_null' => true, + 'data_type' => 'integer', + 'column_default' => "nextval('audit_events_id_seq'::regclass)" + }, + { + 'table_name' => 'wrong_table', + 'column_name' => 'description', + 'not_null' => true, + 'data_type' => 'character varying', + 'column_default' => nil + }, + { + 'table_name' => 'extra_table', + 'column_name' => 'id', + 'not_null' => true, + 'data_type' => 'integer', + 'column_default' => "nextval('audit_events_id_seq'::regclass)" + }, + { + 'table_name' => 'extra_table', + 'column_name' => 'email', + 'not_null' => true, + 'data_type' => 'character varying', + 'column_default' => nil + }, + { + 'table_name' => 'extra_table_columns', + 'column_name' => 'id', + 'not_null' => true, + 'data_type' => 'bigint', + 'column_default' => "nextval('audit_events_id_seq'::regclass)" + }, + { + 'table_name' => 'extra_table_columns', + 'column_name' => 'name', + 'not_null' => true, + 'data_type' => 'character varying(255)', + 'column_default' => nil + }, + { + 'table_name' => 'extra_table_columns', + 'column_name' => 'extra_column', + 'not_null' => true, + 'data_type' => 'character varying(255)', + 'column_default' => nil + }, + { + 'table_name' => 'missing_table_columns', + 'column_name' => 'id', + 'not_null' => true, + 'data_type' => 'bigint', + 'column_default' => 'NOT NULL' + } + ] + end + + before do + allow(connection).to receive(:exec_query).and_return(database_tables) + end + + it 'returns table inconsistencies' do + expect(result.map(&:object_name)).to match_array(expected_result) + expect(result.map(&:type)).to all(eql inconsistency_type) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb new file mode 100644 index 00000000000..13a112275c2 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'trigger validators' do |validator, expected_result| + subject(:result) { validator.new(structure_file, database).execute } + + let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') } + let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, schema) } + let(:inconsistency_type) { validator.name.demodulize.underscore } + let(:database_name) { 'main' } + let(:schema) { 'public' } + let(:database_model) { Gitlab::Database.database_base_models[database_name] } + let(:connection) { database_model.connection } + let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) } + + let(:database_triggers) do + [ + ['trigger', 'CREATE TRIGGER trigger AFTER INSERT ON public.t1 FOR EACH ROW EXECUTE FUNCTION t1()'], + ['wrong_trigger', 'CREATE TRIGGER wrong_trigger BEFORE UPDATE ON public.t2 FOR EACH ROW EXECUTE FUNCTION t2()'], + ['extra_trigger', 'CREATE TRIGGER extra_trigger BEFORE INSERT ON public.t4 FOR EACH ROW EXECUTE FUNCTION t4()'] + ] + end + + before do + allow(connection).to receive(:select_rows).and_return(database_triggers) + end + + it 'returns trigger inconsistencies' do + expect(result.map(&:object_name)).to match_array(expected_result) + expect(result.map(&:type)).to all(eql inconsistency_type) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb index f26b9a4a7bd..d388abb16c6 100644 --- a/spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true def raw_repo_without_container(repository) - Gitlab::Git::Repository.new(repository.shard, - "#{repository.disk_path}.git", - repository.repo_type.identifier_for_container(repository.container), - repository.container.full_path) + Gitlab::Git::Repository.new( + repository.shard, + "#{repository.disk_path}.git", + repository.repo_type.identifier_for_container(repository.container), + repository.container.full_path + ) end RSpec.shared_examples 'Gitaly feature flag actors are inferred from repository' do diff --git a/spec/support/shared_examples/lib/gitlab/json_logger_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/json_logger_shared_examples.rb new file mode 100644 index 00000000000..8a5e8397c3d --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/json_logger_shared_examples.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a json logger' do |extra_params| + let(:now) { Time.now } + let(:correlation_id) { Labkit::Correlation::CorrelationId.current_id } + + it 'formats strings' do + output = subject.format_message('INFO', now, 'test', 'Hello world') + data = Gitlab::Json.parse(output) + + expect(data['severity']).to eq('INFO') + expect(data['time']).to eq(now.utc.iso8601(3)) + expect(data['message']).to eq('Hello world') + expect(data['correlation_id']).to eq(correlation_id) + expect(data).to include(extra_params) + end + + it 'formats hashes' do + output = subject.format_message('INFO', now, 'test', { hello: 1 }) + data = Gitlab::Json.parse(output) + + expect(data['severity']).to eq('INFO') + expect(data['time']).to eq(now.utc.iso8601(3)) + expect(data['hello']).to eq(1) + expect(data['message']).to be_nil + expect(data['correlation_id']).to eq(correlation_id) + expect(data).to include(extra_params) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/local_and_remote_storage_migration_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/local_and_remote_storage_migration_shared_examples.rb index 27ca27a9035..4b0e3234750 100644 --- a/spec/support/shared_examples/lib/gitlab/local_and_remote_storage_migration_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/local_and_remote_storage_migration_shared_examples.rb @@ -8,9 +8,9 @@ RSpec.shared_examples 'local and remote storage migration' do where(:start_store, :end_store, :method) do ObjectStorage::Store::LOCAL | ObjectStorage::Store::REMOTE | :migrate_to_remote_storage - ObjectStorage::Store::REMOTE | ObjectStorage::Store::REMOTE | :migrate_to_remote_storage # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands + ObjectStorage::Store::REMOTE | ObjectStorage::Store::REMOTE | :migrate_to_remote_storage ObjectStorage::Store::REMOTE | ObjectStorage::Store::LOCAL | :migrate_to_local_storage - ObjectStorage::Store::LOCAL | ObjectStorage::Store::LOCAL | :migrate_to_local_storage # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands + ObjectStorage::Store::LOCAL | ObjectStorage::Store::LOCAL | :migrate_to_local_storage end with_them do diff --git a/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb index f83fecee4ea..0016f1e670d 100644 --- a/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb @@ -38,8 +38,7 @@ RSpec.shared_examples 'access restricted confidential issues' do let(:user) { author } it 'lists project confidential issues' do - expect(objects).to contain_exactly(issue, - security_issue_1) + expect(objects).to contain_exactly(issue, security_issue_1) expect(results.limited_issues_count).to eq 2 end end @@ -48,8 +47,7 @@ RSpec.shared_examples 'access restricted confidential issues' do let(:user) { assignee } it 'lists project confidential issues for assignee' do - expect(objects).to contain_exactly(issue, - security_issue_2) + expect(objects).to contain_exactly(issue, security_issue_2) expect(results.limited_issues_count).to eq 2 end end @@ -60,9 +58,7 @@ RSpec.shared_examples 'access restricted confidential issues' do end it 'lists project confidential issues' do - expect(objects).to contain_exactly(issue, - security_issue_1, - security_issue_2) + expect(objects).to contain_exactly(issue, security_issue_1, security_issue_2) expect(results.limited_issues_count).to eq 3 end end @@ -72,9 +68,7 @@ RSpec.shared_examples 'access restricted confidential issues' do context 'when admin mode is enabled', :enable_admin_mode do it 'lists all project issues' do - expect(objects).to contain_exactly(issue, - security_issue_1, - security_issue_2) + expect(objects).to contain_exactly(issue, security_issue_1, security_issue_2) end end diff --git a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb index 025f0d5c7ea..c2898513424 100644 --- a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb @@ -15,7 +15,7 @@ RSpec.shared_examples 'a repo type' do describe '#repository_for' do it 'finds the repository for the repo type' do - expect(described_class.repository_for(expected_container)).to eq(expected_repository) + expect(described_class.repository_for(expected_repository_resolver)).to eq(expected_repository) end it 'returns nil when container is nil' do diff --git a/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb index a3e4379f4d3..18545698c27 100644 --- a/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb @@ -26,29 +26,4 @@ RSpec.shared_examples 'search results filtered by language' do expect(blob_results.size).to eq(5) expect(paths).to match_array(expected_paths) end - - context 'when the search_blobs_language_aggregation feature flag is disabled' do - before do - stub_feature_flags(search_blobs_language_aggregation: false) - end - - it 'does not filter by language', :sidekiq_inline, :aggregate_failures do - expected_paths = %w[ - CHANGELOG - CONTRIBUTING.md - bar/branch-test.txt - custom-highlighting/test.gitlab-custom - files/ruby/popen.rb - files/ruby/regex.rb - files/ruby/version_info.rb - files/whitespace - encoding/test.txt - files/markdown/ruby-style-guide.md - ] - - paths = blob_results.map { |blob| blob.binary_path } - expect(blob_results.size).to eq(10) - expect(paths).to match_array(expected_paths) - end - end end diff --git a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb index ff03051ed37..74570a4da5c 100644 --- a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb @@ -5,7 +5,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| instance_double(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, duplicate_key_ttl: Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DEFAULT_DUPLICATE_KEY_TTL) end - let(:expected_message) { "dropped #{strategy_name.to_s.humanize.downcase}" } + let(:humanized_strategy_name) { strategy_name.to_s.humanize.downcase } subject(:strategy) { Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies.for(strategy_name).new(fake_duplicate_job) } @@ -155,7 +155,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| fake_logger = instance_double(Gitlab::SidekiqLogging::DeduplicationLogger) expect(Gitlab::SidekiqLogging::DeduplicationLogger).to receive(:instance).and_return(fake_logger) - expect(fake_logger).to receive(:deduplicated_log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, {}) + expect(fake_logger).to receive(:deduplicated_log).with(a_hash_including({ 'jid' => 'new jid' }), humanized_strategy_name, {}) strategy.schedule({ 'jid' => 'new jid' }) {} end @@ -165,7 +165,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| expect(Gitlab::SidekiqLogging::DeduplicationLogger).to receive(:instance).and_return(fake_logger) allow(fake_duplicate_job).to receive(:options).and_return({ foo: :bar }) - expect(fake_logger).to receive(:deduplicated_log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, { foo: :bar }) + expect(fake_logger).to receive(:deduplicated_log).with(a_hash_including({ 'jid' => 'new jid' }), humanized_strategy_name, { foo: :bar }) strategy.schedule({ 'jid' => 'new jid' }) {} end diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb index d4802a19202..169fceced7a 100644 --- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events for given event params' do +RSpec.shared_examples 'tracked issuable snowplow and service ping events for given event params' do before do stub_application_setting(usage_ping_enabled: true) end - def count_unique(date_from: 1.minute.ago, date_to: 1.minute.from_now) + def count_unique(date_from: Date.today.beginning_of_week, date_to: 1.week.from_now) Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: action, start_date: date_from, end_date: date_to) end @@ -27,35 +27,23 @@ RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events expect_snowplow_event(**{ category: category, action: event_action, user: user1 }.merge(event_params)) end - - context 'with route_hll_to_snowplow_phase2 disabled' do - before do - stub_feature_flags(route_hll_to_snowplow_phase2: false) - end - - it 'does not emit snowplow event' do - track_action({ author: user1 }.merge(track_params)) - - expect_no_snowplow_event - end - end end -RSpec.shared_examples 'daily tracked issuable snowplow and service ping events with project' do - it_behaves_like 'a daily tracked issuable snowplow and service ping events for given event params' do +RSpec.shared_examples 'tracked issuable snowplow and service ping events with project' do + it_behaves_like 'tracked issuable snowplow and service ping events for given event params' do let(:context) do Gitlab::Tracking::ServicePingContext .new(data_source: :redis_hll, event: event_property) .to_h end - let(:track_params) { { project: project } } - let(:event_params) { track_params.merge(label: event_label, property: event_property, namespace: project.namespace, context: [context]) } + let(:track_params) { original_params || { project: project } } + let(:event_params) { { project: project }.merge(label: event_label, property: event_property, namespace: project.namespace, context: [context]) } end end -RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events with namespace' do - it_behaves_like 'a daily tracked issuable snowplow and service ping events for given event params' do +RSpec.shared_examples 'tracked issuable snowplow and service ping events with namespace' do + it_behaves_like 'tracked issuable snowplow and service ping events for given event params' do let(:context) do Gitlab::Tracking::ServicePingContext .new(data_source: :redis_hll, event: event_property) diff --git a/spec/support/shared_examples/lib/gitlab/utils/username_and_email_generator_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/utils/username_and_email_generator_shared_examples.rb new file mode 100644 index 00000000000..a42d1450e4d --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/utils/username_and_email_generator_shared_examples.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'username and email pair is generated by Gitlab::Utils::UsernameAndEmailGenerator' do + let(:randomhex) { 'randomhex' } + + it 'check email domain' do + expect(subject.email).to end_with("@#{email_domain}") + end + + it 'contains SecureRandom part' do + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + + expect(subject.username).to include("_#{randomhex}") + expect(subject.email).to include("_#{randomhex}@") + end + + it 'email name is the same as username' do + expect(subject.email).to include("#{subject.username}@") + end + + context 'when conflicts' do + let(:reserved_username) { "#{username_prefix}_#{randomhex}" } + let(:reserved_email) { "#{reserved_username}@#{email_domain}" } + + shared_examples 'uniquifies username and email' do + it 'uniquifies username and email' do + expect(subject.username).to eq("#{reserved_username}1") + expect(subject.email).to include("#{subject.username}@") + end + end + + context 'when username is reserved' do + context 'when username is reserved by user' do + before do + create(:user, username: reserved_username) + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + end + + include_examples 'uniquifies username and email' + end + + context 'when it conflicts with top-level group namespace' do + before do + create(:group, path: reserved_username) + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + end + + include_examples 'uniquifies username and email' + end + + context 'when it conflicts with top-level group namespace that includes upcased characters' do + before do + create(:group, path: reserved_username.upcase) + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + end + + include_examples 'uniquifies username and email' + end + end + + context 'when email is reserved' do + context 'when it conflicts with confirmed primary email' do + before do + create(:user, email: reserved_email) + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + end + + include_examples 'uniquifies username and email' + end + + context 'when it conflicts with unconfirmed primary email' do + before do + create(:user, :unconfirmed, email: reserved_email) + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + end + + include_examples 'uniquifies username and email' + end + + context 'when it conflicts with confirmed secondary email' do + before do + create(:email, :confirmed, email: reserved_email) + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + end + + include_examples 'uniquifies username and email' + end + end + + context 'when email and username is reserved' do + before do + create(:user, email: reserved_email) + create(:user, username: "#{reserved_username}1") + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + end + + it 'uniquifies username and email' do + expect(subject.username).to eq("#{reserved_username}2") + + expect(subject.email).to include("#{subject.username}@") + end + end + end +end diff --git a/spec/support/shared_examples/lib/menus_shared_examples.rb b/spec/support/shared_examples/lib/menus_shared_examples.rb index 2c2cb362b07..0aa98517444 100644 --- a/spec/support/shared_examples/lib/menus_shared_examples.rb +++ b/spec/support/shared_examples/lib/menus_shared_examples.rb @@ -37,3 +37,58 @@ RSpec.shared_examples_for 'pill_count formatted results' do expect(pill_count).to eq('112.6k') end end + +RSpec.shared_examples_for 'serializable as super_sidebar_menu_args' do + let(:extra_attrs) { raise NotImplementedError } + + it 'returns hash with provided attributes' do + expect(menu.serialize_as_menu_item_args).to eq({ + title: menu.title, + link: menu.link, + active_routes: menu.active_routes, + container_html_options: menu.container_html_options, + **extra_attrs + }) + end + + it 'returns hash with an item_id' do + expect(menu.serialize_as_menu_item_args[:item_id]).not_to be_nil + end +end + +RSpec.shared_examples_for 'not serializable as super_sidebar_menu_args' do + it 'returns nil' do + expect(menu.serialize_as_menu_item_args).to be_nil + end +end + +RSpec.shared_examples_for 'a panel with uniquely identifiable menu items' do + let(:menu_items) do + subject.instance_variable_get(:@menus) + .flat_map { |menu| menu.instance_variable_get(:@items) } + end + + it 'all menu_items have unique item_id' do + duplicated_ids = menu_items.group_by(&:item_id).reject { |_, v| (v.size < 2) } + + expect(duplicated_ids).to eq({}) + end + + it 'all menu_items have an item_id' do + items_with_nil_id = menu_items.select { |item| item.item_id.nil? } + + expect(items_with_nil_id).to match_array([]) + end +end + +RSpec.shared_examples_for 'a panel with all menu_items categorized' do + let(:uncategorized_menu) do + subject.instance_variable_get(:@menus) + .find { |menu| menu.instance_of?(::Sidebars::UncategorizedMenu) } + end + + it 'has no uncategorized menu_items' do + uncategorized_menu_items = uncategorized_menu.instance_variable_get(:@items) + expect(uncategorized_menu_items).to eq([]) + end +end diff --git a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb index e0b411e1e2a..fa3e9bf5340 100644 --- a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb +++ b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb @@ -90,7 +90,9 @@ RSpec.shared_examples 'Sentry API response size limit' do end it 'raises an exception when response is too large' do - expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError, - 'Sentry API response is too big. Limit is 1 MB.') + expect { subject }.to raise_error( + ErrorTracking::SentryClient::ResponseInvalidSizeError, + 'Sentry API response is too big. Limit is 1 MB.' + ) end end diff --git a/spec/support/shared_examples/lib/sidebars/admin/menus/admin_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/admin/menus/admin_menus_shared_examples.rb new file mode 100644 index 00000000000..f913c6b8a9e --- /dev/null +++ b/spec/support/shared_examples/lib/sidebars/admin/menus/admin_menus_shared_examples.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'Admin menu' do |link:, title:, icon:, separated: false| + let_it_be(:user) { build(:user, :admin) } + + before do + allow(user).to receive(:can_admin_all_resources?).and_return(true) + end + + let(:context) { Sidebars::Context.new(current_user: user, container: nil) } + + subject { described_class.new(context) } + + it 'renders the correct link' do + expect(subject.link).to match link + end + + it 'renders the correct title' do + expect(subject.title).to eq title + end + + it 'renders the correct icon' do + expect(subject.sprite_icon).to be icon + end + + it 'renders the separator if needed' do + expect(subject.separated?).to be separated + end + + describe '#render?' do + context 'when user is admin' do + it 'renders' do + expect(subject.render?).to be true + end + end + + context 'when user is not admin' do + it 'does not render' do + expect(described_class.new(Sidebars::Context.new(current_user: build(:user), + container: nil)).render?).to be false + end + end + + context 'when user is not logged in' do + it 'does not render' do + expect(described_class.new(Sidebars::Context.new(current_user: nil, container: nil)).render?).to be false + end + end + end +end + +RSpec.shared_examples 'Admin menu without sub menus' do |active_routes:| + let_it_be(:user) { build(:user, :admin) } + + let(:context) { Sidebars::Context.new(current_user: user, container: nil) } + + subject { described_class.new(context) } + + it 'does not contain any sub menu(s)' do + expect(subject.has_items?).to be false + end + + it 'defines correct active route' do + expect(subject.active_routes).to eq active_routes + end +end + +RSpec.shared_examples 'Admin menu with sub menus' do + let_it_be(:user) { build(:user, :admin) } + + let(:context) { Sidebars::Context.new(current_user: user, container: nil) } + + subject { described_class.new(context) } + + it 'contains submemus' do + expect(subject.has_items?).to be true + end +end diff --git a/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb new file mode 100644 index 00000000000..5e8aebb4f29 --- /dev/null +++ b/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'User profile menu' do |title:, icon:, active_route:| + let_it_be(:current_user) { build(:user) } + let_it_be(:user) { build(:user) } + + let(:context) { Sidebars::Context.new(current_user: current_user, container: user) } + + subject { described_class.new(context) } + + it 'does not contain any sub menu' do + expect(subject.has_items?).to be false + end + + it 'renders the correct link' do + expect(subject.link).to match link + end + + it 'renders the correct title' do + expect(subject.title).to eq title + end + + it 'renders the correct icon' do + expect(subject.sprite_icon).to eq icon + end + + it 'defines correct active route' do + expect(subject.active_routes[:path]).to be active_route + end + + it 'renders if user is logged in' do + expect(subject.render?).to be true + end + + [:blocked, :banned].each do |trait| + context "when viewed user is #{trait}" do + let_it_be(:viewed_user) { build(:user, trait) } + let(:context) { Sidebars::Context.new(current_user: user, container: viewed_user) } + + context 'when user is not logged in' do + it 'is not allowed to view the menu item' do + expect(described_class.new(Sidebars::Context.new(current_user: nil, + container: viewed_user)).render?).to be false + end + end + + context 'when current user has permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :read_user_profile, viewed_user).and_return(true) + end + + it 'is allowed to view the menu item' do + expect(described_class.new(context).render?).to be true + end + end + + context 'when current user does not have permission' do + it 'is not allowed to view the menu item' do + expect(described_class.new(context).render?).to be false + end + end + end + end +end + +RSpec.shared_examples 'Followers/followees counts' do |symbol| + let_it_be(:current_user) { build(:user) } + let_it_be(:user) { build(:user) } + + let(:context) { Sidebars::Context.new(current_user: current_user, container: user) } + + subject { described_class.new(context) } + + context 'when there are items' do + before do + allow(user).to receive(symbol).and_return([1, 2]) + end + + it 'renders the pill' do + expect(subject.has_pill?).to be(true) + end + + it 'returns the count' do + expect(subject.pill_count).to be(2) + end + end + + context 'when there are no items' do + it 'does not render the pill' do + expect(subject.has_pill?).to be(false) + end + end +end diff --git a/spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb new file mode 100644 index 00000000000..b91386d1935 --- /dev/null +++ b/spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'User settings menu' do |link:, title:, icon:, active_routes:| + let_it_be(:user) { create(:user) } + + let(:context) { Sidebars::Context.new(current_user: user, container: nil) } + + subject { described_class.new(context) } + + it 'does not contain any sub menu' do + expect(subject.has_items?).to be false + end + + it 'renders the correct link' do + expect(subject.link).to match link + end + + it 'renders the correct title' do + expect(subject.title).to eq title + end + + it 'renders the correct icon' do + expect(subject.sprite_icon).to be icon + end + + it 'defines correct active route' do + expect(subject.active_routes).to eq active_routes + end +end + +RSpec.shared_examples 'User settings menu #render? method' do + describe '#render?' do + subject { described_class.new(context) } + + context 'when user is logged in' do + let_it_be(:user) { build(:user) } + let(:context) { Sidebars::Context.new(current_user: user, container: nil) } + + it 'renders' do + expect(subject.render?).to be true + end + end + + context 'when user is not logged in' do + let(:context) { Sidebars::Context.new(current_user: nil, container: nil) } + + it 'does not render' do + expect(subject.render?).to be false + end + end + end +end diff --git a/spec/support/shared_examples/mailers/export_csv_shared_examples.rb b/spec/support/shared_examples/mailers/export_csv_shared_examples.rb new file mode 100644 index 00000000000..731d7c810f9 --- /dev/null +++ b/spec/support/shared_examples/mailers/export_csv_shared_examples.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'export csv email' do |collection_type| + include_context 'gitlab email notification' + + it 'attachment has csv mime type' do + expect(attachment.mime_type).to eq 'text/csv' + end + + it 'generates a useful filename' do + expect(attachment.filename).to include(Date.today.year.to_s) + expect(attachment.filename).to include(collection_type) + expect(attachment.filename).to include('myproject') + expect(attachment.filename).to end_with('.csv') + end + + it 'mentions number of objects and project name' do + expect(subject).to have_content '3' + expect(subject).to have_content empty_project.name + end + + it "doesn't need to mention truncation by default" do + expect(subject).not_to have_content 'truncated' + end + + context 'when truncated' do + let(:export_status) { { truncated: true, rows_expected: 12, rows_written: 10 } } + + it 'mentions that the csv has been truncated' do + expect(subject).to have_content 'truncated' + end + + it 'mentions the number of objects written and expected' do + expect(subject).to have_content "10 of 12 #{collection_type.humanize.downcase}" + end + end +end diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb index 2e182fb399d..cf1ab7697ab 100644 --- a/spec/support/shared_examples/mailers/notify_shared_examples.rb +++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb @@ -59,7 +59,7 @@ end RSpec.shared_examples 'an email with X-GitLab headers containing project details' do it 'has X-GitLab-Project headers' do aggregate_failures do - full_path_as_domain = "#{project.name}.#{project.namespace.path}" + full_path_as_domain = "#{project.path}.#{project.namespace.path}" is_expected.to have_header('X-GitLab-Project', /#{project.name}/) is_expected.to have_header('X-GitLab-Project-Id', /#{project.id}/) is_expected.to have_header('X-GitLab-Project-Path', /#{project.full_path}/) @@ -294,3 +294,17 @@ RSpec.shared_examples 'does not render a manage notifications link' do end end end + +RSpec.shared_examples 'email with default notification reason' do + it do + is_expected.to have_body_text("You're receiving this email because of your account") + is_expected.to have_plain_text_content("You're receiving this email because of your account") + end +end + +RSpec.shared_examples 'email with link to issue' do + it do + is_expected.to have_body_text(%(<a href="#{project_issue_url(project, issue)}">view it on GitLab</a>)) + is_expected.to have_plain_text_content("view it on GitLab: #{project_issue_url(project, issue)}") + end +end diff --git a/spec/support/shared_examples/metrics_instrumentation_shared_examples.rb b/spec/support/shared_examples/metrics_instrumentation_shared_examples.rb new file mode 100644 index 00000000000..cef9860fe25 --- /dev/null +++ b/spec/support/shared_examples/metrics_instrumentation_shared_examples.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a correct instrumented metric value' do |params| + let(:time_frame) { params[:time_frame] } + let(:options) { params[:options] } + let(:metric) { described_class.new(time_frame: time_frame, options: options) } + + around do |example| + freeze_time { example.run } + end + + before do + if metric.respond_to?(:relation, true) && metric.send(:relation).respond_to?(:connection) + allow(metric.send(:relation).connection).to receive(:transaction_open?).and_return(false) + end + end + + it 'has correct value' do + expect(metric.value).to eq(expected_value) + end +end + +RSpec.shared_examples 'a correct instrumented metric query' do |params| + let(:time_frame) { params[:time_frame] } + let(:options) { params[:options] } + let(:metric) { described_class.new(time_frame: time_frame, options: options) } + + around do |example| + freeze_time { example.run } + end + + before do + allow(metric.send(:relation).connection).to receive(:transaction_open?).and_return(false) + end + + it 'has correct generate query' do + expect(metric.to_sql).to eq(expected_query) + end +end + +RSpec.shared_examples 'a correct instrumented metric value and query' do |params| + it_behaves_like 'a correct instrumented metric value', params + it_behaves_like 'a correct instrumented metric query', params +end diff --git a/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb new file mode 100644 index 00000000000..28eac52256f --- /dev/null +++ b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'migration that adds widget to work items definitions' do |widget_name:| + let(:migration) { described_class.new } + let(:work_item_definitions) { table(:work_item_widget_definitions) } + + describe '#up' do + it "creates widget definition in all types" do + work_item_definitions.where(name: widget_name).delete_all + + expect { migrate! }.to change { work_item_definitions.count }.by(7) + expect(work_item_definitions.all.pluck(:name)).to include(widget_name) + end + + it 'logs a warning if the type is missing' do + allow(described_class::WorkItemType).to receive(:find_by_name_and_namespace_id).and_call_original + allow(described_class::WorkItemType).to receive(:find_by_name_and_namespace_id) + .with('Issue', nil).and_return(nil) + + expect(Gitlab::AppLogger).to receive(:warn).with('type Issue is missing, not adding widget') + migrate! + end + end + + describe '#down' do + it "removes definitions for widget" do + migrate! + + expect { migration.down }.to change { work_item_definitions.count }.by(-7) + expect(work_item_definitions.all.pluck(:name)).not_to include(widget_name) + end + end +end diff --git a/spec/support/shared_examples/models/active_record_enum_shared_examples.rb b/spec/support/shared_examples/models/active_record_enum_shared_examples.rb index 3d765b6ca93..10f3263d4fc 100644 --- a/spec/support/shared_examples/models/active_record_enum_shared_examples.rb +++ b/spec/support/shared_examples/models/active_record_enum_shared_examples.rb @@ -10,3 +10,13 @@ RSpec.shared_examples 'having unique enum values' do end end end + +RSpec.shared_examples 'having enum with nil value' do + it 'has enum with nil value' do + subject.public_send("#{attr_value}!") + + expect(subject.public_send("#{attr}_for_database")).to be_nil + expect(subject.public_send("#{attr}?")).to eq(true) + expect(subject.class.public_send(attr_value)).to eq([subject]) + end +end diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb index 085fec6ff1e..addd37cde32 100644 --- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb @@ -221,11 +221,13 @@ RSpec.shared_examples "chat integration" do |integration_name| context "with commit comment" do let_it_be(:note) do - create(:note_on_commit, - author: user, - project: project, - commit_id: project.repository.commit.id, - note: "a comment on a commit") + create( + :note_on_commit, + author: user, + project: project, + commit_id: project.repository.commit.id, + note: "a comment on a commit" + ) end it_behaves_like "triggered #{integration_name} integration" @@ -261,9 +263,11 @@ RSpec.shared_examples "chat integration" do |integration_name| context "with failed pipeline" do let_it_be(:pipeline) do - create(:ci_pipeline, - project: project, status: "failed", - sha: project.commit.sha, ref: project.default_branch) + create( + :ci_pipeline, + project: project, status: "failed", + sha: project.commit.sha, ref: project.default_branch + ) end it_behaves_like "triggered #{integration_name} integration" @@ -271,9 +275,11 @@ RSpec.shared_examples "chat integration" do |integration_name| context "with succeeded pipeline" do let_it_be(:pipeline) do - create(:ci_pipeline, - project: project, status: "success", - sha: project.commit.sha, ref: project.default_branch) + create( + :ci_pipeline, + project: project, status: "success", + sha: project.commit.sha, ref: project.default_branch + ) end context "with default notify_only_broken_pipelines" do diff --git a/spec/support/shared_examples/models/ci/token_format_shared_examples.rb b/spec/support/shared_examples/models/ci/token_format_shared_examples.rb new file mode 100644 index 00000000000..0272982e2d0 --- /dev/null +++ b/spec/support/shared_examples/models/ci/token_format_shared_examples.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'ensures runners_token is prefixed' do |factory| + subject(:record) { FactoryBot.build(factory) } + + describe '#runners_token', feature_category: :system_access do + let(:runners_prefix) { RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX } + + it 'generates runners_token which starts with runner prefix' do + expect(record.runners_token).to match(a_string_starting_with(runners_prefix)) + end + + context 'when record has an invalid token' do + subject(:record) { FactoryBot.build(factory, runners_token: invalid_runners_token) } + + let(:invalid_runners_token) { "not_start_with_runners_prefix" } + + it 'generates runners_token which starts with runner prefix' do + expect(record.runners_token).to match(a_string_starting_with(runners_prefix)) + end + + it 'changes the attribute values for runners_token and runners_token_encrypted' do + expect { record.runners_token } + .to change { record[:runners_token] }.from(invalid_runners_token).to(nil) + .and change { record[:runners_token_encrypted] }.from(nil) + end + end + end +end diff --git a/spec/support/shared_examples/models/clusters/prometheus_client_shared.rb b/spec/support/shared_examples/models/clusters/prometheus_client_shared.rb index 8d6dcfef925..140968da272 100644 --- a/spec/support/shared_examples/models/clusters/prometheus_client_shared.rb +++ b/spec/support/shared_examples/models/clusters/prometheus_client_shared.rb @@ -41,10 +41,12 @@ RSpec.shared_examples '#prometheus_client shared' do subject.cluster.platform_kubernetes.namespace = 'a-namespace' stub_kubeclient_discover(cluster.platform_kubernetes.api_url) - create(:cluster_kubernetes_namespace, - cluster: cluster, - cluster_project: cluster.cluster_project, - project: cluster.cluster_project.project) + create( + :cluster_kubernetes_namespace, + cluster: cluster, + cluster_project: cluster.cluster_project, + project: cluster.cluster_project.project + ) end it 'creates proxy prometheus_client' do diff --git a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb index 122774a9028..a196b63585c 100644 --- a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb @@ -17,8 +17,12 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do [4, 1.second.from_now], # Exceeded the grace period, set by #backoff! [4, Time.current] # Exceeded the grace period, set by #backoff!, edge-case ].map do |(recent_failures, disabled_until)| - create(hook_factory, **default_factory_arguments, recent_failures: recent_failures, -disabled_until: disabled_until) + create( + hook_factory, + **default_factory_arguments, + recent_failures: recent_failures, + disabled_until: disabled_until + ) end end @@ -45,8 +49,12 @@ disabled_until: disabled_until) [0, suspended], [0, expired] ].map do |(recent_failures, disabled_until)| - create(hook_factory, **default_factory_arguments, recent_failures: recent_failures, -disabled_until: disabled_until) + create( + hook_factory, + **default_factory_arguments, + recent_failures: recent_failures, + disabled_until: disabled_until + ) end end @@ -61,6 +69,20 @@ disabled_until: disabled_until) # Nothing is missing expect(find_hooks.executable.to_a + find_hooks.disabled.to_a).to match_array(find_hooks.to_a) end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'causes all hooks to be considered executable' do + expect(find_hooks.executable.count).to eq(16) + end + + it 'causes no hooks to be considered disabled' do + expect(find_hooks.disabled).to be_empty + end + end end describe '#executable?', :freeze_time do @@ -108,6 +130,16 @@ disabled_until: disabled_until) it 'has the correct state' do expect(web_hook.executable?).to eq(executable) end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'is always executable' do + expect(web_hook).to be_executable + end + end end end @@ -151,7 +183,7 @@ disabled_until: disabled_until) context 'when we have exhausted the grace period' do before do - hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD) + hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD) end context 'when the hook is permanently disabled' do @@ -172,6 +204,16 @@ disabled_until: disabled_until) def run_expectation expect { hook.backoff! }.to change { hook.backoff_count }.by(1) end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'does not increment backoff count' do + expect { hook.failed! }.not_to change { hook.backoff_count } + end + end end end end @@ -181,6 +223,16 @@ disabled_until: disabled_until) def run_expectation expect { hook.failed! }.to change { hook.recent_failures }.by(1) end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'does not increment recent failure count' do + expect { hook.failed! }.not_to change { hook.recent_failures } + end + end end end @@ -189,6 +241,16 @@ disabled_until: disabled_until) expect { hook.disable! }.to change { hook.executable? }.from(true).to(false) end + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'does not disable the hook' do + expect { hook.disable! }.not_to change { hook.executable? } + end + end + it 'does nothing if the hook is already disabled' do allow(hook).to receive(:permanently_disabled?).and_return(true) @@ -210,7 +272,7 @@ disabled_until: disabled_until) end it 'allows FAILURE_THRESHOLD initial failures before we back-off' do - WebHook::FAILURE_THRESHOLD.times do + WebHooks::AutoDisabling::FAILURE_THRESHOLD.times do hook.backoff! expect(hook).not_to be_temporarily_disabled end @@ -221,13 +283,23 @@ disabled_until: disabled_until) context 'when hook has been told to back off' do before do - hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD) + hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD) hook.backoff! end it 'is true' do expect(hook).to be_temporarily_disabled end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'is false' do + expect(hook).not_to be_temporarily_disabled + end + end end end @@ -244,6 +316,16 @@ disabled_until: disabled_until) it 'is true' do expect(hook).to be_permanently_disabled end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'is false' do + expect(hook).not_to be_permanently_disabled + end + end end end @@ -258,15 +340,31 @@ disabled_until: disabled_until) end it { is_expected.to eq :disabled } + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it { is_expected.to eq(:executable) } + end end context 'when hook has been backed off' do before do - hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1) + hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1) hook.disabled_until = 1.hour.from_now end it { is_expected.to eq :temporarily_disabled } + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it { is_expected.to eq(:executable) } + end end end end diff --git a/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb b/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb index a4db4e25db3..c51e4999e81 100644 --- a/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb @@ -112,9 +112,10 @@ RSpec.shared_examples 'a cascading namespace setting boolean attribute' do it 'does not allow the local value to be saved' do subgroup_settings.send("#{settings_attribute_name}=", nil) - expect { subgroup_settings.save! } - .to raise_error(ActiveRecord::RecordInvalid, - /cannot be changed because it is locked by an ancestor/) + expect { subgroup_settings.save! }.to raise_error( + ActiveRecord::RecordInvalid, + /cannot be changed because it is locked by an ancestor/ + ) end end @@ -171,15 +172,59 @@ RSpec.shared_examples 'a cascading namespace setting boolean attribute' do end describe "##{settings_attribute_name}=" do - before do - subgroup_settings.update!(settings_attribute_name => nil) - group_settings.update!(settings_attribute_name => true) + using RSpec::Parameterized::TableSyntax + + where(:parent_value, :current_subgroup_value, :new_subgroup_value, :expected_subgroup_value_after_update) do + true | nil | true | nil + true | nil | "true" | nil + true | false | true | true + true | false | "true" | true + true | true | false | false + true | true | "false" | false + false | nil | false | nil + false | nil | true | true + false | true | false | false + false | false | true | true end - it 'does not save the value locally when it matches the cascaded value' do - subgroup_settings.update!(settings_attribute_name => true) + with_them do + before do + subgroup_settings.update!(settings_attribute_name => current_subgroup_value) + group_settings.update!(settings_attribute_name => parent_value) + end - expect(subgroup_settings.read_attribute(settings_attribute_name)).to eq(nil) + it 'validates starting values from before block', :aggregate_failures do + expect(group_settings.reload.read_attribute(settings_attribute_name)).to eq(parent_value) + expect(subgroup_settings.reload.read_attribute(settings_attribute_name)).to eq(current_subgroup_value) + end + + it 'does not save the value locally when it matches cascaded value', :aggregate_failures do + subgroup_settings.send("#{settings_attribute_name}=", new_subgroup_value) + + # Verify dirty value + expect(subgroup_settings.read_attribute(settings_attribute_name)).to eq(expected_subgroup_value_after_update) + + subgroup_settings.save! + + # Verify persisted value + expect(subgroup_settings.reload.read_attribute(settings_attribute_name)) + .to eq(expected_subgroup_value_after_update) + end + + context 'when mass assigned' do + before do + subgroup_settings.attributes = + { settings_attribute_name => new_subgroup_value, "lock_#{settings_attribute_name}" => false } + end + + it 'does not save the value locally when it matches cascaded value', :aggregate_failures do + subgroup_settings.save! + + # Verify persisted value + expect(subgroup_settings.reload.read_attribute(settings_attribute_name)) + .to eq(expected_subgroup_value_after_update) + end + end end end @@ -277,9 +322,10 @@ RSpec.shared_examples 'a cascading namespace setting boolean attribute' do it 'does not allow the attribute to be saved' do subgroup_settings.send("lock_#{settings_attribute_name}=", true) - expect { subgroup_settings.save! } - .to raise_error(ActiveRecord::RecordInvalid, - /cannot be changed because it is locked by an ancestor/) + expect { subgroup_settings.save! }.to raise_error( + ActiveRecord::RecordInvalid, + /cannot be changed because it is locked by an ancestor/ + ) end end @@ -299,9 +345,10 @@ RSpec.shared_examples 'a cascading namespace setting boolean attribute' do it 'does not allow the lock to be saved when the attribute is nil' do subgroup_settings.send("#{settings_attribute_name}=", nil) - expect { subgroup_settings.save! } - .to raise_error(ActiveRecord::RecordInvalid, - /cannot be nil when locking the attribute/) + expect { subgroup_settings.save! }.to raise_error( + ActiveRecord::RecordInvalid, + /cannot be nil when locking the attribute/ + ) end it 'copies the cascaded value when locking the attribute if the local value is nil', :aggregate_failures do @@ -320,9 +367,10 @@ RSpec.shared_examples 'a cascading namespace setting boolean attribute' do it 'does not allow the attribute to be saved' do subgroup_settings.send("lock_#{settings_attribute_name}=", true) - expect { subgroup_settings.save! } - .to raise_error(ActiveRecord::RecordInvalid, - /cannot be changed because it is locked by an ancestor/) + expect { subgroup_settings.save! }.to raise_error( + ActiveRecord::RecordInvalid, + /cannot be changed because it is locked by an ancestor/ + ) end end diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb index 5755b9a56b1..9d189842b28 100644 --- a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb @@ -17,6 +17,11 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| let(:amount) { 10 } let(:increment) { Gitlab::Counters::Increment.new(amount: amount, ref: 3) } let(:counter_key) { model.counter(attribute).key } + let(:returns_current) do + model.class.counter_attributes + .find { |a| a[:attribute] == attribute } + .fetch(:returns_current, false) + end subject { model.increment_counter(attribute, increment) } @@ -61,6 +66,33 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| end end + describe '#increment_amount' do + it 'increases the egress in cache' do + model.increment_amount(attribute, 3) + + expect(model.counter(attribute).get).to eq(3) + end + end + + describe '#current_counter' do + let(:data_transfer_node) do + args = { project: project } + args[attribute] = 2 + create(:project_data_transfer, **args) + end + + it 'increases the amount in cache' do + if returns_current + incremented_by = 4 + db_state = model.read_attribute(attribute) + + model.send("increment_#{attribute}".to_sym, incremented_by) + + expect(model.send(attribute)).to eq(db_state + incremented_by) + end + end + end + context 'when increment amount is 0' do let(:amount) { 0 } @@ -155,14 +187,24 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| end describe '#update_counters_with_lease' do - let(:increments) { { build_artifacts_size: 1, packages_size: 2 } } + let_it_be(:first_attribute) { counter_attributes.first } + let_it_be(:second_attribute) { counter_attributes.second } + + let_it_be(:increments) do + increments_hash = {} + + increments_hash[first_attribute] = 1 + increments_hash[second_attribute] = 2 + + increments_hash + end subject { model.update_counters_with_lease(increments) } it 'updates counters of the record' do expect { subject } - .to change { model.reload.build_artifacts_size }.by(1) - .and change { model.reload.packages_size }.by(2) + .to change { model.reload.send(first_attribute) }.by(1) + .and change { model.reload.send(second_attribute) }.by(2) end it_behaves_like 'obtaining lease to update database' do @@ -193,17 +235,4 @@ RSpec.shared_examples 'obtaining lease to update database' do expect { subject }.not_to raise_error end end - - context 'when feature flag counter_attribute_db_lease_for_update is disabled' do - before do - stub_feature_flags(counter_attribute_db_lease_for_update: false) - allow(model).to receive(:in_lock).and_call_original - end - - it 'does not attempt to get a lock' do - expect(model).not_to receive(:in_lock) - - subject - end - end end diff --git a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb index 2e528f7996c..2dad35dc46e 100644 --- a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb @@ -35,7 +35,6 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:| end it_behaves_like 'Snowplow event tracking with RedisHLL context' do - let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } let(:category) { described_class.to_s } let(:action) { 'perform_integrations_action' } let(:namespace) { project.namespace } diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb index 0ef9ab25505..28d2d4f1597 100644 --- a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb @@ -465,10 +465,13 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name context 'when commit comment event executed' do let(:commit_note) do - create(:note_on_commit, author: user, - project: project, - commit_id: project.repository.commit.id, - note: 'a comment on a commit') + create( + :note_on_commit, + author: user, + project: project, + commit_id: project.repository.commit.id, + note: 'a comment on a commit' + ) end let(:data) do @@ -480,8 +483,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name context 'when merge request comment event executed' do let(:merge_request_note) do - create(:note_on_merge_request, project: project, - note: 'a comment on a merge request') + create(:note_on_merge_request, project: project, note: 'a comment on a merge request') end let(:data) do @@ -493,8 +495,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name context 'when issue comment event executed' do let(:issue_note) do - create(:note_on_issue, project: project, - note: 'a comment on an issue') + create(:note_on_issue, project: project, note: 'a comment on an issue') end let(:data) do @@ -506,8 +507,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name context 'when snippet comment event executed' do let(:snippet_note) do - create(:note_on_project_snippet, project: project, - note: 'a comment on a snippet') + create(:note_on_project_snippet, project: project, note: 'a comment on a snippet') end let(:data) do @@ -522,9 +522,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name let_it_be(:user) { create(:user) } let_it_be_with_refind(:project) { create(:project, :repository, creator: user) } let(:pipeline) do - create(:ci_pipeline, - project: project, status: status, - sha: project.commit.sha, ref: project.default_branch) + create(:ci_pipeline, project: project, status: status, sha: project.commit.sha, ref: project.default_branch) end before do @@ -557,9 +555,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name context 'with failed pipeline' do context 'on default branch' do let(:pipeline) do - create(:ci_pipeline, - project: project, status: :failed, - sha: project.commit.sha, ref: project.default_branch) + create(:ci_pipeline, project: project, status: :failed, sha: project.commit.sha, ref: project.default_branch) end let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } @@ -587,9 +583,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end let(:pipeline) do - create(:ci_pipeline, - project: project, status: :failed, - sha: project.commit.sha, ref: 'a-protected-branch') + create(:ci_pipeline, project: project, status: :failed, sha: project.commit.sha, ref: 'a-protected-branch') end let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } @@ -617,9 +611,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end let(:pipeline) do - create(:ci_pipeline, - project: project, status: :failed, - sha: project.commit.sha, ref: '1-stable') + create(:ci_pipeline, project: project, status: :failed, sha: project.commit.sha, ref: '1-stable') end let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } @@ -643,9 +635,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name context 'on a neither protected nor default branch' do let(:pipeline) do - create(:ci_pipeline, - project: project, status: :failed, - sha: project.commit.sha, ref: 'a-random-branch') + create(:ci_pipeline, project: project, status: :failed, sha: project.commit.sha, ref: 'a-random-branch') end let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } diff --git a/spec/support/shared_examples/models/concerns/protected_branch_access_examples.rb b/spec/support/shared_examples/models/concerns/protected_branch_access_examples.rb new file mode 100644 index 00000000000..dd27ff3844f --- /dev/null +++ b/spec/support/shared_examples/models/concerns/protected_branch_access_examples.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'protected branch access' do + include_examples 'protected ref access', :protected_branch + + it { is_expected.to belong_to(:protected_branch) } + + describe '#project' do + before do + allow(protected_ref).to receive(:project) + end + + it 'delegates project to protected_branch association' do + described_class.new(protected_branch: protected_ref).project + + expect(protected_ref).to have_received(:project) + end + end +end diff --git a/spec/support/shared_examples/models/concerns/protected_ref_access_allowed_access_levels_examples.rb b/spec/support/shared_examples/models/concerns/protected_ref_access_allowed_access_levels_examples.rb new file mode 100644 index 00000000000..8e15720c79a --- /dev/null +++ b/spec/support/shared_examples/models/concerns/protected_ref_access_allowed_access_levels_examples.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'protected ref access allowed_access_levels' do |excludes: []| + describe '::allowed_access_levels' do + subject { described_class.allowed_access_levels } + + let(:all_levels) do + [ + Gitlab::Access::DEVELOPER, + Gitlab::Access::MAINTAINER, + Gitlab::Access::ADMIN, + Gitlab::Access::NO_ACCESS + ] + end + + context 'when running on Gitlab.com?' do + let(:levels) { all_levels.excluding(Gitlab::Access::ADMIN, *excludes) } + + before do + allow(Gitlab).to receive(:com?).and_return(true) + end + + it { is_expected.to match_array(levels) } + end + + context 'when self hosted?' do + let(:levels) { all_levels.excluding(*excludes) } + + before do + allow(Gitlab).to receive(:com?).and_return(false) + end + + it { is_expected.to match_array(levels) } + end + end +end diff --git a/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb b/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb new file mode 100644 index 00000000000..4753d7a4556 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'protected ref access' do |association| + include ExternalAuthorizationServiceHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:protected_ref) { create(association, project: project) } # rubocop:disable Rails/SaveBang + + it { is_expected.to validate_inclusion_of(:access_level).in_array(described_class.allowed_access_levels) } + + it { is_expected.to validate_presence_of(:access_level) } + + context 'when not role?' do + before do + allow(subject).to receive(:role?).and_return(false) + end + + it { is_expected.not_to validate_presence_of(:access_level) } + end + + describe '::human_access_levels' do + subject { described_class.human_access_levels } + + let(:levels) do + { + Gitlab::Access::DEVELOPER => "Developers + Maintainers", + Gitlab::Access::MAINTAINER => "Maintainers", + Gitlab::Access::ADMIN => 'Instance admins', + Gitlab::Access::NO_ACCESS => "No one" + }.slice(*described_class.allowed_access_levels) + end + + it { is_expected.to eq(levels) } + end + + describe '#check_access' do + let_it_be(:current_user) { create(:user) } + + let(:access_level) { ::Gitlab::Access::DEVELOPER } + + before_all do + project.add_developer(current_user) + end + + subject do + described_class.new( + association => protected_ref, + access_level: access_level + ) + end + + context 'when current_user is nil' do + it { expect(subject.check_access(nil)).to eq(false) } + end + + context 'when access_level is NO_ACCESS' do + let(:access_level) { ::Gitlab::Access::NO_ACCESS } + + it { expect(subject.check_access(current_user)).to eq(false) } + end + + context 'when instance admin access is configured' do + let(:access_level) { Gitlab::Access::ADMIN } + + context 'when current_user is a maintainer' do + it { expect(subject.check_access(current_user)).to eq(false) } + end + + context 'when current_user is admin' do + before do + allow(current_user).to receive(:admin?).and_return(true) + end + + it { expect(subject.check_access(current_user)).to eq(true) } + end + end + + context 'when current_user can push_code to project' do + context 'and member access is high enough' do + it { expect(subject.check_access(current_user)).to eq(true) } + + context 'when external authorization denies access' do + before do + external_service_deny_access(current_user, project) + end + + it { expect(subject.check_access(current_user)).to be_falsey } + end + end + + context 'and member access is too low' do + let(:access_level) { ::Gitlab::Access::MAINTAINER } + + it { expect(subject.check_access(current_user)).to eq(false) } + end + end + + context 'when current_user cannot push_code to project' do + before do + allow(current_user).to receive(:can?).with(:push_code, project).and_return(false) + end + + it { expect(subject.check_access(current_user)).to eq(false) } + end + end +end diff --git a/spec/support/shared_examples/models/concerns/protected_tag_access_examples.rb b/spec/support/shared_examples/models/concerns/protected_tag_access_examples.rb new file mode 100644 index 00000000000..49f616d5a59 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/protected_tag_access_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'protected tag access' do + include_examples 'protected ref access', :protected_tag + + let_it_be(:protected_tag) { create(:protected_tag) } + + it { is_expected.to belong_to(:protected_tag) } + + describe '#project' do + before do + allow(protected_tag).to receive(:project) + end + + it 'delegates project to protected_tag association' do + described_class.new(protected_tag: protected_tag).project + + expect(protected_tag).to have_received(:project) + end + end +end diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb index e4958779957..b04ac40b309 100644 --- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb @@ -84,9 +84,12 @@ RSpec.shared_examples 'a timebox' do |timebox_type| let(:max_date) { mid_point + 10.days } def box(from, to) - create(factory, *timebox_args, - start_date: from || open_on_left, - due_date: to || open_on_right) + create( + factory, + *timebox_args, + start_date: from || open_on_left, + due_date: to || open_on_right + ) end it 'can find overlapping timeboxes' do diff --git a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb index 848840ee297..f98528ffedc 100644 --- a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb @@ -18,8 +18,12 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur [3, nil], [3, 1.day.ago] ].map do |(recent_failures, disabled_until)| - create(hook_factory, **default_factory_arguments, recent_failures: recent_failures, -disabled_until: disabled_until) + create( + hook_factory, + **default_factory_arguments, + recent_failures: recent_failures, + disabled_until: disabled_until + ) end end @@ -110,7 +114,7 @@ disabled_until: disabled_until) context 'when we have exhausted the grace period' do before do - hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD) + hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD) end it 'does not disable the hook' do @@ -131,7 +135,7 @@ disabled_until: disabled_until) expect(hook).not_to be_temporarily_disabled # Backing off - WebHook::FAILURE_THRESHOLD.times do + WebHooks::AutoDisabling::FAILURE_THRESHOLD.times do hook.backoff! expect(hook).not_to be_temporarily_disabled end @@ -167,7 +171,7 @@ disabled_until: disabled_until) context 'when hook has been backed off' do before do - hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1) + hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1) hook.disabled_until = 1.hour.from_now end diff --git a/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb index cd6eb8c77fa..113dcc266fc 100644 --- a/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb @@ -19,7 +19,7 @@ RSpec.shared_examples 'something that has web-hooks' do context 'when there is a failed hook' do before do hook = create_hook - hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1) + hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1) end it { is_expected.to eq(true) } @@ -83,7 +83,7 @@ RSpec.shared_examples 'something that has web-hooks' do describe '#fetch_web_hook_failure', :clean_gitlab_redis_shared_state do context 'when a value has not been stored' do - it 'does not call #any_hook_failed?' do + it 'calls #any_hook_failed?' do expect(object.get_web_hook_failure).to be_nil expect(object).to receive(:any_hook_failed?).and_return(true) diff --git a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb index 5eeefacdeb9..3f532629961 100644 --- a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb +++ b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb @@ -290,6 +290,7 @@ RSpec.shared_examples 'value stream analytics label based stage' do context 'when `ProjectLabel is given' do let_it_be(:label) { create(:label) } + let(:expected_error) { s_('CycleAnalyticsStage|is not available for the selected group') } it 'raises error when `ProjectLabel` is given for `start_event_label`' do params = { @@ -300,7 +301,9 @@ RSpec.shared_examples 'value stream analytics label based stage' do end_event_identifier: :issue_closed } - expect { described_class.new(params) }.to raise_error(ActiveRecord::AssociationTypeMismatch) + stage = described_class.new(params) + expect(stage).to be_invalid + expect(stage.errors.messages_for(:start_event_label_id)).to eq([expected_error]) end it 'raises error when `ProjectLabel` is given for `end_event_label`' do @@ -312,7 +315,9 @@ RSpec.shared_examples 'value stream analytics label based stage' do end_event_label: label } - expect { described_class.new(params) }.to raise_error(ActiveRecord::AssociationTypeMismatch) + stage = described_class.new(params) + expect(stage).to be_invalid + expect(stage.errors.messages_for(:end_event_label_id)).to eq([expected_error]) end end end diff --git a/spec/support/shared_examples/models/database_event_tracking_shared_examples.rb b/spec/support/shared_examples/models/database_event_tracking_shared_examples.rb new file mode 100644 index 00000000000..3d98d9136e2 --- /dev/null +++ b/spec/support/shared_examples/models/database_event_tracking_shared_examples.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'database events tracking' do + describe 'events tracking' do + # required definitions: + # :record, :update_params + # + # other available attributes: + # :project, :namespace + + let(:user) { nil } + let(:category) { described_class.to_s } + let(:label) { described_class.table_name } + let(:action) { "database_event_#{property}" } + let(:feature_flag_name) { :product_intelligence_database_event_tracking } + let(:record_tracked_attributes) { record.attributes.slice(*described_class::SNOWPLOW_ATTRIBUTES.map(&:to_s)) } + let(:base_extra) { record_tracked_attributes.merge(project: try(:project), namespace: try(:namespace)) } + + before do + allow(Gitlab::Tracking).to receive(:database_event).and_call_original + end + + describe '#create' do + it_behaves_like 'Snowplow event tracking', overrides: { tracking_method: :database_event } do + subject(:create_record) { record } + + let(:extra) { base_extra } + let(:property) { 'create' } + end + end + + describe '#update', :freeze_time do + it_behaves_like 'Snowplow event tracking', overrides: { tracking_method: :database_event } do + subject(:update_record) { record.update!(update_params) } + + let(:extra) { base_extra.merge(update_params.stringify_keys) } + let(:property) { 'update' } + end + end + + describe '#destroy' do + it_behaves_like 'Snowplow event tracking', overrides: { tracking_method: :database_event } do + subject(:delete_record) { record.destroy! } + + let(:extra) { base_extra } + let(:property) { 'destroy' } + end + end + end +end + +RSpec.shared_examples 'database events tracking batch 2' do + it_behaves_like 'database events tracking' do + let(:feature_flag_name) { :product_intelligence_database_event_tracking_batch2 } + end +end diff --git a/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb b/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb index 64390ccdc25..f1f6d799cf3 100644 --- a/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb +++ b/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb @@ -10,10 +10,12 @@ RSpec.shared_examples 'a valid diff note with after commit callback' do it 'raises an error' do allow(diff_file_from_repository).to receive(:line_for_position).with(position).and_return(nil) - expect { subject.save! }.to raise_error(::DiffNote::NoteDiffFileCreationError, - "Failed to find diff line for: #{diff_file_from_repository.file_path}, "\ - "old_line: #{position.old_line}"\ - ", new_line: #{position.new_line}") + expect { subject.save! }.to raise_error( + ::DiffNote::NoteDiffFileCreationError, + "Failed to find diff line for: #{diff_file_from_repository.file_path}, "\ + "old_line: #{position.old_line}"\ + ", new_line: #{position.new_line}" + ) end end diff --git a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb index 7dfdd24177e..0cf109ce5c5 100644 --- a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb +++ b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb @@ -3,7 +3,6 @@ RSpec.shared_examples Integrations::BaseSlashCommands do describe "Associations" do it { is_expected.to respond_to :token } - it { is_expected.to have_many :chat_names } end describe 'default values' do @@ -85,7 +84,7 @@ RSpec.shared_examples Integrations::BaseSlashCommands do end context 'when the user is authenticated' do - let!(:chat_name) { create(:chat_name, integration: subject) } + let!(:chat_name) { create(:chat_name) } let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } } subject do diff --git a/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb b/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb index 6d519e561ee..d438918eb60 100644 --- a/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb +++ b/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb @@ -10,19 +10,19 @@ end RSpec.shared_examples 'allows project key on reference pattern' do |url_attr| it 'allows underscores in the project name' do - expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' end it 'allows numbers in the project name' do - expect(described_class.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' + expect(subject.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' end it 'requires the project name to begin with A-Z' do - expect(described_class.reference_pattern.match('3EXT_EXT-1234')).to eq nil - expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + expect(subject.reference_pattern.match('3EXT_EXT-1234')).to eq nil + expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' end it 'does not allow issue number to finish with a letter' do - expect(described_class.reference_pattern.match('EXT-123A')).to eq(nil) + expect(subject.reference_pattern.match('EXT-123A')).to eq(nil) end end diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index 7159c55e303..e9e25dee746 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -392,6 +392,30 @@ RSpec.shared_examples_for "bulk member creation" do expect(members.first).to be_invite end + context 'with different source types' do + shared_examples 'supports multiple sources' do + specify do + members = described_class.add_members(sources, [user1, user2], :maintainer) + + expect(members.map(&:user)).to contain_exactly(user1, user2, user1, user2) + expect(members).to all(be_a(member_type)) + expect(members).to all(be_persisted) + end + end + + context 'with an array of sources' do + let_it_be(:sources) { [source, source2] } + + it_behaves_like 'supports multiple sources' + end + + context 'with a query producing sources' do + let_it_be(:sources) { source_type.id_in([source, source2]) } + + it_behaves_like 'supports multiple sources' + end + end + context 'with de-duplication' do it 'has the same user by id and user' do members = described_class.add_members(source, [user1.id, user1, user1.id, user2, user2.id, user2], :maintainer) @@ -484,11 +508,13 @@ RSpec.shared_examples_for "bulk member creation" do create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci)) expect do - described_class.add_members(source, - [user1.id], - :developer, - tasks_to_be_done: %w(issues), - tasks_project_id: task_project.id) + described_class.add_members( + source, + [user1.id], + :developer, + tasks_to_be_done: %w(issues), + tasks_project_id: task_project.id + ) end.not_to change { MemberTask.count } member.reset @@ -498,11 +524,13 @@ RSpec.shared_examples_for "bulk member creation" do it 'adds tasks to be done if they do not exist', :aggregate_failures do expect do - described_class.add_members(source, - [user1.id], - :developer, - tasks_to_be_done: %w(issues), - tasks_project_id: task_project.id) + described_class.add_members( + source, + [user1.id], + :developer, + tasks_to_be_done: %w(issues), + tasks_project_id: task_project.id + ) end.to change { MemberTask.count }.by(1) member = source.members.find_by(user_id: user1.id) diff --git a/spec/support/shared_examples/models/members_notifications_shared_example.rb b/spec/support/shared_examples/models/members_notifications_shared_example.rb index e28220334ac..329cb812a08 100644 --- a/spec/support/shared_examples/models/members_notifications_shared_example.rb +++ b/spec/support/shared_examples/models/members_notifications_shared_example.rb @@ -69,7 +69,7 @@ RSpec.shared_examples 'members notifications' do |entity_type| let(:member) { create(:"#{entity_type}_member", :invited) } it "calls NotificationService.decline_#{entity_type}_invite" do - expect(notification_service).to receive(:"decline_#{entity_type}_invite").with(member) + expect(notification_service).to receive(:decline_invite).with(member) member.decline_invite! end diff --git a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb index 5be0f6349ea..c2c123277ee 100644 --- a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb +++ b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb @@ -20,7 +20,6 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| let_it_be(:component_file_other_architecture, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_2) } let_it_be(:component_file_other_component, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_2, architecture: architecture1_1) } let_it_be(:component_file_other_compression_type, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, compression_type: :xz) } - let_it_be(:component_file_other_file_md5, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_md5: 'other_md5') } let_it_be(:component_file_other_file_sha256, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_sha256: 'other_sha256') } let_it_be(:component_file_other_container, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component2_1, architecture: architecture2_1) } let_it_be_with_refind(:component_file_with_file_type_sources) { create("debian_#{container_type}_component_file", :sources, component: component1_1) } @@ -100,10 +99,6 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| it { is_expected.to validate_presence_of(:file_store) } end - describe '#file_md5' do - it { is_expected.to validate_presence_of(:file_md5) } - end - describe '#file_sha256' do it { is_expected.to validate_presence_of(:file_sha256) } end @@ -231,4 +226,20 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| it { is_expected.to eq("#{component1_1.name}/binary-#{architecture1_1.name}/Packages.xz") } end end + + describe '#empty?' do + subject { component_file_with_architecture.empty? } + + context 'with a non-empty component' do + it { is_expected.to be_falsey } + end + + context 'with an empty component' do + before do + component_file_with_architecture.update! size: 0 + end + + it { is_expected.to be_truthy } + end + end end diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb index ac4ad4525aa..3ea2ff4d8f0 100644 --- a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb +++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb @@ -1,32 +1,13 @@ # frozen_string_literal: true -require 'spec_helper' - -RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| - let_it_be(:distribution_with_suite, freeze: can_freeze) { create(factory, :with_suite) } - let_it_be(:distribution_with_same_container, freeze: can_freeze) { create(factory, container: distribution_with_suite.container ) } - let_it_be(:distribution_with_same_codename, freeze: can_freeze) { create(factory, codename: distribution_with_suite.codename ) } - let_it_be(:distribution_with_same_suite, freeze: can_freeze) { create(factory, suite: distribution_with_suite.suite ) } - let_it_be(:distribution_with_codename_and_suite_flipped, freeze: can_freeze) { create(factory, codename: distribution_with_suite.suite, suite: distribution_with_suite.codename) } - - let_it_be_with_refind(:distribution) { create(factory, container: distribution_with_suite.container ) } - +RSpec.shared_examples 'Debian Distribution for common behavior' do subject { distribution } describe 'relationships' do - it { is_expected.to belong_to(container) } it { is_expected.to belong_to(:creator).class_name('User') } - - it { is_expected.to have_one(:key).class_name("Packages::Debian::#{container.capitalize}DistributionKey").with_foreign_key(:distribution_id).inverse_of(:distribution) } - it { is_expected.to have_many(:components).class_name("Packages::Debian::#{container.capitalize}Component").inverse_of(:distribution) } - it { is_expected.to have_many(:architectures).class_name("Packages::Debian::#{container.capitalize}Architecture").inverse_of(:distribution) } end describe 'validations' do - describe "##{container}" do - it { is_expected.to validate_presence_of(container) } - end - describe "#creator" do it { is_expected.not_to validate_presence_of(:creator) } end @@ -47,57 +28,6 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| it { is_expected.not_to allow_value('hé').for(:suite) } end - describe '#unique_debian_suite_and_codename' do - using RSpec::Parameterized::TableSyntax - - where(:with_existing_suite, :suite, :codename, :errors) do - false | nil | :keep | nil - false | 'testing' | :keep | nil - false | nil | :codename | ["Codename has already been taken"] - false | :codename | :keep | ["Suite has already been taken as Codename"] - false | :codename | :codename | ["Codename has already been taken", "Suite has already been taken as Codename"] - true | nil | :keep | nil - true | 'testing' | :keep | nil - true | nil | :codename | ["Codename has already been taken"] - true | :codename | :keep | ["Suite has already been taken as Codename"] - true | :codename | :codename | ["Codename has already been taken", "Suite has already been taken as Codename"] - true | nil | :suite | ["Codename has already been taken as Suite"] - true | :suite | :keep | ["Suite has already been taken"] - true | :suite | :suite | ["Suite has already been taken", "Codename has already been taken as Suite"] - end - - with_them do - context factory do - let(:new_distribution) { build(factory, container: distribution.container) } - - before do - distribution.update_column(:suite, 'suite-' + distribution.codename) if with_existing_suite - - if suite.is_a?(Symbol) - new_distribution.suite = distribution.send suite unless suite == :keep - else - new_distribution.suite = suite - end - - if codename.is_a?(Symbol) - new_distribution.codename = distribution.send codename unless codename == :keep - else - new_distribution.codename = codename - end - end - - it do - if errors - expect(new_distribution).not_to be_valid - expect(new_distribution.errors.to_a).to eq(errors) - else - expect(new_distribution).to be_valid - end - end - end - end - end - describe '#origin' do it { is_expected.to allow_value(nil).for(:origin) } it { is_expected.to allow_value('Debian').for(:origin) } @@ -179,7 +109,11 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| subject { described_class.with_codename_or_suite(distribution_with_suite.codename) } it 'does not return other distributions' do - expect(subject.to_a).to contain_exactly(distribution_with_suite, distribution_with_same_codename, distribution_with_codename_and_suite_flipped) + expect(subject.to_a) + .to contain_exactly( + distribution_with_suite, + distribution_with_same_codename, + distribution_with_codename_and_suite_flipped) end end @@ -187,54 +121,169 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| subject { described_class.with_codename_or_suite(distribution_with_suite.suite) } it 'does not return other distributions' do - expect(subject.to_a).to contain_exactly(distribution_with_suite, distribution_with_same_suite, distribution_with_codename_and_suite_flipped) + expect(subject.to_a) + .to contain_exactly( + distribution_with_suite, + distribution_with_same_suite, + distribution_with_codename_and_suite_flipped) end end end end +end - if container == :project - describe 'project distribution specifics' do - describe 'relationships' do - it { is_expected.to have_many(:publications).class_name('Packages::Debian::Publication').inverse_of(:distribution).with_foreign_key(:distribution_id) } - it { is_expected.to have_many(:packages).class_name('Packages::Package').through(:publications) } - end +RSpec.shared_examples 'Debian Distribution for specific behavior' do |factory| + describe '#unique_debian_suite_and_codename' do + using RSpec::Parameterized::TableSyntax + + where(:with_existing_suite, :suite, :codename, :errors) do + false | nil | :keep | nil + false | 'testing' | :keep | nil + false | nil | :codename | ["Codename has already been taken"] + false | :codename | :keep | ["Suite has already been taken as Codename"] + false | :codename | :codename | ["Codename has already been taken", "Suite has already been taken as Codename"] + true | nil | :keep | nil + true | 'testing' | :keep | nil + true | nil | :codename | ["Codename has already been taken"] + true | :codename | :keep | ["Suite has already been taken as Codename"] + true | :codename | :codename | ["Codename has already been taken", "Suite has already been taken as Codename"] + true | nil | :suite | ["Codename has already been taken as Suite"] + true | :suite | :keep | ["Suite has already been taken"] + true | :suite | :suite | ["Suite has already been taken", "Codename has already been taken as Suite"] end - else - describe 'group distribution specifics' do - let_it_be(:public_project) { create(:project, :public, group: distribution_with_suite.container) } - let_it_be(:public_distribution_with_same_codename) { create(:debian_project_distribution, container: public_project, codename: distribution_with_suite.codename) } - let_it_be(:public_package_with_same_codename) { create(:debian_package, project: public_project, published_in: public_distribution_with_same_codename) } - let_it_be(:public_distribution_with_same_suite) { create(:debian_project_distribution, container: public_project, suite: distribution_with_suite.suite) } - let_it_be(:public_package_with_same_suite) { create(:debian_package, project: public_project, published_in: public_distribution_with_same_suite) } - - let_it_be(:private_project) { create(:project, :private, group: distribution_with_suite.container) } - let_it_be(:private_distribution_with_same_codename) { create(:debian_project_distribution, container: private_project, codename: distribution_with_suite.codename) } - let_it_be(:private_package_with_same_codename) { create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename) } - let_it_be(:private_distribution_with_same_suite) { create(:debian_project_distribution, container: private_project, suite: distribution_with_suite.suite) } - let_it_be(:private_package_with_same_suite) { create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename) } - - describe '#packages' do - subject { distribution_with_suite.packages } - - it 'returns only public packages with same codename' do - expect(subject.to_a).to contain_exactly(public_package_with_same_codename) + + with_them do + context factory do + let(:new_distribution) { build(factory, container: distribution.container) } + + before do + distribution.update_column(:suite, "suite-#{distribution.codename}") if with_existing_suite + + if suite.is_a?(Symbol) + new_distribution.suite = distribution.send suite unless suite == :keep + else + new_distribution.suite = suite + end + + if codename.is_a?(Symbol) + new_distribution.codename = distribution.send codename unless codename == :keep + else + new_distribution.codename = codename + end + end + + it do + if errors + expect(new_distribution).not_to be_valid + expect(new_distribution.errors.to_a).to eq(errors) + else + expect(new_distribution).to be_valid + end end end + end + end +end - describe '#package_files' do - subject { distribution_with_suite.package_files } +RSpec.shared_examples 'Debian Distribution with project container' do + it_behaves_like 'Debian Distribution for specific behavior', :debian_project_distribution - it 'returns only files from public packages with same codename' do - expect(subject.to_a).to contain_exactly(*public_package_with_same_codename.package_files) - end + describe 'relationships' do + it { is_expected.to belong_to(:project) } - context 'with pending destruction package files' do - let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: public_package_with_same_codename) } + it { is_expected.to have_one(:key).class_name("Packages::Debian::ProjectDistributionKey").with_foreign_key(:distribution_id).inverse_of(:distribution) } + it { is_expected.to have_many(:components).class_name("Packages::Debian::ProjectComponent").inverse_of(:distribution) } + it { is_expected.to have_many(:architectures).class_name("Packages::Debian::ProjectArchitecture").inverse_of(:distribution) } + end - it 'does not return them' do - expect(subject.to_a).not_to include(package_file_pending_destruction) - end + describe "#project" do + it { is_expected.to validate_presence_of(:project) } + end + + describe 'project distribution specifics' do + describe 'relationships' do + it do + is_expected.to have_many(:publications).class_name('Packages::Debian::Publication').inverse_of(:distribution) + .with_foreign_key(:distribution_id) + end + + it { is_expected.to have_many(:packages).class_name('Packages::Package').through(:publications) } + end + end +end + +RSpec.shared_examples 'Debian Distribution with group container' do + it_behaves_like 'Debian Distribution for specific behavior', :debian_group_distribution + + describe 'relationships' do + it { is_expected.to belong_to(:group) } + + it { is_expected.to have_one(:key).class_name("Packages::Debian::GroupDistributionKey").with_foreign_key(:distribution_id).inverse_of(:distribution) } + it { is_expected.to have_many(:components).class_name("Packages::Debian::GroupComponent").inverse_of(:distribution) } + it { is_expected.to have_many(:architectures).class_name("Packages::Debian::GroupArchitecture").inverse_of(:distribution) } + end + + describe "#group" do + it { is_expected.to validate_presence_of(:group) } + end + + describe 'group distribution specifics' do + let_it_be(:public_project) { create(:project, :public, group: distribution_with_suite.container) } + let_it_be(:public_distribution_with_same_codename) do + create(:debian_project_distribution, container: public_project, codename: distribution_with_suite.codename) + end + + let_it_be(:public_package_with_same_codename) do + create(:debian_package, project: public_project, published_in: public_distribution_with_same_codename) + end + + let_it_be(:public_distribution_with_same_suite) do + create(:debian_project_distribution, container: public_project, suite: distribution_with_suite.suite) + end + + let_it_be(:public_package_with_same_suite) do + create(:debian_package, project: public_project, published_in: public_distribution_with_same_suite) + end + + let_it_be(:private_project) { create(:project, :private, group: distribution_with_suite.container) } + let_it_be(:private_distribution_with_same_codename) do + create(:debian_project_distribution, container: private_project, codename: distribution_with_suite.codename) + end + + let_it_be(:private_package_with_same_codename) do + create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename) + end + + let_it_be(:private_distribution_with_same_suite) do + create(:debian_project_distribution, container: private_project, suite: distribution_with_suite.suite) + end + + let_it_be(:private_package_with_same_suite) do + create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename) + end + + describe '#packages' do + subject { distribution_with_suite.packages } + + it 'returns only public packages with same codename' do + expect(subject.to_a).to contain_exactly(public_package_with_same_codename) + end + end + + describe '#package_files' do + subject { distribution_with_suite.package_files } + + it 'returns only files from public packages with same codename' do + expect(subject.to_a).to contain_exactly(*public_package_with_same_codename.package_files) + end + + context 'with pending destruction package files' do + let_it_be(:package_file_pending_destruction) do + create(:package_file, :pending_destruction, package: public_package_with_same_codename) + end + + it 'does not return them' do + expect(subject.to_a).not_to include(package_file_pending_destruction) end end end diff --git a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb index 3caf58da4d2..f1af1760e8d 100644 --- a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb +++ b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb @@ -19,7 +19,7 @@ RSpec.shared_examples 'ci_cd_settings delegation' do end end -RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: ''| +RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: '', default: false| using RSpec::Parameterized::TableSyntax context 'when ci_cd_settings is nil' do @@ -28,7 +28,7 @@ RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: ''| end it 'returns false' do - expect(project.send("#{prefix}#{delegated_method}")).to be(false) + expect(project.send("#{prefix}#{delegated_method}")).to be(default) end end diff --git a/spec/support/shared_examples/models/resource_event_shared_examples.rb b/spec/support/shared_examples/models/resource_event_shared_examples.rb index 038ff33c68a..1409f7caea8 100644 --- a/spec/support/shared_examples/models/resource_event_shared_examples.rb +++ b/spec/support/shared_examples/models/resource_event_shared_examples.rb @@ -10,6 +10,8 @@ RSpec.shared_examples 'a resource event' do let_it_be(:issue2) { create(:issue, author: user1) } let_it_be(:issue3) { create(:issue, author: user2) } + let(:resource_event) { described_class.name.demodulize.underscore.to_sym } + describe 'importable' do it { is_expected.to respond_to(:importing?) } it { is_expected.to respond_to(:imported?) } @@ -36,9 +38,9 @@ RSpec.shared_examples 'a resource event' do let!(:created_at2) { 2.days.ago } let!(:created_at3) { 3.days.ago } - let!(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: created_at1) } - let!(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: created_at2) } - let!(:event3) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: created_at3) } + let!(:event1) { create(resource_event, issue: issue1, created_at: created_at1) } + let!(:event2) { create(resource_event, issue: issue2, created_at: created_at2) } + let!(:event3) { create(resource_event, issue: issue2, created_at: created_at3) } it 'returns the expected events' do events = described_class.created_after(created_at3) @@ -62,9 +64,10 @@ RSpec.shared_examples 'a resource event for issues' do let_it_be(:issue2) { create(:issue, author: user1) } let_it_be(:issue3) { create(:issue, author: user2) } - let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1) } - let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2) } - let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1) } + let_it_be(:resource_event) { described_class.name.demodulize.underscore.to_sym } + let_it_be(:event1) { create(resource_event, issue: issue1) } + let_it_be(:event2) { create(resource_event, issue: issue2) } + let_it_be(:event3) { create(resource_event, issue: issue1) } describe 'associations' do it { is_expected.to belong_to(:issue) } @@ -93,9 +96,9 @@ RSpec.shared_examples 'a resource event for issues' do end describe '.by_created_at_earlier_or_equal_to' do - let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-10') } - let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: '2020-03-10') } - let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-12') } + let_it_be(:event1) { create(resource_event, issue: issue1, created_at: '2020-03-10') } + let_it_be(:event2) { create(resource_event, issue: issue2, created_at: '2020-03-10') } + let_it_be(:event3) { create(resource_event, issue: issue1, created_at: '2020-03-12') } it 'returns the expected events' do events = described_class.by_created_at_earlier_or_equal_to('2020-03-11 23:59:59') @@ -112,7 +115,7 @@ RSpec.shared_examples 'a resource event for issues' do if described_class.method_defined?(:issuable) describe '#issuable' do - let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue2) } + let_it_be(:event1) { create(resource_event, issue: issue2) } it 'returns the expected issuable' do expect(event1.issuable).to eq(issue2) @@ -125,6 +128,7 @@ RSpec.shared_examples 'a resource event for merge requests' do let_it_be(:user1) { create(:user) } let_it_be(:user2) { create(:user) } + let_it_be(:resource_event) { described_class.name.demodulize.underscore.to_sym } let_it_be(:merge_request1) { create(:merge_request, author: user1) } let_it_be(:merge_request2) { create(:merge_request, author: user1) } let_it_be(:merge_request3) { create(:merge_request, author: user2) } @@ -134,9 +138,9 @@ RSpec.shared_examples 'a resource event for merge requests' do end describe '.by_merge_request' do - let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request1) } - let_it_be(:event2) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) } - let_it_be(:event3) { create(described_class.name.underscore.to_sym, merge_request: merge_request1) } + let_it_be(:event1) { create(resource_event, merge_request: merge_request1) } + let_it_be(:event2) { create(resource_event, merge_request: merge_request2) } + let_it_be(:event3) { create(resource_event, merge_request: merge_request1) } it 'returns the expected records for an issue with events' do events = described_class.by_merge_request(merge_request1) @@ -153,7 +157,7 @@ RSpec.shared_examples 'a resource event for merge requests' do if described_class.method_defined?(:issuable) describe '#issuable' do - let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) } + let_it_be(:event1) { create(resource_event, merge_request: merge_request2) } it 'returns the expected issuable' do expect(event1.issuable).to eq(merge_request2) @@ -163,7 +167,7 @@ RSpec.shared_examples 'a resource event for merge requests' do context 'on callbacks' do it 'does not trigger note created subscription' do - event = build(described_class.name.underscore.to_sym, merge_request: merge_request1) + event = build(resource_event, merge_request: merge_request1) expect(GraphqlTriggers).not_to receive(:work_item_note_created) expect(event).not_to receive(:trigger_note_subscription_create) @@ -177,15 +181,17 @@ RSpec.shared_examples 'a note for work item resource event' do let_it_be(:project) { create(:project) } let_it_be(:work_item) { create(:work_item, :task, project: project, author: user) } + let(:resource_event) { described_class.name.demodulize.underscore.to_sym } + it 'builds synthetic note with correct synthetic_note_class' do - event = build(described_class.name.underscore.to_sym, issue: work_item) + event = build(resource_event, issue: work_item) expect(event.work_item_synthetic_system_note.class.name).to eq(event.synthetic_note_class.name) end context 'on callbacks' do it 'triggers note created subscription' do - event = build(described_class.name.underscore.to_sym, issue: work_item) + event = build(resource_event, issue: work_item) expect(GraphqlTriggers).to receive(:work_item_note_created) expect(event).to receive(:trigger_note_subscription_create).and_call_original diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index 7e69a6663d5..017e51ecd24 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -94,6 +94,40 @@ RSpec.shared_examples 'wiki model' do end end + describe '#has_home_page?' do + context 'when home page exists' do + before do + wiki.repository.create_file( + user, + 'home.md', + 'home file', + branch_name: wiki.default_branch, + message: "created home page", + author_email: user.email, + author_name: user.name + ) + end + + it 'returns true' do + expect(wiki.has_home_page?).to eq(true) + end + + it 'returns false when #find_page raise an error' do + allow(wiki) + .to receive(:find_page) + .and_raise(StandardError) + + expect(wiki.has_home_page?).to eq(false) + end + end + + context 'when home page does not exist' do + it 'returns false' do + expect(wiki.has_home_page?).to eq(false) + end + end + end + describe '#to_global_id' do it 'returns a global ID' do expect(wiki.to_global_id.to_s).to eq("gid://gitlab/#{wiki.class.name}/#{wiki.id}") @@ -791,6 +825,21 @@ RSpec.shared_examples 'wiki model' do end end + context 'when the repository fails to update' do + let!(:page) { create(:wiki_page, wiki: subject, title: 'test page') } + + it 'returns false and sets error message', :aggregate_failures do + expect(subject.repository) + .to receive(:update_file) + .and_raise(Gitlab::Git::Index::IndexError.new) + + expect(subject.update_page(page.page, content: 'new content', format: :markdown)) + .to eq(false) + expect(subject.error_message) + .to match("Duplicate page: A page with that title already exists") + end + end + context 'when page path does not have a default extension' do let!(:page) { create(:wiki_page, wiki: subject, title: 'test page') } diff --git a/spec/support/shared_examples/observability/csp_shared_examples.rb b/spec/support/shared_examples/observability/csp_shared_examples.rb index 0cd211f69eb..9d6e7e75f4d 100644 --- a/spec/support/shared_examples/observability/csp_shared_examples.rb +++ b/spec/support/shared_examples/observability/csp_shared_examples.rb @@ -31,19 +31,19 @@ RSpec.shared_examples 'observability csp policy' do |controller_class = describe let(:observability_url) { Gitlab::Observability.observability_url } let(:signin_url) do Gitlab::Utils.append_path(Gitlab.config.gitlab.url, - '/users/sign_in') + '/users/sign_in') end let(:oauth_url) do Gitlab::Utils.append_path(Gitlab.config.gitlab.url, - '/oauth/authorize') + '/oauth/authorize') end before do setup_csp_for_controller(controller_class, csp, any_time: true) group.add_developer(user) login_as(user) - allow(Gitlab::Observability).to receive(:observability_enabled?).and_return(true) + stub_feature_flags(observability_group_tab: true) end subject do @@ -67,7 +67,7 @@ RSpec.shared_examples 'observability csp policy' do |controller_class = describe end before do - allow(Gitlab::Observability).to receive(:observability_enabled?).and_return(false) + stub_feature_flags(observability_group_tab: false) end it 'does not add observability urls to the csp header' do @@ -76,23 +76,6 @@ RSpec.shared_examples 'observability csp policy' do |controller_class = describe end end - context 'when checking if observability is enabled' do - let(:csp) do - ActionDispatch::ContentSecurityPolicy.new do |p| - p.frame_src 'https://something.test' - end - end - - it 'check access for a given user and group' do - allow(Gitlab::Observability).to receive(:observability_enabled?) - - get tested_path - - expect(Gitlab::Observability).to have_received(:observability_enabled?) - .with(user, group).at_least(:once) - end - end - context 'when frame-src exists in the CSP config' do let(:csp) do ActionDispatch::ContentSecurityPolicy.new do |p| diff --git a/spec/support/shared_examples/observability/embed_observabilities_examples.rb b/spec/support/shared_examples/observability/embed_observabilities_examples.rb new file mode 100644 index 00000000000..c8d4e9e0d7e --- /dev/null +++ b/spec/support/shared_examples/observability/embed_observabilities_examples.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'embeds observability' do + it 'renders iframe in description' do + page.within('.description') do + expect_observability_iframe(page.html) + end + end + + it 'renders iframe in comment' do + expect(page).not_to have_css('.note-text') + + page.within('.js-main-target-form') do + fill_in('note[note]', with: observable_url) + click_button('Comment') + end + + wait_for_requests + + page.within('.note-text') do + expect_observability_iframe(page.html) + end + end +end + +RSpec.shared_examples 'does not embed observability' do + it 'does not render iframe in description' do + page.within('.description') do + expect_observability_iframe(page.html, to_be_nil: true) + end + end + + it 'does not render iframe in comment' do + expect(page).not_to have_css('.note-text') + + page.within('.js-main-target-form') do + fill_in('note[note]', with: observable_url) + click_button('Comment') + end + + wait_for_requests + + page.within('.note-text') do + expect_observability_iframe(page.html, to_be_nil: true) + end + end +end + +def expect_observability_iframe(html, to_be_nil: false) + iframe = Nokogiri::HTML.parse(html).at_css('#observability-ui-iframe') + + expect(html).to include(observable_url) + + if to_be_nil + expect(iframe).to be_nil + else + expect(iframe).not_to be_nil + iframe_src = "#{expected_observable_url}&theme=light&username=#{user.username}&kiosk=inline-embed" + expect(iframe.attributes['src'].value).to eq(iframe_src) + end +end diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb index 9ec1b8b3f5a..d1f5a01b10c 100644 --- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb +++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb @@ -401,3 +401,24 @@ RSpec.shared_examples 'package access with repository disabled' do it { is_expected.to be_allowed(:read_package) } end + +RSpec.shared_examples 'equivalent project policy abilities' do + where(:project_visibility, :user_role_on_project) do + project_visibilities = [:public, :internal, :private] + user_role_on_project = [:anonymous, :non_member, :guest, :reporter, :developer, :maintainer, :owner, :admin] + project_visibilities.product(user_role_on_project) + end + + with_them do + it 'evaluates the same' do + project = public_send("#{project_visibility}_project") + current_user = public_send(user_role_on_project) + enable_admin_mode!(current_user) if user_role_on_project == :admin + policy = ProjectPolicy.new(current_user, project) + old_permissions = policy.allowed?(old_policy) + new_permissions = policy.allowed?(new_policy) + + expect(old_permissions).to eq new_permissions + end + end +end diff --git a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb index f70621673d5..f9f8435c211 100644 --- a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb +++ b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb @@ -7,9 +7,9 @@ RSpec.shared_examples 'when regex matching everything is specified' do end it_behaves_like 'removing the expected tags', - service_response_extra: service_response_extra, - supports_caching: supports_caching, - delete_expectations: delete_expectations + service_response_extra: service_response_extra, + supports_caching: supports_caching, + delete_expectations: delete_expectations context 'with deprecated name_regex param' do let(:params) do @@ -17,9 +17,9 @@ RSpec.shared_examples 'when regex matching everything is specified' do end it_behaves_like 'removing the expected tags', - service_response_extra: service_response_extra, - supports_caching: supports_caching, - delete_expectations: delete_expectations + service_response_extra: service_response_extra, + supports_caching: supports_caching, + delete_expectations: delete_expectations end end @@ -31,9 +31,9 @@ RSpec.shared_examples 'when regex matching everything is specified and latest is end it_behaves_like 'removing the expected tags', - service_response_extra: service_response_extra, - supports_caching: supports_caching, - delete_expectations: delete_expectations + service_response_extra: service_response_extra, + supports_caching: supports_caching, + delete_expectations: delete_expectations end RSpec.shared_examples 'when delete regex matching specific tags is used' do @@ -43,9 +43,9 @@ RSpec.shared_examples 'when delete regex matching specific tags is used' do end it_behaves_like 'removing the expected tags', - service_response_extra: service_response_extra, - supports_caching: supports_caching, - delete_expectations: [%w[C D]] + service_response_extra: service_response_extra, + supports_caching: supports_caching, + delete_expectations: [%w[C D]] end RSpec.shared_examples 'when delete regex matching specific tags is used with overriding allow regex' do @@ -58,9 +58,9 @@ RSpec.shared_examples 'when delete regex matching specific tags is used with ove end it_behaves_like 'removing the expected tags', - service_response_extra: service_response_extra, - supports_caching: supports_caching, - delete_expectations: [%w[D]] + service_response_extra: service_response_extra, + supports_caching: supports_caching, + delete_expectations: [%w[D]] context 'with name_regex_delete overriding deprecated name_regex' do let(:params) do @@ -71,9 +71,9 @@ RSpec.shared_examples 'when delete regex matching specific tags is used with ove end it_behaves_like 'removing the expected tags', - service_response_extra: service_response_extra, - supports_caching: supports_caching, - delete_expectations: [%w[D]] + service_response_extra: service_response_extra, + supports_caching: supports_caching, + delete_expectations: [%w[D]] end end @@ -87,9 +87,9 @@ RSpec.shared_examples 'with allow regex value' do end it_behaves_like 'removing the expected tags', - service_response_extra: service_response_extra, - supports_caching: supports_caching, - delete_expectations: delete_expectations + service_response_extra: service_response_extra, + supports_caching: supports_caching, + delete_expectations: delete_expectations end RSpec.shared_examples 'when keeping only N tags' do @@ -135,9 +135,9 @@ RSpec.shared_examples 'when removing keeping only 3' do end it_behaves_like 'removing the expected tags', - service_response_extra: service_response_extra, - supports_caching: supports_caching, - delete_expectations: delete_expectations + service_response_extra: service_response_extra, + supports_caching: supports_caching, + delete_expectations: delete_expectations end RSpec.shared_examples 'when removing older than 1 day' do @@ -150,9 +150,9 @@ RSpec.shared_examples 'when removing older than 1 day' do end it_behaves_like 'removing the expected tags', - service_response_extra: service_response_extra, - supports_caching: supports_caching, - delete_expectations: delete_expectations + service_response_extra: service_response_extra, + supports_caching: supports_caching, + delete_expectations: delete_expectations end RSpec.shared_examples 'when combining all parameters' do @@ -166,9 +166,9 @@ RSpec.shared_examples 'when combining all parameters' do end it_behaves_like 'removing the expected tags', - service_response_extra: service_response_extra, - supports_caching: supports_caching, - delete_expectations: delete_expectations + service_response_extra: service_response_extra, + supports_caching: supports_caching, + delete_expectations: delete_expectations end RSpec.shared_examples 'when running a container_expiration_policy' do diff --git a/spec/support/shared_examples/prometheus/additional_metrics_shared_examples.rb b/spec/support/shared_examples/prometheus/additional_metrics_shared_examples.rb new file mode 100644 index 00000000000..d196114b227 --- /dev/null +++ b/spec/support/shared_examples/prometheus/additional_metrics_shared_examples.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'additional metrics query' do + include Prometheus::MetricBuilders + + let(:metric_group_class) { Gitlab::Prometheus::MetricGroup } + let(:metric_class) { Gitlab::Prometheus::Metric } + + let(:metric_names) { %w[metric_a metric_b] } + + let(:query_range_result) do + [{ metric: {}, values: [[1488758662.506, '0.00002996364761904785'], [1488758722.506, '0.00003090239047619091']] }] + end + + let(:client) { instance_double('Gitlab::PrometheusClient') } + let(:query_result) { described_class.new(client).query(*query_params) } + let(:project) { create(:project, :repository) } + let(:environment) { create(:environment, slug: 'environment-slug', project: project) } + + before do + allow(client).to receive(:label_values).and_return(metric_names) + allow(metric_group_class).to receive(:common_metrics).and_return([simple_metric_group(metrics: [simple_metric])]) + end + + describe 'metrics query context' do + subject! { described_class.new(client) } + + shared_examples 'query context containing environment slug and filter' do + it 'contains ci_environment_slug' do + expect(subject) + .to receive(:query_metrics).with(project, environment, hash_including(ci_environment_slug: environment.slug)) + + subject.query(*query_params) + end + + it 'contains environment filter' do + expect(subject).to receive(:query_metrics).with( + project, + environment, + hash_including( + environment_filter: "container_name!=\"POD\",environment=\"#{environment.slug}\"" + ) + ) + + subject.query(*query_params) + end + end + + describe 'project has Kubernetes service' do + context 'when user configured kubernetes from CI/CD > Clusters' do + let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) } + let(:environment) { create(:environment, slug: 'environment-slug', project: project) } + let(:kube_namespace) { environment.deployment_namespace } + + it_behaves_like 'query context containing environment slug and filter' + + it 'query context contains kube_namespace' do + expect(subject) + .to receive(:query_metrics).with(project, environment, hash_including(kube_namespace: kube_namespace)) + + subject.query(*query_params) + end + end + end + + describe 'project without Kubernetes service' do + it_behaves_like 'query context containing environment slug and filter' + + it 'query context contains empty kube_namespace' do + expect(subject).to receive(:query_metrics).with(project, environment, hash_including(kube_namespace: '')) + + subject.query(*query_params) + end + end + end + + context 'with one group where two metrics is found' do + before do + allow(metric_group_class).to receive(:common_metrics).and_return([simple_metric_group]) + end + + context 'when some queries return results' do + before do + allow(client).to receive(:query_range).with('query_range_a', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_b', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_empty', any_args).and_return([]) + end + + it 'return group data only for queries with results' do + expected = [ + { + group: 'name', + priority: 1, + metrics: [ + { + title: 'title', weight: 1, y_label: 'Values', queries: [ + { query_range: 'query_range_a', result: query_range_result }, + { query_range: 'query_range_b', label: 'label', unit: 'unit', result: query_range_result } + ] + } + ] + } + ] + + expect(query_result.to_json).to match_schema('prometheus/additional_metrics_query_result') + expect(query_result).to eq(expected) + end + end + end + + context 'with two groups with one metric each' do + let(:metrics) { [simple_metric(queries: [simple_query])] } + + before do + allow(metric_group_class).to receive(:common_metrics).and_return( + [ + simple_metric_group(name: 'group_a', metrics: [simple_metric(queries: [simple_query])]), + simple_metric_group(name: 'group_b', metrics: [simple_metric(title: 'title_b', queries: [simple_query('b')])]) + ]) + allow(client).to receive(:label_values).and_return(metric_names) + end + + context 'when both queries return results' do + before do + allow(client).to receive(:query_range).with('query_range_a', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_b', any_args).and_return(query_range_result) + end + + it 'return group data both queries' do + queries_with_result_a = { queries: [{ query_range: 'query_range_a', result: query_range_result }] } + queries_with_result_b = { queries: [{ query_range: 'query_range_b', result: query_range_result }] } + + expect(query_result.to_json).to match_schema('prometheus/additional_metrics_query_result') + + expect(query_result.count).to eq(2) + expect(query_result).to all(satisfy { |r| r[:metrics].count == 1 }) + + expect(query_result[0][:metrics].first).to include(queries_with_result_a) + expect(query_result[1][:metrics].first).to include(queries_with_result_b) + end + end + + context 'when one query returns result' do + before do + allow(client).to receive(:query_range).with('query_range_a', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_b', any_args).and_return([]) + end + + it 'return group data only for query with results' do + queries_with_result = { queries: [{ query_range: 'query_range_a', result: query_range_result }] } + + expect(query_result.to_json).to match_schema('prometheus/additional_metrics_query_result') + + expect(query_result.count).to eq(1) + expect(query_result).to all(satisfy { |r| r[:metrics].count == 1 }) + + expect(query_result.first[:metrics].first).to include(queries_with_result) + end + end + end +end diff --git a/spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb b/spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb new file mode 100644 index 00000000000..f308b4ad372 --- /dev/null +++ b/spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +RSpec.shared_examples "protected tags > access control > CE" do + ProtectedRef::AccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| + it "allows creating protected tags that #{access_type_name} can create" do + visit project_protected_tags_path(project) + + set_protected_tag_name('master') + set_allowed_to('create', access_type_name) + click_on_protect + + expect(ProtectedTag.count).to eq(1) + expect(ProtectedTag.last.create_access_levels.map(&:access_level)).to eq([access_type_id]) + end + + it "allows updating protected tags so that #{access_type_name} can create them" do + visit project_protected_tags_path(project) + + set_protected_tag_name('master') + set_allowed_to('create', 'No one') + click_on_protect + + expect(ProtectedTag.count).to eq(1) + + set_allowed_to('create', access_type_name, form: '.protected-tags-list') + + wait_for_requests + + expect(ProtectedTag.last.create_access_levels.map(&:access_level)).to include(access_type_id) + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb index d8690356f81..7cbaf40721a 100644 --- a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'close quick action' do |issuable_type| - include Spec::Support::Helpers::Features::NotesHelpers + include Features::NotesHelpers before do project.add_maintainer(maintainer) diff --git a/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb b/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb index b5704ad8f17..9b03cdbb3bf 100644 --- a/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb +++ b/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb @@ -23,11 +23,11 @@ RSpec.shared_examples 'does not exceed the issuable size limit' do end note = described_class.new(project, user, opts.merge( - note: note_text, - noteable_type: noteable_type, - noteable_id: issuable.id, - confidential: false - )).execute + note: note_text, + noteable_type: noteable_type, + noteable_id: issuable.id, + confidential: false + )).execute expect(note.errors[:validation]).to match_array([validation_message]) end @@ -44,11 +44,11 @@ RSpec.shared_examples 'does not exceed the issuable size limit' do end note = described_class.new(project, user, opts.merge( - note: note_text, - noteable_type: noteable_type, - noteable_id: issuable.id, - confidential: false - )).execute + note: note_text, + noteable_type: noteable_type, + noteable_id: issuable.id, + confidential: false + )).execute expect(note.errors[:validation]).to be_empty end diff --git a/spec/support/shared_examples/quick_actions/issue/issue_links_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/issue_links_quick_actions_shared_examples.rb new file mode 100644 index 00000000000..811b5ee4de2 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issue/issue_links_quick_actions_shared_examples.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'issues link quick action' do |command| + let_it_be_with_refind(:group) { create(:group) } + let_it_be_with_reload(:other_issue) { create(:issue, project: project) } + let_it_be_with_reload(:second_issue) { create(:issue, project: project) } + let_it_be_with_reload(:third_issue) { create(:issue, project: project) } + + let(:link_type) { command == :relate ? 'relates_to' : 'blocks' } + let(:links_query) do + if command == :blocked_by + IssueLink.where(target: issue, link_type: link_type).map(&:source) + else + IssueLink.where(source: issue, link_type: link_type).map(&:target) + end + end + + shared_examples 'link command' do + it 'links issues' do + service.execute(content, issue) + + expect(links_query).to match_array(issues_linked) + end + end + + context 'when user is member of group' do + before do + group.add_developer(user) + end + + context 'when linking a single issue' do + let(:issues_linked) { [other_issue] } + let(:content) { "/#{command} #{other_issue.to_reference}" } + + it_behaves_like 'link command' + end + + context 'when linking multiple issues at once' do + let(:issues_linked) { [second_issue, third_issue] } + let(:content) { "/#{command} #{second_issue.to_reference} #{third_issue.to_reference}" } + + it_behaves_like 'link command' + end + + context 'when quick action target is unpersisted' do + let(:issue) { build(:issue, project: project) } + let(:issues_linked) { [other_issue] } + let(:content) { "/#{command} #{other_issue.to_reference}" } + + it 'links the issues after the issue is persisted' do + service.execute(content, issue) + + issue.save! + + expect(links_query).to match_array(issues_linked) + end + end + + context 'with empty link command' do + let(:issues_linked) { [] } + let(:content) { "/#{command}" } + + it_behaves_like 'link command' + end + + context 'with already having linked issues' do + let(:issues_linked) { [second_issue, third_issue] } + let(:content) { "/#{command} #{third_issue.to_reference(project)}" } + + before do + create_existing_link(command) + end + + it_behaves_like 'link command' + end + + context 'with cross project' do + let_it_be_with_reload(:another_group) { create(:group, :public) } + let_it_be_with_reload(:other_project) { create(:project, group: another_group) } + + before do + another_group.add_developer(user) + [other_issue, second_issue, third_issue].map { |i| i.update!(project: other_project) } + end + + context 'when linking a cross project issue' do + let(:issues_linked) { [other_issue] } + let(:content) { "/#{command} #{other_issue.to_reference(project)}" } + + it_behaves_like 'link command' + end + + context 'when linking multiple cross projects issues at once' do + let(:issues_linked) { [second_issue, third_issue] } + let(:content) { "/#{command} #{second_issue.to_reference(project)} #{third_issue.to_reference(project)}" } + + it_behaves_like 'link command' + end + + context 'when linking a non-existing issue' do + let(:issues_linked) { [] } + let(:content) { "/#{command} imaginary##{non_existing_record_iid}" } + + it_behaves_like 'link command' + end + + context 'when linking a private issue' do + let_it_be(:private_issue) { create(:issue, project: create(:project, :private)) } + let(:issues_linked) { [] } + let(:content) { "/#{command} #{private_issue.to_reference(project)}" } + + it_behaves_like 'link command' + end + end + end + + def create_existing_link(command) + issues = [issue, second_issue] + source, target = command == :blocked_by ? issues.reverse : issues + + create(:issue_link, source: source, target: target, link_type: link_type) + end +end diff --git a/spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb index 3f1a98ca08e..7bd7500d546 100644 --- a/spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.shared_examples 'promote_to_incident quick action' do + include ListboxHelpers + describe '/promote_to_incident' do context 'when issue can be promoted' do it 'promotes issue to incident' do @@ -52,9 +54,11 @@ RSpec.shared_examples 'promote_to_incident quick action' do context 'when incident is selected for issue type' do it 'promotes issue to incident' do visit new_project_issue_path(project) + wait_for_requests + fill_in('Title', with: 'Title') find('.js-issuable-type-filter-dropdown-wrap').click - click_link('Incident') + select_listbox_item(_('Incident')) fill_in('Description', with: '/promote_to_incident') click_button('Create issue') diff --git a/spec/support/shared_examples/redis/redis_new_instance_shared_examples.rb b/spec/support/shared_examples/redis/redis_new_instance_shared_examples.rb new file mode 100644 index 00000000000..4a3732efe13 --- /dev/null +++ b/spec/support/shared_examples/redis/redis_new_instance_shared_examples.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples "redis_new_instance_shared_examples" do |name, fallback_class| + include TmpdirHelper + + let(:instance_specific_config_file) { "config/redis.#{name}.yml" } + let(:fallback_config_file) { nil } + let(:rails_root) { mktmpdir } + + before do + allow(fallback_class).to receive(:config_file_name).and_return(fallback_config_file) + end + + it_behaves_like "redis_shared_examples" + + describe '#fetch_config' do + subject { described_class.new('test').send(:fetch_config) } + + before do + FileUtils.mkdir_p(File.join(rails_root, 'config')) + + allow(described_class).to receive(:rails_root).and_return(rails_root) + end + + context 'when redis.yml exists' do + before do + allow(described_class).to receive(:config_file_name).and_call_original + allow(described_class).to receive(:redis_yml_path).and_call_original + end + + context 'when the fallback has a redis.yml entry' do + before do + File.write(File.join(rails_root, 'config/redis.yml'), { + 'test' => { + described_class.config_fallback.store_name.underscore => { 'fallback redis.yml' => 123 } + } + }.to_json) + end + + it { expect(subject).to eq({ 'fallback redis.yml' => 123 }) } + + context 'and an instance config file exists' do + before do + File.write(File.join(rails_root, instance_specific_config_file), { + 'test' => { 'instance specific file' => 456 } + }.to_json) + end + + it { expect(subject).to eq({ 'instance specific file' => 456 }) } + + context 'and the instance has a redis.yml entry' do + before do + File.write(File.join(rails_root, 'config/redis.yml'), { + 'test' => { name => { 'instance redis.yml' => 789 } } + }.to_json) + end + + it { expect(subject).to eq({ 'instance redis.yml' => 789 }) } + end + end + end + end + + context 'when no redis config file exsits' do + it 'returns nil' do + expect(subject).to eq(nil) + end + + context 'when resque.yml exists' do + before do + File.write(File.join(rails_root, 'config/resque.yml'), { + 'test' => { 'foobar' => 123 } + }.to_json) + end + + it 'returns the config from resque.yml' do + expect(subject).to eq({ 'foobar' => 123 }) + end + end + end + end +end diff --git a/spec/support/shared_examples/redis/redis_shared_examples.rb b/spec/support/shared_examples/redis/redis_shared_examples.rb new file mode 100644 index 00000000000..9224e01b1fe --- /dev/null +++ b/spec/support/shared_examples/redis/redis_shared_examples.rb @@ -0,0 +1,429 @@ +# frozen_string_literal: true + +RSpec.shared_examples "redis_shared_examples" do + include StubENV + include TmpdirHelper + + let(:test_redis_url) { "redis://redishost:#{redis_port}" } + let(:test_cluster_config) { { cluster: [{ host: "redis://redishost", port: redis_port }] } } + let(:config_file_name) { instance_specific_config_file } + let(:config_old_format_socket) { "spec/fixtures/config/redis_old_format_socket.yml" } + let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" } + let(:old_socket_path) { "/path/to/old/redis.sock" } + let(:new_socket_path) { "/path/to/redis.sock" } + let(:config_old_format_host) { "spec/fixtures/config/redis_old_format_host.yml" } + let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" } + let(:config_cluster_format_host) { "spec/fixtures/config/redis_cluster_format_host.yml" } + let(:redis_port) { 6379 } + let(:redis_database) { 99 } + let(:sentinel_port) { 26379 } + let(:config_with_environment_variable_inside) { "spec/fixtures/config/redis_config_with_env.yml" } + let(:config_env_variable_url) { "TEST_GITLAB_REDIS_URL" } + let(:rails_root) { mktmpdir } + + before do + allow(described_class).to receive(:config_file_name).and_return(Rails.root.join(config_file_name).to_s) + allow(described_class).to receive(:redis_yml_path).and_return('/dev/null') + end + + describe '.config_file_name' do + subject { described_class.config_file_name } + + before do + # Undo top-level stub of config_file_name because we are testing that method now. + allow(described_class).to receive(:config_file_name).and_call_original + + allow(described_class).to receive(:rails_root).and_return(rails_root) + FileUtils.mkdir_p(File.join(rails_root, 'config')) + end + + context 'when there is no config file anywhere' do + it { expect(subject).to be_nil } + end + end + + describe '.store' do + let(:rails_env) { 'development' } + + subject { described_class.new(rails_env).store } + + shared_examples 'redis store' do + let(:redis_store) { ::Redis::Store } + let(:redis_store_to_s) { "Redis Client connected to #{host} against DB #{redis_database}" } + + it 'instantiates Redis::Store' do + is_expected.to be_a(redis_store) + + expect(subject.to_s).to eq(redis_store_to_s) + end + + context 'with the namespace' do + let(:namespace) { 'namespace_name' } + let(:redis_store_to_s) do + "Redis Client connected to #{host} against DB #{redis_database} with namespace #{namespace}" + end + + subject { described_class.new(rails_env).store(namespace: namespace) } + + it "uses specified namespace" do + expect(subject.to_s).to eq(redis_store_to_s) + end + end + end + + context 'with old format' do + it_behaves_like 'redis store' do + let(:config_file_name) { config_old_format_host } + let(:host) { "localhost:#{redis_port}" } + end + end + + context 'with new format' do + it_behaves_like 'redis store' do + let(:config_file_name) { config_new_format_host } + let(:host) { "development-host:#{redis_port}" } + end + end + end + + describe '.params' do + subject { described_class.new(rails_env).params } + + let(:rails_env) { 'development' } + let(:config_file_name) { config_old_format_socket } + + it 'withstands mutation' do + params1 = described_class.params + params2 = described_class.params + params1[:foo] = :bar + + expect(params2).not_to have_key(:foo) + end + + context 'when url contains unix socket reference' do + context 'with old format' do + let(:config_file_name) { config_old_format_socket } + + it 'returns path key instead' do + is_expected.to include(path: old_socket_path) + is_expected.not_to have_key(:url) + end + end + + context 'with new format' do + let(:config_file_name) { config_new_format_socket } + + it 'returns path key instead' do + is_expected.to include(path: new_socket_path) + is_expected.not_to have_key(:url) + end + end + end + + context 'when url is host based' do + context 'with old format' do + let(:config_file_name) { config_old_format_host } + + it 'returns hash with host, port, db, and password' do + is_expected.to include(host: 'localhost', password: 'mypassword', port: redis_port, db: redis_database) + is_expected.not_to have_key(:url) + end + end + + context 'with new format' do + let(:config_file_name) { config_new_format_host } + + where(:rails_env, :host) do + [ + %w[development development-host], + %w[test test-host], + %w[production production-host] + ] + end + + with_them do + it 'returns hash with host, port, db, and password' do + is_expected.to include(host: host, password: 'mynewpassword', port: redis_port, db: redis_database) + is_expected.not_to have_key(:url) + end + end + end + + context 'with redis cluster format' do + let(:config_file_name) { config_cluster_format_host } + + where(:rails_env, :host) do + [ + %w[development development-master], + %w[test test-master], + %w[production production-master] + ] + end + + with_them do + it 'returns hash with cluster and password' do + is_expected.to include( + password: 'myclusterpassword', + cluster: [ + { host: "#{host}1", port: redis_port }, + { host: "#{host}2", port: redis_port } + ] + ) + is_expected.not_to have_key(:url) + end + end + end + end + end + + describe '.url' do + let(:config_file_name) { config_old_format_socket } + + it 'withstands mutation' do + url1 = described_class.url + url2 = described_class.url + url1 << 'foobar' unless url1.frozen? + + expect(url2).not_to end_with('foobar') + end + + context 'when yml file with env variable' do + let(:config_file_name) { config_with_environment_variable_inside } + + before do + stub_env(config_env_variable_url, test_redis_url) + end + + it 'reads redis url from env variable' do + expect(described_class.url).to eq test_redis_url + end + end + end + + describe '.version' do + it 'returns a version' do + expect(described_class.version).to be_present + end + end + + describe '.with' do + let(:config_file_name) { config_old_format_socket } + + before do + clear_pool + end + + after do + clear_pool + end + + context 'when running on single-threaded runtime' do + before do + allow(Gitlab::Runtime).to receive(:multi_threaded?).and_return(false) + end + + it 'instantiates a connection pool with size 5' do + expect(ConnectionPool).to receive(:new).with(size: 5).and_call_original + + described_class.with { |_redis_shared_example| true } + end + end + + context 'when running on multi-threaded runtime' do + before do + allow(Gitlab::Runtime).to receive(:multi_threaded?).and_return(true) + allow(Gitlab::Runtime).to receive(:max_threads).and_return(18) + end + + it 'instantiates a connection pool with a size based on the concurrency of the worker' do + expect(ConnectionPool).to receive(:new).with(size: 18 + 5).and_call_original + + described_class.with { |_redis_shared_example| true } + end + end + + context 'when there is no config at all' do + before do + # Undo top-level stub of config_file_name because we are testing that method now. + allow(described_class).to receive(:config_file_name).and_call_original + + allow(described_class).to receive(:rails_root).and_return(rails_root) + end + + it 'can run an empty block' do + expect { described_class.with { nil } }.not_to raise_error + end + end + end + + describe '#db' do + let(:rails_env) { 'development' } + + subject { described_class.new(rails_env).db } + + context 'with old format' do + let(:config_file_name) { config_old_format_host } + + it 'returns the correct db' do + expect(subject).to eq(redis_database) + end + end + + context 'with new format' do + let(:config_file_name) { config_new_format_host } + + it 'returns the correct db' do + expect(subject).to eq(redis_database) + end + end + + context 'with cluster-mode' do + let(:config_file_name) { config_cluster_format_host } + + it 'returns the correct db' do + expect(subject).to eq(0) + end + end + end + + describe '#sentinels' do + subject { described_class.new(rails_env).sentinels } + + let(:rails_env) { 'development' } + + context 'when sentinels are defined' do + let(:config_file_name) { config_new_format_host } + + where(:rails_env, :hosts) do + [ + ['development', %w[development-replica1 development-replica2]], + ['test', %w[test-replica1 test-replica2]], + ['production', %w[production-replica1 production-replica2]] + ] + end + + with_them do + it 'returns an array of hashes with host and port keys' do + is_expected.to include(host: hosts[0], port: sentinel_port) + is_expected.to include(host: hosts[1], port: sentinel_port) + end + end + end + + context 'when sentinels are not defined' do + let(:config_file_name) { config_old_format_host } + + it 'returns nil' do + is_expected.to be_nil + end + end + + context 'when cluster is defined' do + let(:config_file_name) { config_cluster_format_host } + + it 'returns nil' do + is_expected.to be_nil + end + end + end + + describe '#sentinels?' do + subject { described_class.new(Rails.env).sentinels? } + + context 'when sentinels are defined' do + let(:config_file_name) { config_new_format_host } + + it 'returns true' do + is_expected.to be_truthy + end + end + + context 'when sentinels are not defined' do + let(:config_file_name) { config_old_format_host } + + it { expect(subject).to eq(nil) } + end + + context 'when cluster is defined' do + let(:config_file_name) { config_cluster_format_host } + + it 'returns false' do + is_expected.to be_falsey + end + end + end + + describe '#raw_config_hash' do + it 'returns old-style single url config in a hash' do + expect(subject).to receive(:fetch_config) { test_redis_url } + expect(subject.send(:raw_config_hash)).to eq(url: test_redis_url) + end + + it 'returns cluster config without url key in a hash' do + expect(subject).to receive(:fetch_config) { test_cluster_config } + expect(subject.send(:raw_config_hash)).to eq(test_cluster_config) + end + end + + describe '#fetch_config' do + before do + FileUtils.mkdir_p(File.join(rails_root, 'config')) + # Undo top-level stub of config_file_name because we are testing that method now. + allow(described_class).to receive(:config_file_name).and_call_original + allow(described_class).to receive(:rails_root).and_return(rails_root) + end + + it 'raises an exception when the config file contains invalid yaml' do + Tempfile.open('bad.yml') do |file| + file.write('{"not":"yaml"') + file.flush + allow(described_class).to receive(:config_file_name) { file.path } + + expect { subject.send(:fetch_config) }.to raise_error(Psych::SyntaxError) + end + end + + context 'when redis.yml exists' do + subject { described_class.new('test').send(:fetch_config) } + + before do + allow(described_class).to receive(:redis_yml_path).and_call_original + end + + it 'uses config/redis.yml' do + File.write(File.join(rails_root, 'config/redis.yml'), { + 'test' => { described_class.store_name.underscore => { 'foobar' => 123 } } + }.to_json) + + expect(subject).to eq({ 'foobar' => 123 }) + end + end + + context 'when no config file exsits' do + subject { described_class.new('test').send(:fetch_config) } + + it 'returns nil' do + expect(subject).to eq(nil) + end + + context 'when resque.yml exists' do + before do + FileUtils.mkdir_p(File.join(rails_root, 'config')) + File.write(File.join(rails_root, 'config/resque.yml'), { + 'test' => { 'foobar' => 123 } + }.to_json) + end + + it 'returns the config from resque.yml' do + expect(subject).to eq({ 'foobar' => 123 }) + end + end + end + end + + def clear_pool + described_class.remove_instance_variable(:@pool) + rescue NameError + # raised if @pool was not set; ignore + end +end diff --git a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb index 2170025824f..74dbec063e0 100644 --- a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb +++ b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb @@ -14,7 +14,7 @@ RSpec.shared_examples 'GET resource access tokens available' do it 'lists all available scopes' do get_access_tokens - expect(assigns(:scopes)).to eq(Gitlab::Auth.resource_bot_scopes) + expect(assigns(:scopes)).to eq(Gitlab::Auth.available_scopes_for(resource)) end it 'returns for json response' do diff --git a/spec/support/shared_examples/requests/admin_mode_shared_examples.rb b/spec/support/shared_examples/requests/admin_mode_shared_examples.rb index 07fde7d3f35..4f198dfb740 100644 --- a/spec/support/shared_examples/requests/admin_mode_shared_examples.rb +++ b/spec/support/shared_examples/requests/admin_mode_shared_examples.rb @@ -1,98 +1,79 @@ # frozen_string_literal: true -RSpec.shared_examples 'GET request permissions for admin mode' do - it_behaves_like 'GET request permissions for admin mode when user' - it_behaves_like 'GET request permissions for admin mode when admin' -end - -RSpec.shared_examples 'PUT request permissions for admin mode' do |params| - it_behaves_like 'PUT request permissions for admin mode when user', params - it_behaves_like 'PUT request permissions for admin mode when admin', params -end - -RSpec.shared_examples 'POST request permissions for admin mode' do |params| - it_behaves_like 'POST request permissions for admin mode when user', params - it_behaves_like 'POST request permissions for admin mode when admin', params -end RSpec.shared_examples 'DELETE request permissions for admin mode' do - it_behaves_like 'DELETE request permissions for admin mode when user' - it_behaves_like 'DELETE request permissions for admin mode when admin' -end - -RSpec.shared_examples 'GET request permissions for admin mode when user' do - subject { get api(path, current_user, admin_mode: admin_mode) } + subject { delete api(path, current_user, admin_mode: admin_mode) } - let_it_be(:current_user) { create(:user) } + let_it_be(:success_status_code) { :no_content } + let_it_be(:failed_status_code) { :forbidden } - it_behaves_like 'admin mode on', true, :forbidden - it_behaves_like 'admin mode on', false, :forbidden + it_behaves_like 'when admin' + it_behaves_like 'when user' end -RSpec.shared_examples 'GET request permissions for admin mode when admin' do +RSpec.shared_examples 'GET request permissions for admin mode' do subject { get api(path, current_user, admin_mode: admin_mode) } - let_it_be(:current_user) { create(:admin) } - - it_behaves_like 'admin mode on', true, :ok - it_behaves_like 'admin mode on', false, :forbidden -end - -RSpec.shared_examples 'PUT request permissions for admin mode when user' do |params| - subject { put api(path, current_user, admin_mode: admin_mode), params: params } - - let_it_be(:current_user) { create(:user) } + let_it_be(:success_status_code) { :ok } + let_it_be(:failed_status_code) { :forbidden } - it_behaves_like 'admin mode on', true, :forbidden - it_behaves_like 'admin mode on', false, :forbidden + it_behaves_like 'when admin' + it_behaves_like 'when user' end -RSpec.shared_examples 'PUT request permissions for admin mode when admin' do |params| +RSpec.shared_examples 'PUT request permissions for admin mode' do subject { put api(path, current_user, admin_mode: admin_mode), params: params } - let_it_be(:current_user) { create(:admin) } + let_it_be(:success_status_code) { :ok } + let_it_be(:failed_status_code) { :forbidden } - it_behaves_like 'admin mode on', true, :ok - it_behaves_like 'admin mode on', false, :forbidden + it_behaves_like 'when admin' + it_behaves_like 'when user' end -RSpec.shared_examples 'POST request permissions for admin mode when user' do |params| +RSpec.shared_examples 'POST request permissions for admin mode' do subject { post api(path, current_user, admin_mode: admin_mode), params: params } - let_it_be(:current_user) { create(:user) } + let_it_be(:success_status_code) { :created } + let_it_be(:failed_status_code) { :forbidden } - it_behaves_like 'admin mode on', true, :forbidden - it_behaves_like 'admin mode on', false, :forbidden + it_behaves_like 'when admin' + it_behaves_like 'when user' end -RSpec.shared_examples 'POST request permissions for admin mode when admin' do |params| - subject { post api(path, current_user, admin_mode: admin_mode), params: params } +RSpec.shared_examples 'when user' do + let_it_be(:current_user) { create(:user) } - let_it_be(:current_user) { create(:admin) } + include_examples 'makes request' do + let(:status) { failed_status_code } + let(:admin_mode) { true } + end - it_behaves_like 'admin mode on', true, :created - it_behaves_like 'admin mode on', false, :forbidden + it_behaves_like 'makes request' do + let(:status) { failed_status_code } + let(:admin_mode) { false } + end end -RSpec.shared_examples 'DELETE request permissions for admin mode when user' do - subject { delete api(path, current_user, admin_mode: admin_mode) } +RSpec.shared_examples 'when admin' do + let_it_be(:current_user) { create(:admin) } - let_it_be(:current_user) { create(:user) } + it_behaves_like 'makes request' do + let(:status) { success_status_code } + let(:admin_mode) { true } + end - it_behaves_like 'admin mode on', true, :forbidden - it_behaves_like 'admin mode on', false, :forbidden + it_behaves_like 'makes request' do + let(:status) { failed_status_code } + let(:admin_mode) { false } + end end -RSpec.shared_examples 'DELETE request permissions for admin mode when admin' do - subject { delete api(path, current_user, admin_mode: admin_mode) } - - let_it_be(:current_user) { create(:admin) } - - it_behaves_like 'admin mode on', true, :no_content - it_behaves_like 'admin mode on', false, :forbidden -end +RSpec.shared_examples "makes request" do + let_it_be(:status) { nil } -RSpec.shared_examples "admin mode on" do |admin_mode, status| - let_it_be(:admin_mode) { admin_mode } + it "returns" do + subject - it_behaves_like 'returning response status', status + expect(response).to have_gitlab_http_status(status) + end end diff --git a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb index f5c41416763..3ff52166990 100644 --- a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb @@ -18,7 +18,7 @@ RSpec.shared_examples 'returns repositories for allowed users' do |user_type, sc subject expect(json_response.length).to eq(2) - expect(json_response.map { |repository| repository['id'] }).to contain_exactly( + expect(json_response.pluck('id')).to contain_exactly( root_repository.id, test_repository.id) expect(response.body).not_to include('tags') expect(response.body).not_to include('tags_count') @@ -47,7 +47,7 @@ RSpec.shared_examples 'returns tags for allowed users' do |user_type, scope| subject expect(json_response.length).to eq(2) - expect(json_response.map { |repository| repository['id'] }).to contain_exactly( + expect(json_response.pluck('id')).to contain_exactly( root_repository.id, test_repository.id) expect(response.body).to include('tags') end diff --git a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb index f31cbcfdec1..804221b7416 100644 --- a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb @@ -4,7 +4,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| let!(:custom_attribute1) { attributable.custom_attributes.create! key: 'foo', value: 'foo' } let!(:custom_attribute2) { attributable.custom_attributes.create! key: 'bar', value: 'bar' } - describe "GET /#{attributable_name} with custom attributes filter" do + describe "GET /#{attributable_name} with custom attributes filter", :aggregate_failures do before do other_attributable end @@ -14,13 +14,13 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| get api("/#{attributable_name}", user), params: { custom_attributes: { foo: 'foo', bar: 'bar' } } expect(response).to have_gitlab_http_status(:ok) - expect(json_response.map { |r| r['id'] }).to include(attributable.id, other_attributable.id) + expect(json_response.pluck('id')).to include(attributable.id, other_attributable.id) end end context 'with an authorized user' do it 'filters by custom attributes' do - get api("/#{attributable_name}", admin), params: { custom_attributes: { foo: 'foo', bar: 'bar' } } + get api("/#{attributable_name}", admin, admin_mode: true), params: { custom_attributes: { foo: 'foo', bar: 'bar' } } expect(response).to have_gitlab_http_status(:ok) expect(json_response.size).to be 1 @@ -29,7 +29,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| end end - describe "GET /#{attributable_name} with custom attributes" do + describe "GET /#{attributable_name} with custom attributes", :aggregate_failures do before do other_attributable end @@ -46,7 +46,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| context 'with an authorized user' do it 'does not include custom attributes by default' do - get api("/#{attributable_name}", admin) + get api("/#{attributable_name}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to be_empty @@ -54,7 +54,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| end it 'includes custom attributes if requested' do - get api("/#{attributable_name}", admin), params: { with_custom_attributes: true } + get api("/#{attributable_name}", admin, admin_mode: true), params: { with_custom_attributes: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to be_empty @@ -72,7 +72,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| end end - describe "GET /#{attributable_name}/:id with custom attributes" do + describe "GET /#{attributable_name}/:id with custom attributes", :aggregate_failures do context 'with an unauthorized user' do it 'does not include custom attributes' do get api("/#{attributable_name}/#{attributable.id}", user), params: { with_custom_attributes: true } @@ -84,14 +84,14 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| context 'with an authorized user' do it 'does not include custom attributes by default' do - get api("/#{attributable_name}/#{attributable.id}", admin) + get api("/#{attributable_name}/#{attributable.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to include 'custom_attributes' end it 'includes custom attributes if requested' do - get api("/#{attributable_name}/#{attributable.id}", admin), params: { with_custom_attributes: true } + get api("/#{attributable_name}/#{attributable.id}", admin, admin_mode: true), params: { with_custom_attributes: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response['custom_attributes']).to contain_exactly( @@ -102,7 +102,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| end end - describe "GET /#{attributable_name}/:id/custom_attributes" do + describe "GET /#{attributable_name}/:id/custom_attributes", :aggregate_failures do context 'with an unauthorized user' do subject { get api("/#{attributable_name}/#{attributable.id}/custom_attributes", user) } @@ -111,7 +111,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| context 'with an authorized user' do it 'returns all custom attributes' do - get api("/#{attributable_name}/#{attributable.id}/custom_attributes", admin) + get api("/#{attributable_name}/#{attributable.id}/custom_attributes", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to contain_exactly( @@ -122,7 +122,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| end end - describe "GET /#{attributable_name}/:id/custom_attributes/:key" do + describe "GET /#{attributable_name}/:id/custom_attributes/:key", :aggregate_failures do context 'with an unauthorized user' do subject { get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", user) } @@ -131,7 +131,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| context 'with an authorized user' do it 'returns a single custom attribute' do - get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin) + get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to eq({ 'key' => 'foo', 'value' => 'foo' }) @@ -139,7 +139,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| end end - describe "PUT /#{attributable_name}/:id/custom_attributes/:key" do + describe "PUT /#{attributable_name}/:id/custom_attributes/:key", :aggregate_failures do context 'with an unauthorized user' do subject { put api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", user), params: { value: 'new' } } @@ -149,7 +149,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| context 'with an authorized user' do it 'creates a new custom attribute' do expect do - put api("/#{attributable_name}/#{attributable.id}/custom_attributes/new", admin), params: { value: 'new' } + put api("/#{attributable_name}/#{attributable.id}/custom_attributes/new", admin, admin_mode: true), params: { value: 'new' } end.to change { attributable.custom_attributes.count }.by(1) expect(response).to have_gitlab_http_status(:ok) @@ -159,7 +159,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| it 'updates an existing custom attribute' do expect do - put api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin), params: { value: 'new' } + put api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin, admin_mode: true), params: { value: 'new' } end.not_to change { attributable.custom_attributes.count } expect(response).to have_gitlab_http_status(:ok) @@ -169,7 +169,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| end end - describe "DELETE /#{attributable_name}/:id/custom_attributes/:key" do + describe "DELETE /#{attributable_name}/:id/custom_attributes/:key", :aggregate_failures do context 'with an unauthorized user' do subject { delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", user) } @@ -179,7 +179,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| context 'with an authorized user' do it 'deletes an existing custom attribute' do expect do - delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin) + delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin, admin_mode: true) end.to change { attributable.custom_attributes.count }.by(-1) expect(response).to have_gitlab_http_status(:no_content) diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb index 6d29076da0f..bc7ad570441 100644 --- a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb @@ -165,3 +165,41 @@ RSpec.shared_examples 'Debian packages write endpoint' do |desired_behavior, suc it_behaves_like 'rejects Debian access with unknown container id', :unauthorized, :basic end + +RSpec.shared_examples 'Debian packages endpoint catching ObjectStorage::RemoteStoreError' do + include_context 'Debian repository access', :public, :developer, :basic do + it "returns forbidden" do + expect(::Packages::Debian::CreatePackageFileService).to receive(:new).and_raise ObjectStorage::RemoteStoreError + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end +end + +RSpec.shared_examples 'Debian packages index endpoint' do |success_body| + it_behaves_like 'Debian packages read endpoint', 'GET', :success, success_body + + context 'when no ComponentFile is found' do + let(:target_component_name) { component.name + FFaker::Lorem.word } + + it_behaves_like 'Debian packages read endpoint', 'GET', :no_content, /^$/ + end +end + +RSpec.shared_examples 'Debian packages index sha256 endpoint' do |success_body| + it_behaves_like 'Debian packages read endpoint', 'GET', :success, success_body + + context 'with empty checksum' do + let(:target_sha256) { 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' } + + it_behaves_like 'Debian packages read endpoint', 'GET', :no_content, /^$/ + end + + context 'when ComponentFile is not found' do + let(:target_component_name) { component.name + FFaker::Lorem.word } + + it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /^{"message":"404 Not Found"}$/ + end +end diff --git a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb index f577e2ad323..2996c794e52 100644 --- a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb @@ -123,18 +123,6 @@ RSpec.shared_examples 'discussions API' do |parent_type, noteable_type, id_name, expect_snowplow_event(category: 'Notes::CreateService', action: 'execute', label: 'note', value: anything) end - context 'with notes_create_service_tracking feature flag disabled' do - before do - stub_feature_flags(notes_create_service_tracking: false) - end - - it 'does not track Notes::CreateService events', :snowplow do - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions"), params: { body: 'hi!' } - - expect_no_snowplow_event(category: 'Notes::CreateService', action: 'execute') - end - end - context 'when an admin or owner makes the request' do it 'accepts the creation date to be set' do creation_time = 2.weeks.ago @@ -243,8 +231,7 @@ RSpec.shared_examples 'discussions API' do |parent_type, noteable_type, id_name, it 'returns a 404 error when note id not found' do put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\ - "discussions/#{note.discussion_id}/notes/#{non_existing_record_id}", user), - params: { body: 'Hello!' } + "discussions/#{note.discussion_id}/notes/#{non_existing_record_id}", user), params: { body: 'Hello!' } expect(response).to have_gitlab_http_status(:not_found) end diff --git a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb index 6c8b792bf92..930c47dac52 100644 --- a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb @@ -480,6 +480,7 @@ RSpec.shared_examples 'graphql issue list request spec' do context 'when fetching escalation status' do let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue_a) } + let_it_be(:incident_type) { WorkItems::Type.default_by_type(:incident) } let(:fields) do <<~QUERY @@ -491,7 +492,7 @@ RSpec.shared_examples 'graphql issue list request spec' do end before do - issue_a.update_columns(issue_type: Issue.issue_types[:incident]) + issue_a.update_columns(issue_type: WorkItems::Type.base_types[:incident], work_item_type_id: incident_type.id) end it 'returns the escalation status values' do diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb index b459e479c91..53329c5caec 100644 --- a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb @@ -6,7 +6,7 @@ RSpec.shared_examples 'when the snippet is not found' do end it_behaves_like 'a mutation that returns top-level errors', - errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] end RSpec.shared_examples 'snippet edit usage data counters' do diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb index 40b88ef370f..4dc0264172f 100644 --- a/spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb @@ -36,9 +36,9 @@ RSpec.shared_examples 'a subscribable resource api' do context 'when the user is not authorized' do it_behaves_like 'a mutation that returns top-level errors', - errors: ["The resource that you are attempting to access "\ - "does not exist or you don't have permission to "\ - "perform this action"] + errors: ["The resource that you are attempting to access "\ + "does not exist or you don't have permission to "\ + "perform this action"] end context 'when user is authorized' do diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb index f5835460a77..5e9dfc826d4 100644 --- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb @@ -279,11 +279,11 @@ RSpec.shared_examples 'group and project packages query' do end def npm_pipeline_ids - graphql_data_npm_package.dig('pipelines', 'nodes').map { |pipeline| pipeline['id'] } + graphql_data_npm_package.dig('pipelines', 'nodes').pluck('id') end def composer_pipeline_ids - graphql_data_composer_package.dig('pipelines', 'nodes').map { |pipeline| pipeline['id'] } + graphql_data_composer_package.dig('pipelines', 'nodes').pluck('id') end def graphql_data_npm_package diff --git a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb index b4019d7c232..161f4a02b8c 100644 --- a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb @@ -38,7 +38,7 @@ RSpec.shared_examples 'a package with files' do context 'with package files pending destruction' do let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) } - let(:response_package_file_ids) { package_files_response.map { |pf| pf['id'] } } + let(:response_package_file_ids) { package_files_response.pluck('id') } it 'does not return them' do expect(package.reload.package_files).to include(package_file_pending_destruction) diff --git a/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb b/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb index 6b4d8cae2ce..6648c18fb70 100644 --- a/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb @@ -13,13 +13,15 @@ RSpec.shared_examples 'a GraphQL query for access levels' do |access_level_kind| let(:maintainer_access_level) { access_levels.for_role.first } let(:maintainer_access_level_data) { access_levels_data.first } let(:access_levels_data) do - graphql_data_at('project', - 'branchRules', - 'nodes', - 0, - 'branchProtection', - "#{access_level_kind.to_s.camelize(:lower)}AccessLevels", - 'nodes') + graphql_data_at( + 'project', + 'branchRules', + 'nodes', + 0, + 'branchProtection', + "#{access_level_kind.to_s.camelize(:lower)}AccessLevels", + 'nodes' + ) end let(:query) do diff --git a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb index f2002de4b55..a2c34aa6a54 100644 --- a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true RSpec.shared_examples 'web-hook API endpoints test hook' do |prefix| - describe "POST #{prefix}/:hook_id" do + describe "POST #{prefix}/:hook_id", :aggregate_failures do it 'tests the hook' do expect(WebHookService) .to receive(:new).with(hook, anything, String, force: false) .and_return(instance_double(WebHookService, execute: nil)) - post api(hook_uri, user) + post api(hook_uri, user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:created) end @@ -17,7 +17,7 @@ end RSpec.shared_examples 'web-hook API endpoints with branch-filter' do |prefix| describe "POST #{prefix}/hooks" do it "returns a 422 error if branch filter is not valid" do - post api(collection_uri, user), + post api(collection_uri, user, admin_mode: user.admin?), params: { url: "http://example.com", push_events_branch_filter: '~badbranchname/' } expect(response).to have_gitlab_http_status(:unprocessable_entity) @@ -58,10 +58,10 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| let(:default_values) { {} } - describe "GET #{prefix}/hooks" do + describe "GET #{prefix}/hooks", :aggregate_failures do context "authorized user" do it "returns all hooks" do - get api(collection_uri, user) + get api(collection_uri, user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:ok) expect(response).to match_collection_schema @@ -70,7 +70,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| context "when user is forbidden" do it "prevents access to hooks" do - get api(collection_uri, unauthorized_user) + get api(collection_uri, unauthorized_user, admin_mode: true) expect(response).to have_gitlab_http_status(:forbidden) end @@ -90,7 +90,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| end it 'returns the names of the url variables' do - get api(collection_uri, user) + get api(collection_uri, user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to contain_exactly( @@ -102,10 +102,10 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| end end - describe "GET #{prefix}/hooks/:hook_id" do + describe "GET #{prefix}/hooks/:hook_id", :aggregate_failures do context "authorized user" do it "returns a project hook" do - get api(hook_uri, user) + get api(hook_uri, user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:ok) expect(response).to match_hook_schema @@ -114,7 +114,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| end it "returns a 404 error if hook id is not available" do - get api(hook_uri(non_existing_record_id), user) + get api(hook_uri(non_existing_record_id), user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:not_found) end @@ -125,7 +125,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| end it "has the correct alert status", :aggregate_failures do - get api(hook_uri, user) + get api(hook_uri, user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:ok) @@ -135,12 +135,12 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| context 'the hook is backed-off' do before do - WebHook::FAILURE_THRESHOLD.times { hook.backoff! } + WebHooks::AutoDisabling::FAILURE_THRESHOLD.times { hook.backoff! } hook.backoff! end it "has the correct alert status", :aggregate_failures do - get api(hook_uri, user) + get api(hook_uri, user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:ok) @@ -156,7 +156,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| context "when user is forbidden" do it "does not access an existing hook" do - get api(hook_uri, unauthorized_user) + get api(hook_uri, unauthorized_user, admin_mode: true) expect(response).to have_gitlab_http_status(:forbidden) end @@ -171,13 +171,12 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| end end - describe "POST #{prefix}/hooks" do + describe "POST #{prefix}/hooks", :aggregate_failures do let(:hook_creation_params) { hook_params } it "adds hook", :aggregate_failures do expect do - post api(collection_uri, user), - params: hook_creation_params + post api(collection_uri, user, admin_mode: user.admin?), params: hook_creation_params end.to change { hooks_count }.by(1) expect(response).to have_gitlab_http_status(:created) @@ -201,8 +200,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| token = "secret token" expect do - post api(collection_uri, user), - params: { url: "http://example.com", token: token } + post api(collection_uri, user, admin_mode: user.admin?), params: { url: "http://example.com", token: token } end.to change { hooks_count }.by(1) expect(response).to have_gitlab_http_status(:created) @@ -216,19 +214,19 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| end it "returns a 400 error if url not given" do - post api(collection_uri, user), params: { event_names.first => true } + post api(collection_uri, user, admin_mode: user.admin?), params: { event_names.first => true } expect(response).to have_gitlab_http_status(:bad_request) end it "returns a 400 error if no parameters are provided" do - post api(collection_uri, user) + post api(collection_uri, user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:bad_request) end it 'sets default values for events', :aggregate_failures do - post api(collection_uri, user), params: { url: 'http://mep.mep' } + post api(collection_uri, user, admin_mode: user.admin?), params: { url: 'http://mep.mep' } expect(response).to have_gitlab_http_status(:created) expect(response).to match_hook_schema @@ -239,22 +237,22 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| end it "returns a 422 error if token not valid" do - post api(collection_uri, user), + post api(collection_uri, user, admin_mode: user.admin?), params: { url: "http://example.com", token: "foo\nbar" } expect(response).to have_gitlab_http_status(:unprocessable_entity) end it "returns a 422 error if url not valid" do - post api(collection_uri, user), params: { url: "ftp://example.com" } + post api(collection_uri, user, admin_mode: user.admin?), params: { url: "ftp://example.com" } expect(response).to have_gitlab_http_status(:unprocessable_entity) end end - describe "PUT #{prefix}/hooks/:hook_id" do + describe "PUT #{prefix}/hooks/:hook_id", :aggregate_failures do it "updates an existing hook" do - put api(hook_uri, user), params: update_params + put api(hook_uri, user, admin_mode: user.admin?), params: update_params expect(response).to have_gitlab_http_status(:ok) expect(response).to match_hook_schema @@ -267,7 +265,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| it 'updates the URL variables' do hook.update!(url_variables: { 'abc' => 'some value' }) - put api(hook_uri, user), + put api(hook_uri, user, admin_mode: user.admin?), params: { url_variables: [{ key: 'def', value: 'other value' }] } expect(response).to have_gitlab_http_status(:ok) @@ -280,7 +278,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| it "adds the token without including it in the response" do token = "secret token" - put api(hook_uri, user), params: { url: "http://example.org", token: token } + put api(hook_uri, user, admin_mode: user.admin?), params: { url: "http://example.org", token: token } expect(response).to have_gitlab_http_status(:ok) expect(json_response["url"]).to eq("http://example.org") @@ -291,68 +289,68 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| end it "returns 404 error if hook id not found" do - put api(hook_uri(non_existing_record_id), user), params: { url: 'http://example.org' } + put api(hook_uri(non_existing_record_id), user, admin_mode: user.admin?), params: { url: 'http://example.org' } expect(response).to have_gitlab_http_status(:not_found) end it "returns 400 error if no parameters are provided" do - put api(hook_uri, user) + put api(hook_uri, user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:bad_request) end it "returns a 422 error if url is not valid" do - put api(hook_uri, user), params: { url: 'ftp://example.com' } + put api(hook_uri, user, admin_mode: user.admin?), params: { url: 'ftp://example.com' } expect(response).to have_gitlab_http_status(:unprocessable_entity) end it "returns a 422 error if token is not valid" do - put api(hook_uri, user), params: { token: %w[foo bar].join("\n") } + put api(hook_uri, user, admin_mode: user.admin?), params: { token: %w[foo bar].join("\n") } expect(response).to have_gitlab_http_status(:unprocessable_entity) end end - describe "DELETE /projects/:id/hooks/:hook_id" do + describe "DELETE /projects/:id/hooks/:hook_id", :aggregate_failures do it "deletes hook from project" do expect do - delete api(hook_uri, user) + delete api(hook_uri, user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:no_content) end.to change { hooks_count }.by(-1) end it "returns a 404 error when deleting non existent hook" do - delete api(hook_uri(non_existing_record_id), user) + delete api(hook_uri(non_existing_record_id), user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:not_found) end it "returns a 404 error if hook id not given" do - delete api(collection_uri, user) + delete api(collection_uri, user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:not_found) end it "returns forbidden if a user attempts to delete hooks they do not own" do - delete api(hook_uri, unauthorized_user) + delete api(hook_uri, unauthorized_user, admin_mode: true) expect(response).to have_gitlab_http_status(:forbidden) expect(WebHook.exists?(hook.id)).to be_truthy end it_behaves_like '412 response' do - let(:request) { api(hook_uri, user) } + let(:request) { api(hook_uri, user, admin_mode: user.admin?) } end end describe "PUT #{prefix}/hooks/:hook_id/url_variables/:key", :aggregate_failures do it 'sets the variable' do expect do - put api("#{hook_uri}/url_variables/abc", user), - params: { value: 'some secret value' } + put api("#{hook_uri}/url_variables/abc", user, admin_mode: user.admin?), + params: { value: 'some secret value' } end.to change { hook.reload.url_variables }.to(eq('abc' => 'some secret value')) expect(response).to have_gitlab_http_status(:no_content) @@ -361,30 +359,30 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| it 'overwrites existing values' do hook.update!(url_variables: { 'abc' => 'xyz', 'def' => 'other value' }) - put api("#{hook_uri}/url_variables/abc", user), - params: { value: 'some secret value' } + put api("#{hook_uri}/url_variables/abc", user, admin_mode: user.admin?), + params: { value: 'some secret value' } expect(response).to have_gitlab_http_status(:no_content) expect(hook.reload.url_variables).to eq('abc' => 'some secret value', 'def' => 'other value') end it "returns a 404 error when editing non existent hook" do - put api("#{hook_uri(non_existing_record_id)}/url_variables/abc", user), - params: { value: 'xyz' } + put api("#{hook_uri(non_existing_record_id)}/url_variables/abc", user, admin_mode: user.admin?), + params: { value: 'xyz' } expect(response).to have_gitlab_http_status(:not_found) end it "returns a 422 error when the key is illegal" do - put api("#{hook_uri}/url_variables/abc%20def", user), - params: { value: 'xyz' } + put api("#{hook_uri}/url_variables/abc%20def", user, admin_mode: user.admin?), + params: { value: 'xyz' } expect(response).to have_gitlab_http_status(:unprocessable_entity) end it "returns a 422 error when the value is illegal" do - put api("#{hook_uri}/url_variables/abc", user), - params: { value: '' } + put api("#{hook_uri}/url_variables/abc", user, admin_mode: user.admin?), + params: { value: '' } expect(response).to have_gitlab_http_status(:unprocessable_entity) end @@ -397,7 +395,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| it 'unsets the variable' do expect do - delete api("#{hook_uri}/url_variables/abc", user) + delete api("#{hook_uri}/url_variables/abc", user, admin_mode: user.admin?) end.to change { hook.reload.url_variables }.to(eq({ 'def' => 'other value' })) expect(response).to have_gitlab_http_status(:no_content) @@ -406,13 +404,13 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| it 'returns 404 for keys that do not exist' do hook.update!(url_variables: { 'def' => 'other value' }) - delete api("#{hook_uri}/url_variables/abc", user) + delete api("#{hook_uri}/url_variables/abc", user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:not_found) end it "returns a 404 error when deleting a variable from a non existent hook" do - delete api(hook_uri(non_existing_record_id) + "/url_variables/abc", user) + delete api(hook_uri(non_existing_record_id) + "/url_variables/abc", user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:not_found) end diff --git a/spec/support/shared_examples/requests/api/integrations/github_enterprise_jira_dvcs_end_of_life_shared_examples.rb b/spec/support/shared_examples/requests/api/integrations/github_enterprise_jira_dvcs_end_of_life_shared_examples.rb new file mode 100644 index 00000000000..6799dec7b80 --- /dev/null +++ b/spec/support/shared_examples/requests/api/integrations/github_enterprise_jira_dvcs_end_of_life_shared_examples.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do + it 'is a reachable endpoint' do + subject + + expect(response).not_to have_gitlab_http_status(:not_found) + end + + context 'when the flag is disabled' do + before do + stub_feature_flags(jira_dvcs_end_of_life_amnesty: false) + end + + it 'presents as an endpoint that does not exist' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end +end diff --git a/spec/support/shared_examples/requests/api/integrations/slack/slack_request_verification_shared_examples.rb b/spec/support/shared_examples/requests/api/integrations/slack/slack_request_verification_shared_examples.rb new file mode 100644 index 00000000000..ddda9ca6bcc --- /dev/null +++ b/spec/support/shared_examples/requests/api/integrations/slack/slack_request_verification_shared_examples.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'Slack request verification' do + describe 'unauthorized request' do + shared_examples 'an unauthorized request' do + specify do + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + shared_examples 'a successful request that generates a tracked error' do + specify do + expect(Gitlab::ErrorTracking).to receive(:track_exception).once + + subject + + expect(response).to have_gitlab_http_status(:no_content) + expect(response.body).to be_empty + end + end + + context 'when the slack_app_signing_secret setting is not set' do + before do + stub_application_setting(slack_app_signing_secret: nil) + end + + it_behaves_like 'an unauthorized request' + end + + context 'when the timestamp header has expired' do + before do + headers[::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER] = 5.minutes.ago.to_i.to_s + end + + it_behaves_like 'an unauthorized request' + end + + context 'when the timestamp header is missing' do + before do + headers.delete(::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER) + end + + it_behaves_like 'an unauthorized request' + end + + context 'when the signature header is missing' do + before do + headers.delete(::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER) + end + + it_behaves_like 'an unauthorized request' + end + + context 'when the signature is not verified' do + before do + headers[::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER] = 'unverified_signature' + end + + it_behaves_like 'an unauthorized request' + end + + context 'when type param is missing' do + it_behaves_like 'a successful request that generates a tracked error' + end + end +end diff --git a/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb b/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb index 1045a92f332..e2c9874e7fc 100644 --- a/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb @@ -34,5 +34,14 @@ RSpec.shared_examples 'issuable update endpoint' do expect(json_response['labels']).to include '&' expect(json_response['labels']).to include '?' end + + it 'clears milestone when milestone_id=0' do + entity.update!(milestone: milestone) + + put api(url, user), params: { milestone_id: 0 } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['milestone']).to be_nil + end end end diff --git a/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb b/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb index 41d21490343..fba0533251a 100644 --- a/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb @@ -9,6 +9,6 @@ RSpec.shared_examples 'fetches labels' do expect(json_response).to be_an Array expect(json_response).to all(match_schema('public_api/v4/labels/label')) expect(json_response.size).to eq(expected_labels.size) - expect(json_response.map { |r| r['name'] }).to match_array(expected_labels) + expect(json_response.pluck('name')).to match_array(expected_labels) end end diff --git a/spec/support/shared_examples/requests/api/milestones_shared_examples.rb b/spec/support/shared_examples/requests/api/milestones_shared_examples.rb index 1ea11ba3d7c..ee7d0e86771 100644 --- a/spec/support/shared_examples/requests/api/milestones_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/milestones_shared_examples.rb @@ -52,7 +52,7 @@ RSpec.shared_examples 'group and project milestones' do |route_definition| expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array expect(json_response.length).to eq(2) - expect(json_response.map { |m| m['id'] }).to match_array([closed_milestone.id, other_milestone.id]) + expect(json_response.pluck('id')).to match_array([closed_milestone.id, other_milestone.id]) end it 'does not return any milestone if none found' do @@ -293,7 +293,7 @@ RSpec.shared_examples 'group and project milestones' do |route_definition| expect(json_response).to be_an Array # 2 for projects, 3 for group(which has another project with an issue) expect(json_response.size).to be_between(2, 3) - expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id) + expect(json_response.pluck('id')).to include(issue.id, confidential_issue.id) end it 'does not return confidential issues to team members with guest role' do @@ -306,7 +306,7 @@ RSpec.shared_examples 'group and project milestones' do |route_definition| expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) - expect(json_response.map { |issue| issue['id'] }).to include(issue.id) + expect(json_response.pluck('id')).to include(issue.id) end it 'does not return confidential issues to regular users' do @@ -316,7 +316,7 @@ RSpec.shared_examples 'group and project milestones' do |route_definition| expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) - expect(json_response.map { |issue| issue['id'] }).to include(issue.id) + expect(json_response.pluck('id')).to include(issue.id) end it 'returns issues ordered by label priority' do diff --git a/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb new file mode 100644 index 00000000000..2ca62698daf --- /dev/null +++ b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'MLflow|Not Found - Resource Does Not Exist' do + it "is Resource Does Not Exist", :aggregate_failures do + is_expected.to have_gitlab_http_status(:not_found) + + expect(json_response).to include({ "error_code" => 'RESOURCE_DOES_NOT_EXIST' }) + end +end + +RSpec.shared_examples 'MLflow|Requires api scope' do + context 'when user has access but token has wrong scope' do + let(:access_token) { tokens[:read] } + + it { is_expected.to have_gitlab_http_status(:forbidden) } + end +end + +RSpec.shared_examples 'MLflow|Requires read_api scope' do + context 'when user has access but token has wrong scope' do + let(:access_token) { tokens[:no_access] } + + it { is_expected.to have_gitlab_http_status(:forbidden) } + end +end + +RSpec.shared_examples 'MLflow|Bad Request' do + it "is Bad Request" do + is_expected.to have_gitlab_http_status(:bad_request) + end +end + +RSpec.shared_examples 'MLflow|shared error cases' do + context 'when not authenticated' do + let(:headers) { {} } + + it "is Unauthorized" do + is_expected.to have_gitlab_http_status(:unauthorized) + end + end + + context 'when user does not have access' do + let(:access_token) { tokens[:different_user] } + + it "is Not Found" do + is_expected.to have_gitlab_http_status(:not_found) + end + end + + context 'when ff is disabled' do + let(:ff_value) { false } + + it "is Not Found" do + is_expected.to have_gitlab_http_status(:not_found) + end + end +end + +RSpec.shared_examples 'MLflow|Bad Request on missing required' do |keys| + keys.each do |key| + context "when \"#{key}\" is missing" do + let(:params) { default_params.tap { |p| p.delete(key) } } + + it "is Bad Request" do + is_expected.to have_gitlab_http_status(:bad_request) + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb index efe5ed3bcf9..b44ff952cdf 100644 --- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| - describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes" do + describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes", :aggregate_failures do context 'sorting' do before do params = { noteable: noteable, author: user } @@ -12,9 +12,9 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| context 'without sort params' do it 'sorts by created_at in descending order by default' do - get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user) + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?) - response_dates = json_response.map { |note| note['created_at'] } + response_dates = json_response.pluck('created_at') expect(json_response.length).to eq(4) expect(response_dates).to eq(response_dates.sort.reverse) @@ -23,7 +23,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| it 'fetches notes using parent path as id paremeter' do parent_id = CGI.escape(parent.full_path) - get api("/#{parent_type}/#{parent_id}/#{noteable_type}/#{noteable[id_name]}/notes", user) + get api("/#{parent_type}/#{parent_id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:ok) end @@ -40,18 +40,18 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end it 'page breaks first page correctly' do - get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?per_page=4", user) + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?per_page=4", user, admin_mode: user.admin?) - response_ids = json_response.map { |note| note['id'] } + response_ids = json_response.pluck('id') expect(response_ids).to include(@note2.id) expect(response_ids).not_to include(@first_note.id) end it 'page breaks second page correctly' do - get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?per_page=4&page=2", user) + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?per_page=4&page=2", user, admin_mode: user.admin?) - response_ids = json_response.map { |note| note['id'] } + response_ids = json_response.pluck('id') expect(response_ids).not_to include(@note2.id) expect(response_ids).to include(@first_note.id) @@ -60,27 +60,27 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end it 'sorts by ascending order when requested' do - get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?sort=asc", user) + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?sort=asc", user, admin_mode: user.admin?) - response_dates = json_response.map { |note| note['created_at'] } + response_dates = json_response.pluck('created_at') expect(json_response.length).to eq(4) expect(response_dates).to eq(response_dates.sort) end it 'sorts by updated_at in descending order when requested' do - get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?order_by=updated_at", user) + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?order_by=updated_at", user, admin_mode: user.admin?) - response_dates = json_response.map { |note| note['updated_at'] } + response_dates = json_response.pluck('updated_at') expect(json_response.length).to eq(4) expect(response_dates).to eq(response_dates.sort.reverse) end it 'sorts by updated_at in ascending order when requested' do - get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?order_by=updated_at&sort=asc", user) + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?order_by=updated_at&sort=asc", user, admin_mode: user.admin?) - response_dates = json_response.map { |note| note['updated_at'] } + response_dates = json_response.pluck('updated_at') expect(json_response.length).to eq(4) expect(response_dates).to eq(response_dates.sort) @@ -88,7 +88,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end it "returns an array of notes" do - get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user) + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -97,7 +97,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end it "returns a 404 error when noteable id not found" do - get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{non_existing_record_id}/notes", user) + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{non_existing_record_id}/notes", user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:not_found) end @@ -105,36 +105,36 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| it "returns 404 when not authorized" do parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", private_user) + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", private_user, admin_mode: private_user.admin?) expect(response).to have_gitlab_http_status(:not_found) end end - describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id" do + describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id", :aggregate_failures do it "returns a note by id" do - get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user) + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:ok) expect(json_response['body']).to eq(note.note) end it "returns a 404 error if note not found" do - get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{non_existing_record_id}", user) + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{non_existing_record_id}", user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:not_found) end end - describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes" do + describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes", :aggregate_failures do let(:params) { { body: 'hi!' } } subject do - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: params end it "creates a new note" do - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!' } + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: { body: 'hi!' } expect(response).to have_gitlab_http_status(:created) expect(json_response['body']).to eq('hi!') @@ -143,7 +143,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end it "returns a 400 bad request error if body not given" do - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user) + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:bad_request) end @@ -158,7 +158,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| uri = "/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes" expect do - post api(uri, user), params: { body: 'hi!' } + post api(uri, user, admin_mode: user.admin?), params: { body: 'hi!' } end.to change { Event.count }.by(1) end @@ -169,7 +169,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| context 'by an admin' do it 'sets the creation time on the new note' do admin = create(:admin) - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", admin), params: params + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:created) expect(json_response['body']).to eq('hi!') @@ -185,7 +185,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| let(:user) { project.first_owner } it 'sets the creation time on the new note' do - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: params expect(response).to have_gitlab_http_status(:created) expect(json_response['body']).to eq('hi!') @@ -215,7 +215,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| when 'groups' context 'by a group owner' do it 'sets the creation time on the new note' do - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: params expect(response).to have_gitlab_http_status(:created) expect(json_response['body']).to eq('hi!') @@ -253,7 +253,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| context 'when the user is posting an award emoji on their own noteable' do it 'creates a new note' do - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: ':+1:' } + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: { body: ':+1:' } expect(response).to have_gitlab_http_status(:created) expect(json_response['body']).to eq(':+1:') @@ -266,7 +266,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end it 'responds with 404' do - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", private_user), + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", private_user, admin_mode: private_user.admin?), params: { body: 'Foo' } expect(response).to have_gitlab_http_status(:not_found) @@ -299,11 +299,11 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end end - describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id" do + describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id", :aggregate_failures do let(:params) { { body: 'Hello!' } } subject do - put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user), params: params + put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user, admin_mode: user.admin?), params: params end context 'when only body param is present' do @@ -329,40 +329,40 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end it 'returns a 404 error when note id not found' do - put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{non_existing_record_id}", user), - params: { body: 'Hello!' } + put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{non_existing_record_id}", user, admin_mode: user.admin?), + params: { body: 'Hello!' } expect(response).to have_gitlab_http_status(:not_found) end it 'returns a 400 bad request error if body is empty' do put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\ - "notes/#{note.id}", user), params: { body: '' } + "notes/#{note.id}", user, admin_mode: user.admin?), params: { body: '' } expect(response).to have_gitlab_http_status(:bad_request) end end - describe "DELETE /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id" do + describe "DELETE /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id", :aggregate_failures do it 'deletes a note' do delete api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\ - "notes/#{note.id}", user) + "notes/#{note.id}", user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:no_content) # Check if note is really deleted delete api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\ - "notes/#{note.id}", user) + "notes/#{note.id}", user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:not_found) end it 'returns a 404 error when note id not found' do - delete api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{non_existing_record_id}", user) + delete api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{non_existing_record_id}", user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:not_found) end it_behaves_like '412 response' do - let(:request) { api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user) } + let(:request) { api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user, admin_mode: user.admin?) } end end end @@ -370,16 +370,16 @@ end RSpec.shared_examples 'noteable API with confidential notes' do |parent_type, noteable_type, id_name| it_behaves_like 'noteable API', parent_type, noteable_type, id_name - describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes" do + describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes", :aggregate_failures do let(:params) { { body: 'hi!' } } subject do - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: params end context 'with internal param' do it "creates a confidential note if internal is set to true" do - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params.merge(internal: true) + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: params.merge(internal: true) expect(response).to have_gitlab_http_status(:created) expect(json_response['body']).to eq('hi!') @@ -391,7 +391,7 @@ RSpec.shared_examples 'noteable API with confidential notes' do |parent_type, no context 'with deprecated confidential param' do it "creates a confidential note if confidential is set to true" do - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params.merge(confidential: true) + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user, admin_mode: user.admin?), params: params.merge(confidential: true) expect(response).to have_gitlab_http_status(:created) expect(json_response['body']).to eq('hi!') diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb index b55639a6b82..f53532d00d7 100644 --- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb @@ -507,55 +507,118 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project| it_behaves_like 'returning response status', status end - shared_examples 'handling different package names, visibilities and user roles' do - where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do - :scoped_naming_convention | :public | :anonymous | :accept | :ok - :scoped_naming_convention | :public | :guest | :accept | :ok - :scoped_naming_convention | :public | :reporter | :accept | :ok - :scoped_no_naming_convention | :public | :anonymous | :accept | :ok - :scoped_no_naming_convention | :public | :guest | :accept | :ok - :scoped_no_naming_convention | :public | :reporter | :accept | :ok - :unscoped | :public | :anonymous | :accept | :ok - :unscoped | :public | :guest | :accept | :ok - :unscoped | :public | :reporter | :accept | :ok - :non_existing | :public | :anonymous | :reject | :not_found - :non_existing | :public | :guest | :reject | :not_found - :non_existing | :public | :reporter | :reject | :not_found - - :scoped_naming_convention | :private | :anonymous | :reject | :not_found - :scoped_naming_convention | :private | :guest | :reject | :forbidden - :scoped_naming_convention | :private | :reporter | :accept | :ok - :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found - :scoped_no_naming_convention | :private | :guest | :reject | :forbidden - :scoped_no_naming_convention | :private | :reporter | :accept | :ok - :unscoped | :private | :anonymous | :reject | :not_found - :unscoped | :private | :guest | :reject | :forbidden - :unscoped | :private | :reporter | :accept | :ok - :non_existing | :private | :anonymous | :reject | :not_found - :non_existing | :private | :guest | :reject | :forbidden - :non_existing | :private | :reporter | :reject | :not_found - - :scoped_naming_convention | :internal | :anonymous | :reject | :not_found - :scoped_naming_convention | :internal | :guest | :accept | :ok - :scoped_naming_convention | :internal | :reporter | :accept | :ok - :scoped_no_naming_convention | :internal | :anonymous | :reject | :not_found - :scoped_no_naming_convention | :internal | :guest | :accept | :ok - :scoped_no_naming_convention | :internal | :reporter | :accept | :ok - :unscoped | :internal | :anonymous | :reject | :not_found - :unscoped | :internal | :guest | :accept | :ok - :unscoped | :internal | :reporter | :accept | :ok - :non_existing | :internal | :anonymous | :reject | :not_found - :non_existing | :internal | :guest | :reject | :not_found - :non_existing | :internal | :reporter | :reject | :not_found + shared_examples 'handling all conditions' do + where(:auth, :package_name_type, :visibility, :user_role, :expected_result, :expected_status) do + nil | :scoped_naming_convention | :public | nil | :accept | :ok + nil | :scoped_no_naming_convention | :public | nil | :accept | :ok + nil | :unscoped | :public | nil | :accept | :ok + nil | :non_existing | :public | nil | :reject | :not_found + nil | :scoped_naming_convention | :private | nil | :reject | :not_found + nil | :scoped_no_naming_convention | :private | nil | :reject | :not_found + nil | :unscoped | :private | nil | :reject | :not_found + nil | :non_existing | :private | nil | :reject | :not_found + nil | :scoped_naming_convention | :internal | nil | :reject | :not_found + nil | :scoped_no_naming_convention | :internal | nil | :reject | :not_found + nil | :unscoped | :internal | nil | :reject | :not_found + nil | :non_existing | :internal | nil | :reject | :not_found + + :oauth | :scoped_naming_convention | :public | :guest | :accept | :ok + :oauth | :scoped_naming_convention | :public | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | :public | :guest | :accept | :ok + :oauth | :scoped_no_naming_convention | :public | :reporter | :accept | :ok + :oauth | :unscoped | :public | :guest | :accept | :ok + :oauth | :unscoped | :public | :reporter | :accept | :ok + :oauth | :non_existing | :public | :guest | :reject | :not_found + :oauth | :non_existing | :public | :reporter | :reject | :not_found + :oauth | :scoped_naming_convention | :private | :guest | :reject | :forbidden + :oauth | :scoped_naming_convention | :private | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden + :oauth | :scoped_no_naming_convention | :private | :reporter | :accept | :ok + :oauth | :unscoped | :private | :guest | :reject | :forbidden + :oauth | :unscoped | :private | :reporter | :accept | :ok + :oauth | :non_existing | :private | :guest | :reject | :forbidden + :oauth | :non_existing | :private | :reporter | :reject | :not_found + :oauth | :scoped_naming_convention | :internal | :guest | :accept | :ok + :oauth | :scoped_naming_convention | :internal | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | :internal | :guest | :accept | :ok + :oauth | :scoped_no_naming_convention | :internal | :reporter | :accept | :ok + :oauth | :unscoped | :internal | :guest | :accept | :ok + :oauth | :unscoped | :internal | :reporter | :accept | :ok + :oauth | :non_existing | :internal | :guest | :reject | :not_found + :oauth | :non_existing | :internal | :reporter | :reject | :not_found + + :personal_access_token | :scoped_naming_convention | :public | :guest | :accept | :ok + :personal_access_token | :scoped_naming_convention | :public | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :public | :guest | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :public | :reporter | :accept | :ok + :personal_access_token | :unscoped | :public | :guest | :accept | :ok + :personal_access_token | :unscoped | :public | :reporter | :accept | :ok + :personal_access_token | :non_existing | :public | :guest | :reject | :not_found + :personal_access_token | :non_existing | :public | :reporter | :reject | :not_found + :personal_access_token | :scoped_naming_convention | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_naming_convention | :private | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_no_naming_convention | :private | :reporter | :accept | :ok + :personal_access_token | :unscoped | :private | :guest | :reject | :forbidden + :personal_access_token | :unscoped | :private | :reporter | :accept | :ok + :personal_access_token | :non_existing | :private | :guest | :reject | :forbidden + :personal_access_token | :non_existing | :private | :reporter | :reject | :not_found + :personal_access_token | :scoped_naming_convention | :internal | :guest | :accept | :ok + :personal_access_token | :scoped_naming_convention | :internal | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :internal | :guest | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :internal | :reporter | :accept | :ok + :personal_access_token | :unscoped | :internal | :guest | :accept | :ok + :personal_access_token | :unscoped | :internal | :reporter | :accept | :ok + :personal_access_token | :non_existing | :internal | :guest | :reject | :not_found + :personal_access_token | :non_existing | :internal | :reporter | :reject | :not_found + + :job_token | :scoped_naming_convention | :public | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | :public | :developer | :accept | :ok + :job_token | :unscoped | :public | :developer | :accept | :ok + :job_token | :non_existing | :public | :developer | :reject | :not_found + :job_token | :scoped_naming_convention | :private | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | :private | :developer | :accept | :ok + :job_token | :unscoped | :private | :developer | :accept | :ok + :job_token | :non_existing | :private | :developer | :reject | :not_found + :job_token | :scoped_naming_convention | :internal | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | :internal | :developer | :accept | :ok + :job_token | :unscoped | :internal | :developer | :accept | :ok + :job_token | :non_existing | :internal | :developer | :reject | :not_found + + :deploy_token | :scoped_naming_convention | :public | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | :public | nil | :accept | :ok + :deploy_token | :unscoped | :public | nil | :accept | :ok + :deploy_token | :non_existing | :public | nil | :reject | :not_found + :deploy_token | :scoped_naming_convention | :private | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | :private | nil | :accept | :ok + :deploy_token | :unscoped | :private | nil | :accept | :ok + :deploy_token | :non_existing | :private | nil | :reject | :not_found + :deploy_token | :scoped_naming_convention | :internal | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | :internal | nil | :accept | :ok + :deploy_token | :unscoped | :internal | nil | :accept | :ok + :deploy_token | :non_existing | :internal | nil | :reject | :not_found end with_them do - let(:anonymous) { user_role == :anonymous } + let(:headers) do + case auth + when :oauth + build_token_auth_header(token.plaintext_token) + when :personal_access_token + build_token_auth_header(personal_access_token.token) + when :job_token + build_token_auth_header(job.token) + when :deploy_token + build_token_auth_header(deploy_token.token) + else + {} + end + end - subject { get(url, headers: anonymous ? {} : headers) } + subject { get(url, headers: headers) } before do - project.send("add_#{user_role}", user) unless anonymous + project.send("add_#{user_role}", user) if user_role project.update!(visibility: visibility.to_s) end @@ -571,20 +634,6 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project| end end - shared_examples 'handling all conditions' do - context 'with oauth token' do - let(:headers) { build_token_auth_header(token.plaintext_token) } - - it_behaves_like 'handling different package names, visibilities and user roles' - end - - context 'with personal access token' do - let(:headers) { build_token_auth_header(personal_access_token.token) } - - it_behaves_like 'handling different package names, visibilities and user roles' - end - end - context 'with a group namespace' do it_behaves_like 'handling all conditions' end @@ -599,7 +648,6 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project| end RSpec.shared_examples 'handling create dist tag requests' do |scope: :project| - using RSpec::Parameterized::TableSyntax include_context 'set package name from package name type' let_it_be(:tag_name) { 'test' } @@ -617,82 +665,10 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project| it_behaves_like 'returning response status', status end - shared_examples 'handling different package names, visibilities and user roles' do - where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do - :scoped_naming_convention | :public | :anonymous | :reject | :forbidden - :scoped_naming_convention | :public | :guest | :reject | :forbidden - :scoped_naming_convention | :public | :developer | :accept | :ok - :scoped_no_naming_convention | :public | :anonymous | :reject | :forbidden - :scoped_no_naming_convention | :public | :guest | :reject | :forbidden - :scoped_no_naming_convention | :public | :developer | :accept | :ok - :unscoped | :public | :anonymous | :reject | :forbidden - :unscoped | :public | :guest | :reject | :forbidden - :unscoped | :public | :developer | :accept | :ok - :non_existing | :public | :anonymous | :reject | :forbidden - :non_existing | :public | :guest | :reject | :forbidden - :non_existing | :public | :developer | :reject | :not_found - - :scoped_naming_convention | :private | :anonymous | :reject | :not_found - :scoped_naming_convention | :private | :guest | :reject | :forbidden - :scoped_naming_convention | :private | :developer | :accept | :ok - :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found - :scoped_no_naming_convention | :private | :guest | :reject | :forbidden - :scoped_no_naming_convention | :private | :developer | :accept | :ok - :unscoped | :private | :anonymous | :reject | :not_found - :unscoped | :private | :guest | :reject | :forbidden - :unscoped | :private | :developer | :accept | :ok - :non_existing | :private | :anonymous | :reject | :not_found - :non_existing | :private | :guest | :reject | :forbidden - :non_existing | :private | :developer | :reject | :not_found - - :scoped_naming_convention | :internal | :anonymous | :reject | :forbidden - :scoped_naming_convention | :internal | :guest | :reject | :forbidden - :scoped_naming_convention | :internal | :developer | :accept | :ok - :scoped_no_naming_convention | :internal | :anonymous | :reject | :forbidden - :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden - :scoped_no_naming_convention | :internal | :developer | :accept | :ok - :unscoped | :internal | :anonymous | :reject | :forbidden - :unscoped | :internal | :guest | :reject | :forbidden - :unscoped | :internal | :developer | :accept | :ok - :non_existing | :internal | :anonymous | :reject | :forbidden - :non_existing | :internal | :guest | :reject | :forbidden - :non_existing | :internal | :developer | :reject | :not_found - end - - with_them do - let(:anonymous) { user_role == :anonymous } - - subject { put(url, env: env, headers: headers) } - - before do - project.send("add_#{user_role}", user) unless anonymous - project.update!(visibility: visibility.to_s) - end - - example_name = "#{params[:expected_result]} create package tag request" - status = params[:expected_status] - - if scope == :instance && params[:package_name_type] != :scoped_naming_convention - example_name = 'reject create package tag request' - status = :not_found - end - - it_behaves_like example_name, status: status - end - end - shared_examples 'handling all conditions' do - context 'with oauth token' do - let(:headers) { build_token_auth_header(token.plaintext_token) } + subject { put(url, env: env, headers: headers) } - it_behaves_like 'handling different package names, visibilities and user roles' - end - - context 'with personal access token' do - let(:headers) { build_token_auth_header(personal_access_token.token) } - - it_behaves_like 'handling different package names, visibilities and user roles' - end + it_behaves_like 'handling different package names, visibilities and user roles for tags create or delete', action: :create, scope: scope end context 'with a group namespace' do @@ -709,7 +685,6 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project| end RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project| - using RSpec::Parameterized::TableSyntax include_context 'set package name from package name type' let_it_be(:package_tag) { create(:packages_tag, package: package) } @@ -725,82 +700,10 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project| it_behaves_like 'returning response status', status end - shared_examples 'handling different package names, visibilities and user roles' do - where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do - :scoped_naming_convention | :public | :anonymous | :reject | :forbidden - :scoped_naming_convention | :public | :guest | :reject | :forbidden - :scoped_naming_convention | :public | :maintainer | :accept | :ok - :scoped_no_naming_convention | :public | :anonymous | :reject | :forbidden - :scoped_no_naming_convention | :public | :guest | :reject | :forbidden - :scoped_no_naming_convention | :public | :maintainer | :accept | :ok - :unscoped | :public | :anonymous | :reject | :forbidden - :unscoped | :public | :guest | :reject | :forbidden - :unscoped | :public | :maintainer | :accept | :ok - :non_existing | :public | :anonymous | :reject | :forbidden - :non_existing | :public | :guest | :reject | :forbidden - :non_existing | :public | :maintainer | :reject | :not_found - - :scoped_naming_convention | :private | :anonymous | :reject | :not_found - :scoped_naming_convention | :private | :guest | :reject | :forbidden - :scoped_naming_convention | :private | :maintainer | :accept | :ok - :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found - :scoped_no_naming_convention | :private | :guest | :reject | :forbidden - :scoped_no_naming_convention | :private | :maintainer | :accept | :ok - :unscoped | :private | :anonymous | :reject | :not_found - :unscoped | :private | :guest | :reject | :forbidden - :unscoped | :private | :maintainer | :accept | :ok - :non_existing | :private | :anonymous | :reject | :not_found - :non_existing | :private | :guest | :reject | :forbidden - :non_existing | :private | :maintainer | :reject | :not_found - - :scoped_naming_convention | :internal | :anonymous | :reject | :forbidden - :scoped_naming_convention | :internal | :guest | :reject | :forbidden - :scoped_naming_convention | :internal | :maintainer | :accept | :ok - :scoped_no_naming_convention | :internal | :anonymous | :reject | :forbidden - :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden - :scoped_no_naming_convention | :internal | :maintainer | :accept | :ok - :unscoped | :internal | :anonymous | :reject | :forbidden - :unscoped | :internal | :guest | :reject | :forbidden - :unscoped | :internal | :maintainer | :accept | :ok - :non_existing | :internal | :anonymous | :reject | :forbidden - :non_existing | :internal | :guest | :reject | :forbidden - :non_existing | :internal | :maintainer | :reject | :not_found - end - - with_them do - let(:anonymous) { user_role == :anonymous } - - subject { delete(url, headers: headers) } - - before do - project.send("add_#{user_role}", user) unless anonymous - project.update!(visibility: visibility.to_s) - end - - example_name = "#{params[:expected_result]} delete package tag request" - status = params[:expected_status] - - if scope == :instance && params[:package_name_type] != :scoped_naming_convention - example_name = 'reject delete package tag request' - status = :not_found - end - - it_behaves_like example_name, status: status - end - end - shared_examples 'handling all conditions' do - context 'with oauth token' do - let(:headers) { build_token_auth_header(token.plaintext_token) } - - it_behaves_like 'handling different package names, visibilities and user roles' - end - - context 'with personal access token' do - let(:headers) { build_token_auth_header(personal_access_token.token) } + subject { delete(url, headers: headers) } - it_behaves_like 'handling different package names, visibilities and user roles' - end + it_behaves_like 'handling different package names, visibilities and user roles for tags create or delete', action: :delete, scope: scope end context 'with a group namespace' do @@ -815,3 +718,134 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project| end end end + +RSpec.shared_examples 'handling different package names, visibilities and user roles for tags create or delete' do |action:, scope: :project| + using RSpec::Parameterized::TableSyntax + + role = action == :create ? :developer : :maintainer + + where(:auth, :package_name_type, :visibility, :user_role, :expected_result, :expected_status) do + nil | :scoped_naming_convention | :public | nil | :reject | :unauthorized + nil | :scoped_no_naming_convention | :public | nil | :reject | :unauthorized + nil | :unscoped | :public | nil | :reject | :unauthorized + nil | :non_existing | :public | nil | :reject | :unauthorized + nil | :scoped_naming_convention | :private | nil | :reject | :unauthorized + nil | :scoped_no_naming_convention | :private | nil | :reject | :unauthorized + nil | :unscoped | :private | nil | :reject | :unauthorized + nil | :non_existing | :private | nil | :reject | :unauthorized + nil | :scoped_naming_convention | :internal | nil | :reject | :unauthorized + nil | :scoped_no_naming_convention | :internal | nil | :reject | :unauthorized + nil | :unscoped | :internal | nil | :reject | :unauthorized + nil | :non_existing | :internal | nil | :reject | :unauthorized + + :oauth | :scoped_naming_convention | :public | :guest | :reject | :forbidden + :oauth | :scoped_naming_convention | :public | role | :accept | :ok + :oauth | :scoped_no_naming_convention | :public | :guest | :reject | :forbidden + :oauth | :scoped_no_naming_convention | :public | role | :accept | :ok + :oauth | :unscoped | :public | :guest | :reject | :forbidden + :oauth | :unscoped | :public | role | :accept | :ok + :oauth | :non_existing | :public | :guest | :reject | :forbidden + :oauth | :non_existing | :public | role | :reject | :not_found + :oauth | :scoped_naming_convention | :private | :guest | :reject | :forbidden + :oauth | :scoped_naming_convention | :private | role | :accept | :ok + :oauth | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden + :oauth | :scoped_no_naming_convention | :private | role | :accept | :ok + :oauth | :unscoped | :private | :guest | :reject | :forbidden + :oauth | :unscoped | :private | role | :accept | :ok + :oauth | :non_existing | :private | :guest | :reject | :forbidden + :oauth | :non_existing | :private | role | :reject | :not_found + :oauth | :scoped_naming_convention | :internal | :guest | :reject | :forbidden + :oauth | :scoped_naming_convention | :internal | role | :accept | :ok + :oauth | :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden + :oauth | :scoped_no_naming_convention | :internal | role | :accept | :ok + :oauth | :unscoped | :internal | :guest | :reject | :forbidden + :oauth | :unscoped | :internal | role | :accept | :ok + :oauth | :non_existing | :internal | :guest | :reject | :forbidden + :oauth | :non_existing | :internal | role | :reject | :not_found + + :personal_access_token | :scoped_naming_convention | :public | :guest | :reject | :forbidden + :personal_access_token | :scoped_naming_convention | :public | role | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :public | :guest | :reject | :forbidden + :personal_access_token | :scoped_no_naming_convention | :public | role | :accept | :ok + :personal_access_token | :unscoped | :public | :guest | :reject | :forbidden + :personal_access_token | :unscoped | :public | role | :accept | :ok + :personal_access_token | :non_existing | :public | :guest | :reject | :forbidden + :personal_access_token | :non_existing | :public | role | :reject | :not_found + :personal_access_token | :scoped_naming_convention | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_naming_convention | :private | role | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_no_naming_convention | :private | role | :accept | :ok + :personal_access_token | :unscoped | :private | :guest | :reject | :forbidden + :personal_access_token | :unscoped | :private | role | :accept | :ok + :personal_access_token | :non_existing | :private | :guest | :reject | :forbidden + :personal_access_token | :non_existing | :private | role | :reject | :not_found + :personal_access_token | :scoped_naming_convention | :internal | :guest | :reject | :forbidden + :personal_access_token | :scoped_naming_convention | :internal | role | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden + :personal_access_token | :scoped_no_naming_convention | :internal | role | :accept | :ok + :personal_access_token | :unscoped | :internal | :guest | :reject | :forbidden + :personal_access_token | :unscoped | :internal | role | :accept | :ok + :personal_access_token | :non_existing | :internal | :guest | :reject | :forbidden + :personal_access_token | :non_existing | :internal | role | :reject | :not_found + + :job_token | :scoped_naming_convention | :public | role | :accept | :ok + :job_token | :scoped_no_naming_convention | :public | role | :accept | :ok + :job_token | :unscoped | :public | role | :accept | :ok + :job_token | :non_existing | :public | role | :reject | :not_found + :job_token | :scoped_naming_convention | :private | role | :accept | :ok + :job_token | :scoped_no_naming_convention | :private | role | :accept | :ok + :job_token | :unscoped | :private | role | :accept | :ok + :job_token | :non_existing | :private | role | :reject | :not_found + :job_token | :scoped_naming_convention | :internal | role | :accept | :ok + :job_token | :scoped_no_naming_convention | :internal | role | :accept | :ok + :job_token | :unscoped | :internal | role | :accept | :ok + :job_token | :non_existing | :internal | role | :reject | :not_found + + :deploy_token | :scoped_naming_convention | :public | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | :public | nil | :accept | :ok + :deploy_token | :unscoped | :public | nil | :accept | :ok + :deploy_token | :non_existing | :public | nil | :reject | :not_found + :deploy_token | :scoped_naming_convention | :private | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | :private | nil | :accept | :ok + :deploy_token | :unscoped | :private | nil | :accept | :ok + :deploy_token | :non_existing | :private | nil | :reject | :not_found + :deploy_token | :scoped_naming_convention | :internal | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | :internal | nil | :accept | :ok + :deploy_token | :unscoped | :internal | nil | :accept | :ok + :deploy_token | :non_existing | :internal | nil | :reject | :not_found + end + + with_them do + let(:headers) do + case auth + when :oauth + build_token_auth_header(token.plaintext_token) + when :personal_access_token + build_token_auth_header(personal_access_token.token) + when :job_token + build_token_auth_header(job.token) + when :deploy_token + build_token_auth_header(deploy_token.token) + else + {} + end + end + + before do + project.send("add_#{user_role}", user) if user_role + project.update!(visibility: visibility.to_s) + end + + example_name = "#{params[:expected_result]} #{action} package tag request" + status = params[:expected_status] + + if scope == :instance && params[:package_name_type] != :scoped_naming_convention + example_name = "reject #{action} package tag request" + # Due to #authenticate_non_get, anonymous requests on private resources + # are rejected with unauthorized status + status = params[:auth].nil? ? :unauthorized : :not_found + end + + it_behaves_like example_name, status: status + end +end diff --git a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb index 1d79a61fbb0..7c20ea661b5 100644 --- a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb @@ -1,13 +1,5 @@ # frozen_string_literal: true -RSpec.shared_examples 'rejects package tags access' do |status:| - before do - package.update!(name: package_name) unless package_name == 'non-existing-package' - end - - it_behaves_like 'returning response status', status -end - RSpec.shared_examples 'accept package tags request' do |status:| using RSpec::Parameterized::TableSyntax include_context 'dependency proxy helpers context' @@ -23,6 +15,7 @@ RSpec.shared_examples 'accept package tags request' do |status:| end it_behaves_like 'returning response status', status + it_behaves_like 'track event', :list_tags it 'returns a valid json response' do subject @@ -63,6 +56,7 @@ RSpec.shared_examples 'accept create package tag request' do |user_type| end it_behaves_like 'returning response status', :no_content + it_behaves_like 'track event', :create_tag it 'creates the package tag' do expect { subject }.to change { Packages::Tag.count }.by(1) @@ -145,6 +139,7 @@ RSpec.shared_examples 'accept delete package tag request' do |user_type| end it_behaves_like 'returning response status', :no_content + it_behaves_like 'track event', :delete_tag it 'returns a valid response' do subject @@ -190,3 +185,21 @@ RSpec.shared_examples 'accept delete package tag request' do |user_type| end end end + +RSpec.shared_examples 'track event' do |event_name| + let(:event_user) do + if auth == :deploy_token + deploy_token + elsif user_role + user + end + end + + let(:snowplow_gitlab_standard_context) do + { project: project, namespace: project.namespace, property: 'i_package_npm_user' }.tap do |context| + context[:user] = event_user if event_user + end + end + + it_behaves_like 'a package tracking event', described_class.name, event_name.to_s +end diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb index 17d8b9c7fab..7cafe8bb368 100644 --- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb @@ -36,6 +36,7 @@ RSpec.shared_examples 'handling nuget service requests' do |example_names_with_s with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) } subject { get api(url), headers: headers } @@ -72,6 +73,7 @@ RSpec.shared_examples 'handling nuget service requests' do |example_names_with_s with_them do let(:job) { user_token ? create(:ci_build, project: project, user: user, status: :running) : double(token: 'wrong') } let(:headers) { user_role == :anonymous ? {} : job_basic_auth_header(job) } + let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) } subject { get api(url), headers: headers } @@ -140,6 +142,7 @@ RSpec.shared_examples 'handling nuget metadata requests with package name' do |e with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) } subject { get api(url), headers: headers } @@ -207,6 +210,7 @@ RSpec.shared_examples 'handling nuget metadata requests with package name and pa with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) } subject { get api(url), headers: headers } @@ -277,6 +281,7 @@ RSpec.shared_examples 'handling nuget search requests' do |example_names_with_st with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) } subject { get api(url), headers: headers } diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb index 98264baa61d..3168f25e4fa 100644 --- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -143,37 +143,37 @@ RSpec.shared_examples 'job token for package uploads' do |authorize_endpoint: fa end RSpec.shared_examples 'a package tracking event' do |category, action, service_ping_context = true| - before do - stub_feature_flags(collect_package_events: true) - end - let(:context) do - [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, - event: snowplow_gitlab_standard_context[:property]).to_h] + [ + Gitlab::Tracking::ServicePingContext.new( + data_source: :redis_hll, + event: snowplow_gitlab_standard_context[:property] + ).to_h + ] end it "creates a gitlab tracking event #{action}", :snowplow, :aggregate_failures do - expect { subject }.to change { Packages::Event.count }.by(1) + subject if service_ping_context - expect_snowplow_event(category: category, action: action, - label: "redis_hll_counters.user_packages.user_packages_total_unique_counts_monthly", - context: context, **snowplow_gitlab_standard_context) + expect_snowplow_event( + category: category, + action: action, + label: "redis_hll_counters.user_packages.user_packages_total_unique_counts_monthly", + context: context, + **snowplow_gitlab_standard_context + ) else expect_snowplow_event(category: category, action: action, **snowplow_gitlab_standard_context) end end end -RSpec.shared_examples 'not a package tracking event' do - before do - stub_feature_flags(collect_package_events: true) - end - +RSpec.shared_examples 'not a package tracking event' do |category, action| it 'does not create a gitlab tracking event', :snowplow, :aggregate_failures do - expect { subject }.not_to change { Packages::Event.count } + subject - expect_no_snowplow_event + expect_no_snowplow_event category: category, action: action end end @@ -183,3 +183,15 @@ RSpec.shared_examples 'bumping the package last downloaded at field' do .to change { package.reload.last_downloaded_at }.from(nil).to(instance_of(ActiveSupport::TimeWithZone)) end end + +RSpec.shared_examples 'a successful package creation' do + it 'creates npm package with file' do + expect { subject } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + .and change { Packages::Tag.count }.by(1) + .and change { Packages::Npm::Metadatum.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + end +end diff --git a/spec/support/shared_examples/requests/api/pipelines/visibility_table_shared_examples.rb b/spec/support/shared_examples/requests/api/pipelines/visibility_table_shared_examples.rb index 8dd2ef6ccc6..9847ea4e1e2 100644 --- a/spec/support/shared_examples/requests/api/pipelines/visibility_table_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/pipelines/visibility_table_shared_examples.rb @@ -224,10 +224,10 @@ RSpec.shared_examples 'pipelines visibility table' do project.project_feature.update!(project_feature_attributes) project.add_role(ci_user, user_role) if user_role && user_role != :non_member - get api(pipelines_api_path, api_user) + get api(pipelines_api_path, api_user, admin_mode: is_admin) end - it do + specify do expect(response).to have_gitlab_http_status(response_status) expect(api_response).to match(expected_response) end diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb index 6065b1163c4..9bd430c3b4f 100644 --- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb @@ -254,6 +254,13 @@ RSpec.shared_examples 'pypi simple API endpoint' do with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:snowplow_gitlab_standard_context) do + if user_role == :anonymous || (visibility_level == :public && !user_token) + snowplow_context + else + snowplow_context.merge(user: user) + end + end before do project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s)) @@ -269,7 +276,7 @@ RSpec.shared_examples 'pypi simple API endpoint' do let(:url) { "/projects/#{project.id}/packages/pypi/simple/my-package" } let(:headers) { basic_auth_header(user.username, personal_access_token.token) } - let(:snowplow_gitlab_standard_context) { { project: project, namespace: group, property: 'i_package_pypi_user' } } + let(:snowplow_gitlab_standard_context) { snowplow_context.merge({ project: project, user: user }) } it_behaves_like 'PyPI package versions', :developer, :success end diff --git a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb index 2154a76d765..3913d29e086 100644 --- a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb @@ -9,7 +9,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type| let(:repository_storage_move_id) { storage_move.id } def get_container_repository_storage_move - get api(url, user) + get api(url, user, admin_mode: user.admin?) end it 'returns a container repository storage move', :aggregate_failures do @@ -39,7 +39,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type| shared_examples 'get container repository storage move list' do def get_container_repository_storage_moves - get api(url, user) + get api(url, user, admin_mode: user.admin?) end it 'returns container repository storage moves', :aggregate_failures do @@ -70,7 +70,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type| get_container_repository_storage_moves - json_ids = json_response.map { |storage_move| storage_move['id'] } + json_ids = json_response.pluck('id') expect(json_ids).to eq([storage_move.id, storage_move_middle.id, storage_move_oldest.id]) end @@ -90,7 +90,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type| let(:container_id) { non_existing_record_id } it 'returns not found' do - get api(url, user) + get api(url, user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:not_found) end @@ -108,7 +108,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type| let(:repository_storage_move_id) { storage_move.id } it 'returns not found' do - get api(url, user) + get api(url, user, admin_mode: user.admin?) expect(response).to have_gitlab_http_status(:not_found) end @@ -127,20 +127,20 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type| end end - describe "POST /#{container_type}/:id/repository_storage_moves" do + describe "POST /#{container_type}/:id/repository_storage_moves", :aggregate_failures do let(:container_id) { container.id } let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves" } let(:destination_storage_name) { 'test_second_storage' } def create_container_repository_storage_move - post api(url, user), params: { destination_storage_name: destination_storage_name } + post api(url, user, admin_mode: user.admin?), params: { destination_storage_name: destination_storage_name } end before do stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' }) end - it 'schedules a container repository storage move', :aggregate_failures do + it 'schedules a container repository storage move' do create_container_repository_storage_move storage_move = container.repository_storage_moves.last @@ -158,7 +158,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type| it { expect { create_container_repository_storage_move }.to be_denied_for(:user) } end - context 'destination_storage_name is missing', :aggregate_failures do + context 'destination_storage_name is missing' do let(:destination_storage_name) { nil } it 'schedules a container repository storage move' do @@ -192,7 +192,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type| let(:destination_storage_name) { 'test_second_storage' } def create_container_repository_storage_moves - post api(url, user), params: { + post api(url, user, admin_mode: user.admin?), params: { source_storage_name: source_storage_name, destination_storage_name: destination_storage_name } diff --git a/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb index b5139bd8c99..2770e293683 100644 --- a/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb @@ -71,8 +71,7 @@ RSpec.shared_examples 'resolvable discussions API' do |parent_type, noteable_typ it 'returns a 404 error when note id not found' do put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\ - "discussions/#{note.discussion_id}/notes/#{non_existing_record_id}", user), - params: { body: 'Hello!' } + "discussions/#{note.discussion_id}/notes/#{non_existing_record_id}", user), params: { body: 'Hello!' } expect(response).to have_gitlab_http_status(:not_found) end diff --git a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb index 1b92eb56f54..56f2394c005 100644 --- a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb @@ -1,12 +1,19 @@ # frozen_string_literal: true RSpec.shared_examples 'raw snippet files' do - let_it_be(:user_token) { create(:personal_access_token, user: snippet.author) } let(:snippet_id) { snippet.id } - let(:user) { snippet.author } + let_it_be(:user) { snippet.author } let(:file_path) { '%2Egitattributes' } let(:ref) { 'master' } + let_it_be(:user_token) do + if user.admin? + create(:personal_access_token, :admin_mode, user: user) + else + create(:personal_access_token, user: user) + end + end + subject { get api(api_path, personal_access_token: user_token) } context 'with an invalid snippet ID' do @@ -15,8 +22,10 @@ RSpec.shared_examples 'raw snippet files' do it 'returns 404' do subject - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq('404 Snippet Not Found') + aggregate_failures do + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Snippet Not Found') + end end end @@ -185,7 +194,7 @@ RSpec.shared_examples 'snippet individual non-file updates' do end RSpec.shared_examples 'invalid snippet updates' do - it 'returns 404 for invalid snippet id' do + it 'returns 404 for invalid snippet id', :aggregate_failures do update_snippet(snippet_id: non_existing_record_id, params: { title: 'foo' }) expect(response).to have_gitlab_http_status(:not_found) @@ -204,7 +213,7 @@ RSpec.shared_examples 'invalid snippet updates' do expect(response).to have_gitlab_http_status(:bad_request) end - it 'returns 400 if title is blank' do + it 'returns 400 if title is blank', :aggregate_failures do update_snippet(params: { title: '' }) expect(response).to have_gitlab_http_status(:bad_request) @@ -236,7 +245,9 @@ RSpec.shared_examples 'snippet access with different users' do it 'returns the correct response' do request_user = user_for(requester) - get api(path, request_user) + admin_mode = requester == :admin + + get api(path, request_user, admin_mode: admin_mode) expect(response).to have_gitlab_http_status(status) end @@ -250,8 +261,6 @@ RSpec.shared_examples 'snippet access with different users' do other_user when :admin admin - else - nil end end diff --git a/spec/support/shared_examples/requests/api/status_shared_examples.rb b/spec/support/shared_examples/requests/api/status_shared_examples.rb index 40843ccbd15..ff3947c0e73 100644 --- a/spec/support/shared_examples/requests/api/status_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/status_shared_examples.rb @@ -21,6 +21,23 @@ RSpec.shared_examples '400 response' do end end +RSpec.shared_examples '401 response' do + let(:message) { nil } + + before do + # Fires the request + request + end + + it 'returns 401' do + expect(response).to have_gitlab_http_status(:unauthorized) + + if message.present? + expect(json_response['message']).to eq(message) + end + end +end + RSpec.shared_examples '403 response' do before do # Fires the request @@ -54,7 +71,7 @@ RSpec.shared_examples '412 response' do let(:params) { nil } let(:success_status) { 204 } - context 'for a modified ressource' do + context 'for a modified resource' do before do delete request, params: params, headers: { 'HTTP_IF_UNMODIFIED_SINCE' => '1990-01-12T00:00:48-0600' } end @@ -65,7 +82,7 @@ RSpec.shared_examples '412 response' do end end - context 'for an unmodified ressource' do + context 'for an unmodified resource' do before do delete request, params: params, headers: { 'HTTP_IF_UNMODIFIED_SINCE' => Time.now } end diff --git a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb index 86a1fd76d09..398421c7a79 100644 --- a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb @@ -173,8 +173,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name| describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do it "returns the time stats for #{issuable_name}" do - issuable.update!(spend_time: { duration: 1800, user_id: user.id }, - time_estimate: 3600) + issuable.update!(spend_time: { duration: 1800, user_id: user.id }, time_estimate: 3600) get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_stats", user) diff --git a/spec/support/shared_examples/requests/applications_controller_shared_examples.rb b/spec/support/shared_examples/requests/applications_controller_shared_examples.rb index 642930dd982..4a7a7492398 100644 --- a/spec/support/shared_examples/requests/applications_controller_shared_examples.rb +++ b/spec/support/shared_examples/requests/applications_controller_shared_examples.rb @@ -7,40 +7,14 @@ RSpec.shared_examples 'applications controller - GET #show' do expect(response).to render_template :show end - - context 'when application is viewed after being created' do - before do - create_application - stub_feature_flags(hash_oauth_secrets: false) - end - - it 'sets `@created` instance variable to `true`' do - get show_path - - expect(assigns[:created]).to eq(true) - end - end - - context 'when application is reviewed' do - before do - stub_feature_flags(hash_oauth_secrets: false) - end - - it 'sets `@created` instance variable to `false`' do - get show_path - - expect(assigns[:created]).to eq(false) - end - end end end RSpec.shared_examples 'applications controller - POST #create' do - it "sets `#{OauthApplications::CREATED_SESSION_KEY}` session key to `true`" do - stub_feature_flags(hash_oauth_secrets: false) + it "sets `@created` instance variable to `true`" do create_application - expect(session[OauthApplications::CREATED_SESSION_KEY]).to eq(true) + expect(assigns[:created]).to eq(true) end end diff --git a/spec/support/shared_examples/requests/graphql_shared_examples.rb b/spec/support/shared_examples/requests/graphql_shared_examples.rb index d133c5ea641..2c08f946468 100644 --- a/spec/support/shared_examples/requests/graphql_shared_examples.rb +++ b/spec/support/shared_examples/requests/graphql_shared_examples.rb @@ -58,5 +58,5 @@ end RSpec.shared_examples 'a mutation on an unauthorized resource' do it_behaves_like 'a mutation that returns top-level errors', - errors: [::Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + errors: [::Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] end diff --git a/spec/support/shared_examples/requests/projects/aws/aws__ff_examples.rb b/spec/support/shared_examples/requests/projects/aws/aws__ff_examples.rb new file mode 100644 index 00000000000..2221baf5b90 --- /dev/null +++ b/spec/support/shared_examples/requests/projects/aws/aws__ff_examples.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'requires feature flag `cloudseed_aws` enabled' do + context 'when feature flag is disabled' do + before do + project.add_maintainer(user) + stub_feature_flags(cloudseed_aws: false) + end + + it 'renders not found' do + sign_in(user) + + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end +end diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb index 3f457890f35..dafa324b3c6 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -567,8 +567,8 @@ RSpec.shared_examples 'rate-limited unauthenticated requests' do it 'does not throttle the requests' do (1 + requests_per_period).times do post registry_endpoint, - params: { events: events }.to_json, - headers: registry_headers.merge('Authorization' => secret_token) + params: { events: events }.to_json, + headers: registry_headers.merge('Authorization' => secret_token) expect(response).to have_gitlab_http_status(:ok) end diff --git a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb deleted file mode 100644 index f8a752a5673..00000000000 --- a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb +++ /dev/null @@ -1,130 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'not accessible to non-admin users' do - context 'with unauthenticated user' do - it 'redirects to signin page' do - subject - - expect(response).to redirect_to(new_user_session_path) - end - end - - context 'with authenticated non-admin user' do - before do - login_as(create(:user)) - end - - it 'returns status not_found' do - subject - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'with authenticated admin user without admin mode' do - before do - login_as(create(:admin)) - end - - it 'redirects to enable admin mode' do - subject - - expect(response).to redirect_to(new_admin_session_path) - end - end -end - -# Requires subject and worker_class and status_api to be defined -# let(:worker_class) { SelfMonitoringProjectCreateWorker } -# let(:status_api) { status_create_self_monitoring_project_admin_application_settings_path } -# subject { post create_self_monitoring_project_admin_application_settings_path } -RSpec.shared_examples 'triggers async worker, returns sidekiq job_id with response accepted' do - before do - allow(worker_class).to receive(:with_status).and_return(worker_class) - end - - it 'returns sidekiq job_id of expected length' do - subject - - job_id = json_response['job_id'] - - aggregate_failures do - expect(job_id).to be_present - expect(job_id.length).to be <= Admin::ApplicationSettingsController::PARAM_JOB_ID_MAX_SIZE - end - end - - it 'triggers async worker' do - expect(worker_class).to receive(:perform_async) - - subject - end - - it 'returns accepted response' do - subject - - aggregate_failures do - expect(response).to have_gitlab_http_status(:accepted) - expect(json_response.keys).to contain_exactly('job_id', 'monitor_status') - expect(json_response).to include( - 'monitor_status' => status_api - ) - end - end - - it 'returns job_id' do - fake_job_id = 'b5b28910d97563e58c2fe55f' - allow(worker_class).to receive(:perform_async).and_return(fake_job_id) - - subject - - expect(json_response).to include('job_id' => fake_job_id) - end -end - -# Requires job_id and subject to be defined -# let(:job_id) { 'job_id' } -# subject do -# get status_create_self_monitoring_project_admin_application_settings_path, -# params: { job_id: job_id } -# end -RSpec.shared_examples 'handles invalid job_id' do - context 'with invalid job_id' do - let(:job_id) { 'a' * 51 } - - it 'returns bad_request if job_id too long' do - subject - - aggregate_failures do - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response).to eq('message' => 'Parameter "job_id" cannot ' \ - "exceed length of #{Admin::ApplicationSettingsController::PARAM_JOB_ID_MAX_SIZE}") - end - end - end -end - -# Requires in_progress_message and subject to be defined -# let(:in_progress_message) { 'Job to create self-monitoring project is in progress' } -# subject do -# get status_create_self_monitoring_project_admin_application_settings_path, -# params: { job_id: job_id } -# end -RSpec.shared_examples 'sets polling header and returns accepted' do - it 'sets polling header' do - expect(::Gitlab::PollingInterval).to receive(:set_header) - - subject - end - - it 'returns accepted' do - subject - - aggregate_failures do - expect(response).to have_gitlab_http_status(:accepted) - expect(json_response).to eq( - 'message' => in_progress_message - ) - end - end -end diff --git a/spec/support/shared_examples/requests/user_activity_shared_examples.rb b/spec/support/shared_examples/requests/user_activity_shared_examples.rb index 37da1ce5c63..9c0165f7150 100644 --- a/spec/support/shared_examples/requests/user_activity_shared_examples.rb +++ b/spec/support/shared_examples/requests/user_activity_shared_examples.rb @@ -5,7 +5,7 @@ RSpec.shared_examples 'updating of user activity' do |paths_to_visit| before do group = create(:group, name: 'group') - project = create(:project, :public, namespace: group, name: 'project') + project = create(:project, :public, namespace: group, path: 'project') create(:issue, project: project, iid: 10) create(:merge_request, source_project: project, iid: 15) diff --git a/spec/support/shared_examples/security_training_providers_importer.rb b/spec/support/shared_examples/security_training_providers_importer.rb index 69d92964270..81b3d22ab23 100644 --- a/spec/support/shared_examples/security_training_providers_importer.rb +++ b/spec/support/shared_examples/security_training_providers_importer.rb @@ -8,7 +8,7 @@ RSpec.shared_examples 'security training providers importer' do end it 'upserts security training providers' do - expect { 2.times { subject } }.to change { security_training_providers.count }.from(0).to(2) - expect(security_training_providers.all.map(&:name)).to match_array(['Kontra', 'Secure Code Warrior']) + expect { 3.times { subject } }.to change { security_training_providers.count }.from(0).to(3) + expect(security_training_providers.all.map(&:name)).to match_array(['Kontra', 'Secure Code Warrior', 'SecureFlag']) end end diff --git a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb index 32adf98969c..df01f9a5b0b 100644 --- a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb +++ b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb @@ -2,13 +2,15 @@ RSpec.shared_examples 'diff file base entity' do it 'exposes essential attributes' do - expect(subject).to include(:content_sha, :submodule, :submodule_link, - :submodule_tree_url, :old_path_html, - :new_path_html, :blob, :can_modify_blob, - :file_hash, :file_path, :old_path, :new_path, - :viewer, :diff_refs, :stored_externally, - :external_storage, :renamed_file, :deleted_file, - :a_mode, :b_mode, :new_file, :file_identifier_hash) + expect(subject).to include( + :content_sha, :submodule, :submodule_link, + :submodule_tree_url, :old_path_html, + :new_path_html, :blob, :can_modify_blob, + :file_hash, :file_path, :old_path, :new_path, + :viewer, :diff_refs, :stored_externally, + :external_storage, :renamed_file, :deleted_file, + :a_mode, :b_mode, :new_file, :file_identifier_hash + ) end # Converted diff files from GitHub import does not contain blob file @@ -30,13 +32,70 @@ RSpec.shared_examples 'diff file entity' do it_behaves_like 'diff file base entity' it 'exposes correct attributes' do - expect(subject).to include(:added_lines, :removed_lines, - :context_lines_path) + expect(subject).to include(:added_lines, :removed_lines, :context_lines_path) end - it 'includes viewer' do - expect(subject[:viewer].with_indifferent_access) + context 'when a viewer' do + let(:collapsed) { false } + let(:added_lines) { 1 } + let(:removed_lines) { 0 } + let(:highlighted_lines) { nil } + + before do + allow(diff_file).to receive(:diff_lines_for_serializer) + .and_return(highlighted_lines) + + allow(diff_file).to receive(:added_lines) + .and_return(added_lines) + + allow(diff_file).to receive(:removed_lines) + .and_return(removed_lines) + + allow(diff_file).to receive(:collapsed?) + .and_return(collapsed) + end + + it 'matches the schema' do + expect(subject[:viewer].with_indifferent_access) .to match_schema('entities/diff_viewer') + end + + context 'when it is a whitespace only change' do + it 'has whitespace_only true' do + expect(subject[:viewer][:whitespace_only]) + .to eq(true) + end + end + + context 'when the highlighted lines arent shown' do + before do + allow(diff_file).to receive(:text?) + .and_return(false) + end + + it 'has whitespace_only nil' do + expect(subject[:viewer][:whitespace_only]) + .to eq(nil) + end + end + + context 'when it is a new file' do + let(:added_lines) { 0 } + + it 'has whitespace_only false' do + expect(subject[:viewer][:whitespace_only]) + .to eq(false) + end + end + + context 'when it is a collapsed file' do + let(:collapsed) { true } + + it 'has whitespace_only false' do + expect(subject[:viewer][:whitespace_only]) + .to eq(false) + end + end end context 'diff files' do diff --git a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb index b5e3a407b53..e8238480ced 100644 --- a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb +++ b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb @@ -18,7 +18,8 @@ RSpec.shared_examples 'note entity' do :noteable_note_url, :report_abuse_path, :resolvable, - :type + :type, + :external_author ) end diff --git a/spec/support/shared_examples/services/base_helm_service_shared_examples.rb b/spec/support/shared_examples/services/base_helm_service_shared_examples.rb deleted file mode 100644 index c2252c83140..00000000000 --- a/spec/support/shared_examples/services/base_helm_service_shared_examples.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'logs kubernetes errors' do - let(:error_hash) do - { - service: service.class.name, - app_id: application.id, - project_ids: application.cluster.project_ids, - group_ids: [], - error_code: error_code - } - end - - it 'logs into kubernetes.log and Sentry' do - expect(Gitlab::ErrorTracking).to receive(:track_exception).with( - error, - hash_including(error_hash) - ) - - service.execute - end -end diff --git a/spec/support/shared_examples/services/clusters/create_service_shared_examples.rb b/spec/support/shared_examples/services/clusters/create_service_shared_examples.rb new file mode 100644 index 00000000000..7cd76e45ecd --- /dev/null +++ b/spec/support/shared_examples/services/clusters/create_service_shared_examples.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'create cluster service success' do + it 'creates a cluster object' do + expect { subject } + .to change { Clusters::Cluster.count }.by(1) + .and change { Clusters::Providers::Gcp.count }.by(1) + + expect(subject.name).to eq('test-cluster') + expect(subject.user).to eq(user) + expect(subject.project).to eq(project) + expect(subject.provider.gcp_project_id).to eq('gcp-project') + expect(subject.provider.zone).to eq('us-central1-a') + expect(subject.provider.num_nodes).to eq(1) + expect(subject.provider.machine_type).to eq('machine_type-a') + expect(subject.provider.access_token).to eq(access_token) + expect(subject.provider).to be_legacy_abac + expect(subject.platform).to be_nil + expect(subject.namespace_per_environment).to eq true + end +end + +RSpec.shared_examples 'create cluster service error' do + it 'returns an error' do + expect { subject }.to change { Clusters::Cluster.count }.by(0) + expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present + end +end diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb index 58659775d8c..493a96b8dae 100644 --- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb +++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb @@ -79,7 +79,8 @@ RSpec.shared_examples 'an accessible' do let(:access) do [{ 'type' => 'repository', 'name' => project.full_path, - 'actions' => actions }] + 'actions' => actions, + 'meta' => { 'project_path' => project.full_path } }] end it_behaves_like 'a valid token' @@ -244,12 +245,14 @@ RSpec.shared_examples 'a container registry auth service' do { 'type' => 'repository', 'name' => project.full_path, - 'actions' => ['pull'] + 'actions' => ['pull'], + 'meta' => { 'project_path' => project.full_path } }, { 'type' => 'repository', 'name' => "#{project.full_path}/*", - 'actions' => ['pull'] + 'actions' => ['pull'], + 'meta' => { 'project_path' => project.full_path } } ] end @@ -822,16 +825,20 @@ RSpec.shared_examples 'a container registry auth service' do [ { 'type' => 'repository', 'name' => internal_project.full_path, - 'actions' => ['pull'] }, + 'actions' => ['pull'], + 'meta' => { 'project_path' => internal_project.full_path } }, { 'type' => 'repository', 'name' => private_project.full_path, - 'actions' => ['pull'] }, + 'actions' => ['pull'], + 'meta' => { 'project_path' => private_project.full_path } }, { 'type' => 'repository', 'name' => public_project.full_path, - 'actions' => ['pull'] }, + 'actions' => ['pull'], + 'meta' => { 'project_path' => public_project.full_path } }, { 'type' => 'repository', 'name' => public_project_private_container_registry.full_path, - 'actions' => ['pull'] } + 'actions' => ['pull'], + 'meta' => { 'project_path' => public_project_private_container_registry.full_path } } ] end end @@ -845,10 +852,12 @@ RSpec.shared_examples 'a container registry auth service' do [ { 'type' => 'repository', 'name' => internal_project.full_path, - 'actions' => ['pull'] }, + 'actions' => ['pull'], + 'meta' => { 'project_path' => internal_project.full_path } }, { 'type' => 'repository', 'name' => public_project.full_path, - 'actions' => ['pull'] } + 'actions' => ['pull'], + 'meta' => { 'project_path' => public_project.full_path } } ] end end @@ -862,7 +871,8 @@ RSpec.shared_examples 'a container registry auth service' do [ { 'type' => 'repository', 'name' => public_project.full_path, - 'actions' => ['pull'] } + 'actions' => ['pull'], + 'meta' => { 'project_path' => public_project.full_path } } ] end end @@ -1258,4 +1268,29 @@ RSpec.shared_examples 'a container registry auth service' do end end end + + context 'with a project with a path containing special characters' do + let_it_be(:bad_project) { create(:project) } + + before do + bad_project.update_attribute(:path, "#{bad_project.path}_") + end + + describe '#access_token' do + let(:token) { described_class.access_token(['pull'], [bad_project.full_path]) } + let(:access) do + [{ 'type' => 'repository', + 'name' => bad_project.full_path, + 'actions' => ['pull'] }] + end + + subject { { token: token } } + + it_behaves_like 'a valid token' + + it 'has the correct scope' do + expect(payload).to include('access' => access) + end + end + end end diff --git a/spec/support/shared_examples/services/deploy_token_shared_examples.rb b/spec/support/shared_examples/services/deploy_token_shared_examples.rb new file mode 100644 index 00000000000..814b6565497 --- /dev/null +++ b/spec/support/shared_examples/services/deploy_token_shared_examples.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a deploy token creation service' do + let(:user) { create(:user) } + let(:deploy_token_params) { attributes_for(:deploy_token) } + + describe '#execute' do + subject { described_class.new(entity, user, deploy_token_params).execute } + + context 'when the deploy token is valid' do + it 'creates a new DeployToken' do + expect { subject }.to change { DeployToken.count }.by(1) + end + + it 'creates a new ProjectDeployToken' do + expect { subject }.to change { deploy_token_class.count }.by(1) + end + + it 'returns a DeployToken' do + expect(subject[:deploy_token]).to be_an_instance_of DeployToken + end + + it 'sets the creator_id as the id of the current_user' do + expect(subject[:deploy_token].read_attribute(:creator_id)).to eq(user.id) + end + end + + context 'when expires at date is not passed' do + let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') } + + it 'sets Forever.date' do + expect(subject[:deploy_token].read_attribute(:expires_at)).to eq(Forever.date) + end + end + + context 'when username is empty string' do + let(:deploy_token_params) { attributes_for(:deploy_token, username: '') } + + it 'converts it to nil' do + expect(subject[:deploy_token].read_attribute(:username)).to be_nil + end + end + + context 'when username is provided' do + let(:deploy_token_params) { attributes_for(:deploy_token, username: 'deployer') } + + it 'keeps the provided username' do + expect(subject[:deploy_token].read_attribute(:username)).to eq('deployer') + end + end + + context 'when the deploy token is invalid' do + let(:deploy_token_params) do + attributes_for(:deploy_token, read_repository: false, read_registry: false, write_registry: false) + end + + it 'does not create a new DeployToken' do + expect { subject }.not_to change { DeployToken.count } + end + + it 'does not create a new ProjectDeployToken' do + expect { subject }.not_to change { deploy_token_class.count } + end + end + end +end + +RSpec.shared_examples 'a deploy token deletion service' do + let(:user) { create(:user) } + let(:deploy_token_params) { { token_id: deploy_token.id } } + + describe '#execute' do + subject { described_class.new(entity, user, deploy_token_params).execute } + + it "destroys a token record and it's associated DeployToken" do + expect { subject }.to change { deploy_token_class.count }.by(-1) + .and change { DeployToken.count }.by(-1) + end + + context 'with invalid token id' do + let(:deploy_token_params) { { token_id: 9999 } } + + it 'raises an error' do + expect { subject }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/spec/support/shared_examples/services/import_csv_service_shared_examples.rb b/spec/support/shared_examples/services/import_csv_service_shared_examples.rb new file mode 100644 index 00000000000..1555497ae48 --- /dev/null +++ b/spec/support/shared_examples/services/import_csv_service_shared_examples.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples_for 'importer with email notification' do + it 'notifies user of import result' do + expect(Notify).to receive_message_chain(email_method, :deliver_later) + + subject + end +end + +RSpec.shared_examples 'correctly handles invalid files' do + shared_examples_for 'invalid file' do + it 'returns invalid file error' do + expect(subject[:success]).to eq(0) + expect(subject[:parse_error]).to eq(true) + end + end + + context 'when given file with unsupported extension' do + let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') } + + it_behaves_like 'invalid file' + end + + context 'when given empty file' do + let(:file) { fixture_file_upload('spec/fixtures/csv_empty.csv') } + + it_behaves_like 'invalid file' + end + + context 'when given file without headers' do + let(:file) { fixture_file_upload('spec/fixtures/csv_no_headers.csv') } + + it_behaves_like 'invalid file' + end +end diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb index a87e7c1f801..db2b448f567 100644 --- a/spec/support/shared_examples/services/incident_shared_examples.rb +++ b/spec/support/shared_examples/services/incident_shared_examples.rb @@ -12,7 +12,6 @@ # include_examples 'incident issue' RSpec.shared_examples 'incident issue' do it 'has incident as issue type' do - expect(issue.issue_type).to eq('incident') expect(issue.work_item_type.base_type).to eq('incident') end end @@ -29,7 +28,6 @@ end # include_examples 'not an incident issue' RSpec.shared_examples 'not an incident issue' do it 'has not incident as issue type' do - expect(issue.issue_type).not_to eq('incident') expect(issue.work_item_type.base_type).not_to eq('incident') end end diff --git a/spec/support/shared_examples/services/issuable/issuable_description_quick_actions_shared_examples.rb b/spec/support/shared_examples/services/issuable/issuable_description_quick_actions_shared_examples.rb new file mode 100644 index 00000000000..1970301e4c9 --- /dev/null +++ b/spec/support/shared_examples/services/issuable/issuable_description_quick_actions_shared_examples.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +# Specifications for behavior common to all objects with executable attributes. +# It can take a `default_params`. + +RSpec.shared_examples 'issuable record that supports quick actions' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + let_it_be(:assignee) { create(:user) } + let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:labels) { create_list(:label, 3, project: project) } + + let(:base_params) { { title: 'My issuable title' } } + let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) } + + before_all do + project.add_maintainer(user) + project.add_maintainer(assignee) + end + + before do + issuable.reload + end + + context 'with labels in command only' do + let(:example_params) do + { + description: "/label ~#{labels.first.name} ~#{labels.second.name}\n/unlabel ~#{labels.third.name}" + } + end + + it 'attaches labels to issuable' do + expect(issuable.label_ids).to match_array([labels.first.id, labels.second.id]) + end + end + + context 'with labels in params and command' do + let(:example_params) do + { + label_ids: [labels.second.id], + description: "/label ~#{labels.first.name}\n/unlabel ~#{labels.third.name}" + } + end + + it 'attaches all labels to issuable' do + expect(issuable.label_ids).to match_array([labels.first.id, labels.second.id]) + end + end + + context 'with assignee and milestone in command only' do + let(:example_params) do + { + description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}") + } + end + + it 'assigns and sets milestone to issuable' do + expect(issuable.assignees).to eq([assignee]) + expect(issuable.milestone).to eq(milestone) + end + end +end diff --git a/spec/support/shared_examples/services/issuable/issuable_import_csv_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/issuable_import_csv_service_shared_examples.rb new file mode 100644 index 00000000000..5336e0f4c2f --- /dev/null +++ b/spec/support/shared_examples/services/issuable/issuable_import_csv_service_shared_examples.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'issuable import csv service' do |issuable_type| + let_it_be_with_refind(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + subject { service.execute } + + shared_examples_for 'an issuable importer' do + if issuable_type == 'issue' + it 'records the import attempt if resource is an issue' do + expect { subject } + .to change { Issues::CsvImport.where(project: project, user: user).count } + .by 1 + end + end + end + + describe '#execute' do + before do + project.add_developer(user) + end + + it_behaves_like 'correctly handles invalid files' do + it_behaves_like 'importer with email notification' + it_behaves_like 'an issuable importer' + end + + context 'with a file generated by Gitlab CSV export' do + let(:file) { fixture_file_upload('spec/fixtures/csv_gitlab_export.csv') } + + it 'imports the CSV without errors' do + expect(subject[:success]).to eq(4) + expect(subject[:error_lines]).to eq([]) + expect(subject[:parse_error]).to eq(false) + end + + it 'correctly sets the issuable attributes' do + expect { subject }.to change { issuables.count }.by 4 + + expect(issuables.reload).to include(have_attributes({ title: 'Test Title', description: 'Test Description' })) + end + + it_behaves_like 'importer with email notification' + it_behaves_like 'an issuable importer' + end + + context 'with comma delimited file' do + let(:file) { fixture_file_upload('spec/fixtures/csv_comma.csv') } + + it 'imports CSV without errors' do + expect(subject[:success]).to eq(3) + expect(subject[:error_lines]).to eq([]) + expect(subject[:parse_error]).to eq(false) + end + + it 'correctly sets the issuable attributes' do + expect { subject }.to change { issuables.count }.by 3 + + expect(issuables.reload).to include(have_attributes(title: 'Title with quote"', description: 'Description')) + end + + it_behaves_like 'importer with email notification' + it_behaves_like 'an issuable importer' + end + + context 'with tab delimited file with error row' do + let(:file) { fixture_file_upload('spec/fixtures/csv_tab.csv') } + + it 'imports CSV with some error rows' do + expect(subject[:success]).to eq(2) + expect(subject[:error_lines]).to eq([3]) + expect(subject[:parse_error]).to eq(false) + end + + it 'correctly sets the issuable attributes' do + expect { subject }.to change { issuables.count }.by 2 + + expect(issuables.reload).to include(have_attributes(title: 'Hello', description: 'World')) + end + + it_behaves_like 'importer with email notification' + it_behaves_like 'an issuable importer' + end + + context 'with semicolon delimited file with CRLF' do + let(:file) { fixture_file_upload('spec/fixtures/csv_semicolon.csv') } + + it 'imports CSV with a blank row' do + expect(subject[:success]).to eq(3) + expect(subject[:error_lines]).to eq([4]) + expect(subject[:parse_error]).to eq(false) + end + + it 'correctly sets the issuable attributes' do + expect { subject }.to change { issuables.count }.by 3 + + expect(issuables.reload).to include(have_attributes(title: 'Hello', description: 'World')) + end + + it_behaves_like 'importer with email notification' + it_behaves_like 'an issuable importer' + end + end +end diff --git a/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb new file mode 100644 index 00000000000..85a05bbe56d --- /dev/null +++ b/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'issuable update service' do + def update_issuable(opts) + described_class.new(project, user, opts).execute(open_issuable) + end + + describe 'changing state' do + let(:hook_event) { :"#{closed_issuable.class.name.underscore.to_sym}_hooks" } + + describe 'to reopened' do + let(:expected_payload) do + include( + changes: include( + state_id: { current: 1, previous: 2 }, + updated_at: { current: kind_of(Time), previous: kind_of(Time) } + ), + object_attributes: include( + state: 'opened', + action: 'reopen' + ) + ) + end + + it 'executes hooks' do + hooks_container = described_class < Issues::BaseService ? project.project_namespace : project + expect(hooks_container).to receive(:execute_hooks).with(expected_payload, hook_event) + expect(hooks_container).to receive(:execute_integrations).with(expected_payload, hook_event) + + described_class.new( + **described_class.constructor_container_arg(project), + current_user: user, + params: { state_event: 'reopen' } + ).execute(closed_issuable) + end + end + + describe 'to closed' do + let(:expected_payload) do + include( + changes: include( + state_id: { current: 2, previous: 1 }, + updated_at: { current: kind_of(Time), previous: kind_of(Time) } + ), + object_attributes: include( + state: 'closed', + action: 'close' + ) + ) + end + + it 'executes hooks' do + hooks_container = described_class < Issues::BaseService ? project.project_namespace : project + expect(hooks_container).to receive(:execute_hooks).with(expected_payload, hook_event) + expect(hooks_container).to receive(:execute_integrations).with(expected_payload, hook_event) + + described_class.new( + **described_class.constructor_container_arg(project), + current_user: user, + params: { state_event: 'close' } + ).execute(open_issuable) + end + end + end +end + +RSpec.shared_examples 'keeps issuable labels sorted after update' do + before do + update_issuable(label_ids: [label_b.id]) + end + + context 'when label is changed' do + it 'keeps the labels sorted by title ASC' do + update_issuable({ add_label_ids: [label_a.id] }) + + expect(issuable.labels).to eq([label_a, label_b]) + end + end +end + +RSpec.shared_examples 'broadcasting issuable labels updates' do + before do + update_issuable(label_ids: [label_a.id]) + end + + context 'when label is added' do + it 'triggers the GraphQL subscription' do + expect(GraphqlTriggers).to receive(:issuable_labels_updated).with(issuable) + + update_issuable(add_label_ids: [label_b.id]) + end + end + + context 'when label is removed' do + it 'triggers the GraphQL subscription' do + expect(GraphqlTriggers).to receive(:issuable_labels_updated).with(issuable) + + update_issuable(remove_label_ids: [label_a.id]) + end + end + + context 'when label is unchanged' do + it 'does not trigger the GraphQL subscription' do + expect(GraphqlTriggers).not_to receive(:issuable_labels_updated).with(issuable) + + update_issuable(label_ids: [label_a.id]) + end + end +end + +RSpec.shared_examples_for 'issuable update service updating last_edited_at values' do + context 'when updating the title of the issuable' do + let(:update_params) { { title: 'updated title' } } + + it 'does not update last_edited values' do + expect { update_issuable }.to change { issuable.title }.from(issuable.title).to('updated title').and( + not_change(issuable, :last_edited_at) + ).and( + not_change(issuable, :last_edited_by) + ) + end + end + + context 'when updating the description of the issuable' do + let(:update_params) { { description: 'updated description' } } + + it 'updates last_edited values' do + expect do + update_issuable + end.to change { issuable.description }.from(issuable.description).to('updated description').and( + change { issuable.last_edited_at } + ).and( + change { issuable.last_edited_by } + ) + end + end +end diff --git a/spec/support/shared_examples/services/issuable/update_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/update_service_shared_examples.rb deleted file mode 100644 index ff7acc7e907..00000000000 --- a/spec/support/shared_examples/services/issuable/update_service_shared_examples.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples_for 'issuable update service updating last_edited_at values' do - context 'when updating the title of the issuable' do - let(:update_params) { { title: 'updated title' } } - - it 'does not update last_edited values' do - expect { update_issuable }.to change { issuable.title }.from(issuable.title).to('updated title').and( - not_change(issuable, :last_edited_at) - ).and( - not_change(issuable, :last_edited_by) - ) - end - end - - context 'when updating the description of the issuable' do - let(:update_params) { { description: 'updated description' } } - - it 'updates last_edited values' do - expect do - update_issuable - end.to change { issuable.description }.from(issuable.description).to('updated description').and( - change { issuable.last_edited_at } - ).and( - change { issuable.last_edited_by } - ) - end - end -end diff --git a/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb index e47ff2fcd59..0bf8bc4ff04 100644 --- a/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb +++ b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb @@ -34,7 +34,11 @@ RSpec.shared_examples 'issuable link creation' do end it 'returns error' do - is_expected.to eq(message: "No matching #{issuable_type} found. Make sure that you are adding a valid #{issuable_type} URL.", status: :error, http_status: 404) + if issuable_type == :issue + is_expected.to eq(message: "Couldn't link #{issuable_type}. You must have at least the Reporter role in both projects.", status: :error, http_status: 403) + else + is_expected.to eq(message: "No matching #{issuable_type} found. Make sure that you are adding a valid #{issuable_type} URL.", status: :error, http_status: 404) + end end it 'no relationship is created' do diff --git a/spec/support/shared_examples/services/issues/move_and_clone_services_shared_examples.rb b/spec/support/shared_examples/services/issues/move_and_clone_services_shared_examples.rb new file mode 100644 index 00000000000..2b2e90c0461 --- /dev/null +++ b/spec/support/shared_examples/services/issues/move_and_clone_services_shared_examples.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'copy or reset relative position' do + before do + # ensure we have a relative position and it is known + old_issue.update!(relative_position: 1000) + end + + context 'when moved to a project within same group hierarchy' do + it 'does not reset the relative_position' do + expect(subject.relative_position).to eq(1000) + end + end + + context 'when moved to a project in a different group hierarchy' do + let_it_be(:new_project) { create(:project, group: create(:group)) } + + it 'does reset the relative_position' do + expect(subject.relative_position).to be_nil + end + end +end diff --git a/spec/support/shared_examples/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/shared_examples/services/migrate_to_ghost_user_service_shared_examples.rb new file mode 100644 index 00000000000..e77d73d1c72 --- /dev/null +++ b/spec/support/shared_examples/services/migrate_to_ghost_user_service_shared_examples.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +RSpec.shared_examples "migrating a deleted user's associated records to the ghost user" do |record_class, fields| + record_class_name = record_class.to_s.titleize.downcase + + let(:project) do + case record_class + when MergeRequest + create(:project, :repository) + else + create(:project) + end + end + + before do + project.add_developer(user) + end + + context "for a #{record_class_name} the user has created" do + let!(:record) { created_record } + let(:migrated_fields) { fields || [:author] } + + it "does not delete the #{record_class_name}" do + service.execute + + expect(record_class.find_by_id(record.id)).to be_present + end + + it "blocks the user before migrating #{record_class_name}s to the 'Ghost User'" do + service.execute + + expect(user).to be_blocked + end + + it 'migrates all associated fields to the "Ghost user"' do + service.execute + + migrated_record = record_class.find_by_id(record.id) + + migrated_fields.each do |field| + expect(migrated_record.public_send(field)).to eq(User.ghost) + end + end + + it 'will only migrate specific records during a hard_delete' do + service.execute(hard_delete: true) + + migrated_record = record_class.find_by_id(record.id) + + check_user = always_ghost ? User.ghost : user + + migrated_fields.each do |field| + expect(migrated_record.public_send(field)).to eq(check_user) + end + end + + describe "race conditions" do + context "when #{record_class_name} migration fails and is rolled back" do + before do + allow_next_instance_of(ActiveRecord::Associations::CollectionProxy) + .to receive(:update_all).and_raise(ActiveRecord::StatementTimeout) + end + + it 'rolls back the user block' do + expect { service.execute }.to raise_error(ActiveRecord::StatementTimeout) + + expect(user.reload).not_to be_blocked + end + + it "doesn't unblock a previously-blocked user" do + expect(user.starred_projects).to receive(:update_all).and_call_original + user.block + + expect { service.execute }.to raise_error(ActiveRecord::StatementTimeout) + + expect(user.reload).to be_blocked + end + end + + it "blocks the user before #{record_class_name} migration begins" do + expect(service).to receive("migrate_#{record_class_name.parameterize(separator: '_').pluralize}".to_sym) do + expect(user.reload).to be_blocked + end + + service.execute + end + end + end +end diff --git a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb index a3042ac2e26..cb544f42765 100644 --- a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb +++ b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb @@ -29,26 +29,76 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do let_it_be(:architecture_amd64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'amd64') } let_it_be(:architecture_arm64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'arm64') } - let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T08:00:00Z', file_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', file_md5: 'd41d8cd98f00b204e9800998ecf8427e', file_fixture: nil, size: 0) } # updated - let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_all, updated_at: '2020-01-24T09:00:00Z', file_sha256: 'a') } # destroyed - let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T10:54:59Z', file_sha256: 'b') } # destroyed, 1 second before last generation - let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'c') } # kept, last generation - let_it_be(:component_file5) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'd') } # kept, last generation - let_it_be(:component_file6) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'e') } # kept, less than 1 hour ago - - def check_component_file(release_date, component_name, component_file_type, architecture_name, expected_content) + let_it_be(:component_file_old_main_amd64) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T08:00:00Z', file_sha256: 'a') } # destroyed + + let_it_be(:component_file_oldest_kept_contrib_all) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'b') } # oldest kept + let_it_be(:component_file_oldest_kept_contrib_amd64) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'c') } # oldest kept + let_it_be(:component_file_recent_contrib_amd64) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'd') } # kept, less than 1 hour ago + + let_it_be(:component_file_empty_contrib_all_di) { create("debian_#{container_type}_component_file", :di_packages, :empty, component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z') } # oldest kept + let_it_be(:component_file_empty_contrib_amd64_di) { create("debian_#{container_type}_component_file", :di_packages, :empty, component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-24T10:55:00Z') } # touched, as last empty + let_it_be(:component_file_recent_contrib_amd64_di) { create("debian_#{container_type}_component_file", :di_packages, component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'f') } # kept, less than 1 hour ago + + let(:pool_prefix) do + prefix = "pool/#{distribution.codename}" + prefix += "/#{project.id}" if container_type == :group + prefix += "/#{package.name[0]}/#{package.name}/#{package.version}" + prefix + end + + let(:expected_main_amd64_di_content) do + <<~MAIN_AMD64_DI_CONTENT + Section: misc + Priority: extra + Filename: #{pool_prefix}/sample-udeb_1.2.3~alpha2_amd64.udeb + Size: 409600 + SHA256: #{package.package_files.with_debian_file_type(:udeb).first.file_sha256} + MAIN_AMD64_DI_CONTENT + end + + let(:expected_main_amd64_di_sha256) { Digest::SHA256.hexdigest(expected_main_amd64_di_content) } + let!(:component_file_old_main_amd64_di) do # touched + create("debian_#{container_type}_component_file", :di_packages, component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T08:00:00Z', file_sha256: expected_main_amd64_di_sha256).tap do |cf| + cf.update! file: CarrierWaveStringFile.new(expected_main_amd64_di_content), size: expected_main_amd64_di_content.size + end + end + + def check_component_file( + release_date, component_name, component_file_type, architecture_name, expected_content, + updated: true, id_of: nil + ) component_file = distribution .component_files .with_component_name(component_name) .with_file_type(component_file_type) .with_architecture_name(architecture_name) + .with_compression_type(nil) .order_updated_asc .last + if expected_content.nil? + expect(component_file).to be_nil + return + end + expect(component_file).not_to be_nil - expect(component_file.updated_at).to eq(release_date) - unless expected_content.nil? + if id_of + expect(component_file&.id).to eq(id_of.id) + else + # created + expect(component_file&.id).to be > component_file_old_main_amd64_di.id + end + + if updated + expect(component_file.updated_at).to eq(release_date) + else + expect(component_file.updated_at).not_to eq(release_date) + end + + if expected_content == '' + expect(component_file.size).to eq(0) + else expect(expected_content).not_to include('MD5') component_file.file.use_file do |file_path| expect(File.read(file_path)).to eq(expected_content) @@ -57,30 +107,23 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do end it 'generates Debian distribution and component files', :aggregate_failures do - current_time = Time.utc(2020, 01, 25, 15, 17, 18, 123456) + current_time = Time.utc(2020, 1, 25, 15, 17, 19) travel_to(current_time) do expect(Gitlab::ErrorTracking).not_to receive(:log_exception) - components_count = 2 - architectures_count = 3 - - initial_count = 6 - destroyed_count = 2 - updated_count = 1 - created_count = components_count * (architectures_count * 2 + 1) - updated_count + initial_count = 8 + destroyed_count = 1 + created_count = 4 # main_amd64 + main_sources + empty contrib_all + empty contrib_amd64 expect { subject } .to not_change { Packages::Package.count } .and not_change { Packages::PackageFile.count } .and change { distribution.reload.updated_at }.to(current_time.round) .and change { distribution.component_files.reset.count }.from(initial_count).to(initial_count - destroyed_count + created_count) - .and change { component_file1.reload.updated_at }.to(current_time.round) + .and change { component_file_old_main_amd64_di.reload.updated_at }.to(current_time.round) package_files = package.package_files.order(id: :asc).preload_debian_file_metadata.to_a - pool_prefix = "pool/#{distribution.codename}" - pool_prefix += "/#{project.id}" if container_type == :group - pool_prefix += "/#{package.name[0]}/#{package.name}/#{package.version}" expected_main_amd64_content = <<~EOF Package: libsample0 Source: #{package.name} @@ -120,17 +163,9 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do SHA256: #{package_files[3].file_sha256} EOF - expected_main_amd64_di_content = <<~EOF - Section: misc - Priority: extra - Filename: #{pool_prefix}/sample-udeb_1.2.3~alpha2_amd64.udeb - Size: 409600 - SHA256: #{package_files[4].file_sha256} - EOF - expected_main_sources_content = <<~EOF Package: #{package.name} - Binary: sample-dev, libsample0, sample-udeb + Binary: sample-dev, libsample0, sample-udeb, sample-ddeb Version: #{package.version} Maintainer: #{package_files[1].debian_fields['Maintainer']} Build-Depends: debhelper-compat (= 13) @@ -139,13 +174,13 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do Format: 3.0 (native) Files: #{package_files[1].file_md5} #{package_files[1].size} #{package_files[1].file_name} - d5ca476e4229d135a88f9c729c7606c9 864 sample_1.2.3~alpha2.tar.xz + #{package_files[0].file_md5} 964 #{package_files[0].file_name} Checksums-Sha256: #{package_files[1].file_sha256} #{package_files[1].size} #{package_files[1].file_name} - 40e4682bb24a73251ccd7c7798c0094a649091e5625d6a14bcec9b4e7174f3da 864 sample_1.2.3~alpha2.tar.xz + #{package_files[0].file_sha256} 964 #{package_files[0].file_name} Checksums-Sha1: #{package_files[1].file_sha1} #{package_files[1].size} #{package_files[1].file_name} - c5cfc111ea924842a89a06d5673f07dfd07de8ca 864 sample_1.2.3~alpha2.tar.xz + #{package_files[0].file_sha1} 964 #{package_files[0].file_name} Homepage: #{package_files[1].debian_fields['Homepage']} Section: misc Priority: extra @@ -157,42 +192,38 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do check_component_file(current_time.round, 'main', :packages, 'arm64', nil) check_component_file(current_time.round, 'main', :di_packages, 'all', nil) - check_component_file(current_time.round, 'main', :di_packages, 'amd64', expected_main_amd64_di_content) + check_component_file(current_time.round, 'main', :di_packages, 'amd64', expected_main_amd64_di_content, id_of: component_file_old_main_amd64_di) check_component_file(current_time.round, 'main', :di_packages, 'arm64', nil) check_component_file(current_time.round, 'main', :sources, nil, expected_main_sources_content) - check_component_file(current_time.round, 'contrib', :packages, 'all', nil) - check_component_file(current_time.round, 'contrib', :packages, 'amd64', nil) + check_component_file(current_time.round, 'contrib', :packages, 'all', '') + check_component_file(current_time.round, 'contrib', :packages, 'amd64', '') check_component_file(current_time.round, 'contrib', :packages, 'arm64', nil) - check_component_file(current_time.round, 'contrib', :di_packages, 'all', nil) - check_component_file(current_time.round, 'contrib', :di_packages, 'amd64', nil) + check_component_file(current_time.round, 'contrib', :di_packages, 'all', '', updated: false, id_of: component_file_empty_contrib_all_di) + check_component_file(current_time.round, 'contrib', :di_packages, 'amd64', '', id_of: component_file_empty_contrib_amd64_di) check_component_file(current_time.round, 'contrib', :di_packages, 'arm64', nil) check_component_file(current_time.round, 'contrib', :sources, nil, nil) - main_amd64_size = expected_main_amd64_content.length - main_amd64_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content) + expected_main_amd64_size = expected_main_amd64_content.bytesize + expected_main_amd64_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content) - contrib_all_size = component_file1.size - contrib_all_sha256 = component_file1.file_sha256 + expected_main_amd64_di_size = expected_main_amd64_di_content.length - main_amd64_di_size = expected_main_amd64_di_content.length - main_amd64_di_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_di_content) - - main_sources_size = expected_main_sources_content.length - main_sources_sha256 = Digest::SHA256.hexdigest(expected_main_sources_content) + expected_main_sources_size = expected_main_sources_content.length + expected_main_sources_sha256 = Digest::SHA256.hexdigest(expected_main_sources_content) expected_release_content = <<~EOF Codename: #{distribution.codename} - Date: Sat, 25 Jan 2020 15:17:18 +0000 - Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000 + Date: Sat, 25 Jan 2020 15:17:19 +0000 + Valid-Until: Mon, 27 Jan 2020 15:17:19 +0000 Acquire-By-Hash: yes Architectures: all amd64 arm64 Components: contrib main SHA256: - #{contrib_all_sha256} #{contrib_all_size.to_s.rjust(8)} contrib/binary-all/Packages + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-all/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-all/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-amd64/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-amd64/Packages @@ -201,11 +232,11 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/source/Sources e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-all/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-all/Packages - #{main_amd64_sha256} #{main_amd64_size.to_s.rjust(8)} main/binary-amd64/Packages - #{main_amd64_di_sha256} #{main_amd64_di_size.to_s.rjust(8)} main/debian-installer/binary-amd64/Packages + #{expected_main_amd64_sha256} #{expected_main_amd64_size.to_s.rjust(8)} main/binary-amd64/Packages + #{expected_main_amd64_di_sha256} #{expected_main_amd64_di_size.to_s.rjust(8)} main/debian-installer/binary-amd64/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-arm64/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-arm64/Packages - #{main_sources_sha256} #{main_sources_size.to_s.rjust(8)} main/source/Sources + #{expected_main_sources_sha256} #{expected_main_sources_size.to_s.rjust(8)} main/source/Sources EOF expected_release_content = "Suite: #{distribution.suite}\n#{expected_release_content}" if distribution.suite @@ -222,7 +253,7 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do context 'without components and architectures' do it 'generates minimal distribution', :aggregate_failures do - travel_to(Time.utc(2020, 01, 25, 15, 17, 18, 123456)) do + travel_to(Time.utc(2020, 1, 25, 15, 17, 18, 123456)) do expect(Gitlab::ErrorTracking).not_to receive(:log_exception) expect { subject } diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index f63693dbf26..7a4d7f81e96 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -76,7 +76,7 @@ RSpec.shared_examples 'returns packages' do |container_type, user_type| subject expect(json_response.length).to eq(2) - expect(json_response.map { |package| package['id'] }).to contain_exactly(package1.id, package2.id) + expect(json_response.pluck('id')).to contain_exactly(package1.id, package2.id) end end end @@ -123,7 +123,7 @@ RSpec.shared_examples 'returns packages with subgroups' do |container_type, user subject expect(json_response.length).to eq(3) - expect(json_response.map { |package| package['id'] }).to contain_exactly(package1.id, package2.id, package3.id) + expect(json_response.pluck('id')).to contain_exactly(package1.id, package2.id, package3.id) end end end @@ -138,7 +138,7 @@ RSpec.shared_examples 'package sorting' do |order_by| it 'returns the sorted packages' do subject - expect(json_response.map { |package| package['id'] }).to eq(packages.map(&:id)) + expect(json_response.pluck('id')).to eq(packages.map(&:id)) end end @@ -148,7 +148,7 @@ RSpec.shared_examples 'package sorting' do |order_by| it 'returns the sorted packages' do subject - expect(json_response.map { |package| package['id'] }).to eq(packages.reverse.map(&:id)) + expect(json_response.pluck('id')).to eq(packages.reverse.map(&:id)) end end end @@ -225,7 +225,7 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false| subject expect(json_response.length).to eq(1) - expect(json_response.map { |package| package['package_type'] }).to contain_exactly(package_type) + expect(json_response.pluck('package_type')).to contain_exactly(package_type) end end end @@ -253,7 +253,7 @@ RSpec.shared_examples 'with versionless packages' do it 'does not return the package' do subject - expect(json_response.map { |package| package['id'] }).not_to include(versionless_package.id) + expect(json_response.pluck('id')).not_to include(versionless_package.id) end end @@ -268,7 +268,7 @@ RSpec.shared_examples 'with versionless packages' do it 'returns the package' do subject - expect(json_response.map { |package| package['id'] }).to include(versionless_package.id) + expect(json_response.pluck('id')).to include(versionless_package.id) end end end @@ -295,7 +295,7 @@ RSpec.shared_examples 'with status param' do it 'does not return the package' do subject - expect(json_response.map { |package| package['id'] }).not_to include(hidden_package.id) + expect(json_response.pluck('id')).not_to include(hidden_package.id) end end @@ -309,7 +309,7 @@ RSpec.shared_examples 'with status param' do it 'returns the package' do subject - expect(json_response.map { |package| package['id'] }).to include(hidden_package.id) + expect(json_response.pluck('id')).to include(hidden_package.id) end end end diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb index 9f940d27341..2070cac24b0 100644 --- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb +++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb @@ -63,35 +63,6 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| expect(gitlab_shell.repository_exists?('default', old_project_repository_path)).to be(false) expect(gitlab_shell.repository_exists?('default', old_repository_path)).to be(false) end - - context ':repack_after_shard_migration feature flag disabled' do - before do - stub_feature_flags(repack_after_shard_migration: false) - end - - it 'does not enqueue a GC run' do - expect { subject.execute } - .not_to change { Projects::GitGarbageCollectWorker.jobs.count } - end - end - - context ':repack_after_shard_migration feature flag enabled' do - before do - stub_feature_flags(repack_after_shard_migration: true) - end - - it 'does not enqueue a GC run if housekeeping is disabled' do - stub_application_setting(housekeeping_enabled: false) - - expect { subject.execute } - .not_to change { Projects::GitGarbageCollectWorker.jobs.count } - end - - it 'enqueues a GC run' do - expect { subject.execute } - .to change { Projects::GitGarbageCollectWorker.jobs.count }.by(1) - end - end end context 'when the filesystems are the same' do diff --git a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb index 209be09c807..21dc3c2bf70 100644 --- a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb +++ b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb @@ -114,7 +114,8 @@ RSpec.shared_examples_for 'services security ci configuration create service' do it 'fails with error' do expect(project).to receive(:ci_config_for).and_return(unsupported_yaml) - expect { result }.to raise_error(Gitlab::Graphql::Errors::MutationError, '.gitlab-ci.yml with aliases/anchors is not supported. Please change the CI configuration manually.') + expect { result }.to raise_error(Gitlab::Graphql::Errors::MutationError, Gitlab::Utils::ErrorMessage.to_user_facing( + _(".gitlab-ci.yml with aliases/anchors is not supported. Please change the CI configuration manually."))) end end @@ -145,7 +146,7 @@ RSpec.shared_examples_for 'services security ci configuration create service' do let_it_be(:repository) { project.repository } it 'is successful' do - expect(repository).to receive(:root_ref_sha).and_raise(StandardError) + expect(repository).to receive(:commit).and_return(nil) expect(result.status).to eq(:success) end end @@ -168,7 +169,7 @@ RSpec.shared_examples_for 'services security ci configuration create service' do it 'returns an error' do expect { result }.to raise_error { |error| expect(error).to be_a(Gitlab::Graphql::Errors::MutationError) - expect(error.message).to eq('You must <a target="_blank" rel="noopener noreferrer" ' \ + expect(error.message).to eq('UF You must <a target="_blank" rel="noopener noreferrer" ' \ 'href="http://localhost/help/user/project/repository/index.md' \ '#add-files-to-a-repository">add at least one file to the repository' \ '</a> before using Security features.') diff --git a/spec/support/shared_examples/services/service_response_shared_examples.rb b/spec/support/shared_examples/services/service_response_shared_examples.rb new file mode 100644 index 00000000000..e55f16a2994 --- /dev/null +++ b/spec/support/shared_examples/services/service_response_shared_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'returning an error service response' do |message: nil| + it 'returns an error service response' do + result = subject + + expect(result).to be_error + + expect(result.message).to eq(message) if message + end +end + +RSpec.shared_examples 'returning a success service response' do |message: nil| + it 'returns a success service response' do + result = subject + + expect(result).to be_success + + expect(result.message).to eq(message) if message + end +end diff --git a/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb b/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb index e72e8e79411..d3b3434b339 100644 --- a/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb +++ b/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb @@ -5,7 +5,6 @@ RSpec.shared_examples 'issue_edit snowplow tracking' do let(:action) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_ACTION } let(:label) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_LABEL } let(:namespace) { project.namespace } - let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } it_behaves_like 'Snowplow event tracking with RedisHLL context' end diff --git a/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb b/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb deleted file mode 100644 index ac064ed4c33..00000000000 --- a/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples "setting work item's milestone" do - context "when 'milestone' param does not exist" do - let(:params) { {} } - - it "does not set the work item's milestone" do - expect { execute_callback }.to not_change(work_item, :milestone) - end - end - - context "when 'milestone' is not in the work item's project's hierarchy" do - let(:another_group_milestone) { create(:milestone, group: create(:group)) } - let(:params) { { milestone_id: another_group_milestone.id } } - - it "does not set the work item's milestone" do - expect { execute_callback }.to not_change(work_item, :milestone) - end - end - - context 'when assigning a group milestone' do - let(:params) { { milestone_id: group_milestone.id } } - - it "sets the work item's milestone" do - expect { execute_callback } - .to change { work_item.milestone } - .from(nil) - .to(group_milestone) - end - end - - context 'when assigning a project milestone' do - let(:params) { { milestone_id: project_milestone.id } } - - it "sets the work item's milestone" do - expect { execute_callback } - .to change { work_item.milestone } - .from(nil) - .to(project_milestone) - end - end -end diff --git a/spec/support/shared_examples/views/pipeline_status_changes_email.rb b/spec/support/shared_examples/views/pipeline_status_changes_email.rb index 698f11c2216..fe6cc5e03d2 100644 --- a/spec/support/shared_examples/views/pipeline_status_changes_email.rb +++ b/spec/support/shared_examples/views/pipeline_status_changes_email.rb @@ -8,12 +8,14 @@ RSpec.shared_examples 'pipeline status changes email' do let(:merge_request) { create(:merge_request, :simple, source_project: project) } let(:pipeline) do - create(:ci_pipeline, - project: project, - user: user, - ref: project.default_branch, - sha: project.commit.sha, - status: status) + create( + :ci_pipeline, + project: project, + user: user, + ref: project.default_branch, + sha: project.commit.sha, + status: status + ) end before do diff --git a/spec/support/shared_examples/work_items/export_and_import_shared_examples.rb b/spec/support/shared_examples/work_items/export_and_import_shared_examples.rb new file mode 100644 index 00000000000..bbbfacfdf53 --- /dev/null +++ b/spec/support/shared_examples/work_items/export_and_import_shared_examples.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'a exported file that can be imported' do + before do + origin_project.add_reporter(user) + target_project.add_reporter(user) + end + + def export_work_items_for(project) + origin_work_items = WorkItem.where(project: origin_project) + export = described_class.new(origin_work_items, project) + export.email(user) + attachment = ActionMailer::Base.deliveries.last.attachments.first + file = Tempfile.new('temp_work_item_export.csv') + file.write(attachment.read) + + file + end + + def import_file_for(project, file) + uploader = FileUploader.new(project) + uploader.store!(file) + import_service = WorkItems::ImportCsvService.new(user, target_project, uploader) + + import_service.execute + end + + it 'imports work item with correct attributes', :aggregate_failures do + csv_file = export_work_items_for(origin_project) + + imported_work_items = ::WorkItems::WorkItemsFinder.new(user, project: target_project).execute + expect { import_file_for(target_project, csv_file) }.to change { imported_work_items.count }.by 1 + imported_work_item = imported_work_items.first + expect(imported_work_item.author).to eq(user) + expected_matching_fields.each do |field| + expect(imported_work_item.public_send(field)).to eq(work_item.public_send(field)) + end + end +end diff --git a/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb b/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb index e224b71da91..095c32c3136 100644 --- a/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb +++ b/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb @@ -50,14 +50,20 @@ RSpec.shared_examples 'batched background migrations execution worker' do end describe '.max_running_jobs' do - it 'returns MAX_RUNNING_MIGRATIONS' do - expect(described_class.max_running_jobs).to eq(described_class::MAX_RUNNING_MIGRATIONS) + it 'returns database_max_running_batched_background_migrations application setting' do + stub_application_setting(database_max_running_batched_background_migrations: 3) + + expect(described_class.max_running_jobs) + .to eq(Gitlab::CurrentSettings.database_max_running_batched_background_migrations) end end describe '#max_running_jobs' do - it 'returns MAX_RUNNING_MIGRATIONS' do - expect(described_class.new.max_running_jobs).to eq(described_class::MAX_RUNNING_MIGRATIONS) + it 'returns database_max_running_batched_background_migrations application setting' do + stub_application_setting(database_max_running_batched_background_migrations: 3) + + expect(described_class.new.max_running_jobs) + .to eq(Gitlab::CurrentSettings.database_max_running_batched_background_migrations) end end diff --git a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb index 8ec955940c0..06877aee565 100644 --- a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb +++ b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb @@ -88,9 +88,9 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d end end - context 'when the feature flag is disabled' do + context 'when the tracking database is shared' do before do - stub_feature_flags(execute_batched_migrations_on_schedule: false) + skip_if_database_exists(tracking_database) end it 'does nothing' do @@ -101,22 +101,17 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d end end - context 'when the feature flag is enabled' do - let(:base_model) { Gitlab::Database.database_base_models[tracking_database] } - + context 'when the tracking database is not shared' do before do - stub_feature_flags(execute_batched_migrations_on_schedule: true) - - allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration) - .with(connection: base_model.connection) - .and_return(nil) + skip_if_shared_database(tracking_database) end - context 'when database config is shared' do - it 'does nothing' do - expect(Gitlab::Database).to receive(:db_config_share_with) - .with(base_model.connection_db_config).and_return('main') + context 'when the feature flag is disabled' do + before do + stub_feature_flags(execute_batched_migrations_on_schedule: false) + end + it 'does nothing' do expect(worker).not_to receive(:active_migration) expect(worker).not_to receive(:run_active_migration) @@ -124,123 +119,146 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d end end - context 'when no active migrations exist' do - context 'when parallel execution is disabled' do - before do - stub_feature_flags(batched_migrations_parallel_execution: false) - end + context 'when the feature flag is enabled' do + let(:base_model) { Gitlab::Database.database_base_models[tracking_database] } + let(:connection) { base_model.connection } + + before do + stub_feature_flags(execute_batched_migrations_on_schedule: true) + allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration) + .with(connection: connection) + .and_return(nil) + end + + context 'when database config is shared' do it 'does nothing' do + expect(Gitlab::Database).to receive(:db_config_share_with) + .with(base_model.connection_db_config).and_return('main') + + expect(worker).not_to receive(:active_migration) expect(worker).not_to receive(:run_active_migration) worker.perform end end - context 'when parallel execution is enabled' do - before do - stub_feature_flags(batched_migrations_parallel_execution: true) - end + context 'when no active migrations exist' do + context 'when parallel execution is disabled' do + before do + stub_feature_flags(batched_migrations_parallel_execution: false) + end - it 'does nothing' do - expect(worker).not_to receive(:queue_migrations_for_execution) + it 'does nothing' do + expect(worker).not_to receive(:run_active_migration) - worker.perform + worker.perform + end end - end - end - context 'when active migrations exist' do - let(:job_interval) { 5.minutes } - let(:lease_timeout) { 15.minutes } - let(:lease_key) { described_class.name.demodulize.underscore } - let(:migration_id) { 123 } - let(:migration) do - build( - :batched_background_migration, :active, - id: migration_id, interval: job_interval, table_name: table_name - ) - end + context 'when parallel execution is enabled' do + before do + stub_feature_flags(batched_migrations_parallel_execution: true) + end - let(:execution_worker_class) do - case tracking_database - when :main - Database::BatchedBackgroundMigration::MainExecutionWorker - when :ci - Database::BatchedBackgroundMigration::CiExecutionWorker + it 'does nothing' do + expect(worker).not_to receive(:queue_migrations_for_execution) + + worker.perform + end end end - before do - allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration) - .with(connection: base_model.connection) - .and_return(migration) - end + context 'when active migrations exist' do + let(:job_interval) { 5.minutes } + let(:lease_timeout) { 15.minutes } + let(:lease_key) { described_class.name.demodulize.underscore } + let(:migration_id) { 123 } + let(:migration) do + build( + :batched_background_migration, :active, + id: migration_id, interval: job_interval, table_name: table_name + ) + end + + let(:execution_worker_class) do + case tracking_database + when :main + Database::BatchedBackgroundMigration::MainExecutionWorker + when :ci + Database::BatchedBackgroundMigration::CiExecutionWorker + end + end - context 'when parallel execution is disabled' do before do - stub_feature_flags(batched_migrations_parallel_execution: false) + allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration) + .with(connection: connection) + .and_return(migration) end - let(:execution_worker) { instance_double(execution_worker_class) } + context 'when parallel execution is disabled' do + before do + stub_feature_flags(batched_migrations_parallel_execution: false) + end - context 'when the calculated timeout is less than the minimum allowed' do - let(:minimum_timeout) { described_class::MINIMUM_LEASE_TIMEOUT } - let(:job_interval) { 2.minutes } + let(:execution_worker) { instance_double(execution_worker_class) } - it 'sets the lease timeout to the minimum value' do - expect_to_obtain_exclusive_lease(lease_key, timeout: minimum_timeout) + context 'when the calculated timeout is less than the minimum allowed' do + let(:minimum_timeout) { described_class::MINIMUM_LEASE_TIMEOUT } + let(:job_interval) { 2.minutes } - expect(execution_worker_class).to receive(:new).and_return(execution_worker) - expect(execution_worker).to receive(:perform_work).with(tracking_database, migration_id) + it 'sets the lease timeout to the minimum value' do + expect_to_obtain_exclusive_lease(lease_key, timeout: minimum_timeout) - expect(worker).to receive(:run_active_migration).and_call_original + expect(execution_worker_class).to receive(:new).and_return(execution_worker) + expect(execution_worker).to receive(:perform_work).with(tracking_database, migration_id) - worker.perform - end - end + expect(worker).to receive(:run_active_migration).and_call_original - it 'always cleans up the exclusive lease' do - lease = stub_exclusive_lease_taken(lease_key, timeout: lease_timeout) + worker.perform + end + end - expect(lease).to receive(:try_obtain).and_return(true) + it 'always cleans up the exclusive lease' do + lease = stub_exclusive_lease_taken(lease_key, timeout: lease_timeout) - expect(worker).to receive(:run_active_migration).and_raise(RuntimeError, 'I broke') - expect(lease).to receive(:cancel) + expect(lease).to receive(:try_obtain).and_return(true) - expect { worker.perform }.to raise_error(RuntimeError, 'I broke') - end + expect(worker).to receive(:run_active_migration).and_raise(RuntimeError, 'I broke') + expect(lease).to receive(:cancel) - it 'delegetes the execution to ExecutionWorker' do - base_model = Gitlab::Database.database_base_models[tracking_database] + expect { worker.perform }.to raise_error(RuntimeError, 'I broke') + end - expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(base_model.connection).and_yield - expect(execution_worker_class).to receive(:new).and_return(execution_worker) - expect(execution_worker).to receive(:perform_work).with(tracking_database, migration_id) + it 'delegetes the execution to ExecutionWorker' do + expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(connection).and_yield + expect(execution_worker_class).to receive(:new).and_return(execution_worker) + expect(execution_worker).to receive(:perform_work).with(tracking_database, migration_id) - worker.perform + worker.perform + end end - end - context 'when parallel execution is enabled' do - before do - stub_feature_flags(batched_migrations_parallel_execution: true) - end + context 'when parallel execution is enabled' do + before do + stub_feature_flags(batched_migrations_parallel_execution: true) + end - it 'delegetes the execution to ExecutionWorker' do - expect(Gitlab::Database::BackgroundMigration::BatchedMigration) - .to receive(:active_migrations_distinct_on_table).with( - connection: base_model.connection, - limit: execution_worker_class.max_running_jobs - ).and_return([migration]) + it 'delegetes the execution to ExecutionWorker' do + expect(Gitlab::Database::BackgroundMigration::BatchedMigration) + .to receive(:active_migrations_distinct_on_table).with( + connection: base_model.connection, + limit: execution_worker_class.max_running_jobs + ).and_return([migration]) - expected_arguments = [ - [tracking_database.to_s, migration_id] - ] + expected_arguments = [ + [tracking_database.to_s, migration_id] + ] - expect(execution_worker_class).to receive(:perform_with_capacity).with(expected_arguments) + expect(execution_worker_class).to receive(:perform_with_capacity).with(expected_arguments) - worker.perform + worker.perform + end end end end @@ -248,7 +266,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d end end - describe 'executing an entire migration', :freeze_time, if: Gitlab::Database.has_config?(tracking_database) do + describe 'executing an entire migration', :freeze_time, if: Gitlab::Database.has_database?(tracking_database) do include Gitlab::Database::DynamicModelHelpers include Database::DatabaseHelpers diff --git a/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb b/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb deleted file mode 100644 index e6da96e12ec..00000000000 --- a/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -# This shared_example requires the following variables: -# let(:service_class) { Gitlab::DatabaseImporters::SelfMonitoring::Project::DeleteService } -# let(:service) { instance_double(service_class) } -RSpec.shared_examples 'executes service' do - before do - allow(service_class).to receive(:new) { service } - end - - it 'runs the service' do - expect(service).to receive(:execute) - - subject.perform - end -end - -RSpec.shared_examples 'returns in_progress based on Sidekiq::Status' do - it 'returns true when job is enqueued' do - jid = described_class.with_status.perform_async - - expect(described_class.in_progress?(jid)).to eq(true) - end - - it 'returns false when job does not exist' do - expect(described_class.in_progress?('fake_jid')).to eq(false) - end -end |